#!/bin/bash set -euo pipefail readonly NIVORA_VERSION='1.0.0' readonly DEFAULT_REPOSITORY='nivora' language="${NIVORA_LANG:-}" repository="${NIVORA_REPO:-${DEFAULT_REPOSITORY}}" dry_run=0 color_mode="${NIVORA_COLOR:-auto}" detect_language() { [[ -n "$language" ]] && return case "${LC_ALL:-${LC_MESSAGES:-${LANG:-en}}}" in ru* | RU*) language='ru' ;; *) language='en' ;; esac } message() { local key="$1" case "${language}:${key}" in ru:title) printf 'Простое управление пакетами Stapler' ;; ru:usage) printf 'Использование' ;; ru:packages) printf 'Пакеты' ;; ru:system) printf 'Система' ;; ru:install) printf 'Установить' ;; ru:remove) printf 'Удалить' ;; ru:upgrade) printf 'Обновить' ;; ru:search) printf 'Найти' ;; ru:info) printf 'Информация' ;; ru:all_packages) printf 'Все пакеты' ;; ru:refresh) printf 'Обновить репозитории' ;; ru:repositories) printf 'Репозитории' ;; ru:fix) printf 'Исправить' ;; ru:doctor) printf 'Диагностика' ;; ru:config) printf 'Конфигурация' ;; ru:help) printf 'Справка' ;; ru:exit) printf 'Выход' ;; ru:choose) printf 'Выберите действие' ;; ru:package_prompt) printf 'Имя пакета' ;; ru:query_prompt) printf 'Поисковый запрос' ;; ru:unknown_command) printf 'неизвестная команда' ;; ru:unknown_choice) printf 'нет такого пункта' ;; ru:missing_argument) printf 'необходим аргумент' ;; ru:invalid_language) printf 'поддерживаются языки ru и en' ;; ru:invalid_repository) printf 'некорректное имя репозитория' ;; ru:stapler_missing) printf 'команда stplr не найдена' ;; ru:privilege_missing) printf 'команда повышения привилегий не найдена' ;; ru:running) printf 'Выполняется' ;; ru:preview) printf 'Предпросмотр' ;; ru:diagnostics) printf 'Диагностика окружения' ;; ru:ready) printf 'Система готова к работе' ;; ru:problems) printf 'Обнаружены проблемы' ;; ru:available) printf 'доступен' ;; ru:missing) printf 'не найден' ;; ru:language) printf 'Язык' ;; ru:repository) printf 'Репозиторий' ;; ru:privileges) printf 'Привилегии' ;; ru:commands) printf 'Короткие команды' ;; ru:options) printf 'Параметры' ;; ru:examples) printf 'Примеры' ;; en:title) printf 'Simple Stapler package management' ;; en:usage) printf 'Usage' ;; en:packages) printf 'Packages' ;; en:system) printf 'System' ;; en:install) printf 'Install' ;; en:remove) printf 'Remove' ;; en:upgrade) printf 'Upgrade' ;; en:search) printf 'Search' ;; en:info) printf 'Information' ;; en:all_packages) printf 'All packages' ;; en:refresh) printf 'Refresh repositories' ;; en:repositories) printf 'Repositories' ;; en:fix) printf 'Repair' ;; en:doctor) printf 'Diagnostics' ;; en:config) printf 'Configuration' ;; en:help) printf 'Help' ;; en:exit) printf 'Exit' ;; en:choose) printf 'Choose an action' ;; en:package_prompt) printf 'Package name' ;; en:query_prompt) printf 'Search query' ;; en:unknown_command) printf 'unknown command' ;; en:unknown_choice) printf 'unknown choice' ;; en:missing_argument) printf 'an argument is required' ;; en:invalid_language) printf 'supported languages are ru and en' ;; en:invalid_repository) printf 'invalid repository name' ;; en:stapler_missing) printf 'stplr command was not found' ;; en:privilege_missing) printf 'privilege command was not found' ;; en:running) printf 'Running' ;; en:preview) printf 'Preview' ;; en:diagnostics) printf 'Environment diagnostics' ;; en:ready) printf 'The system is ready' ;; en:problems) printf 'Problems found' ;; en:available) printf 'available' ;; en:missing) printf 'missing' ;; en:language) printf 'Language' ;; en:repository) printf 'Repository' ;; en:privileges) printf 'Privileges' ;; en:commands) printf 'Short commands' ;; en:options) printf 'Options' ;; en:examples) printf 'Examples' ;; *) printf '%s' "$key" ;; esac } setup_style() { if [[ "$color_mode" == 'never' || -n "${NO_COLOR:-}" || ! -t 1 ]]; then bold=''; dim=''; cyan=''; blue=''; green=''; red=''; reset='' return fi bold=$'\033[1m' dim=$'\033[2m' cyan=$'\033[38;5;44m' blue=$'\033[38;5;75m' green=$'\033[38;5;78m' red=$'\033[38;5;203m' reset=$'\033[0m' } fail() { printf '%s%sNivora:%s %s\n' "$bold" "$red" "$reset" "$*" >&2 exit 2 } validate_settings() { case "$language" in ru | en) ;; *) local invalid_language="$language" language='en' fail "$(message invalid_language): ${invalid_language}" ;; esac if [[ "$repository" != 'none' && ! "$repository" =~ ^[[:alnum:]_.-]+$ ]]; then fail "$(message invalid_repository): ${repository}" fi } parse_options() { arguments=() while [[ "$#" -gt 0 ]]; do case "$1" in --lang) [[ "$#" -ge 2 ]] || fail "$(message missing_argument): --lang" language="$2" shift 2 ;; --lang=*) language="${1#--lang=}" shift ;; --repo) [[ "$#" -ge 2 ]] || fail "$(message missing_argument): --repo" repository="$2" shift 2 ;; --repo=*) repository="${1#--repo=}" shift ;; --dry-run) dry_run=1 shift ;; --no-color) color_mode='never' shift ;; --) shift arguments+=("$@") break ;; *) arguments+=("$1") shift ;; esac done setup_style validate_settings } print_header() { printf '%s%s╭─ NIVORA ─────────────────────────────╮%s\n' "$bold" "$cyan" "$reset" if [[ "$language" == 'ru' ]]; then printf '%s%s│%s Простое управление пакетами Stapler %s%s│%s\n' \ "$cyan" "$bold" "$reset" "$bold" "$cyan" "$reset" else printf '%s%s│%s Simple Stapler package management %s%s│%s\n' \ "$cyan" "$bold" "$reset" "$bold" "$cyan" "$reset" fi printf '%s%s╰──────────────────────────────────────╯%s\n' "$bold" "$cyan" "$reset" } menu_row() { local key="$1" local label="$2" local shortcut="$3" local padding=$((23 - ${#label})) [[ "$padding" -lt 1 ]] && padding=1 printf ' %s[%s]%s %s' "$cyan" "$key" "$reset" "$label" printf '%*s' "$padding" '' printf '%s%s%s\n' "$dim" "$shortcut" "$reset" } interactive_menu() { if [[ ! -t 0 || ! -t 1 ]]; then show_help return fi print_header printf '\n %s%s%s\n' "$bold" "$(message packages)" "$reset" menu_row i "$(message install)" nvi menu_row e "$(message remove)" nve menu_row u "$(message upgrade)" nvu menu_row s "$(message search)" nvs menu_row q "$(message info)" nvqi menu_row a "$(message all_packages)" nvqa printf '\n %s%s%s\n' "$bold" "$(message system)" "$reset" menu_row r "$(message refresh)" nvr menu_row l "$(message repositories)" nvrl menu_row f "$(message fix)" nvf menu_row d "$(message doctor)" nvd local help_label help_label="$(message help)" printf '\n %s[h]%s %s' "$cyan" "$reset" "$help_label" printf '%*s' "$((23 - ${#help_label}))" '' printf '%s[x]%s %s\n\n' "$cyan" "$reset" "$(message exit)" printf '%s › ' "$(message choose)" read -r choice case "$choice" in i) prompt_and_dispatch install package ;; e) prompt_and_dispatch remove package ;; u) dispatch upgrade ;; s) prompt_and_dispatch search query ;; q) prompt_and_dispatch info package ;; a) dispatch list ;; r) dispatch refresh ;; l) dispatch repo-list ;; f) dispatch fix ;; d) doctor ;; h) show_help ;; x | q!) return ;; *) fail "$(message unknown_choice): ${choice}" ;; esac } prompt_and_dispatch() { local command_name="$1" local prompt_type="$2" local value if [[ "$prompt_type" == 'package' ]]; then printf '%s › ' "$(message package_prompt)" else printf '%s › ' "$(message query_prompt)" fi read -r value [[ -n "$value" ]] || fail "$(message missing_argument): ${command_name}" dispatch "$command_name" "$value" } show_help() { print_header if [[ "$language" == 'ru' ]]; then cat < [аргументы] выполнить команду ${bold}$(message commands)${reset} ${cyan}nvi${reset} <пакет...> установить ${cyan}nvr${reset} обновить репозитории ${cyan}nve${reset} <пакет...> удалить ${cyan}nvrl${reset} показать репозитории ${cyan}nvu${reset} обновить пакеты ${cyan}nvf${reset} исправить Stapler ${cyan}nvs${reset} <запрос> найти ${cyan}nvd${reset} запустить диагностику ${cyan}nvqi${reset} <пакет> информация ${cyan}nvc${reset} конфигурация Stapler ${cyan}nvqa${reset} каталог пакетов ${bold}$(message options)${reset} --repo <имя|none> репозиторий коротких имён (по умолчанию: nivora) --lang язык интерфейса --dry-run показать команду без выполнения --no-color отключить цвета ${bold}$(message examples)${reset} nvi codex nvs editor nvqi codex nv --repo none install other/package EOF else cat < [arguments] run a command ${bold}$(message commands)${reset} ${cyan}nvi${reset} install ${cyan}nvr${reset} refresh repositories ${cyan}nve${reset} remove ${cyan}nvrl${reset} list repositories ${cyan}nvu${reset} upgrade packages ${cyan}nvf${reset} repair Stapler ${cyan}nvs${reset} search ${cyan}nvd${reset} run diagnostics ${cyan}nvqi${reset} information ${cyan}nvc${reset} configure Stapler ${cyan}nvqa${reset} package catalog ${bold}$(message options)${reset} --repo repository for short names (default: nivora) --lang interface language --dry-run print the command without running it --no-color disable colors ${bold}$(message examples)${reset} nvi codex nvs editor nvqi codex nv --repo none install other/package EOF fi } need_stapler() { command -v stplr >/dev/null 2>&1 || fail "$(message stapler_missing)" } privilege_command() { [[ "$EUID" -eq 0 ]] && return local command_name="${NIVORA_SUDO:-sudo}" command -v "$command_name" >/dev/null 2>&1 || fail "$(message privilege_missing): ${command_name}" printf '%s\n' "$command_name" } is_package_name() { local value="$1" [[ -n "$value" && "$value" != -* && "$value" != */* && "$value" != ./* && "$value" != ../* && "$value" != /* && "$value" != *.rpm && "$value" != *.deb && "$value" != *.pkg.tar.* ]] } normalize_packages() { local argument for argument in "$@"; do if [[ "$repository" != 'none' ]] && is_package_name "$argument"; then printf '%s/%s\n' "$repository" "$argument" else printf '%s\n' "$argument" fi done } print_command() { local label if [[ "$dry_run" -eq 1 ]]; then label="$(message preview)" else label="$(message running)" fi printf '%s%s%s%s %s›%s' "$bold" "$blue" "$label" "$reset" "$cyan" "$reset" >&2 printf ' %q' "$@" >&2 printf '\n' >&2 } run_stapler() { local access="$1" shift need_stapler local -a command=() local privilege if [[ "$access" == 'privileged' ]]; then privilege="$(privilege_command)" [[ -n "$privilege" ]] && command+=("$privilege") fi command+=(stplr "$@") [[ "${NIVORA_QUIET:-0}" == '1' ]] || print_command "${command[@]}" [[ "$dry_run" -eq 1 ]] && return exec "${command[@]}" } require_arguments() { local command_name="$1" local count="$2" [[ "$count" -gt 0 ]] || fail "$(message missing_argument): ${command_name}" } doctor() { local failures=0 local privilege="${NIVORA_SUDO:-sudo}" printf '%s%s%s\n\n' "$bold" "$(message diagnostics)" "$reset" if command -v stplr >/dev/null 2>&1; then printf ' %s●%s %-14s %s\n' "$green" "$reset" 'Stapler' "$(message available)" else printf ' %s●%s %-14s %s\n' "$red" "$reset" 'Stapler' "$(message missing)" failures=1 fi if [[ "$EUID" -eq 0 ]] || command -v "$privilege" >/dev/null 2>&1; then printf ' %s●%s %-14s %s\n' \ "$green" "$reset" "$(message privileges)" \ "$([[ "$EUID" -eq 0 ]] && printf 'root' || printf '%s' "$privilege")" else printf ' %s●%s %-14s %s\n' \ "$red" "$reset" "$(message privileges)" "$(message missing)" failures=1 fi printf ' %s●%s %-14s %s\n' "$green" "$reset" "$(message language)" "$language" printf ' %s●%s %-14s %s\n\n' \ "$green" "$reset" "$(message repository)" "$repository" if [[ "$failures" -eq 0 ]]; then printf '%s✓ %s%s\n' "$green" "$(message ready)" "$reset" else printf '%s✗ %s%s\n' "$red" "$(message problems)" "$reset" fi return "$failures" } completion() { local shell="${1:-}" case "$shell" in bash) cat <<'EOF' _nivora() { local commands='install remove upgrade search info list refresh repo-list fix doctor config completion version help' if (( COMP_CWORD == 1 )); then COMPREPLY=($(compgen -W "$commands" -- "${COMP_WORDS[COMP_CWORD]}")) fi } complete -F _nivora nv EOF ;; fish) cat <<'EOF' complete -c nv -f -n '__fish_use_subcommand' -a 'install remove upgrade search info list refresh repo-list fix doctor config completion version help' EOF ;; zsh) cat <<'EOF' #compdef nv _arguments '1:command:(install remove upgrade search info list refresh repo-list fix doctor config completion version help)' '*::argument:->arguments' EOF ;; *) fail "$(message missing_argument): completion " ;; esac } dispatch() { local command_name="${1:-help}" [[ "$#" -gt 0 ]] && shift case "$command_name" in install | i) require_arguments "$command_name" "$#" local -a packages=() mapfile -t packages < <(normalize_packages "$@") run_stapler privileged install "${packages[@]}" ;; remove | erase | e) require_arguments "$command_name" "$#" local -a packages=() mapfile -t packages < <(normalize_packages "$@") run_stapler privileged remove "${packages[@]}" ;; upgrade | update | u) run_stapler privileged up "$@" ;; search | s) require_arguments "$command_name" "$#" run_stapler user search "$@" ;; info | query-info | qi) require_arguments "$command_name" "$#" local -a packages=() mapfile -t packages < <(normalize_packages "$@") run_stapler user info "${packages[@]}" ;; list | packages | query-all | qa) run_stapler user list "$@" ;; refresh | r) run_stapler privileged refresh "$@" ;; repo-list | rl) run_stapler user repo list "$@" ;; repo) if [[ "$#" -eq 0 ]]; then run_stapler user repo list elif [[ "${1:-}" == 'list' || "${1:-}" == 'ls' ]]; then run_stapler user repo "$@" else run_stapler privileged repo "$@" fi ;; fix | f) run_stapler privileged fix "$@" ;; doctor | d) doctor ;; config | c) if [[ "$#" -eq 0 ]]; then run_stapler user config show else run_stapler user config "$@" fi ;; completion) completion "$@" ;; support) run_stapler user support "$@" ;; version | v | --version) printf 'Nivora CLI %s\n' "$NIVORA_VERSION" command -v stplr >/dev/null 2>&1 && stplr version ;; help | h | -h | --help) show_help ;; *) fail "$(message unknown_command): ${command_name}" ;; esac } main() { detect_language setup_style parse_options "$@" set -- "${arguments[@]}" case "$(basename "$0")" in nv) if [[ "$#" -eq 0 ]]; then interactive_menu; else dispatch "$@"; fi ;; nvi) dispatch install "$@" ;; nve) dispatch remove "$@" ;; nvu) dispatch upgrade "$@" ;; nvs) dispatch search "$@" ;; nvqi) dispatch info "$@" ;; nvqa) dispatch list "$@" ;; nvr) dispatch refresh "$@" ;; nvrl) dispatch repo-list "$@" ;; nvf) dispatch fix "$@" ;; nvd) doctor ;; nvc) dispatch config "$@" ;; nivora) dispatch "$@" ;; *) fail "$(message unknown_command): $(basename "$0")" ;; esac } main "$@"