#!/usr/bin/env bash set -euo pipefail readonly EPCTL_NAME="epctl" readonly REPO_OWNER="GMWalletApp" readonly REPO_NAME="epusdt" readonly SERVICE_NAME="epusdt" readonly SYSTEM_USER="epusdt" readonly INSTALL_DIR="/opt/epusdt" readonly BIN_PATH="${INSTALL_DIR}/epusdt" readonly CONFIG_PATH="${INSTALL_DIR}/.env" readonly CONFIG_EXAMPLE_PATH="${INSTALL_DIR}/.env.example" readonly WWW_PATH="${INSTALL_DIR}/www" readonly CACHE_ROOT="/tmp/epusdt" readonly SYSTEMD_UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service" readonly SELF_INSTALL_PATH="/usr/local/bin/${EPCTL_NAME}" readonly DEFAULT_APP_URI="http://127.0.0.1:8000" readonly DEFAULT_LISTEN="127.0.0.1:8000" readonly SCRIPT_SOURCE_PATH="$(readlink -f "${BASH_SOURCE[0]}")" COLOR_RESET="" COLOR_BOLD="" COLOR_DIM="" COLOR_CYAN="" COLOR_GREEN="" COLOR_YELLOW="" COLOR_RED="" COLOR_BLUE="" ACTIVE_LANG="" POSITIONAL_ARGS=() detect_default_language() { local raw normalized raw="${EPCTL_LANG:-}" if [[ -z "${raw}" ]]; then printf 'zh\n' return fi normalized="${raw,,}" case "${normalized}" in zh|zh-*|zh_*|cn|chs|cht|中文) printf 'zh\n' ;; en|en-*|en_*|english) printf 'en\n' ;; *) printf '%s\n' "${raw}" ;; esac } set_language() { local requested normalized fallback_lang requested="${1:-$(detect_default_language)}" normalized="${requested,,}" fallback_lang="${ACTIVE_LANG:-$(detect_default_language)}" case "${normalized}" in zh|zh-*|zh_*|cn|chs|cht|中文) ACTIVE_LANG="zh" ;; en|en-*|en_*|english) ACTIVE_LANG="en" ;; *) ACTIVE_LANG="${fallback_lang}" die "$(trf unsupported_language "${requested}")" ;; esac export EPCTL_LANG="${ACTIVE_LANG}" } trf() { local key="$1" shift || true local template case "${ACTIVE_LANG}:${key}" in en:label_warn) template='warn' ;; zh:label_warn) template='警告' ;; en:label_ok) template='ok' ;; zh:label_ok) template='完成' ;; en:label_error) template='error' ;; zh:label_error) template='错误' ;; en:only_linux) template='this script only supports Linux' ;; zh:only_linux) template='本脚本仅支持 Linux' ;; en:missing_dependency) template='missing dependency: %s' ;; zh:missing_dependency) template='缺少依赖:%s' ;; en:root_required) template='this command must be run as root' ;; zh:root_required) template='此命令必须以 root 身份执行' ;; en:release_tag_required) template='release tag is required' ;; zh:release_tag_required) template='必须提供 release tag' ;; en:release_tag_invalid) template='--tag must match a release tag such as v1.0.6' ;; zh:release_tag_invalid) template='--tag 必须是有效的 release tag,例如 v1.0.6' ;; en:unsupported_arch) template='unsupported architecture: %s' ;; zh:unsupported_arch) template='不支持的架构:%s' ;; en:checksum_missing) template='checksum entry for %s not found in %s' ;; zh:checksum_missing) template='在 %s 中找不到 %s 的校验项' ;; en:latest_release_resolve_failed) template='failed to resolve the latest release tag from GitHub' ;; zh:latest_release_resolve_failed) template='无法从 GitHub 解析最新 release tag' ;; en:using_latest_without_confirmation) template='using latest release tag %s without confirmation because EPCTL_ASSUME_YES=1' ;; zh:using_latest_without_confirmation) template='检测到 EPCTL_ASSUME_YES=1,直接使用最新 release tag %s,不再二次确认' ;; en:latest_tag_no_tty) template='no --tag provided and no interactive terminal available; pass --tag explicitly or set EPCTL_ASSUME_YES=1' ;; zh:latest_tag_no_tty) template='未提供 --tag,且当前不是交互终端;请显式传入 --tag,或设置 EPCTL_ASSUME_YES=1' ;; en:latest_prompt) template='latest release tag is %s. Continue? [y/N] ' ;; zh:latest_prompt) template='检测到最新 release tag 为 %s,是否继续?[y/N] ' ;; en:cancelled) template='cancelled' ;; zh:cancelled) template='已取消' ;; en:cached_release_files) template='using cached release files in %s' ;; zh:cached_release_files) template='使用缓存中的 release 文件:%s' ;; en:downloading_archive) template='downloading %s' ;; zh:downloading_archive) template='正在下载 %s' ;; en:checksum_skipped) template='sha256sum not found; skipped checksum verification' ;; zh:checksum_skipped) template='未检测到 sha256sum,已跳过校验' ;; en:archive_missing_binary) template='release archive is missing the epusdt binary' ;; zh:archive_missing_binary) template='release 压缩包中缺少 epusdt 二进制文件' ;; en:archive_missing_env_example) template='release archive is missing .env.example' ;; zh:archive_missing_env_example) template='release 压缩包中缺少 .env.example' ;; en:keeping_existing_config) template='keeping existing config at %s' ;; zh:keeping_existing_config) template='保留现有配置文件:%s' ;; en:service_failed_active) template='service %s failed to become active' ;; zh:service_failed_active) template='服务 %s 未能成功进入 active 状态' ;; en:service_installed) template='installed %s to %s from %s' ;; zh:service_installed) template='已将 %s 安装到 %s,来源版本:%s' ;; en:service_upgraded_restarted) template='updated %s in %s from %s and restarted the service' ;; zh:service_upgraded_restarted) template='已将 %s 在 %s 升级到 %s,并完成服务重启' ;; en:upgrade_existing_config_required) template='upgrade requires an existing config file at %s; run install first' ;; zh:upgrade_existing_config_required) template='升级要求 %s 已存在;请先执行 install' ;; en:upgrade_restart_flag_conflict) template='cannot combine --prompt-restart and --no-restart' ;; zh:upgrade_restart_flag_conflict) template='--prompt-restart 和 --no-restart 不能同时使用' ;; en:upgrade_deploy_failed_rollback_start) template='upgrading %s failed before restart; attempting to roll back the release files' ;; zh:upgrade_deploy_failed_rollback_start) template='升级 %s 在重启前失败,正在尝试回滚 release 文件' ;; en:upgrade_deploy_failed_rolled_back) template='upgrading %s to %s failed before restart; previous release files were restored' ;; zh:upgrade_deploy_failed_rolled_back) template='升级 %s 到 %s 在重启前失败,已恢复到之前的 release 文件' ;; en:upgrade_deploy_failed_rollback_failed) template='upgrading %s to %s failed before restart, and rollback also failed' ;; zh:upgrade_deploy_failed_rollback_failed) template='升级 %s 到 %s 在重启前失败,且回滚也失败了' ;; en:upgrade_restart_failed_rollback_start) template='restart failed after upgrading %s; attempting to roll back the release files' ;; zh:upgrade_restart_failed_rollback_start) template='升级 %s 后重启失败,正在尝试回滚 release 文件' ;; en:upgrade_restart_failed_rolled_back) template='restart failed after upgrading %s to %s; previous release files were restored' ;; zh:upgrade_restart_failed_rolled_back) template='升级 %s 到 %s 后重启失败,已恢复到之前的 release 文件' ;; en:upgrade_restart_failed_rollback_failed) template='restart failed after upgrading %s to %s, and rollback also failed' ;; zh:upgrade_restart_failed_rollback_failed) template='升级 %s 到 %s 后重启失败,且回滚也失败了' ;; en:self_install_current) template='%s already points to the current script' ;; zh:self_install_current) template='%s 已经指向当前脚本,无需重复安装' ;; en:self_install_done) template='installed %s into /usr/local/bin' ;; zh:self_install_done) template='已将 %s 安装到 /usr/local/bin' ;; en:config_not_found) template='config file not found: %s' ;; zh:config_not_found) template='找不到配置文件:%s' ;; en:http_listen_not_found) template='http_listen not found in %s' ;; zh:http_listen_not_found) template='在 %s 中找不到 http_listen' ;; en:http_listen_parse_failed) template='failed to parse the port from http_listen=%s' ;; zh:http_listen_parse_failed) template='无法从 http_listen=%s 中解析端口' ;; en:curl_partial_body) template='curl failed and returned a partial response body:' ;; zh:curl_partial_body) template='curl 请求失败,并返回了部分响应体:' ;; en:init_password_http_failed) template='init-password request failed with HTTP %s: %s' ;; zh:init_password_http_failed) template='init-password 请求失败,HTTP %s:%s' ;; en:response_body_label) template='response body:' ;; zh:response_body_label) template='响应体:' ;; en:response_body_empty) template='response body is empty' ;; zh:response_body_empty) template='响应体为空' ;; en:lines_positive_integer) template='--lines must be a positive integer' ;; zh:lines_positive_integer) template='--lines 必须是正整数' ;; en:state_not_installed) template='not installed' ;; zh:state_not_installed) template='未安装' ;; en:state_no_systemctl) template='unit exists, but systemctl is unavailable' ;; zh:state_no_systemctl) template='unit 文件已存在,但 systemctl 不可用' ;; en:state_no_systemd) template='unit exists, but systemd is unavailable' ;; zh:state_no_systemd) template='unit 文件已存在,但 systemd 不可用' ;; en:state_active) template='active' ;; zh:state_active) template='运行中' ;; en:state_inactive) template='installed but inactive' ;; zh:state_inactive) template='已安装,但未运行' ;; en:menu_title) template='EPCTL Console' ;; zh:menu_title) template='EPCTL 控制台' ;; en:menu_subtitle) template='Linux binary manager for epusdt' ;; zh:menu_subtitle) template='面向 epusdt 的 Linux 二进制管理工具' ;; en:label_service) template='service' ;; zh:label_service) template='服务状态' ;; en:label_install_dir) template='install dir' ;; zh:label_install_dir) template='安装目录' ;; en:label_config) template='config' ;; zh:label_config) template='配置文件' ;; en:menu_section_setup) template='Setup and release management' ;; zh:menu_section_setup) template='部署与发布' ;; en:menu_section_runtime) template='Runtime inspection' ;; zh:menu_section_runtime) template='运行查看' ;; en:menu_section_tools) template='Language and help' ;; zh:menu_section_tools) template='语言与帮助' ;; en:menu_badge_direct) template='direct' ;; zh:menu_badge_direct) template='直达' ;; en:menu_badge_confirm) template='confirm' ;; zh:menu_badge_confirm) template='确认' ;; en:menu_exit) template='Quit' ;; zh:menu_exit) template='退出' ;; en:menu_hint_direct) template='Direct items run immediately after selection.' ;; zh:menu_hint_direct) template='带 [直达] 的项目会在选择后直接执行。' ;; en:menu_hint_confirm) template='Confirm items show an extra confirmation step first.' ;; zh:menu_hint_confirm) template='带 [确认] 的项目会先显示确认步骤。' ;; en:command_completed) template='command completed' ;; zh:command_completed) template='命令执行完成' ;; en:command_failed_exit) template='command failed with exit code %s' ;; zh:command_failed_exit) template='命令执行失败,退出码:%s' ;; en:invalid_selection) template='invalid selection: %s' ;; zh:invalid_selection) template='无效选择:%s' ;; en:arg_requires_value) template='%s requires a value' ;; zh:arg_requires_value) template='%s 需要一个参数值' ;; en:unknown_arg) template='unknown argument for %s: %s' ;; zh:unknown_arg) template='%s 命令不支持该参数:%s' ;; en:unknown_command) template='unknown command: %s' ;; zh:unknown_command) template='未知命令:%s' ;; en:input_release_tag) template='Release tag (blank = latest)' ;; zh:input_release_tag) template='Release tag(留空则使用 latest)' ;; en:input_upgrade_tag) template='Upgrade tag (blank = latest)' ;; zh:input_upgrade_tag) template='升级目标 tag(留空则使用 latest)' ;; en:input_app_uri) template='app_uri' ;; zh:input_app_uri) template='app_uri' ;; en:input_http_listen) template='http_listen' ;; zh:input_http_listen) template='http_listen' ;; en:input_log_lines) template='Log lines' ;; zh:input_log_lines) template='日志行数' ;; en:press_enter_continue) template='Press Enter to return to the menu...' ;; zh:press_enter_continue) template='按回车返回菜单...' ;; en:menu_select_action) template='Select an action [0-10]: ' ;; zh:menu_select_action) template='请选择操作 [0-10]:' ;; en:menu_language_prompt) template='Choose a language / 请选择语言: 1) 中文 2) English' ;; zh:menu_language_prompt) template='请选择语言 / Choose a language:1)中文 2)English' ;; en:menu_language_input) template='Language selection(语言选择)' ;; zh:menu_language_input) template='语言选择(Language selection)' ;; en:menu_confirm_prompt) template='Continue? / 是否继续? [y/N] ' ;; zh:menu_confirm_prompt) template='是否继续?/ Continue? [y/N] ' ;; en:upgrade_restart_prompt) template='Release files are updated. Restart %s now? [Y/n] ' ;; zh:upgrade_restart_prompt) template='Release 文件已更新,是否立即重启 %s?[Y/n] ' ;; en:menu_cancelled_back) template='Cancelled and returned to the menu / 已取消,返回菜单' ;; zh:menu_cancelled_back) template='已取消,返回菜单 / Cancelled and returned to the menu' ;; en:language_switched) template='Language switched to %s / 语言已切换为 %s' ;; zh:language_switched) template='语言已切换为 %s / Language switched to %s' ;; en:language_name_zh) template='中文' ;; zh:language_name_zh) template='中文' ;; en:language_name_en) template='English' ;; zh:language_name_en) template='English' ;; en:unsupported_language) template='unsupported language: %s' ;; zh:unsupported_language) template='不支持的语言:%s' ;; en:current_language) template='current language' ;; zh:current_language) template='当前语言' ;; en:base_image_label) template='base image' ;; zh:base_image_label) template='基础镜像' ;; en:action_download_title) template='Download release package' ;; zh:action_download_title) template='下载 release 包' ;; en:action_download_desc) template='This will download the selected GitHub release archive into %s, and verify SHA256 when sha256sum is available. It will not install or start the service.' ;; zh:action_download_desc) template='此操作会把选定的 GitHub release 压缩包下载到 %s,并在本机有 sha256sum 时校验 SHA256。它不会安装,也不会启动服务。' ;; en:action_install_title) template='Install or refresh the service' ;; zh:action_install_title) template='安装或刷新服务' ;; en:action_install_desc) template='This will install epusdt into %s, create or reuse %s, write %s, and enable %s through systemd. Existing .env will be kept.' ;; zh:action_install_desc) template='此操作会把 epusdt 安装到 %s,创建或复用 %s,写入 %s,并通过 systemd 启用 %s。若已有 .env,则会保留。' ;; en:action_upgrade_title) template='Upgrade the service' ;; zh:action_upgrade_title) template='升级服务' ;; en:action_upgrade_desc) template='This will download and install a newer release into %s, keep the existing .env configuration, and then ask whether to restart %s. The direct CLI command restarts by default unless you pass --no-restart.' ;; zh:action_upgrade_desc) template='此操作会下载并安装较新的 release 到 %s,保留现有 .env 配置,然后再询问是否重启 %s。直接执行 CLI 时默认会重启,除非显式传入 --no-restart。' ;; en:action_upgrade_restart_notice) template='Restarting %s will briefly interrupt the service and switch the running process to the new binary.' ;; zh:action_upgrade_restart_notice) template='重启 %s 会带来短暂服务中断,并让运行中的进程切换到新的二进制文件。' ;; en:action_upgrade_restart_skipped) template='Release files for %s were updated, but the service was not restarted. Run `systemctl restart %s` manually to apply the new binary.' ;; zh:action_upgrade_restart_skipped) template='已更新 %s 的 release 文件,但没有重启服务。请手动执行 `systemctl restart %s` 以应用新的二进制文件。' ;; en:action_upgrade_restart_no_tty) template='--prompt-restart requires an interactive terminal. Re-run in a TTY, use plain `upgrade` to restart immediately, or pass `--no-restart` to skip the restart.' ;; zh:action_upgrade_restart_no_tty) template='--prompt-restart 需要交互终端。请在 TTY 中重新执行,或直接使用默认 `upgrade` 立即重启,或传入 `--no-restart` 跳过重启。' ;; en:action_self_install_title) template='Install epctl into PATH' ;; zh:action_self_install_title) template='安装 epctl 到 PATH' ;; en:action_self_install_desc) template='This will install %s into /usr/local/bin so it can be called from anywhere. Use %s to force Chinese or %s to force English.' ;; zh:action_self_install_desc) template='此操作会把 %s 安装到 /usr/local/bin,便于在任意目录直接调用。之后可用 %s 强制中文,或用 %s 强制英文。' ;; en:action_show_config_title) template='Show active config' ;; zh:action_show_config_title) template='查看当前配置' ;; en:action_show_config_desc) template='This will print the full active configuration file %s to the terminal. Sensitive values will be visible.' ;; zh:action_show_config_desc) template='此操作会把当前生效的配置文件 %s 完整输出到终端,其中敏感配置也会直接显示。' ;; en:action_init_password_title) template='Request initial admin password' ;; zh:action_init_password_title) template='获取初始化密码' ;; en:action_init_password_desc) template='This will call the local HTTP route /admin/api/v1/auth/init-password based on http_listen in %s. It does not read the database directly.' ;; zh:action_init_password_desc) template='此操作会根据 %s 中的 http_listen,请求本地 HTTP 路由 /admin/api/v1/auth/init-password。它不会直接读取数据库。' ;; en:action_status_title) template='Show service status' ;; zh:action_status_title) template='查看服务状态' ;; en:action_status_desc) template='This will run systemctl status for %s and print the current runtime state, recent logs, and unit summary.' ;; zh:action_status_desc) template='此操作会对 %s 执行 systemctl status,并输出当前运行状态、最近日志和 unit 摘要。' ;; en:action_logs_title) template='Show recent logs' ;; zh:action_logs_title) template='查看最近日志' ;; en:action_logs_desc) template='This will read recent journal logs for %s. You can choose how many lines to print next.' ;; zh:action_logs_desc) template='此操作会读取 %s 的最近 journal 日志。下一步你可以选择要输出多少行。' ;; en:action_language_title) template='Switch language(切换语言)' ;; zh:action_language_title) template='Switch language(切换语言)' ;; en:action_language_desc) template='This only changes the current interactive interface language. It does not change the service config or installation files. / 此操作只会切换当前交互界面的语言,不会修改服务配置,也不会改动安装文件。' ;; zh:action_language_desc) template='此操作只会切换当前交互界面的语言,不会修改服务配置,也不会改动安装文件。 / This only changes the current interactive interface language. It does not change the service config or installation files.' ;; en:action_help_title) template='Show help' ;; zh:action_help_title) template='查看帮助' ;; en:action_help_desc) template='This will print the full command help, including all non-interactive commands and global options.' ;; zh:action_help_desc) template='此操作会输出完整命令帮助,包括所有非交互命令和全局参数。' ;; *) template="missing translation: ${key}" ;; esac printf -- "${template}" "$@" } init_colors() { if [[ -n "${NO_COLOR:-}" || "${EPCTL_NO_COLOR:-}" == "1" ]]; then return fi if [[ -t 1 || -t 2 ]]; then COLOR_RESET=$'\033[0m' COLOR_BOLD=$'\033[1m' COLOR_DIM=$'\033[2m' COLOR_CYAN=$'\033[36m' COLOR_GREEN=$'\033[32m' COLOR_YELLOW=$'\033[33m' COLOR_RED=$'\033[31m' COLOR_BLUE=$'\033[34m' fi } log() { printf '%b[%s]%b %s\n' "${COLOR_CYAN}${COLOR_BOLD}" "${EPCTL_NAME}" "${COLOR_RESET}" "$*" >&2 } warn() { printf '%b[%s] %s:%b %s\n' "${COLOR_YELLOW}${COLOR_BOLD}" "${EPCTL_NAME}" "$(trf label_warn)" "${COLOR_RESET}" "$*" >&2 } success() { printf '%b[%s] %s:%b %s\n' "${COLOR_GREEN}${COLOR_BOLD}" "${EPCTL_NAME}" "$(trf label_ok)" "${COLOR_RESET}" "$*" >&2 } die() { printf '%b[%s] %s:%b %s\n' "${COLOR_RED}${COLOR_BOLD}" "${EPCTL_NAME}" "$(trf label_error)" "${COLOR_RESET}" "$*" >&2 exit 1 } usage() { if [[ "${ACTIVE_LANG}" == "zh" ]]; then cat <<'EOF' 用法: epctl [--lang zh|en] [--no-color] epctl zh epctl en epctl [--lang zh|en] menu epctl [--lang zh|en] download [--tag ] epctl [--lang zh|en] install [--tag ] [--app-uri URL] [--listen ADDR:PORT] epctl [--lang zh|en] upgrade [--tag ] [--prompt-restart|--no-restart] epctl [--lang zh|en] self-install epctl [--lang zh|en] show-config epctl [--lang zh|en] init-password epctl [--lang zh|en] status epctl [--lang zh|en] logs [--lines N] 全局选项: --lang zh|en 切换界面语言 --no-color 禁用彩色输出 快捷方式 `epctl zh` 直接进入中文;`epctl en` 直接进入英文 命令说明: menu 进入交互式菜单;直接执行 `epctl` 也会进入 download 下载 GitHub Release 包到 /tmp/epusdt// install 安装或刷新 /opt/epusdt,并注册 epusdt.service upgrade 升级到新的 release,保留现有 .env;默认直接重启,菜单会改为先询问 self-install 安装 epctl 到 /usr/local/bin show-config 输出当前生效的 /opt/epusdt/.env init-password 通过本地 HTTP 路由请求初始化管理员密码 status 显示 epusdt 的 systemd 状态 logs 显示最近的服务日志 说明: - 仅支持 Linux - 仅支持二进制安装 - install / upgrade / self-install 会写入 /opt、/etc/systemd、/usr/local/bin - 需要提权的命令会自动通过 sudo 重新执行 - tag 需要是真实的 GitHub release tag,例如 v1.0.6 - 未提供 --tag 时,会自动解析 latest,并要求二次确认 - upgrade 只接受已有安装;不会创建新的 .env - upgrade 默认会在文件替换后立即重启服务 - upgrade --prompt-restart 会在交互终端下询问是否重启 - upgrade --no-restart 只替换文件,不重启服务 EOF else cat <<'EOF' Usage: epctl [--lang zh|en] [--no-color] epctl zh epctl en epctl [--lang zh|en] menu epctl [--lang zh|en] download [--tag ] epctl [--lang zh|en] install [--tag ] [--app-uri URL] [--listen ADDR:PORT] epctl [--lang zh|en] upgrade [--tag ] [--prompt-restart|--no-restart] epctl [--lang zh|en] self-install epctl [--lang zh|en] show-config epctl [--lang zh|en] init-password epctl [--lang zh|en] status epctl [--lang zh|en] logs [--lines N] Global options: --lang zh|en Select the interface language --no-color Disable colored output Shortcut `epctl zh` enters Chinese directly; `epctl en` enters English directly Command summary: menu Open the interactive menu; `epctl` with no arguments does this too download Download a GitHub release archive into /tmp/epusdt// install Install or refresh /opt/epusdt and register epusdt.service upgrade Upgrade to a newer release while keeping the existing .env; direct CLI restarts by default, menu mode asks first self-install Install epctl into /usr/local/bin show-config Print the active /opt/epusdt/.env file init-password Request the initial admin password from the local HTTP route status Show the systemd status of epusdt logs Show recent service logs Notes: - Linux only - Binary installation only - install / upgrade / self-install write into /opt, /etc/systemd, and /usr/local/bin - privileged commands automatically re-exec through sudo - tags must match real GitHub release tags such as v1.0.6 - if --tag is omitted, epctl resolves the latest release and asks for confirmation - upgrade requires an existing install and does not create a new .env - upgrade restarts the service by default after files are replaced - upgrade --prompt-restart asks whether to restart in an interactive terminal - upgrade --no-restart replaces files only and prints a manual restart warning EOF fi } parse_global_options() { POSITIONAL_ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --lang) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--lang")" set_language "$2" shift 2 ;; --lang=*) set_language "${1#*=}" shift ;; --no-color) export EPCTL_NO_COLOR=1 shift ;; *) POSITIONAL_ARGS+=("$1") shift ;; esac done } require_linux() { [[ "$(uname -s)" == "Linux" ]] || die "$(trf only_linux)" } have_command() { command -v "$1" >/dev/null 2>&1 } require_command() { have_command "$1" || die "$(trf missing_dependency "$1")" } is_interactive_terminal() { [[ -t 0 && -t 1 ]] } require_root() { [[ "${EUID}" -eq 0 ]] || die "$(trf root_required)" } ensure_root_via_sudo() { if [[ "${EUID}" -eq 0 ]]; then return fi require_command sudo exec sudo -- "${SCRIPT_SOURCE_PATH}" --lang "${ACTIVE_LANG}" "$@" } require_release_tag() { local tag="$1" [[ -n "${tag}" ]] || die "$(trf release_tag_required)" [[ "${tag}" == v* ]] || die "$(trf release_tag_invalid)" } map_arch() { case "$(uname -m)" in x86_64|amd64) printf 'amd64\n' ;; aarch64|arm64) printf 'arm64\n' ;; *) die "$(trf unsupported_arch "$(uname -m)")" ;; esac } asset_version_from_tag() { local tag="$1" printf '%s\n' "${tag#v}" } cache_dir_for_tag() { local tag="$1" printf '%s/%s\n' "${CACHE_ROOT}" "${tag}" } archive_name_for_tag() { local tag="$1" local arch="$2" printf 'epusdt-%s-linux-%s.tar.gz\n' "$(asset_version_from_tag "${tag}")" "${arch}" } release_base_url_for_tag() { local tag="$1" printf 'https://github.com/%s/%s/releases/download/%s\n' "${REPO_OWNER}" "${REPO_NAME}" "${tag}" } download_file() { local url="$1" local destination="$2" local partial="${destination}.part" rm -f "${partial}" curl --fail --location --silent --show-error --output "${partial}" "${url}" mv "${partial}" "${destination}" } verify_checksum() { local sums_path="$1" local archive_path="$2" local archive_name local checksum_line archive_name="$(basename "${archive_path}")" checksum_line="$(grep -F " ${archive_name}" "${sums_path}" || true)" [[ -n "${checksum_line}" ]] || die "$(trf checksum_missing "${archive_name}" "$(basename "${sums_path}")")" ( cd "$(dirname "${archive_path}")" printf '%s\n' "${checksum_line}" | sha256sum -c - >&2 ) } ensure_download_dependencies() { require_command curl require_command grep require_command sed } latest_release_tag() { local api_url="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" local response tag ensure_download_dependencies response="$(curl --fail --location --silent --show-error "${api_url}")" tag="$(printf '%s\n' "${response}" | sed -n 's/^[[:space:]]*"tag_name":[[:space:]]*"\([^"]\+\)".*$/\1/p' | head -n 1)" [[ -n "${tag}" ]] || die "$(trf latest_release_resolve_failed)" require_release_tag "${tag}" printf '%s\n' "${tag}" } is_affirmative_reply() { local reply="${1:-}" reply="${reply,,}" case "${reply}" in y|yes|ok|true|1|s|shi|是|确认|確認) return 0 ;; *) return 1 ;; esac } confirm_latest_release_tag() { local tag="$1" local reply if [[ "${EPCTL_ASSUME_YES:-}" == "1" ]]; then log "$(trf using_latest_without_confirmation "${tag}")" printf '%s\n' "${tag}" return fi if [[ ! -t 0 ]]; then die "$(trf latest_tag_no_tty)" fi printf '%b[%s]%b %s' "${COLOR_CYAN}${COLOR_BOLD}" "${EPCTL_NAME}" "${COLOR_RESET}" "$(trf latest_prompt "${tag}")" >&2 read -r reply if is_affirmative_reply "${reply}"; then printf '%s\n' "${tag}" return fi die "$(trf cancelled)" } resolve_effective_tag() { local requested_tag="$1" local latest_tag if [[ -n "${requested_tag}" ]]; then require_release_tag "${requested_tag}" printf '%s\n' "${requested_tag}" return fi latest_tag="$(latest_release_tag)" confirm_latest_release_tag "${latest_tag}" } download_release() { local tag="$1" local arch archive_name release_base_url cache_dir archive_path sums_path require_linux ensure_download_dependencies arch="$(map_arch)" archive_name="$(archive_name_for_tag "${tag}" "${arch}")" release_base_url="$(release_base_url_for_tag "${tag}")" cache_dir="$(cache_dir_for_tag "${tag}")" archive_path="${cache_dir}/${archive_name}" sums_path="${cache_dir}/SHA256SUMS" mkdir -p "${cache_dir}" if [[ -f "${archive_path}" && -f "${sums_path}" ]]; then log "$(trf cached_release_files "${cache_dir}")" else log "$(trf downloading_archive "${archive_name}")" download_file "${release_base_url}/${archive_name}" "${archive_path}" download_file "${release_base_url}/SHA256SUMS" "${sums_path}" fi if have_command sha256sum; then verify_checksum "${sums_path}" "${archive_path}" else warn "$(trf checksum_skipped)" fi printf '%s\n' "${archive_path}" } escape_sed_replacement() { printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g' } set_env_key() { local file="$1" local key="$2" local value="$3" local escaped escaped="$(escape_sed_replacement "${value}")" if grep -q "^${key}=" "${file}"; then sed -i "s|^${key}=.*$|${key}=${escaped}|" "${file}" else printf '%s=%s\n' "${key}" "${value}" >> "${file}" fi } ensure_system_user() { if ! getent group "${SYSTEM_USER}" >/dev/null 2>&1; then groupadd --system "${SYSTEM_USER}" fi if ! id -u "${SYSTEM_USER}" >/dev/null 2>&1; then useradd --system --gid "${SYSTEM_USER}" --home-dir "${INSTALL_DIR}" --shell /usr/sbin/nologin "${SYSTEM_USER}" fi } extract_release_archive() { local tag="$1" local archive_path="$2" local extract_dir extract_dir="$(cache_dir_for_tag "${tag}")/extract" rm -rf "${extract_dir}" mkdir -p "${extract_dir}" tar -xzf "${archive_path}" -C "${extract_dir}" [[ -f "${extract_dir}/epusdt" ]] || die "$(trf archive_missing_binary)" [[ -f "${extract_dir}/.env.example" ]] || die "$(trf archive_missing_env_example)" printf '%s\n' "${extract_dir}" } create_default_config_if_missing() { local app_uri="$1" local listen="$2" if [[ -f "${CONFIG_PATH}" ]]; then log "$(trf keeping_existing_config "${CONFIG_PATH}")" return fi cp "${CONFIG_EXAMPLE_PATH}" "${CONFIG_PATH}" set_env_key "${CONFIG_PATH}" "install" "false" set_env_key "${CONFIG_PATH}" "app_uri" "${app_uri}" set_env_key "${CONFIG_PATH}" "http_listen" "${listen}" chmod 0644 "${CONFIG_PATH}" } write_systemd_unit() { cat > "${SYSTEMD_UNIT_PATH}" <&2 if ! read -r reply; then reply="n" fi if [[ -z "${reply}" ]] || is_affirmative_reply "${reply}"; then restart_service_and_verify "${tag}" "${backup_dir}" return 0 fi warn "$(trf action_upgrade_restart_skipped "${SERVICE_NAME}" "${SERVICE_NAME}")" ;; skip) warn "$(trf action_upgrade_restart_skipped "${SERVICE_NAME}" "${SERVICE_NAME}")" ;; *) die "$(trf upgrade_restart_flag_conflict)" ;; esac } command_download() { local tag="" local effective_tag archive_path while [[ $# -gt 0 ]]; do case "$1" in --tag) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--tag")" tag="$2" shift 2 ;; --tag=*) tag="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "download" "$1")" ;; esac done effective_tag="$(resolve_effective_tag "${tag}")" archive_path="$(download_release "${effective_tag}")" printf '%s\n' "${archive_path}" } command_install() { local tag="" local app_uri="${DEFAULT_APP_URI}" local listen="${DEFAULT_LISTEN}" local original_args=("$@") local effective_tag local extract_dir while [[ $# -gt 0 ]]; do case "$1" in --tag) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--tag")" tag="$2" shift 2 ;; --tag=*) tag="${1#*=}" shift ;; --app-uri) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--app-uri")" app_uri="$2" shift 2 ;; --app-uri=*) app_uri="${1#*=}" shift ;; --listen) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--listen")" listen="$2" shift 2 ;; --listen=*) listen="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "install" "$1")" ;; esac done ensure_root_via_sudo install "${original_args[@]}" require_root require_linux require_command tar require_command install require_command systemctl require_command cp require_command grep require_command sed require_command getent require_command groupadd require_command useradd require_command chown effective_tag="$(resolve_effective_tag "${tag}")" extract_dir="$(prepare_release_extract "${effective_tag}")" deploy_release_payload "${extract_dir}" create_default_config_if_missing "${app_uri}" "${listen}" finalize_install_dir_ownership refresh_service_unit systemctl enable --now "${SERVICE_NAME}" if ! systemctl is-active --quiet "${SERVICE_NAME}"; then render_service_debug_info die "$(trf service_failed_active "${SERVICE_NAME}")" fi log "$(trf service_installed "${SERVICE_NAME}" "${INSTALL_DIR}" "${effective_tag}")" } command_self_install() { local original_args=("$@") require_linux require_command install while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "self-install" "$1")" ;; esac done ensure_root_via_sudo self-install "${original_args[@]}" if [[ "${SCRIPT_SOURCE_PATH}" == "${SELF_INSTALL_PATH}" ]]; then chmod 0755 "${SELF_INSTALL_PATH}" log "$(trf self_install_current "${SELF_INSTALL_PATH}")" else install -m 0755 "${SCRIPT_SOURCE_PATH}" "${SELF_INSTALL_PATH}" fi log "$(trf self_install_done "${EPCTL_NAME}")" } command_upgrade() { local tag="" local restart_mode="restart" local original_args=("$@") local effective_tag local extract_dir backup_dir while [[ $# -gt 0 ]]; do case "$1" in --tag) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--tag")" tag="$2" shift 2 ;; --tag=*) tag="${1#*=}" shift ;; --prompt-restart) [[ "${restart_mode}" == "restart" ]] || die "$(trf upgrade_restart_flag_conflict)" restart_mode="prompt" shift ;; --no-restart) [[ "${restart_mode}" == "restart" ]] || die "$(trf upgrade_restart_flag_conflict)" restart_mode="skip" shift ;; -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "upgrade" "$1")" ;; esac done ensure_root_via_sudo upgrade "${original_args[@]}" require_root require_linux require_command tar require_command install require_command systemctl require_command cp require_command grep require_command sed require_command getent require_command groupadd require_command useradd require_command chown effective_tag="$(resolve_effective_tag "${tag}")" ensure_existing_upgrade_config extract_dir="$(prepare_release_extract "${effective_tag}")" backup_dir="$(create_upgrade_backup_dir "${effective_tag}")" backup_release_files "${backup_dir}" if ! apply_upgrade_release "${extract_dir}"; then warn "$(trf upgrade_deploy_failed_rollback_start "${SERVICE_NAME}")" if restore_release_files "${backup_dir}"; then die "$(trf upgrade_deploy_failed_rolled_back "${SERVICE_NAME}" "${effective_tag}")" fi die "$(trf upgrade_deploy_failed_rollback_failed "${SERVICE_NAME}" "${effective_tag}")" fi handle_upgrade_restart "${effective_tag}" "${restart_mode}" "${backup_dir}" } command_show_config() { require_linux while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "show-config" "$1")" ;; esac done [[ -f "${CONFIG_PATH}" ]] || die "$(trf config_not_found "${CONFIG_PATH}")" cat "${CONFIG_PATH}" } resolve_http_listen() { [[ -f "${CONFIG_PATH}" ]] || die "$(trf config_not_found "${CONFIG_PATH}")" local listen listen="$(grep -E '^http_listen=' "${CONFIG_PATH}" | tail -n 1 | cut -d'=' -f2- || true)" [[ -n "${listen}" ]] || die "$(trf http_listen_not_found "${CONFIG_PATH}")" printf '%s\n' "${listen}" } init_password_url() { local listen="$1" local host port if [[ "${listen}" =~ ^\[([^]]+)\]:(.+)$ ]]; then host="${BASH_REMATCH[1]}" port="${BASH_REMATCH[2]}" elif [[ "${listen}" == :* ]]; then host="127.0.0.1" port="${listen#:}" else host="${listen%:*}" port="${listen##*:}" fi case "${host}" in ""|"0.0.0.0"|"::"|"[::]") host="127.0.0.1" ;; esac [[ -n "${port}" ]] || die "$(trf http_listen_parse_failed "${listen}")" printf 'http://%s:%s/admin/api/v1/auth/init-password\n' "${host}" "${port}" } command_init_password() { local listen url body_file http_code curl_rc require_linux require_command curl while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "init-password" "$1")" ;; esac done listen="$(resolve_http_listen)" url="$(init_password_url "${listen}")" body_file="$(mktemp)" if http_code="$(curl --silent --show-error --location --output "${body_file}" --write-out '%{http_code}' "${url}")"; then : else curl_rc="$?" if [[ -s "${body_file}" ]]; then warn "$(trf curl_partial_body)" cat "${body_file}" >&2 printf '\n' >&2 fi rm -f "${body_file}" return "${curl_rc}" fi if [[ "${http_code}" =~ ^2[0-9][0-9]$ ]]; then cat "${body_file}" rm -f "${body_file}" return 0 fi warn "$(trf init_password_http_failed "${http_code}" "${url}")" if [[ -s "${body_file}" ]]; then printf '%b%s%b\n' "${COLOR_YELLOW}${COLOR_BOLD}" "$(trf response_body_label)" "${COLOR_RESET}" >&2 cat "${body_file}" >&2 printf '\n' >&2 else warn "$(trf response_body_empty)" fi rm -f "${body_file}" return 1 } command_status() { local original_args=("$@") require_linux while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "status" "$1")" ;; esac done ensure_root_via_sudo status "${original_args[@]}" require_root require_command systemctl systemctl status "${SERVICE_NAME}" --no-pager } command_logs() { local lines="100" local original_args=("$@") while [[ $# -gt 0 ]]; do case "$1" in --lines) [[ $# -ge 2 ]] || die "$(trf arg_requires_value "--lines")" lines="$2" shift 2 ;; --lines=*) lines="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) die "$(trf unknown_arg "logs" "$1")" ;; esac done [[ "${lines}" =~ ^[1-9][0-9]*$ ]] || die "$(trf lines_positive_integer)" require_linux ensure_root_via_sudo logs "${original_args[@]}" require_root require_command journalctl journalctl -u "${SERVICE_NAME}" --no-pager -q -n "${lines}" } prompt_input() { local label="$1" local default_value="${2:-}" local input if [[ -n "${default_value}" ]]; then printf '%b%s%b [%s]: ' "${COLOR_BOLD}" "${label}" "${COLOR_RESET}" "${default_value}" >&2 else printf '%b%s%b: ' "${COLOR_BOLD}" "${label}" "${COLOR_RESET}" >&2 fi read -r input || return 1 if [[ -z "${input}" ]]; then printf '%s\n' "${default_value}" else printf '%s\n' "${input}" fi } pause_for_menu() { printf '\n' printf '%b%s%b' "${COLOR_DIM}" "$(trf press_enter_continue)" "${COLOR_RESET}" read -r _ || true } print_rule() { local char="${1:--}" local width="${2:-64}" local line printf -v line '%*s' "${width}" '' line="${line// /${char}}" printf '%b%s%b\n' "${COLOR_DIM}" "${line}" "${COLOR_RESET}" } format_badge() { local text="$1" local color="${2:-${COLOR_BLUE}}" printf '%b[%s]%b' "${color}${COLOR_BOLD}" "${text}" "${COLOR_RESET}" } format_service_state_badge() { local state="$1" local color="${COLOR_RED}" case "${state}" in "$(trf state_active)") color="${COLOR_GREEN}" ;; "$(trf state_inactive)") color="${COLOR_YELLOW}" ;; esac format_badge "${state}" "${color}" } format_menu_item_badge() { local kind="$1" local label color case "${kind}" in direct) label="$(trf menu_badge_direct)" color="${COLOR_GREEN}" ;; confirm) label="$(trf menu_badge_confirm)" color="${COLOR_YELLOW}" ;; *) return 0 ;; esac format_badge "${label}" "${color}" } print_menu_meta_line() { local label="$1" local value="$2" printf ' %b%s%b %s\n' "${COLOR_DIM}" "${label}:" "${COLOR_RESET}" "${value}" } print_menu_section_title() { printf '\n%b%s%b\n' "${COLOR_BLUE}${COLOR_BOLD}" "$1" "${COLOR_RESET}" } print_menu_item() { local index="$1" local title="$2" local mode="${3:-}" printf ' %b%2s%b. %s' "${COLOR_CYAN}${COLOR_BOLD}" "${index}" "${COLOR_RESET}" "${title}" if [[ -n "${mode}" ]]; then printf ' %s' "$(format_menu_item_badge "${mode}")" fi printf '\n' } print_menu_action_preview() { local title="$1" local description="$2" printf '\n' >&2 print_rule "-" 64 >&2 printf '%b> %s%b\n' "${COLOR_BLUE}${COLOR_BOLD}" "${title}" "${COLOR_RESET}" >&2 printf '%s\n' "${description}" >&2 print_rule "-" 64 >&2 } confirm_menu_action() { local title="$1" local description="$2" local reply print_menu_action_preview "${title}" "${description}" printf '%b%s%b' "${COLOR_BOLD}" "$(trf menu_confirm_prompt)" "${COLOR_RESET}" >&2 read -r reply || return 1 if is_affirmative_reply "${reply}"; then return 0 fi warn "$(trf menu_cancelled_back)" pause_for_menu return 1 } service_state_summary() { if [[ ! -f "${SYSTEMD_UNIT_PATH}" ]]; then printf '%s\n' "$(trf state_not_installed)" return fi if ! have_command systemctl; then printf '%s\n' "$(trf state_no_systemctl)" return fi if [[ ! -d /run/systemd/system ]]; then printf '%s\n' "$(trf state_no_systemd)" return fi if systemctl is-active --quiet "${SERVICE_NAME}" >/dev/null 2>&1; then printf '%s\n' "$(trf state_active)" return fi printf '%s\n' "$(trf state_inactive)" } print_menu_header() { local current_lang_label service_state if [[ "${ACTIVE_LANG}" == "zh" ]]; then current_lang_label="$(trf language_name_zh)" else current_lang_label="$(trf language_name_en)" fi service_state="$(service_state_summary)" printf '\n' print_rule "=" 64 printf '%b%s%b\n' "${COLOR_CYAN}${COLOR_BOLD}" "$(trf menu_title)" "${COLOR_RESET}" printf '%b%s%b\n' "${COLOR_DIM}" "$(trf menu_subtitle)" "${COLOR_RESET}" print_rule "-" 64 print_menu_meta_line "$(trf label_service)" "$(format_service_state_badge "${service_state}")" print_menu_meta_line "$(trf label_install_dir)" "${INSTALL_DIR}" print_menu_meta_line "$(trf label_config)" "${CONFIG_PATH}" print_menu_meta_line "$(trf current_language)" "$(format_badge "${current_lang_label}" "${COLOR_CYAN}")" print_rule "-" 64 } print_menu_options() { print_menu_section_title "$(trf menu_section_setup)" print_menu_item "1" "$(trf action_download_title)" "confirm" print_menu_item "2" "$(trf action_install_title)" "confirm" print_menu_item "3" "$(trf action_upgrade_title)" "confirm" print_menu_item "4" "$(trf action_self_install_title)" "confirm" print_menu_section_title "$(trf menu_section_runtime)" print_menu_item "5" "$(trf action_show_config_title)" "direct" print_menu_item "6" "$(trf action_init_password_title)" "confirm" print_menu_item "7" "$(trf action_status_title)" "direct" print_menu_item "8" "$(trf action_logs_title)" "direct" print_menu_section_title "$(trf menu_section_tools)" print_menu_item "9" "$(trf action_language_title)" "direct" print_menu_item "10" "$(trf action_help_title)" "direct" print_menu_item "0" "$(trf menu_exit)" print_rule "-" 64 printf '%b%s%b\n' "${COLOR_DIM}" "$(trf menu_hint_direct)" "${COLOR_RESET}" printf '%b%s%b\n' "${COLOR_DIM}" "$(trf menu_hint_confirm)" "${COLOR_RESET}" } run_menu_command() { local rc printf '\n' if EPCTL_LANG="${ACTIVE_LANG}" "${SCRIPT_SOURCE_PATH}" --lang "${ACTIVE_LANG}" "$@"; then success "$(trf command_completed)" else rc="$?" warn "$(trf command_failed_exit "${rc}")" fi pause_for_menu } interactive_download() { local tag confirm_menu_action \ "$(trf action_download_title)" \ "$(trf action_download_desc "${CACHE_ROOT}")" || return 0 tag="$(prompt_input "$(trf input_release_tag)")" || return 0 if [[ -n "${tag}" ]]; then run_menu_command download --tag "${tag}" else run_menu_command download fi } interactive_install() { local tag app_uri listen confirm_menu_action \ "$(trf action_install_title)" \ "$(trf action_install_desc "${INSTALL_DIR}" "${SYSTEM_USER}" "${SYSTEMD_UNIT_PATH}" "${SERVICE_NAME}")" || return 0 tag="$(prompt_input "$(trf input_release_tag)")" || return 0 app_uri="$(prompt_input "$(trf input_app_uri)" "${DEFAULT_APP_URI}")" || return 0 listen="$(prompt_input "$(trf input_http_listen)" "${DEFAULT_LISTEN}")" || return 0 if [[ -n "${tag}" ]]; then run_menu_command install --tag "${tag}" --app-uri "${app_uri}" --listen "${listen}" else run_menu_command install --app-uri "${app_uri}" --listen "${listen}" fi } interactive_upgrade() { local tag confirm_menu_action \ "$(trf action_upgrade_title)" \ "$(trf action_upgrade_desc "${INSTALL_DIR}" "${SERVICE_NAME}")" || return 0 tag="$(prompt_input "$(trf input_upgrade_tag)")" || return 0 if [[ -n "${tag}" ]]; then run_menu_command upgrade --tag "${tag}" --prompt-restart else run_menu_command upgrade --prompt-restart fi } interactive_logs() { local lines print_menu_action_preview \ "$(trf action_logs_title)" \ "$(trf action_logs_desc "${SERVICE_NAME}")" lines="$(prompt_input "$(trf input_log_lines)" "100")" || return 0 run_menu_command logs --lines "${lines}" } interactive_switch_language() { local choice print_menu_action_preview \ "$(trf action_language_title)" \ "$(trf action_language_desc)" printf '\n%b%s%b\n' "${COLOR_BOLD}" "$(trf menu_language_prompt)" "${COLOR_RESET}" choice="$(prompt_input "$(trf menu_language_input)")" || return 0 case "${choice}" in 1|zh|ZH|cn|CN|中文) set_language "zh" success "$(trf language_switched "$(trf language_name_zh)" "$(trf language_name_zh)")" ;; 2|en|EN|english|English|英文|英语) set_language "en" success "$(trf language_switched "$(trf language_name_en)" "$(trf language_name_en)")" ;; *) warn "$(trf invalid_selection "${choice}")" ;; esac pause_for_menu } interactive_menu() { local choice require_linux while true; do print_menu_header print_menu_options printf '%b%s%b' "${COLOR_BOLD}" "$(trf menu_select_action)" "${COLOR_RESET}" read -r choice || return 0 case "${choice}" in 1) interactive_download ;; 2) interactive_install ;; 3) interactive_upgrade ;; 4) if confirm_menu_action \ "$(trf action_self_install_title)" \ "$(trf action_self_install_desc "${EPCTL_NAME}" "${EPCTL_NAME} --lang zh" "${EPCTL_NAME} --lang en")"; then run_menu_command self-install fi ;; 5) print_menu_action_preview \ "$(trf action_show_config_title)" \ "$(trf action_show_config_desc "${CONFIG_PATH}")" run_menu_command show-config ;; 6) if confirm_menu_action \ "$(trf action_init_password_title)" \ "$(trf action_init_password_desc "${CONFIG_PATH}")"; then run_menu_command init-password fi ;; 7) print_menu_action_preview \ "$(trf action_status_title)" \ "$(trf action_status_desc "${SERVICE_NAME}")" run_menu_command status ;; 8) interactive_logs ;; 9) interactive_switch_language ;; 10) print_menu_action_preview \ "$(trf action_help_title)" \ "$(trf action_help_desc)" usage pause_for_menu ;; 0|q|Q|quit|QUIT|exit|EXIT) return 0 ;; *) warn "$(trf invalid_selection "${choice}")" pause_for_menu ;; esac done } main() { local command="${1:-}" shift || true case "${command}" in zh|zh-cn|zh_cn|cn|中文) set_language "zh" if [[ $# -gt 0 ]]; then main "$@" elif is_interactive_terminal; then interactive_menu else usage fi ;; en|en-us|en_us|english) set_language "en" if [[ $# -gt 0 ]]; then main "$@" elif is_interactive_terminal; then interactive_menu else usage fi ;; menu|interactive) interactive_menu ;; download) command_download "$@" ;; install) command_install "$@" ;; upgrade) command_upgrade "$@" ;; self-install) command_self_install "$@" ;; show-config) command_show_config "$@" ;; init-password) command_init_password "$@" ;; status) command_status "$@" ;; logs) command_logs "$@" ;; -h|--help|help) usage ;; "") if is_interactive_terminal; then interactive_menu else usage exit 1 fi ;; *) die "$(trf unknown_command "${command}")" ;; esac } set_language "" parse_global_options "$@" init_colors main "${POSITIONAL_ARGS[@]}"