24d5b7445f
- install.sh: - 滚动更新流程新增「选择 Git 访问方式」步骤(HTTPS 登录 / SSH 访问) - SSH 方式支持指定私钥文件路径或粘贴私钥内容(临时文件用后即删) - 仓库地址输入回填当前 remote(同协议时)作为默认值 - 选环境后立即检测工作区变更,提供暂存/放弃/取消处理 - 更新结束后按 HEAD 是否变化决定自动恢复暂存或提示手动恢复 - scripts/update.sh: - 新增 --ssh-key 参数,SSH 协议下通过 GIT_SSH_COMMAND -i 指定私钥 - 新增 --skip-dirty-check 参数,供 install.sh 预检后透传 - scripts/common.sh: - ctms_ask_line 提示语改输出到 stderr,避免被命令替换吞掉 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
498 lines
17 KiB
Bash
498 lines
17 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
# 公共脚本函数。被 install/update/uninstall/status 入口 source,不直接执行。
|
||
# 这是整个安装体系唯一的共享库:颜色、UI、环境派生、compose 调用、
|
||
# 流程输出、依赖检查、.env 读写、交互菜单全部集中在此,避免重复实现。
|
||
|
||
CTMS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
CTMS_ROOT_DIR="$(cd "$CTMS_SCRIPT_DIR/.." && pwd)"
|
||
CTMS_ENV_FILE="$CTMS_ROOT_DIR/.env"
|
||
CTMS_LOG_DIR="$CTMS_ROOT_DIR/.install-logs"
|
||
|
||
# ── 配色体系 ─────────────────────────────────
|
||
# 优先使用 256 色语义色板;终端仅支持基础 8 色时回退到 ANSI 原色;
|
||
# 无色彩能力(管道/CI)时全部置空。C_* 为兼容旧脚本保留的别名。
|
||
_ctms_color_depth=0
|
||
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
|
||
_ctms_color_depth="$(tput colors 2>/dev/null || printf 0)"
|
||
fi
|
||
|
||
if [[ "$_ctms_color_depth" -ge 256 ]]; then
|
||
C_RESET="$(tput sgr0)"
|
||
C_BOLD="$(tput bold)"
|
||
C_DIM="$(tput dim 2>/dev/null || printf '')"
|
||
# 语义色板(xterm-256 索引)
|
||
CC_PRIMARY="$(tput setaf 39)" # 主色:明亮天蓝(边框/标题)
|
||
CC_ACCENT="$(tput setaf 213)" # 强调:粉紫(次级面板/高亮)
|
||
CC_SUCCESS="$(tput setaf 78)" # 成功:清新绿
|
||
CC_WARN="$(tput setaf 214)" # 警告:琥珀橙
|
||
CC_DANGER="$(tput setaf 203)" # 危险:珊瑚红
|
||
CC_INFO="$(tput setaf 117)" # 信息:淡青
|
||
CC_MUTED="$(tput setaf 245)" # 弱化:中灰
|
||
CC_TEXT="$(tput setaf 252)" # 正文:近白
|
||
elif [[ "$_ctms_color_depth" -ge 8 ]]; then
|
||
C_RESET="$(tput sgr0)"
|
||
C_BOLD="$(tput bold)"
|
||
C_DIM="$(tput dim 2>/dev/null || printf '')"
|
||
CC_PRIMARY="$(tput setaf 4)"
|
||
CC_ACCENT="$(tput setaf 5)"
|
||
CC_SUCCESS="$(tput setaf 2)"
|
||
CC_WARN="$(tput setaf 3)"
|
||
CC_DANGER="$(tput setaf 1)"
|
||
CC_INFO="$(tput setaf 6)"
|
||
CC_MUTED="$(tput setaf 7)"
|
||
CC_TEXT="$(tput setaf 7)"
|
||
else
|
||
C_RESET="" C_BOLD="" C_DIM=""
|
||
CC_PRIMARY="" CC_ACCENT="" CC_SUCCESS="" CC_WARN="" CC_DANGER="" CC_INFO="" CC_MUTED="" CC_TEXT=""
|
||
fi
|
||
|
||
# C_* 兼容别名(旧脚本/面板仍在引用)
|
||
C_BLUE="$CC_PRIMARY"
|
||
C_MAGENTA="$CC_ACCENT"
|
||
C_CYAN="$CC_INFO"
|
||
C_GREEN="$CC_SUCCESS"
|
||
C_YELLOW="$CC_WARN"
|
||
C_RED="$CC_DANGER"
|
||
C_WHITE="$CC_TEXT"
|
||
|
||
# ── 图标体系 ─────────────────────────────────
|
||
# 统一的状态/装饰图标,集中定义便于风格一致。
|
||
ICON_OK="✔"
|
||
ICON_FAIL="✖"
|
||
ICON_WARN="⚠"
|
||
ICON_INFO="•"
|
||
ICON_STEP="▸"
|
||
ICON_PROMPT="❯"
|
||
ICON_BULLET="◆"
|
||
ICON_BULLET_OFF="◇"
|
||
ICON_RUNNING="●"
|
||
ICON_STOPPED="○"
|
||
ICON_ARROW="→"
|
||
|
||
# ── 基础输出 ─────────────────────────────────
|
||
ctms_fail() {
|
||
printf '\n %s%s 出错%s %s%s%s\n' "${CC_DANGER}${C_BOLD}" "$ICON_FAIL" "${C_RESET}" "${CC_DANGER}" "$*" "${C_RESET}" >&2
|
||
exit 1
|
||
}
|
||
|
||
ctms_info() {
|
||
printf ' %s%s%s %s\n' "${CC_MUTED}" "$ICON_INFO" "${C_RESET}" "$*"
|
||
}
|
||
|
||
ctms_ok() {
|
||
printf ' %s%s%s %s\n' "${CC_SUCCESS}${C_BOLD}" "$ICON_OK" "${C_RESET}" "$*"
|
||
}
|
||
|
||
ctms_warn() {
|
||
printf ' %s%s%s %s\n' "${CC_WARN}${C_BOLD}" "$ICON_WARN" "${C_RESET}" "$*" >&2
|
||
}
|
||
|
||
# 带步骤计数的分节标题
|
||
ctms_step() {
|
||
_CTMS_STEP_IDX=$(( ${_CTMS_STEP_IDX:-0} + 1 ))
|
||
printf '\n %s%s%s %s步骤 %s%s %s%s%s\n' \
|
||
"${CC_PRIMARY}${C_BOLD}" "$ICON_STEP" "${C_RESET}" \
|
||
"${CC_MUTED}" "$_CTMS_STEP_IDX" "${C_RESET}" \
|
||
"${C_BOLD}" "$*" "${C_RESET}"
|
||
}
|
||
|
||
# 回显并执行命令
|
||
ctms_run() {
|
||
printf ' %s$%s %s%s%s\n' "${CC_MUTED}" "${C_RESET}" "${CC_TEXT}" "$*" "${C_RESET}"
|
||
"$@"
|
||
}
|
||
|
||
# ── box UI ───────────────────────────────────
|
||
ctms_repeat_char() {
|
||
local char="$1"
|
||
local count="$2"
|
||
local out=""
|
||
|
||
while [[ "$count" -gt 0 ]]; do
|
||
out="${out}${char}"
|
||
count=$(( count - 1 ))
|
||
done
|
||
printf '%s' "$out"
|
||
}
|
||
|
||
# 计算字符串的终端显示宽度(列数)。先剥离 ANSI 转义。
|
||
# 优先用 perl 精确判断:东亚宽字符/emoji 计 2 列,其余(含 ◆✔▸ 等几何符号)计 1 列。
|
||
# 无 perl 时回退到字节估算(中文准确,几何符号会高估 1 列)。
|
||
if command -v perl >/dev/null 2>&1; then _CTMS_HAS_PERL=1; else _CTMS_HAS_PERL=0; fi
|
||
|
||
ctms_text_width() {
|
||
local text="$1"
|
||
if [[ "$_CTMS_HAS_PERL" -eq 1 ]]; then
|
||
printf '%s' "$text" | perl -CSD -e '
|
||
local $/; my $s = <STDIN>;
|
||
$s =~ s/\e\[[0-9;?]*[ -\/]*[@-~]//g; # CSI 序列(颜色等)
|
||
$s =~ s/\e[()][0-9A-Za-z]//g; # 字符集切换,如 sgr0 的 \e(B
|
||
$s =~ s/\e[]PX^_].*?(\e\\|\a)//g; # OSC/DCS 等带终止符的序列
|
||
my $w = 0;
|
||
for my $c (split //, $s) {
|
||
my $o = ord $c;
|
||
next if $o == 10 || $o == 13 || $o == 27;
|
||
if (($o>=0x1100&&$o<=0x115F)||($o>=0x2E80&&$o<=0x303E)||($o>=0x3041&&$o<=0x33FF)||
|
||
($o>=0x3400&&$o<=0x4DBF)||($o>=0x4E00&&$o<=0x9FFF)||($o>=0xA000&&$o<=0xA4CF)||
|
||
($o>=0xAC00&&$o<=0xD7A3)||($o>=0xF900&&$o<=0xFAFF)||($o>=0xFE30&&$o<=0xFE4F)||
|
||
($o>=0xFF00&&$o<=0xFF60)||($o>=0xFFE0&&$o<=0xFFE6)||($o>=0x1F300&&$o<=0x1FAFF)||
|
||
($o>=0x20000&&$o<=0x3FFFD)) { $w += 2 } else { $w += 1 }
|
||
}
|
||
print $w;
|
||
'
|
||
return
|
||
fi
|
||
|
||
local esc chars bytes extra
|
||
esc="$(printf '\033')"
|
||
text="$(printf '%s' "$text" | sed -e "s/${esc}\\[[0-9;?]*[A-Za-z]//g" -e "s/${esc}[()][0-9A-Za-z]//g")"
|
||
chars="$(printf '%s' "$text" | wc -m | tr -d ' ')"
|
||
bytes="$(printf '%s' "$text" | LC_ALL=C wc -c | tr -d ' ')"
|
||
extra=$(( (bytes - chars) / 2 ))
|
||
printf '%s' "$(( chars + extra ))"
|
||
}
|
||
|
||
# 按终端显示宽度右填充字符串到指定列数(中文按 2 列计),用于对齐含中文的标签列。
|
||
ctms_pad_right() {
|
||
local text="$1" target="$2"
|
||
local w pad
|
||
w="$(ctms_text_width "$text")"
|
||
pad=$(( target - w ))
|
||
[[ "$pad" -lt 0 ]] && pad=0
|
||
printf '%s%s' "$text" "$(ctms_repeat_char ' ' "$pad")"
|
||
}
|
||
|
||
ctms_box_top() {
|
||
local color="$1"
|
||
local title="$2"
|
||
local width="$3"
|
||
local label="─ ${title} "
|
||
local label_width fill_width
|
||
|
||
label_width="$(ctms_text_width "$label")"
|
||
fill_width=$(( width - 2 - label_width ))
|
||
[[ "$fill_width" -lt 0 ]] && fill_width=0
|
||
printf '%s╭%s%s╮%s\n' "$color" "$label" "$(ctms_repeat_char '─' "$fill_width")" "$C_RESET"
|
||
}
|
||
|
||
ctms_box_bottom() {
|
||
local color="$1"
|
||
local width="$2"
|
||
printf '%s╰%s╯%s\n' "$color" "$(ctms_repeat_char '─' "$(( width - 2 ))")" "$C_RESET"
|
||
}
|
||
|
||
ctms_box_line() {
|
||
local color="$1"
|
||
local width="$2"
|
||
local plain="$3"
|
||
local styled="${4:-$3}"
|
||
local max_content_width=$(( width - 4 ))
|
||
local content=" ${plain}"
|
||
local content_width fill_width
|
||
local chunk chunk_width
|
||
local prefix=" "
|
||
|
||
content_width="$(ctms_text_width "$content")"
|
||
if [[ "$content_width" -le "$(( width - 2 ))" ]]; then
|
||
fill_width=$(( width - 2 - content_width ))
|
||
[[ "$fill_width" -lt 0 ]] && fill_width=0
|
||
printf '%s│%s %b%s%s│%s\n' "$color" "$C_RESET" "$styled" "$(ctms_repeat_char ' ' "$fill_width")" "$color" "$C_RESET"
|
||
return 0
|
||
fi
|
||
|
||
while [[ -n "$plain" ]]; do
|
||
chunk="${plain:0:$max_content_width}"
|
||
while [[ -n "$chunk" && "$(ctms_text_width " ${chunk}")" -gt "$(( width - 2 ))" ]]; do
|
||
chunk="${chunk%?}"
|
||
done
|
||
[[ -n "$chunk" ]] || chunk="${plain:0:1}"
|
||
|
||
chunk_width="$(ctms_text_width "${prefix}${chunk}")"
|
||
fill_width=$(( width - 2 - chunk_width ))
|
||
[[ "$fill_width" -lt 0 ]] && fill_width=0
|
||
printf '%s│%s%s%s%s%s│%s\n' "$color" "$C_RESET" "$prefix" "$chunk" "$(ctms_repeat_char ' ' "$fill_width")" "$color" "$C_RESET"
|
||
plain="${plain:${#chunk}}"
|
||
prefix=" "
|
||
done
|
||
}
|
||
|
||
# ── 环境校验与派生 ───────────────────────────
|
||
ctms_require_env() {
|
||
case "${1:-}" in
|
||
dev|main|release) ;;
|
||
*) ctms_fail "未知环境 \"${1:-}\",有效值为 dev、main、release" ;;
|
||
esac
|
||
}
|
||
|
||
ctms_project_name() {
|
||
ctms_require_env "$1"
|
||
case "$1" in
|
||
dev) printf 'ctms_dev' ;;
|
||
main) printf 'ctms_main' ;;
|
||
release) printf 'ctms_release' ;;
|
||
esac
|
||
}
|
||
|
||
ctms_compose_file() {
|
||
ctms_require_env "$1"
|
||
if [[ "$1" == "dev" ]]; then
|
||
printf '%s/docker-compose.dev.yaml' "$CTMS_ROOT_DIR"
|
||
else
|
||
printf '%s/docker-compose.yaml' "$CTMS_ROOT_DIR"
|
||
fi
|
||
}
|
||
|
||
# 运行模式:dev → development,其余 → production
|
||
ctms_app_env() {
|
||
ctms_require_env "$1"
|
||
[[ "$1" == "dev" ]] && printf 'development' || printf 'production'
|
||
}
|
||
|
||
# 登录 RSA 密钥 ID
|
||
ctms_key_id() {
|
||
ctms_require_env "$1"
|
||
local today
|
||
today="$(date +%Y%m%d)"
|
||
case "$1" in
|
||
dev) printf 'default' ;;
|
||
main) printf 'main-%s' "$today" ;;
|
||
release) printf 'release-%s' "$today" ;;
|
||
esac
|
||
}
|
||
|
||
# 默认健康检查地址;release 无默认值(必须显式提供 HTTPS)
|
||
ctms_default_base_url() {
|
||
ctms_require_env "$1"
|
||
case "$1" in
|
||
dev|main) printf 'http://127.0.0.1:8888' ;;
|
||
release) printf '' ;;
|
||
esac
|
||
}
|
||
|
||
# 统一的 compose 调用:始终显式 -f <file> -p <project>,确保环境隔离
|
||
# 用法: ctms_compose <env> <compose 参数...>
|
||
ctms_compose() {
|
||
local env="$1"; shift
|
||
docker compose -f "$(ctms_compose_file "$env")" -p "$(ctms_project_name "$env")" "$@"
|
||
}
|
||
|
||
# ── 依赖检查 ─────────────────────────────────
|
||
ctms_require_command() {
|
||
local cmd="$1"
|
||
local hint="$2"
|
||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||
ctms_warn "缺少依赖: ${C_BOLD}${cmd}${C_RESET}"
|
||
ctms_warn "安装方式: $hint"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
ctms_require_docker_compose() {
|
||
local missing=0
|
||
ctms_require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1
|
||
[[ "$missing" -eq 0 ]] || ctms_fail "系统依赖不完整,请按上方提示安装后重试"
|
||
|
||
if ! docker compose version >/dev/null 2>&1; then
|
||
ctms_fail "未检测到 Docker Compose v2,请确认 docker compose version 可正常执行"
|
||
fi
|
||
}
|
||
|
||
# 安装/更新所需依赖。openssl、curl 仅在非 dev 环境要求(dev 用默认密钥、不做 RSA 生成)。
|
||
ctms_require_dependencies() {
|
||
local env="$1"
|
||
ctms_require_env "$env"
|
||
local missing=0
|
||
|
||
ctms_require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1
|
||
ctms_require_command curl "Linux: sudo apt-get install -y curl | macOS: brew install curl" || missing=1
|
||
# openssl 仅非 dev 需要:dev 用 dev-secret 与空 RSA key,不生成密钥
|
||
if [[ "$env" != "dev" ]]; then
|
||
ctms_require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1
|
||
fi
|
||
[[ "$missing" -eq 0 ]] || ctms_fail "系统依赖不完整,请按上方提示安装后重试"
|
||
|
||
if ! docker compose version >/dev/null 2>&1; then
|
||
ctms_fail "未检测到 Docker Compose v2,请确认 docker compose version 可正常执行"
|
||
fi
|
||
}
|
||
|
||
# ── 长命令执行(折叠输出 + spinner) ──────────
|
||
# 用法: ctms_run_quiet <提示语> -- <命令...>
|
||
# VERBOSE=1 时展开实时输出;否则折叠为 spinner,失败时回放完整日志。
|
||
ctms_run_quiet() {
|
||
local label="$1"; shift
|
||
[[ "${1:-}" == "--" ]] && shift
|
||
|
||
if [[ "${VERBOSE:-0}" -eq 1 ]]; then
|
||
printf ' %s$%s %s%s%s\n' "${CC_MUTED}" "${C_RESET}" "${CC_TEXT}" "$*" "${C_RESET}"
|
||
"$@"
|
||
return $?
|
||
fi
|
||
|
||
mkdir -p "$CTMS_LOG_DIR"
|
||
local log_file
|
||
log_file="$(mktemp "$CTMS_LOG_DIR/cmd_XXXXXXXX")"
|
||
local start_ts elapsed exit_code
|
||
start_ts="$(date +%s)"
|
||
|
||
set +e
|
||
if [[ -t 1 ]]; then
|
||
"$@" >"$log_file" 2>&1 &
|
||
local cmd_pid=$!
|
||
_ctms_spinner "$cmd_pid" "$label" "$start_ts" &
|
||
local spin_pid=$!
|
||
wait "$cmd_pid"
|
||
exit_code=$?
|
||
kill "$spin_pid" 2>/dev/null || true
|
||
wait "$spin_pid" 2>/dev/null || true
|
||
printf '\r\033[K'
|
||
else
|
||
printf ' %s%s%s %s …\n' "${CC_MUTED}" "$ICON_INFO" "${C_RESET}" "$label"
|
||
"$@" >"$log_file" 2>&1
|
||
exit_code=$?
|
||
fi
|
||
set -e
|
||
|
||
elapsed=$(( $(date +%s) - start_ts ))
|
||
if [[ "$exit_code" -eq 0 ]]; then
|
||
printf ' %s%s%s %s %s(%ss)%s\n' \
|
||
"${CC_SUCCESS}${C_BOLD}" "$ICON_OK" "${C_RESET}" "$label" "${CC_MUTED}" "$elapsed" "${C_RESET}"
|
||
return 0
|
||
fi
|
||
|
||
printf '\n %s%s%s %s%s 失败(耗时 %ss),完整输出:%s\n\n' \
|
||
"${CC_DANGER}${C_BOLD}" "$ICON_FAIL" "${C_RESET}" "${CC_DANGER}" "$label" "$elapsed" "${C_RESET}" >&2
|
||
sed 's/^/ /' "$log_file" >&2
|
||
printf '\n' >&2
|
||
ctms_fail "命令执行失败,详细日志保留于:${log_file#"$CTMS_ROOT_DIR"/}(追加 --verbose 可看实时输出)"
|
||
}
|
||
|
||
_ctms_spinner() {
|
||
local cmd_pid="$1" label="$2" start_ts="$3"
|
||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||
local i=0
|
||
while kill -0 "$cmd_pid" 2>/dev/null; do
|
||
local elapsed=$(( $(date +%s) - start_ts ))
|
||
printf '\r\033[K %s%s%s %s %s(%ss)%s' \
|
||
"${CC_INFO}${C_BOLD}" "${frames[i]}" "${C_RESET}" \
|
||
"$label" "${CC_MUTED}" "$elapsed" "${C_RESET}"
|
||
i=$(( (i + 1) % ${#frames[@]} ))
|
||
sleep 0.1
|
||
done
|
||
}
|
||
|
||
# ── .env 读写 ─────────────────────────────────
|
||
ctms_read_env_value() {
|
||
local key="$1"
|
||
[[ -f "$CTMS_ENV_FILE" ]] || return 0
|
||
awk -v key="$key" '
|
||
BEGIN { prefix = key "=" }
|
||
index($0, prefix) == 1 {
|
||
value = substr($0, length(prefix) + 1)
|
||
if (value ~ /^".*"$/ || value ~ /^\047.*\047$/) {
|
||
value = substr(value, 2, length(value) - 2)
|
||
}
|
||
print value
|
||
exit
|
||
}
|
||
' "$CTMS_ENV_FILE"
|
||
}
|
||
|
||
ctms_upsert_env_value() {
|
||
local key="$1" value="$2"
|
||
local temp_file
|
||
temp_file="$(mktemp "$CTMS_ROOT_DIR/.env.tmp.XXXXXX")"
|
||
if [[ -f "$CTMS_ENV_FILE" && -n "$(ctms_read_env_value "$key" || true)" ]]; then
|
||
awk -v key="$key" -v value="$value" '
|
||
BEGIN { prefix = key "=" }
|
||
index($0, prefix) == 1 { print key "=" value; next }
|
||
{ print }
|
||
' "$CTMS_ENV_FILE" > "$temp_file"
|
||
else
|
||
[[ -f "$CTMS_ENV_FILE" ]] && cat "$CTMS_ENV_FILE" > "$temp_file"
|
||
printf '%s=%s\n' "$key" "$value" >> "$temp_file"
|
||
fi
|
||
mv "$temp_file" "$CTMS_ENV_FILE"
|
||
}
|
||
|
||
ctms_write_env_file() {
|
||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
||
local jwt_secret="$4" rsa_private_key="$5"
|
||
local allow_insecure_dev_login="false"
|
||
[[ "$runtime_env" == "development" ]] && allow_insecure_dev_login="true"
|
||
local temp_file
|
||
temp_file="$(mktemp "$CTMS_ROOT_DIR/.env.tmp.XXXXXX")"
|
||
|
||
{
|
||
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
|
||
printf 'ENV=%s\n' "$runtime_env"
|
||
printf 'VITE_RUNTIME_ENV=%s\n' "$runtime_env"
|
||
printf 'VITE_ALLOW_INSECURE_DEV_LOGIN=%s\n' "$allow_insecure_dev_login"
|
||
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
|
||
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
|
||
printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key"
|
||
} >> "$temp_file"
|
||
|
||
mv "$temp_file" "$CTMS_ENV_FILE"
|
||
}
|
||
|
||
# ── 危险操作确认 ─────────────────────────────
|
||
ctms_confirm_danger() {
|
||
local operation="$1"
|
||
local scope="$2"
|
||
local risk="$3"
|
||
local answer=""
|
||
local width=72
|
||
|
||
printf '\n'
|
||
ctms_box_top "${CC_DANGER}${C_BOLD}" "$ICON_WARN 危险操作确认" "$width"
|
||
ctms_box_line "${CC_DANGER}${C_BOLD}" "$width" "操作类型 $operation" "${CC_MUTED}操作类型${C_RESET} $operation"
|
||
ctms_box_line "${CC_DANGER}${C_BOLD}" "$width" "影响范围 $scope" "${CC_MUTED}影响范围${C_RESET} $scope"
|
||
ctms_box_line "${CC_DANGER}${C_BOLD}" "$width" "风险评估 $risk" "${CC_MUTED}风险评估${C_RESET} $risk"
|
||
ctms_box_bottom "${CC_DANGER}${C_BOLD}" "$width"
|
||
printf '\n %s%s%s 输入 %s是%s / %s确认%s / %s继续%s 以执行,其它任意键取消: ' \
|
||
"${CC_WARN}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" \
|
||
"${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||
|
||
IFS= read -r answer || answer=""
|
||
case "$answer" in
|
||
是|确认|继续) ;;
|
||
*) ctms_fail "用户取消操作" ;;
|
||
esac
|
||
}
|
||
|
||
# ── 交互输入 ─────────────────────────────────
|
||
ctms_restore_cursor() {
|
||
printf '\033[?25h'
|
||
}
|
||
|
||
ctms_read_key() {
|
||
local key=""
|
||
IFS= read -rsn1 key || true
|
||
if [[ "$key" == $'\x1b' ]]; then
|
||
key=""
|
||
IFS= read -rsn2 -t 1 key || true
|
||
printf '%s' "$key"
|
||
return
|
||
fi
|
||
printf '%s' "$key"
|
||
}
|
||
|
||
# 单行文本输入,带可选默认值
|
||
ctms_ask_line() {
|
||
local prompt="$1"
|
||
local default_value="${2:-}"
|
||
local value=""
|
||
|
||
# 提示语输出到 stderr:调用方多用 $(ctms_ask_line ...) 捕获返回值,
|
||
# 若提示语走 stdout 会被一并吞掉,导致终端只剩光标、提示语还混进返回值。
|
||
if [[ -n "$default_value" ]]; then
|
||
printf '\n%s%s%s %s %s[%s]%s: ' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "$prompt" "${CC_MUTED}" "$default_value" "${C_RESET}" >&2
|
||
else
|
||
printf '\n%s%s%s %s: ' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "$prompt" >&2
|
||
fi
|
||
IFS= read -r value || value=""
|
||
printf '%s' "${value:-$default_value}"
|
||
}
|