美化终端脚本 UI 并修复对齐与闪屏
- 配色体系:256 色语义色板(primary/accent/success/warn/danger)与统一图标常量 - 修复右边框对齐:ctms_text_width 用 perl 精确计算显示宽度,剥离 sgr0 的 \e(B 序列 - 修复菜单闪屏:改用光标归位原地覆盖重绘,去掉每帧清屏 - 美化各面板:install.sh banner/确认/成功框、根菜单、status.sh 资源表格,标签列对齐 - 新增 ctms_pad_right 按显示宽度填充含中文标签 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# CTMS 项目快速上手
|
# CTMS 项目快速上手
|
||||||
|
|
||||||
|
## 交互安装入口
|
||||||
|
- 推荐执行 `./install.sh`,通过键盘 `↑/↓` 选择安装、更新、卸载、资源状态等操作;该入口不接受命令行参数。
|
||||||
|
- 菜单顶部会常驻显示当前 CTMS 容器部署状态,包括已检测到的环境、Compose 项目和容器运行数量。
|
||||||
|
- 底层脚本位于 `scripts/`:`install.sh`、`update.sh`、`uninstall.sh`、`status.sh`;状态脚本使用 `docker compose stats --no-stream` 采集资源快照并美化展示。
|
||||||
|
|
||||||
## 生产部署
|
## 生产部署
|
||||||
- 生产入口:`docker-compose.yaml`
|
- 生产入口:`docker-compose.yaml`
|
||||||
- 初始化方式:`docker compose run --rm backend-init`
|
- 初始化方式:`docker compose run --rm backend-init`
|
||||||
|
|||||||
+11
-2
@@ -32,8 +32,15 @@ services:
|
|||||||
fi
|
fi
|
||||||
npm run dev -- --host 0.0.0.0
|
npm run dev -- --host 0.0.0.0
|
||||||
environment:
|
environment:
|
||||||
|
NPM_CONFIG_REGISTRY: ${NPM_CONFIG_REGISTRY:-https://registry.npmmirror.com}
|
||||||
VITE_RUNTIME_ENV: ${ENV:-development}
|
VITE_RUNTIME_ENV: ${ENV:-development}
|
||||||
VITE_ALLOW_INSECURE_DEV_LOGIN: ${VITE_ALLOW_INSECURE_DEV_LOGIN:-true}
|
VITE_ALLOW_INSECURE_DEV_LOGIN: ${VITE_ALLOW_INSECURE_DEV_LOGIN:-true}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:5173/"]
|
||||||
|
interval: 2s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 90
|
||||||
|
start_period: 5s
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- frontend_node_modules:/app/node_modules
|
- frontend_node_modules:/app/node_modules
|
||||||
@@ -53,8 +60,10 @@ services:
|
|||||||
- ./nginx/certs:/etc/nginx/certs:ro
|
- ./nginx/certs:/etc/nginx/certs:ro
|
||||||
- ./frontend/public/favicon.ico:/usr/share/nginx/html/favicon.ico:ro
|
- ./frontend/public/favicon.ico:/usr/share/nginx/html/favicon.ico:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
- frontend-dev
|
condition: service_started
|
||||||
|
frontend-dev:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- ctms_net
|
- ctms_net
|
||||||
|
|
||||||
|
|||||||
@@ -94,18 +94,44 @@ grep -n "^\\.env\\.\\*$" .gitignore
|
|||||||
|
|
||||||
## 推荐脚本安装
|
## 推荐脚本安装
|
||||||
|
|
||||||
安装脚本入口:
|
推荐使用根目录交互入口:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/install-ctms.sh dev|main|release [选项]
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
执行后通过键盘 `↑/↓` 选择安装、更新、卸载、资源状态等操作;`install.sh` 不接受命令行参数。菜单顶部会常驻显示当前 CTMS 容器部署状态,包括已检测到的环境、Compose 项目和容器运行数量。
|
||||||
|
|
||||||
|
底层脚本入口(适合自动化或排障时直接调用):
|
||||||
|
|
||||||
|
```text
|
||||||
|
scripts/install.sh 安装:依赖检查、配置准备、构建启动、迁移、健康检查
|
||||||
|
scripts/update.sh 更新:复用安装流程,不执行 git pull
|
||||||
|
scripts/uninstall.sh 卸载:默认仅停止并移除容器,不删除数据
|
||||||
|
scripts/status.sh 状态:使用 docker compose stats --no-stream 采集资源快照并美化展示
|
||||||
|
scripts/common.sh 公共函数:环境映射、输出样式、危险操作确认
|
||||||
|
```
|
||||||
|
|
||||||
|
安装脚本直接调用格式:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/install.sh dev|main|release [选项]
|
||||||
|
```
|
||||||
|
|
||||||
|
更新、卸载、状态脚本直接调用格式:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/update.sh dev|main|release [安装脚本选项]
|
||||||
|
bash scripts/uninstall.sh dev|main|release
|
||||||
|
bash scripts/status.sh dev|main|release
|
||||||
```
|
```
|
||||||
|
|
||||||
常用示例:
|
常用示例:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/install-ctms.sh dev
|
bash scripts/install.sh dev
|
||||||
bash scripts/install-ctms.sh main --base-url http://127.0.0.1:8888
|
bash scripts/install.sh main --base-url http://127.0.0.1:8888
|
||||||
bash scripts/install-ctms.sh release --base-url https://ctms.example.com
|
bash scripts/install.sh release --base-url https://ctms.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
可选参数:
|
可选参数:
|
||||||
@@ -143,7 +169,7 @@ docker compose down -v
|
|||||||
如果只想查看参数说明:
|
如果只想查看参数说明:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/install-ctms.sh --help
|
bash scripts/install.sh --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## 手工安装/排障:dev 分支
|
## 手工安装/排障:dev 分支
|
||||||
|
|||||||
Executable
+252
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/common.sh
|
||||||
|
source "$ROOT_DIR/scripts/common.sh"
|
||||||
|
|
||||||
|
SELECTED_INDEX=0
|
||||||
|
SELECTED_ENV=""
|
||||||
|
SELECTED_BASE_URL=""
|
||||||
|
ENV_STATUS_LINES=""
|
||||||
|
UI_BOX_WIDTH=76
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${C_BOLD}用法:${C_RESET}
|
||||||
|
./install.sh
|
||||||
|
|
||||||
|
${C_BOLD}说明:${C_RESET}
|
||||||
|
install.sh 不接受命令行参数。
|
||||||
|
执行后通过键盘 ↑/↓ 选择安装、更新、卸载、状态等操作,按 Q 退出。
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_interactive_terminal() {
|
||||||
|
[[ -t 0 && -t 1 ]] || ctms_fail "install.sh 需要在交互式终端中运行"
|
||||||
|
}
|
||||||
|
|
||||||
|
banner() {
|
||||||
|
printf '\n'
|
||||||
|
ctms_box_top "${C_BLUE}${C_BOLD}" "CTMS DEPLOYMENT CONSOLE" "$UI_BOX_WIDTH"
|
||||||
|
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} 安装 · 更新 · 卸载 · 资源状态" "${CC_INFO}${C_BOLD}${ICON_BULLET}${C_RESET} 安装 · 更新 · 卸载 · 资源状态"
|
||||||
|
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "↑/↓ 选择 Enter 确认 Q 退出" "${CC_MUTED}↑/↓ 选择 Enter 确认 Q 退出${C_RESET}"
|
||||||
|
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
|
||||||
|
printf '\n'
|
||||||
|
render_status_panel
|
||||||
|
}
|
||||||
|
|
||||||
|
render_status_panel() {
|
||||||
|
ctms_box_top "${C_MAGENTA}${C_BOLD}" "环境状态" "$UI_BOX_WIDTH"
|
||||||
|
printf '%b\n' "$ENV_STATUS_LINES" | while IFS= read -r line; do
|
||||||
|
ctms_box_line "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH" "$line" "$line"
|
||||||
|
done
|
||||||
|
ctms_box_bottom "${C_MAGENTA}${C_BOLD}" "$UI_BOX_WIDTH"
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
build_env_status() {
|
||||||
|
local output=""
|
||||||
|
local detected=0
|
||||||
|
local docker_error=""
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未安装或 docker 命令不可用${C_RESET}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker ps >/dev/null 2>&1; then
|
||||||
|
docker_error="$(docker ps 2>&1 >/dev/null || true)"
|
||||||
|
ENV_STATUS_LINES=" ${C_YELLOW}Docker 未运行或当前用户无权限访问 Docker${C_RESET}"
|
||||||
|
[[ -n "$docker_error" ]] && ENV_STATUS_LINES="${ENV_STATUS_LINES}\n ${C_DIM}${docker_error}${C_RESET}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local env project rows total running stopped status_label
|
||||||
|
for env in dev main release; do
|
||||||
|
project="$(ctms_project_name "$env")"
|
||||||
|
rows="$(docker ps -a \
|
||||||
|
--filter "label=com.docker.compose.project=$project" \
|
||||||
|
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
|
||||||
|
[[ -n "$rows" ]] || continue
|
||||||
|
|
||||||
|
detected=1
|
||||||
|
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
|
||||||
|
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
|
||||||
|
stopped=$(( total - running ))
|
||||||
|
|
||||||
|
if [[ "$running" -eq "$total" && "$total" -gt 0 ]]; then
|
||||||
|
status_label="${CC_SUCCESS}${ICON_RUNNING} 运行中${C_RESET}"
|
||||||
|
elif [[ "$running" -gt 0 ]]; then
|
||||||
|
status_label="${CC_WARN}${ICON_RUNNING} 部分运行${C_RESET}"
|
||||||
|
else
|
||||||
|
status_label="${CC_DANGER}${ICON_STOPPED} 已停止${C_RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
output="${output} ${CC_INFO}${C_BOLD}${env}${C_RESET} ${CC_MUTED}(${project})${C_RESET} ${status_label} ${CC_MUTED}${running}/${total} 运行"
|
||||||
|
[[ "$stopped" -gt 0 ]] && output="${output},${stopped} 已停止"
|
||||||
|
output="${output}${C_RESET}\n"
|
||||||
|
output="${output}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " %-18s %-8s %s\n", $1, $2, $3 }')\n"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$detected" -eq 0 ]]; then
|
||||||
|
rows="$(docker ps -a \
|
||||||
|
--filter "name=ctms_" \
|
||||||
|
--format '{{.Names}}|{{.State}}|{{.Status}}' 2>/dev/null || true)"
|
||||||
|
if [[ -n "$rows" ]]; then
|
||||||
|
total="$(printf '%s\n' "$rows" | awk 'NF { count++ } END { print count + 0 }')"
|
||||||
|
running="$(printf '%s\n' "$rows" | awk -F'|' '$2 == "running" { count++ } END { print count + 0 }')"
|
||||||
|
ENV_STATUS_LINES=" ${C_YELLOW}检测到 CTMS 命名容器,但无法从 Compose 标签确认环境${C_RESET}\n"
|
||||||
|
ENV_STATUS_LINES="${ENV_STATUS_LINES} ${running}/${total} 运行\n"
|
||||||
|
ENV_STATUS_LINES="${ENV_STATUS_LINES}$(printf '%s\n' "$rows" | awk -F'|' '{ printf " - %-18s %-8s %s\n", $1, $2, $3 }')"
|
||||||
|
else
|
||||||
|
ENV_STATUS_LINES=" ${C_DIM}未检测到 CTMS Compose 容器(ctms_dev / ctms_main / ctms_release)${C_RESET}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ENV_STATUS_LINES="${output%\\n}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
select_menu() {
|
||||||
|
local title="$1"
|
||||||
|
shift
|
||||||
|
local options=("$@")
|
||||||
|
local selected=0
|
||||||
|
local key=""
|
||||||
|
|
||||||
|
[[ "${#options[@]}" -gt 0 ]] || ctms_fail "菜单没有可选项"
|
||||||
|
|
||||||
|
trap ctms_restore_cursor EXIT INT TERM
|
||||||
|
printf '\033[?25l\033[2J'
|
||||||
|
while true; do
|
||||||
|
printf '\033[H'
|
||||||
|
banner
|
||||||
|
ctms_box_top "${C_BLUE}${C_BOLD}" "$title" "$UI_BOX_WIDTH"
|
||||||
|
|
||||||
|
local i
|
||||||
|
for i in "${!options[@]}"; do
|
||||||
|
if [[ "$i" -eq "$selected" ]]; then
|
||||||
|
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET} ${options[$i]}" "${CC_INFO}${C_BOLD}${ICON_BULLET} ${options[$i]}${C_RESET}"
|
||||||
|
else
|
||||||
|
ctms_box_line "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH" "${ICON_BULLET_OFF} ${options[$i]}" "${CC_MUTED}${ICON_BULLET_OFF} ${options[$i]}${C_RESET}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
ctms_box_bottom "${C_BLUE}${C_BOLD}" "$UI_BOX_WIDTH"
|
||||||
|
printf '\033[J'
|
||||||
|
|
||||||
|
key="$(ctms_read_key)"
|
||||||
|
case "$key" in
|
||||||
|
"[A") selected=$(( (selected - 1 + ${#options[@]}) % ${#options[@]} )) ;;
|
||||||
|
"[B") selected=$(( (selected + 1) % ${#options[@]} )) ;;
|
||||||
|
"") ctms_restore_cursor; SELECTED_INDEX="$selected"; return ;;
|
||||||
|
q|Q) ctms_restore_cursor; exit 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_env() {
|
||||||
|
select_menu "选择环境" \
|
||||||
|
"dev 本地开发环境 Vite 热加载 / 默认密钥" \
|
||||||
|
"main 内网测试环境 预发布验证 / 生产模式" \
|
||||||
|
"release 生产环境 HTTPS / RSA 密钥"
|
||||||
|
case "$SELECTED_INDEX" in
|
||||||
|
0) SELECTED_ENV="dev" ;;
|
||||||
|
1) SELECTED_ENV="main" ;;
|
||||||
|
2) SELECTED_ENV="release" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_install_options() {
|
||||||
|
local env="$1"
|
||||||
|
SELECTED_BASE_URL=""
|
||||||
|
|
||||||
|
if [[ "$env" == "release" ]]; then
|
||||||
|
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址,例如 https://ctms.example.com")"
|
||||||
|
[[ -n "$SELECTED_BASE_URL" ]] || ctms_fail "release 环境必须提供健康检查地址"
|
||||||
|
elif [[ "$env" == "main" ]]; then
|
||||||
|
SELECTED_BASE_URL="$(ctms_ask_line "请输入健康检查地址" "http://127.0.0.1:8888")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_script() {
|
||||||
|
local script="$1"
|
||||||
|
shift
|
||||||
|
[[ -x "$ROOT_DIR/scripts/$script" ]] || ctms_fail "脚本不可执行: $ROOT_DIR/scripts/$script"
|
||||||
|
"$ROOT_DIR/scripts/$script" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_install_like_script() {
|
||||||
|
local script="$1"
|
||||||
|
local env="$2"
|
||||||
|
|
||||||
|
if [[ -n "$SELECTED_BASE_URL" ]]; then
|
||||||
|
run_script "$script" "$env" --base-url "$SELECTED_BASE_URL"
|
||||||
|
else
|
||||||
|
run_script "$script" "$env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 操作完成后暂停,等用户确认再返回菜单
|
||||||
|
pause_return() {
|
||||||
|
printf '\n %s%s%s 按 Enter 返回主菜单…' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}"
|
||||||
|
read -r _ || true
|
||||||
|
}
|
||||||
|
|
||||||
|
interactive_main() {
|
||||||
|
select_menu "选择操作" \
|
||||||
|
"安装部署 配置环境、构建容器、迁移数据库、健康检查" \
|
||||||
|
"滚动更新 重建服务、执行迁移、复用安装健康检查" \
|
||||||
|
"安全卸载 停止并移除容器,保留 .env 与数据文件" \
|
||||||
|
"资源状态 查看 CPU、内存、网络与磁盘 I/O 快照" \
|
||||||
|
"使用帮助 查看入口和底层脚本说明" \
|
||||||
|
"退出控制台"
|
||||||
|
|
||||||
|
case "$SELECTED_INDEX" in
|
||||||
|
0)
|
||||||
|
pick_env
|
||||||
|
collect_install_options "$SELECTED_ENV"
|
||||||
|
run_install_like_script "install.sh" "$SELECTED_ENV"
|
||||||
|
pause_return
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
pick_env
|
||||||
|
collect_install_options "$SELECTED_ENV"
|
||||||
|
run_install_like_script "update.sh" "$SELECTED_ENV"
|
||||||
|
pause_return
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
pick_env
|
||||||
|
run_script "uninstall.sh" "$SELECTED_ENV"
|
||||||
|
pause_return
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
pick_env
|
||||||
|
run_script "status.sh" "$SELECTED_ENV"
|
||||||
|
pause_return
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
usage
|
||||||
|
pause_return
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
usage
|
||||||
|
ctms_fail "install.sh 不接受参数,请直接执行 ./install.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_interactive_terminal
|
||||||
|
while true; do
|
||||||
|
build_env_status
|
||||||
|
interactive_main || break
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
#!/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=""
|
||||||
|
|
||||||
|
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}"
|
||||||
|
else
|
||||||
|
printf '\n%s%s%s %s: ' "${CC_INFO}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "$prompt"
|
||||||
|
fi
|
||||||
|
IFS= read -r value || value=""
|
||||||
|
printf '%s' "${value:-$default_value}"
|
||||||
|
}
|
||||||
@@ -3,152 +3,57 @@ set -euo pipefail
|
|||||||
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# CTMS 一键安装脚本
|
# CTMS 一键安装脚本
|
||||||
|
# 复用 scripts/common.sh 的共享库(颜色、UI、compose、依赖、.env)。
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/common.sh
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
TARGET_ENV=""
|
TARGET_ENV=""
|
||||||
BASE_URL=""
|
BASE_URL=""
|
||||||
ASSUME_YES=0
|
ASSUME_YES=0
|
||||||
SKIP_BUILD=0
|
SKIP_BUILD=0
|
||||||
SKIP_MIGRATE=0
|
SKIP_MIGRATE=0
|
||||||
VERBOSE=0
|
VERBOSE=0
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$CTMS_ROOT_DIR"
|
||||||
ENV_FILE="$ROOT_DIR/.env"
|
ENV_FILE="$CTMS_ENV_FILE"
|
||||||
LOG_DIR="$ROOT_DIR/.install-logs"
|
|
||||||
|
|
||||||
# ── 颜色 & 样式 ──────────────────────────────
|
|
||||||
_has_color() { [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null)" -ge 8 ]]; }
|
|
||||||
|
|
||||||
if _has_color; then
|
|
||||||
C_RESET="$(tput sgr0)"
|
|
||||||
C_BOLD="$(tput bold)"
|
|
||||||
C_DIM="$(tput dim 2>/dev/null || printf '')"
|
|
||||||
C_BLUE="$(tput setaf 4)"
|
|
||||||
C_CYAN="$(tput setaf 6)"
|
|
||||||
C_GREEN="$(tput setaf 2)"
|
|
||||||
C_YELLOW="$(tput setaf 3)"
|
|
||||||
C_RED="$(tput setaf 1)"
|
|
||||||
C_WHITE="$(tput setaf 7)"
|
|
||||||
else
|
|
||||||
C_RESET="" C_BOLD="" C_DIM="" C_BLUE="" C_CYAN=""
|
|
||||||
C_GREEN="" C_YELLOW="" C_RED="" C_WHITE=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 输出函数 ─────────────────────────────────
|
|
||||||
|
|
||||||
|
# ── Banner ───────────────────────────────────
|
||||||
_banner() {
|
_banner() {
|
||||||
|
local b="${CC_PRIMARY}${C_BOLD}" # 边框主色
|
||||||
|
local a="${CC_INFO}" # 字形填充色
|
||||||
|
local r="${C_RESET}"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf '%s╔══════════════════════════════════════════════╗%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}"
|
printf '%s╔══════════════════════════════════════════════╗%s\n' "$b" "$r"
|
||||||
printf '%s║%s ██████╗████████╗███╗ ███╗███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s ██████╗████████╗███╗ ███╗███████╗ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s║%s ██╔════╝╚══██╔══╝████╗ ████║██╔════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s ██╔════╝╚══██╔══╝████╗ ████║██╔════╝ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s║%s ██║ ██║ ██╔████╔██║███████╗ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s ██║ ██║ ██╔████╔██║███████╗ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s║%s ██║ ██║ ██║╚██╔╝██║╚════██║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s ██║ ██║ ██║╚██╔╝██║╚════██║ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s║%s ╚██████╗ ██║ ██║ ╚═╝ ██║███████║ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s ╚██████╗ ██║ ██║ ╚═╝ ██║███████║ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s║%s ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_CYAN}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ %s║%s\n' "$b" "$a" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s║%s 临床试验管理系统 安装程序 %s║%s\n' "${C_BLUE}${C_BOLD}" "${C_DIM}${C_WHITE}" "${C_BLUE}" "${C_RESET}"
|
printf '%s║%s 临床试验管理系统 · 安装部署程序 %s║%s\n' "$b" "${CC_MUTED}" "${CC_PRIMARY}" "$r"
|
||||||
printf '%s╚══════════════════════════════════════════════╝%s\n' "${C_BLUE}${C_BOLD}" "${C_RESET}"
|
printf '%s╚══════════════════════════════════════════════╝%s\n' "$b" "$r"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
log() { printf ' %s·%s %s\n' "${C_DIM}" "${C_RESET}" "$*"; }
|
# ── 简短别名(统一走 common 的 ctms_*) ────────
|
||||||
ok() { printf ' %s✔%s %s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$*"; }
|
log() { ctms_info "$@"; }
|
||||||
warn() { printf ' %s⚠%s %s\n' "${C_YELLOW}${C_BOLD}" "${C_RESET}" "$*" >&2; }
|
ok() { ctms_ok "$@"; }
|
||||||
|
warn() { ctms_warn "$@"; }
|
||||||
step() {
|
fail() { ctms_fail "$@"; }
|
||||||
local idx="${_STEP_IDX:-0}"
|
step() { ctms_step "$@"; }
|
||||||
_STEP_IDX=$(( idx + 1 ))
|
|
||||||
printf '\n%s┌─ 步骤 %s · %s%s\n' \
|
|
||||||
"${C_BLUE}${C_BOLD}" "$_STEP_IDX" "$*" "${C_RESET}"
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
printf '\n %s✖ 安装中止%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" >&2
|
|
||||||
printf ' %s%s%s\n\n' "${C_RED}" "$*" "${C_RESET}" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
run() {
|
|
||||||
printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}"
|
|
||||||
"$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# 当前环境的 compose 调用
|
||||||
compose_cmd() {
|
compose_cmd() {
|
||||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
ctms_compose "$TARGET_ENV" "$@"
|
||||||
docker compose -f "$ROOT_DIR/docker-compose.dev.yaml" "$@"
|
|
||||||
else
|
|
||||||
docker compose "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 运行长命令,默认折叠输出。失败或 --verbose 时回放完整日志。
|
|
||||||
# 用法: run_quiet <提示语> -- <命令...>
|
|
||||||
run_quiet() {
|
|
||||||
local label="$1"; shift
|
|
||||||
[[ "${1:-}" == "--" ]] && shift
|
|
||||||
|
|
||||||
if [[ "$VERBOSE" -eq 1 ]]; then
|
|
||||||
printf ' %s$%s %s%s%s\n' "${C_DIM}" "${C_RESET}" "${C_WHITE}" "$*" "${C_RESET}"
|
|
||||||
"$@"
|
|
||||||
return $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
local log_file
|
|
||||||
log_file="$(mktemp "$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=$!
|
|
||||||
_spinner_for "$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 …\n' "$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(%ss)%s\n' \
|
|
||||||
"${C_GREEN}${C_BOLD}" "${C_RESET}" "$label" "${C_DIM}" "$elapsed" "${C_RESET}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '\n %s✖%s %s 失败(耗时 %ss),完整输出:\n\n' \
|
|
||||||
"${C_RED}${C_BOLD}" "${C_RESET}" "$label" "$elapsed" >&2
|
|
||||||
sed 's/^/ /' "$log_file" >&2
|
|
||||||
printf '\n' >&2
|
|
||||||
fail "命令执行失败,详细日志保留于:${log_file#$ROOT_DIR/}(追加 --verbose 可看实时输出)"
|
|
||||||
}
|
|
||||||
|
|
||||||
_spinner_for() {
|
|
||||||
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' \
|
|
||||||
"${C_CYAN}${C_BOLD}" "${frames[i]}" "${C_RESET}" \
|
|
||||||
"$label" "${C_DIM}" "$elapsed" "${C_RESET}"
|
|
||||||
i=$(( (i + 1) % ${#frames[@]} ))
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 帮助 ─────────────────────────────────────
|
# ── 帮助 ─────────────────────────────────────
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
${C_BOLD}用法:${C_RESET}
|
${C_BOLD}用法:${C_RESET}
|
||||||
bash scripts/install-ctms.sh ${C_CYAN}<环境>${C_RESET} [选项]
|
bash scripts/install.sh ${C_CYAN}<环境>${C_RESET} [选项]
|
||||||
|
|
||||||
${C_BOLD}环境:${C_RESET}
|
${C_BOLD}环境:${C_RESET}
|
||||||
${C_CYAN}dev${C_RESET} 本地开发环境(使用默认密钥,不生成 RSA 密钥对)
|
${C_CYAN}dev${C_RESET} 本地开发环境(使用默认密钥,不生成 RSA 密钥对)
|
||||||
@@ -167,14 +72,13 @@ ${C_BOLD}选项:${C_RESET}
|
|||||||
${C_WHITE}-h, --help${C_RESET} 显示此帮助信息
|
${C_WHITE}-h, --help${C_RESET} 显示此帮助信息
|
||||||
|
|
||||||
${C_BOLD}示例:${C_RESET}
|
${C_BOLD}示例:${C_RESET}
|
||||||
bash scripts/install-ctms.sh dev
|
bash scripts/install.sh dev
|
||||||
bash scripts/install-ctms.sh main --base-url http://127.0.0.1:8888
|
bash scripts/install.sh main --base-url http://127.0.0.1:8888
|
||||||
bash scripts/install-ctms.sh release --base-url https://ctms.example.com --yes
|
bash scripts/install.sh release --base-url https://ctms.example.com --yes
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 参数解析 ─────────────────────────────────
|
# ── 参数解析 ─────────────────────────────────
|
||||||
|
|
||||||
parse_args() {
|
parse_args() {
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
usage
|
usage
|
||||||
@@ -200,11 +104,11 @@ parse_args() {
|
|||||||
BASE_URL="$2"
|
BASE_URL="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--yes) ASSUME_YES=1; shift ;;
|
--yes) ASSUME_YES=1; shift ;;
|
||||||
--skip-build) SKIP_BUILD=1; shift ;;
|
--skip-build) SKIP_BUILD=1; shift ;;
|
||||||
--skip-migrate) SKIP_MIGRATE=1; shift ;;
|
--skip-migrate) SKIP_MIGRATE=1; shift ;;
|
||||||
--verbose) VERBOSE=1; shift ;;
|
--verbose) VERBOSE=1; shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*)
|
*)
|
||||||
usage
|
usage
|
||||||
fail "未知参数: $1"
|
fail "未知参数: $1"
|
||||||
@@ -214,79 +118,20 @@ parse_args() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── 依赖检查 ─────────────────────────────────
|
# ── 依赖检查 ─────────────────────────────────
|
||||||
|
|
||||||
_require_command() {
|
|
||||||
local cmd="$1"
|
|
||||||
local hint="$2"
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
warn "缺少依赖: ${C_BOLD}${cmd}${C_RESET}"
|
|
||||||
warn "安装方式: $hint"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dependencies() {
|
check_dependencies() {
|
||||||
step "检查系统依赖"
|
step "检查系统依赖"
|
||||||
local missing=0
|
ctms_require_dependencies "$TARGET_ENV"
|
||||||
_require_command docker "安装 Docker Engine 或 Docker Desktop,并确认 docker 命令可用" || missing=1
|
|
||||||
_require_command openssl "Linux: sudo apt-get install -y openssl | macOS: brew install openssl" || missing=1
|
|
||||||
_require_command curl "Linux: sudo apt-get install -y curl | macOS: brew install curl" || missing=1
|
|
||||||
[[ "$missing" -eq 0 ]] || fail "系统依赖不完整,请按上方提示安装后重试"
|
|
||||||
|
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
|
||||||
fail "未检测到 Docker Compose v2,请确认 docker compose version 可正常执行"
|
|
||||||
fi
|
|
||||||
ok "系统依赖检查通过"
|
ok "系统依赖检查通过"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 环境参数计算 ──────────────────────────────
|
# ── 环境参数计算 ──────────────────────────────
|
||||||
|
compose_project_name() { ctms_project_name "$TARGET_ENV"; }
|
||||||
|
app_env() { ctms_app_env "$TARGET_ENV"; }
|
||||||
|
key_id() { ctms_key_id "$TARGET_ENV"; }
|
||||||
|
default_base_url() { ctms_default_base_url "$TARGET_ENV"; }
|
||||||
|
|
||||||
compose_project_name() {
|
# ── .env 读写(复用 common) ──────────────────
|
||||||
case "$TARGET_ENV" in
|
read_env_value() { ctms_read_env_value "$@"; }
|
||||||
dev) printf 'ctms_dev' ;;
|
|
||||||
main) printf 'ctms_main' ;;
|
|
||||||
release) printf 'ctms_release' ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
app_env() {
|
|
||||||
[[ "$TARGET_ENV" == "dev" ]] && printf 'development' || printf 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
key_id() {
|
|
||||||
local today
|
|
||||||
today="$(date +%Y%m%d)"
|
|
||||||
case "$TARGET_ENV" in
|
|
||||||
dev) printf 'default' ;;
|
|
||||||
main) printf 'main-%s' "$today" ;;
|
|
||||||
release) printf 'release-%s' "$today" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
default_base_url() {
|
|
||||||
case "$TARGET_ENV" in
|
|
||||||
dev|main) printf 'http://127.0.0.1:8888' ;;
|
|
||||||
release) printf '' ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── .env 读写 ─────────────────────────────────
|
|
||||||
|
|
||||||
read_env_value() {
|
|
||||||
local key="$1"
|
|
||||||
[[ -f "$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
|
|
||||||
}
|
|
||||||
' "$ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_effective_config() {
|
resolve_effective_config() {
|
||||||
local desired_project_name="$1"
|
local desired_project_name="$1"
|
||||||
@@ -320,9 +165,10 @@ resolve_effective_config() {
|
|||||||
EFFECTIVE_LOGIN_KEY_ID="$env_login_key_id"
|
EFFECTIVE_LOGIN_KEY_ID="$env_login_key_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 生成单行转义 PEM:行间用 \n 连接,末尾不追加多余 \n
|
||||||
generate_escaped_private_key() {
|
generate_escaped_private_key() {
|
||||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null \
|
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null \
|
||||||
| awk '{printf "%s\\n", $0}'
|
| awk 'NR > 1 { printf "\\n" } { printf "%s", $0 }'
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_jwt_secret() {
|
generate_jwt_secret() {
|
||||||
@@ -330,14 +176,13 @@ generate_jwt_secret() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── 地址解析 ─────────────────────────────────
|
# ── 地址解析 ─────────────────────────────────
|
||||||
|
|
||||||
resolve_base_url() {
|
resolve_base_url() {
|
||||||
[[ -z "$BASE_URL" ]] && BASE_URL="$(default_base_url)"
|
[[ -z "$BASE_URL" ]] && BASE_URL="$(default_base_url)"
|
||||||
|
|
||||||
if [[ "$TARGET_ENV" == "release" && -z "$BASE_URL" && "$ASSUME_YES" -eq 0 ]]; then
|
if [[ "$TARGET_ENV" == "release" && -z "$BASE_URL" && "$ASSUME_YES" -eq 0 ]]; then
|
||||||
printf ' %s▸%s 请输入 release 健康检查域名(例如 https://ctms.example.com): ' \
|
printf ' %s▸%s 请输入 release 健康检查域名(例如 https://ctms.example.com): ' \
|
||||||
"${C_YELLOW}${C_BOLD}" "${C_RESET}"
|
"${C_YELLOW}${C_BOLD}" "${C_RESET}"
|
||||||
read -r BASE_URL
|
read -r BASE_URL || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -n "$BASE_URL" ]] || fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址"
|
[[ -n "$BASE_URL" ]] || fail "release 环境必须通过 --base-url 或交互输入提供 HTTPS 地址"
|
||||||
@@ -348,86 +193,60 @@ resolve_base_url() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── 安装确认 ─────────────────────────────────
|
# ── 安装确认 ─────────────────────────────────
|
||||||
|
|
||||||
confirm_install() {
|
confirm_install() {
|
||||||
local project_name="$1"
|
local project_name="$1"
|
||||||
local runtime_env="$2"
|
local runtime_env="$2"
|
||||||
step "确认安装参数"
|
step "确认安装参数"
|
||||||
|
|
||||||
local env_status build_status migrate_status
|
local env_status build_status migrate_status pg_status
|
||||||
env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在' || printf '新建')"
|
env_status="$([[ -f "$ENV_FILE" ]] && printf '已存在' || printf '新建')"
|
||||||
build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')"
|
build_status="$([[ "$SKIP_BUILD" -eq 1 ]] && printf '跳过' || printf '执行')"
|
||||||
migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')"
|
migrate_status="$([[ "$SKIP_MIGRATE" -eq 1 ]] && printf '跳过' || printf '执行')"
|
||||||
|
pg_status="$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在' || printf '新建')"
|
||||||
|
|
||||||
|
local w=66
|
||||||
|
local row
|
||||||
|
row() {
|
||||||
|
local label="$1" value="$2" vcolor="${3:-$CC_TEXT}"
|
||||||
|
local padded
|
||||||
|
padded="$(ctms_pad_right "$label" 14)"
|
||||||
|
ctms_box_line "${CC_PRIMARY}${C_BOLD}" "$w" \
|
||||||
|
"${padded}${value}" \
|
||||||
|
"${CC_MUTED}${padded}${C_RESET}${vcolor}${value}${C_RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "目标环境" "${C_RESET}" "$TARGET_ENV"
|
ctms_box_top "${CC_PRIMARY}${C_BOLD}" "安装参数预览" "$w"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "运行模式" "${C_RESET}" "$runtime_env"
|
row "目标环境" "$TARGET_ENV" "${CC_INFO}${C_BOLD}"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "Compose 项目" "${C_RESET}" "$project_name"
|
row "运行模式" "$runtime_env"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "健康检查地址" "${C_RESET}" "$BASE_URL"
|
row "Compose 项目" "$project_name"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" ".env 文件" "${C_RESET}" "$env_status"
|
row "健康检查地址" "$BASE_URL" "${CC_INFO}"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "pg_data 目录" "${C_RESET}" "$([[ -d "$ROOT_DIR/pg_data" ]] && printf '已存在' || printf '新建')"
|
row ".env 文件" "$env_status"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "镜像构建" "${C_RESET}" "$build_status"
|
row "pg_data 目录" "$pg_status"
|
||||||
printf ' %s%-18s%s %s\n' "${C_BOLD}" "数据库迁移" "${C_RESET}" "$migrate_status"
|
row "镜像构建" "$build_status"
|
||||||
printf '\n'
|
row "数据库迁移" "$migrate_status"
|
||||||
|
ctms_box_bottom "${CC_PRIMARY}${C_BOLD}" "$w"
|
||||||
|
|
||||||
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
if [[ "$ASSUME_YES" -eq 1 ]]; then
|
||||||
log "已启用 --yes,跳过确认"
|
log "已启用 --yes,跳过确认"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf ' %s▸%s 确认以上参数并继续安装?输入 %syes%s 继续: ' \
|
printf '\n %s%s%s 确认以上参数并继续安装?输入 %syes%s 继续: ' \
|
||||||
"${C_YELLOW}${C_BOLD}" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
"${CC_WARN}${C_BOLD}" "$ICON_PROMPT" "${C_RESET}" "${C_BOLD}" "${C_RESET}"
|
||||||
local answer
|
local answer
|
||||||
read -r answer
|
read -r answer || answer=""
|
||||||
[[ "$answer" == "yes" ]] || fail "用户取消安装"
|
[[ "$answer" == "yes" ]] || fail "用户取消安装"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── .env 写入 ─────────────────────────────────
|
# ── .env 准备 ─────────────────────────────────
|
||||||
|
|
||||||
write_env_file() {
|
|
||||||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
|
||||||
local jwt_secret="$4" rsa_private_key="$5"
|
|
||||||
local temp_file
|
|
||||||
temp_file="$(mktemp "$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' "$([[ "$runtime_env" == "development" ]] && printf 'true' || printf 'false')"
|
|
||||||
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" "$ENV_FILE"
|
|
||||||
ok ".env 写入完成"
|
|
||||||
}
|
|
||||||
|
|
||||||
upsert_env_value() {
|
|
||||||
local key="$1" value="$2"
|
|
||||||
local temp_file
|
|
||||||
temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")"
|
|
||||||
if [[ -f "$ENV_FILE" && -n "$(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 }
|
|
||||||
' "$ENV_FILE" > "$temp_file"
|
|
||||||
else
|
|
||||||
[[ -f "$ENV_FILE" ]] && cat "$ENV_FILE" > "$temp_file"
|
|
||||||
printf '%s=%s\n' "$key" "$value" >> "$temp_file"
|
|
||||||
fi
|
|
||||||
mv "$temp_file" "$ENV_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_frontend_build_env() {
|
sync_frontend_build_env() {
|
||||||
local runtime_env="$1"
|
local runtime_env="$1"
|
||||||
local allow_insecure_dev_login="false"
|
local allow_insecure_dev_login="false"
|
||||||
[[ "$runtime_env" == "development" ]] && allow_insecure_dev_login="true"
|
[[ "$runtime_env" == "development" ]] && allow_insecure_dev_login="true"
|
||||||
|
|
||||||
upsert_env_value VITE_RUNTIME_ENV "$runtime_env"
|
ctms_upsert_env_value VITE_RUNTIME_ENV "$runtime_env"
|
||||||
upsert_env_value VITE_ALLOW_INSECURE_DEV_LOGIN "$allow_insecure_dev_login"
|
ctms_upsert_env_value VITE_ALLOW_INSECURE_DEV_LOGIN "$allow_insecure_dev_login"
|
||||||
ok "前端构建环境变量已同步"
|
ok "前端构建环境变量已同步"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,36 +272,35 @@ prepare_env_file() {
|
|||||||
ok "已生成新的 LOGIN_RSA_PRIVATE_KEY"
|
ok "已生成新的 LOGIN_RSA_PRIVATE_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key"
|
ctms_write_env_file "$project_name" "$runtime_env" "$login_key_id" "$jwt_secret" "$rsa_private_key"
|
||||||
|
ok ".env 写入完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Docker 流程 ───────────────────────────────
|
# ── Docker 流程 ───────────────────────────────
|
||||||
|
|
||||||
run_compose_config() {
|
run_compose_config() {
|
||||||
step "校验 Docker Compose 配置"
|
step "校验 Docker Compose 配置"
|
||||||
run_quiet "Docker Compose 配置语法校验" -- compose_cmd config
|
ctms_run_quiet "Docker Compose 配置语法校验" -- compose_cmd config
|
||||||
}
|
}
|
||||||
|
|
||||||
run_backend_init() {
|
run_backend_init() {
|
||||||
[[ "$TARGET_ENV" != "dev" ]] || return 0
|
|
||||||
step "初始化数据库与管理员账号"
|
step "初始化数据库与管理员账号"
|
||||||
run_quiet "运行 backend-init 容器" -- compose_cmd run --rm backend-init
|
ctms_run_quiet "运行 backend-init 容器" -- compose_cmd run --rm backend-init
|
||||||
}
|
}
|
||||||
|
|
||||||
run_build_and_start() {
|
run_build_and_start() {
|
||||||
if [[ "$SKIP_BUILD" -eq 1 ]]; then
|
if [[ "$SKIP_BUILD" -eq 1 ]]; then
|
||||||
step "启动服务(跳过镜像构建)"
|
step "启动服务(跳过镜像构建)"
|
||||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||||
run_quiet "启动开发容器并刷新后端/Nginx/Vite" -- compose_cmd up -d --force-recreate backend nginx frontend-dev
|
ctms_run_quiet "启动开发容器并刷新后端/Nginx/Vite" -- compose_cmd up -d --force-recreate backend nginx frontend-dev
|
||||||
else
|
else
|
||||||
run_quiet "启动容器" -- compose_cmd up -d
|
ctms_run_quiet "启动容器" -- compose_cmd up -d
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
step "构建镜像并启动服务"
|
step "构建镜像并启动服务"
|
||||||
if [[ "$TARGET_ENV" == "dev" ]]; then
|
if [[ "$TARGET_ENV" == "dev" ]]; then
|
||||||
run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend nginx frontend-dev
|
ctms_run_quiet "构建并刷新开发容器(首次较慢)" -- compose_cmd up -d --build --force-recreate backend nginx frontend-dev
|
||||||
else
|
else
|
||||||
run_quiet "构建镜像并启动容器(首次较慢)" -- compose_cmd up -d --build
|
ctms_run_quiet "构建镜像并启动容器(首次较慢)" -- compose_cmd up -d --build
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -493,23 +311,23 @@ run_migrations() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
step "执行数据库迁移"
|
step "执行数据库迁移"
|
||||||
run_quiet "Alembic upgrade head" -- compose_cmd run --rm backend python -m alembic upgrade head
|
ctms_run_quiet "Alembic upgrade head" -- compose_cmd run --rm backend python -m alembic upgrade head
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 健康检查 ─────────────────────────────────
|
# ── 健康检查 ─────────────────────────────────
|
||||||
|
|
||||||
check_container_status() {
|
check_container_status() {
|
||||||
step "检查容器运行状态"
|
step "检查容器运行状态"
|
||||||
local running
|
local running
|
||||||
running="$(compose_cmd ps --services --filter status=running 2>/dev/null || true)"
|
running="$(compose_cmd ps --services --filter status=running 2>/dev/null || true)"
|
||||||
local services=(db backend nginx)
|
local services=(db backend nginx)
|
||||||
[[ "$TARGET_ENV" != "dev" ]] || services+=(frontend-dev)
|
[[ "$TARGET_ENV" != "dev" ]] || services+=(frontend-dev)
|
||||||
|
local svc
|
||||||
for svc in "${services[@]}"; do
|
for svc in "${services[@]}"; do
|
||||||
if printf '%s\n' "$running" | grep -qx "$svc"; then
|
if printf '%s\n' "$running" | grep -qx "$svc"; then
|
||||||
printf ' %s✔%s %-8s 运行中\n' "${C_GREEN}${C_BOLD}" "${C_RESET}" "$svc"
|
printf ' %s%s%s %-12s %s运行中%s\n' "${CC_SUCCESS}${C_BOLD}" "$ICON_RUNNING" "${C_RESET}" "$svc" "${CC_SUCCESS}" "${C_RESET}"
|
||||||
else
|
else
|
||||||
printf ' %s✖%s %-8s %s未运行%s\n' "${C_RED}${C_BOLD}" "${C_RESET}" "$svc" "${C_RED}" "${C_RESET}"
|
printf ' %s%s%s %-12s %s未运行%s\n' "${CC_DANGER}${C_BOLD}" "$ICON_STOPPED" "${C_RESET}" "$svc" "${CC_DANGER}" "${C_RESET}"
|
||||||
fail "$svc 容器未正常运行,请执行 'bash scripts/install-ctms.sh $TARGET_ENV --skip-build --skip-migrate --verbose' 或对应 compose ps 命令排查"
|
fail "$svc 容器未正常运行,请执行 'bash scripts/install.sh $TARGET_ENV --skip-build --skip-migrate --verbose' 或对应 compose ps 命令排查"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -543,7 +361,7 @@ if expect_rsa:
|
|||||||
)
|
)
|
||||||
PY
|
PY
|
||||||
)
|
)
|
||||||
run_quiet "执行后端配置自检" -- compose_cmd exec -T \
|
ctms_run_quiet "执行后端配置自检" -- compose_cmd exec -T \
|
||||||
-e EXPECTED_ENV="$expected_env" \
|
-e EXPECTED_ENV="$expected_env" \
|
||||||
-e EXPECTED_KEY_ID="$expected_key_id" \
|
-e EXPECTED_KEY_ID="$expected_key_id" \
|
||||||
-e EXPECT_RSA="$expect_rsa" \
|
-e EXPECT_RSA="$expect_rsa" \
|
||||||
@@ -575,6 +393,18 @@ check_http_endpoint() {
|
|||||||
fail "接口探测失败: $url"
|
fail "接口探测失败: $url"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_dev_nginx_mode() {
|
||||||
|
[[ "$TARGET_ENV" == "dev" ]] || return 0
|
||||||
|
|
||||||
|
step "校验开发 Nginx 代理配置"
|
||||||
|
if compose_cmd exec -T nginx sh -c 'grep -q "frontend_dev" /etc/nginx/nginx.conf && grep -q "proxy_pass http://frontend_dev" /etc/nginx/nginx.conf' >/dev/null 2>&1; then
|
||||||
|
ok "Nginx 已代理到 Vite dev server"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail "当前 Nginx 未加载开发配置,请重试:bash scripts/install.sh dev --skip-migrate --verbose"
|
||||||
|
}
|
||||||
|
|
||||||
run_health_checks() {
|
run_health_checks() {
|
||||||
local runtime_env="$1" login_key_id="$2"
|
local runtime_env="$1" login_key_id="$2"
|
||||||
local expect_rsa=0
|
local expect_rsa=0
|
||||||
@@ -591,33 +421,25 @@ run_health_checks() {
|
|||||||
ok "HTTP 接口全部就绪"
|
ok "HTTP 接口全部就绪"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_dev_nginx_mode() {
|
|
||||||
[[ "$TARGET_ENV" == "dev" ]] || return 0
|
|
||||||
|
|
||||||
step "校验开发 Nginx 代理配置"
|
|
||||||
if compose_cmd exec -T nginx sh -c 'grep -q "frontend_dev" /etc/nginx/nginx.conf && grep -q "proxy_pass http://frontend_dev" /etc/nginx/nginx.conf' >/dev/null 2>&1; then
|
|
||||||
ok "Nginx 已代理到 Vite dev server"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
fail "当前 Nginx 未加载开发配置,请重试:bash scripts/install-ctms.sh dev --skip-migrate --verbose"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── 完成提示 ─────────────────────────────────
|
# ── 完成提示 ─────────────────────────────────
|
||||||
|
|
||||||
show_success() {
|
show_success() {
|
||||||
|
local w=66
|
||||||
|
local pa pe
|
||||||
|
pa="$(ctms_pad_right "访问地址" 14)"
|
||||||
|
pe="$(ctms_pad_right "环境模式" 14)"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
printf '%s┌──────────────────────────────────────────────┐%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
|
ctms_box_top "${CC_SUCCESS}${C_BOLD}" "$ICON_OK CTMS 安装成功" "$w"
|
||||||
printf '%s│%s ✔ CTMS 安装成功 %s│%s\n' "${C_GREEN}${C_BOLD}" "${C_GREEN}" "${C_GREEN}${C_BOLD}" "${C_RESET}"
|
ctms_box_line "${CC_SUCCESS}${C_BOLD}" "$w" \
|
||||||
printf '%s└──────────────────────────────────────────────┘%s\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
|
"${pa}${BASE_URL}" \
|
||||||
printf '\n'
|
"${CC_MUTED}${pa}${C_RESET}${CC_INFO}${BASE_URL}${C_RESET}"
|
||||||
printf ' %s访问地址%s %s%s%s\n' "${C_BOLD}" "${C_RESET}" "${C_CYAN}" "$BASE_URL" "${C_RESET}"
|
ctms_box_line "${CC_SUCCESS}${C_BOLD}" "$w" \
|
||||||
printf ' %s环境模式%s %s\n' "${C_BOLD}" "${C_RESET}" "$TARGET_ENV"
|
"${pe}${TARGET_ENV}" \
|
||||||
|
"${CC_MUTED}${pe}${C_RESET}${CC_TEXT}${TARGET_ENV}${C_RESET}"
|
||||||
|
ctms_box_bottom "${CC_SUCCESS}${C_BOLD}" "$w"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 入口 ─────────────────────────────────────
|
# ── 入口 ─────────────────────────────────────
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
_banner
|
_banner
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/common.sh
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${C_BOLD}用法:${C_RESET}
|
||||||
|
bash scripts/status.sh <环境>
|
||||||
|
|
||||||
|
${C_BOLD}环境:${C_RESET}
|
||||||
|
dev | main | release
|
||||||
|
|
||||||
|
${C_BOLD}说明:${C_RESET}
|
||||||
|
使用 docker compose stats --no-stream 获取资源快照,并输出整理后的状态面板。
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
STATS_OUTPUT=""
|
||||||
|
STATS_ERROR=""
|
||||||
|
STATUS_BOX_WIDTH=127
|
||||||
|
|
||||||
|
collect_compose_stats() {
|
||||||
|
local env="$1"
|
||||||
|
local output="" exit_code=0
|
||||||
|
STATS_OUTPUT=""
|
||||||
|
STATS_ERROR=""
|
||||||
|
|
||||||
|
set +e
|
||||||
|
output="$(ctms_compose "$env" stats --all --no-stream \
|
||||||
|
--format '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}' 2>&1)"
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$exit_code" -ne 0 ]]; then
|
||||||
|
set +e
|
||||||
|
output="$(ctms_compose "$env" stats --all --no-stream \
|
||||||
|
--format '{{.Container}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}' 2>&1)"
|
||||||
|
exit_code=$?
|
||||||
|
set -e
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$exit_code" -ne 0 ]]; then
|
||||||
|
STATS_ERROR="$output"
|
||||||
|
return "$exit_code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STATS_OUTPUT="$output"
|
||||||
|
}
|
||||||
|
|
||||||
|
render_stats_panel() {
|
||||||
|
local env="$1"
|
||||||
|
local project="$2"
|
||||||
|
local generated_at
|
||||||
|
generated_at="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
ctms_box_top "${CC_PRIMARY}${C_BOLD}" "CTMS 资源状态快照" "$STATUS_BOX_WIDTH"
|
||||||
|
ctms_box_line "${CC_PRIMARY}${C_BOLD}" "$STATUS_BOX_WIDTH" "环境 ${env} Compose 项目 ${project} 时间 ${generated_at}" "${CC_MUTED}环境${C_RESET} ${CC_INFO}${C_BOLD}${env}${C_RESET} ${CC_MUTED}Compose 项目${C_RESET} ${CC_INFO}${C_BOLD}${project}${C_RESET} ${CC_MUTED}时间 ${generated_at}${C_RESET}"
|
||||||
|
ctms_box_bottom "${CC_PRIMARY}${C_BOLD}" "$STATUS_BOX_WIDTH"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [[ -n "$STATS_ERROR" ]]; then
|
||||||
|
printf ' %s%s%s %s无法获取 docker compose stats 快照%s\n' "${CC_DANGER}${C_BOLD}" "$ICON_FAIL" "${C_RESET}" "${CC_DANGER}" "${C_RESET}"
|
||||||
|
printf ' %s环境%s %s %sCompose 项目%s %s\n' "${CC_MUTED}" "${C_RESET}" "$env" "${CC_MUTED}" "${C_RESET}" "$project"
|
||||||
|
printf ' %s原因%s %s\n\n' "${CC_MUTED}" "${C_RESET}" "$STATS_ERROR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$STATS_OUTPUT" ]]; then
|
||||||
|
printf ' %s%s%s %s未获取到容器资源数据%s\n' "${CC_WARN}${C_BOLD}" "$ICON_WARN" "${C_RESET}" "${CC_WARN}" "${C_RESET}"
|
||||||
|
printf ' %s可能原因:该环境尚未部署,或所有容器已被移除。%s\n\n' "${CC_MUTED}" "${C_RESET}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
ctms_box_top "${CC_ACCENT}${C_BOLD}" "容器资源" "$STATUS_BOX_WIDTH"
|
||||||
|
ctms_box_line "${CC_ACCENT}${C_BOLD}" "$STATUS_BOX_WIDTH" "$(printf '%-22s %-9s %-27s %-8s %-21s %-21s %-6s' "容器" "CPU" "内存使用 / 限制" "内存%" "网络 I/O" "磁盘 I/O" "PIDs")" "${CC_MUTED}$(printf '%-22s %-9s %-27s %-8s %-21s %-21s %-6s' "容器" "CPU" "内存使用 / 限制" "内存%" "网络 I/O" "磁盘 I/O" "PIDs")${C_RESET}"
|
||||||
|
printf '%s├%s┤%s\n' "${CC_ACCENT}${C_BOLD}" "$(ctms_repeat_char '─' "$(( STATUS_BOX_WIDTH - 2 ))")" "${C_RESET}"
|
||||||
|
|
||||||
|
printf '%s\n' "$STATS_OUTPUT" | while IFS='|' read -r name cpu mem mem_percent net_io block_io pids; do
|
||||||
|
[[ -n "$name" ]] || continue
|
||||||
|
ctms_box_line "${CC_ACCENT}${C_BOLD}" "$STATUS_BOX_WIDTH" "$(printf '%-22.22s %-9.9s %-27.27s %-8.8s %-21.21s %-21.21s %-6.6s' "$name" "$cpu" "$mem" "$mem_percent" "$net_io" "$block_io" "$pids")"
|
||||||
|
done
|
||||||
|
|
||||||
|
ctms_box_bottom "${CC_ACCENT}${C_BOLD}" "$STATUS_BOX_WIDTH"
|
||||||
|
printf '\n'
|
||||||
|
printf ' %s数据来源%s docker compose stats --all --no-stream\n\n' "${CC_MUTED}" "${C_RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local env="$1"
|
||||||
|
local project=""
|
||||||
|
ctms_require_env "$env"
|
||||||
|
project="$(ctms_project_name "$env")"
|
||||||
|
|
||||||
|
ctms_require_docker_compose
|
||||||
|
cd "$CTMS_ROOT_DIR"
|
||||||
|
collect_compose_stats "$env" || true
|
||||||
|
render_stats_panel "$env" "$project"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/common.sh
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${C_BOLD}用法:${C_RESET}
|
||||||
|
bash scripts/uninstall.sh <环境>
|
||||||
|
|
||||||
|
${C_BOLD}环境:${C_RESET}
|
||||||
|
dev | main | release
|
||||||
|
|
||||||
|
${C_BOLD}说明:${C_RESET}
|
||||||
|
默认仅停止并移除对应 Compose 项目的容器和网络。
|
||||||
|
不删除 .env、pg_data、Docker volume 或镜像。
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local env="$1"
|
||||||
|
local project=""
|
||||||
|
ctms_require_env "$env"
|
||||||
|
project="$(ctms_project_name "$env")"
|
||||||
|
|
||||||
|
ctms_require_docker_compose
|
||||||
|
ctms_confirm_danger \
|
||||||
|
"停止并移除 CTMS 容器" \
|
||||||
|
"Compose 项目 ${project};不会删除 .env、pg_data、volume 或镜像。" \
|
||||||
|
"服务会停止,对外访问中断;数据默认保留。"
|
||||||
|
|
||||||
|
cd "$CTMS_ROOT_DIR"
|
||||||
|
ctms_compose "$env" down
|
||||||
|
ctms_ok "容器已停止并移除,数据文件未删除"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/common.sh
|
||||||
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${C_BOLD}用法:${C_RESET}
|
||||||
|
bash scripts/update.sh <环境> [安装脚本选项]
|
||||||
|
|
||||||
|
${C_BOLD}环境:${C_RESET}
|
||||||
|
dev | main | release
|
||||||
|
|
||||||
|
${C_BOLD}说明:${C_RESET}
|
||||||
|
更新流程复用 scripts/install.sh:校验配置、重建/刷新容器、执行迁移并健康检查。
|
||||||
|
本脚本不会执行 git pull,请先确认代码已处于目标版本。
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local env="$1"
|
||||||
|
shift
|
||||||
|
ctms_require_env "$env"
|
||||||
|
|
||||||
|
local install_script="$CTMS_ROOT_DIR/scripts/install.sh"
|
||||||
|
[[ -x "$install_script" ]] || ctms_fail "安装脚本不可执行: $install_script"
|
||||||
|
|
||||||
|
ctms_info "更新会复用现有安装流程:校验配置、重建容器、执行迁移并健康检查。"
|
||||||
|
ctms_info "该命令不会执行 git pull;请先确认代码已处于目标版本。"
|
||||||
|
"$install_script" "$env" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"skills": {
|
|
||||||
"design-taste-frontend": {
|
|
||||||
"source": "Leonxlnx/taste-skill",
|
|
||||||
"sourceType": "github",
|
|
||||||
"skillPath": "skills/taste-skill/SKILL.md",
|
|
||||||
"computedHash": "6d838b246d0e35d0b53f4f23f98ba7a1dd561937e64f7d0c7553b0928e376c3e"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user