#!/usr/bin/env bash # OpsxShell launcher — scene profiles(对齐 opsxdb / opsxapi) # # ./run.sh # = web:仅 Gin 官网 + docs(:8789) # ./run.sh web # 同上(浏览器不当操作客户端) # ./run.sh gui # 桌面唯一操作入口:临时 .app(Dock 图标)+ 默认 Gin # ./run.sh all # 同 gui(兼容旧命令) # ./run.sh server|desktop # ./run.sh clean-apps # 全量开发机卫生(~/devops/scripts/clean-opsx-apps.sh) # ./run.sh stop|restart|status|help # # 产品形态:浏览器 = 官网/docs;操作 = 桌面 Tauri(src/ + src-tauri/)。 # Vite :5175 仅作桌面 WebView 源,不要用浏览器当工作台。 set -euo pipefail PROJECT_NAME="OpsxShell" SERVER_PORT="${OPSXSHELL_SERVER_PORT:-8789}" WEB_PORT="${OPSXSHELL_WEB_PORT:-5175}" SERVER_DIR="server" SERVER_BIN="opsxshell-server" SERVER_LABEL="com.opsx.shell.server" WEB_LABEL="com.opsx.shell.web" DESKTOP_LABEL="com.opsx.shell.desktop" # gui 默认是否顺带起 Gin(可用 OPSXSHELL_GUI_WITH_SERVER=0 关掉) GUI_WITH_SERVER="${OPSXSHELL_GUI_WITH_SERVER:-1}" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warning() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERR]${NC} $*"; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$SCRIPT_DIR" LOG_DIR="$SCRIPT_DIR/logs" mkdir -p "$LOG_DIR" "$SCRIPT_DIR/$SERVER_DIR/bin" SERVER_LOG="$LOG_DIR/server.log" WEB_LOG="$LOG_DIR/web.log" DESKTOP_LOG="$LOG_DIR/desktop.log" # shellcheck source=gui-dev-app.sh source "$SCRIPT_DIR/scripts/gui-dev-app.sh" OPSX_CLEAN_SCRIPT="${OPSX_CLEAN_SCRIPT:-$HOME/devops/scripts/clean-opsx-apps.sh}" # gui 启动前轻量清:推残留 DMG + 注销非 /Applications、非 keep-path 的幽灵。 # 不删 target、不 ResetLaunchPad / kill Dock。逻辑在 clean-opsx-apps.sh --light。 opsx_light_clean_before_gui() { local keep="$1" if [ "$(uname -s)" != "Darwin" ]; then return 0 fi if [ ! -x "$OPSX_CLEAN_SCRIPT" ]; then warning "找不到清理脚本: $OPSX_CLEAN_SCRIPT(跳过轻量清)" return 0 fi info "gui light-clean (keep-path=${keep})..." "$OPSX_CLEAN_SCRIPT" --light --keep-path="$keep" --no-verify \ || warning "轻量清未完全成功(继续启动)" } run_clean_apps() { if [ ! -x "$OPSX_CLEAN_SCRIPT" ]; then error "找不到: $OPSX_CLEAN_SCRIPT" return 1 fi # 转发剩余参数(如 --delete-target-apps);默认 full "$OPSX_CLEAN_SCRIPT" "$@" } # Same path as src-tauri infrastructure/file_log.rs (dirs::data_local_dir). if [ "$(uname -s)" = "Darwin" ]; then APP_LOG="$HOME/Library/Application Support/opsxshell/logs/opsxshell.log" else APP_LOG="${XDG_DATA_HOME:-$HOME/.local/share}/opsxshell/logs/opsxshell.log" fi export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.cargo/bin:$PATH" NODE_BIN_DIR="" detect_node() { if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then NODE_BIN_DIR="$(cd "$(dirname "$(command -v node)")" && pwd)" return 0 fi local roots=() d v best="" [ -n "${NVM_DIR:-}" ] && roots+=("$NVM_DIR/versions/node") roots+=("${HOME}/.nvm/versions/node") for d in "${roots[@]}"; do [ -d "$d" ] || continue v="$(ls -1 "$d" 2>/dev/null | sort -V | tail -n 1)" if [ -n "$v" ] && [ -x "$d/$v/bin/node" ]; then best="$d/$v/bin" break fi done if [ -z "$best" ]; then for d in /opt/homebrew/bin /usr/local/bin; do if [ -x "$d/node" ]; then best="$d" break fi done fi if [ -z "$best" ]; then error "未找到 node/npm。请先安装 Node.js 或 nvm。" exit 1 fi NODE_BIN_DIR="$best" export PATH="$NODE_BIN_DIR:$PATH" info "使用 Node.js: $NODE_BIN_DIR" } kill_port() { local pids pids="$(lsof -ti:"$1" 2>/dev/null || true)" if [ -n "$pids" ]; then info "释放端口 $1" # shellcheck disable=SC2086 kill -9 $pids 2>/dev/null || true sleep 0.3 fi } start_detached() { local label="$1" log_file="$2" command="$3" if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then launchctl remove "$label" 2>/dev/null || true launchctl submit -l "$label" -o "$log_file" -e "$log_file" -- /bin/bash -lc "$command" else nohup /bin/bash -lc "$command" >>"$log_file" 2>&1 /dev/null 2>&1; then launchctl remove "$label" 2>/dev/null || true fi } ensure_npm() { detect_node if [ ! -d "$SCRIPT_DIR/node_modules" ]; then warning "缺少 node_modules,正在 npm install..." npm install fi } build_server() { info "编译 Gin 官网/docs..." ( cd "$SCRIPT_DIR/$SERVER_DIR" export GOPROXY="${GOPROXY:-https://goproxy.io,direct}" CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" \ -o "$SCRIPT_DIR/$SERVER_DIR/bin/$SERVER_BIN" ./cmd/opsxshell-site ) } wait_http() { local url="$1" name="$2" retries="${3:-60}" local i=0 while ! curl -fsS "$url" >/dev/null 2>&1; do i=$((i + 1)) if [ "$i" -ge "$retries" ]; then error "$name 启动超时:$url" return 1 fi sleep 0.5 done } start_server() { if curl -fsS "http://127.0.0.1:$SERVER_PORT/health" >/dev/null 2>&1; then success "官网服务已在运行 :$SERVER_PORT" return 0 fi info "启动官网/docs :$SERVER_PORT ..." : >"$SERVER_LOG" kill_port "$SERVER_PORT" build_server local bind_env="OPSXSHELL_ADDR=127.0.0.1:$SERVER_PORT" if [ -n "${OPSXSHELL_ADDR:-}" ]; then bind_env="OPSXSHELL_ADDR=$OPSXSHELL_ADDR" elif [ "${OPSXSHELL_BIND_PUBLIC:-0}" = "1" ] || [ "${OPSXSHELL_BIND_PUBLIC:-}" = "true" ]; then bind_env="OPSXSHELL_BIND_PUBLIC=1" fi start_detached "$SERVER_LABEL" "$SERVER_LOG" \ "export PATH=\"/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR/$SERVER_DIR\" && exec env $bind_env GIN_MODE=debug \"./bin/$SERVER_BIN\"" wait_http "http://127.0.0.1:$SERVER_PORT/health" "官网服务" success "官网 UP http://127.0.0.1:$SERVER_PORT/ · docs → http://127.0.0.1:$SERVER_PORT/docs" } stop_server() { stop_detached "$SERVER_LABEL" kill_port "$SERVER_PORT" success "官网服务已停止" } # 仅供桌面 Tauri devUrl;不是浏览器产品入口 start_vite_for_desktop() { if curl -fsS "http://127.0.0.1:$WEB_PORT/" >/dev/null 2>&1; then success "桌面 UI(Vite) 已在运行 :$WEB_PORT" return 0 fi info "启动桌面 UI 源 :${WEB_PORT}(仅本机给 Tauri,勿当浏览器客户端)..." : >"$WEB_LOG" ensure_npm kill_port "$WEB_PORT" start_detached "$WEB_LABEL" "$WEB_LOG" \ "export PATH=\"$NODE_BIN_DIR:/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR\" && exec npm run dev:ui" wait_http "http://127.0.0.1:$WEB_PORT/" "Vite" 80 success "桌面 UI(Vite) UP http://127.0.0.1:${WEB_PORT}(请用 ./run.sh gui)" } stop_web() { stop_detached "$WEB_LABEL" kill_port "$WEB_PORT" success "Vite 已停止" } start_desktop() { opsx_light_clean_before_gui "$GUI_APP" info "启动桌面 OpsxShell(临时 .app + 当前图标)..." : >"$DESKTOP_LOG" ensure_npm kill_opsxshell_gui ensure_gui_dev_app || return 1 if [ "${GUI_DEV_NEEDS_VITE:-0}" = "1" ]; then start_vite_for_desktop fi open "$GUI_APP" 2>>"$DESKTOP_LOG" || { error "open 失败,见 $DESKTOP_LOG"; return 1; } local i=0 while true; do if gui_is_running; then success "桌面 OpsxShell 已启动(临时 .app,Dock 用 icon.icns / 名称 OpsxShell)" info "路径: $GUI_APP" return 0 fi i=$((i + 1)) if [ "$i" -ge 30 ]; then error "桌面启动超时,见 $DESKTOP_LOG" tail -n 40 "$DESKTOP_LOG" || true return 1 fi sleep 1 done } stop_desktop() { kill_opsxshell_gui success "桌面已停止" } status() { echo "$PROJECT_NAME 状态" if curl -fsS "http://127.0.0.1:$SERVER_PORT/health" >/dev/null 2>&1; then success "server :$SERVER_PORT UP $(curl -fsS "http://127.0.0.1:$SERVER_PORT/health")" else warning "server :$SERVER_PORT DOWN" fi if curl -fsS "http://127.0.0.1:$WEB_PORT/" >/dev/null 2>&1; then success "vite :${WEB_PORT} UP(桌面源,非浏览器产品)" else warning "vite :$WEB_PORT DOWN" fi if curl -fsS "http://127.0.0.1:$SERVER_PORT/" >/dev/null 2>&1 \ || curl -fsS "http://127.0.0.1:$SERVER_PORT/docs" >/dev/null 2>&1; then success "site :$SERVER_PORT 官网/docs 可达" fi if gui_is_running; then success "gui OpsxShell UP" else warning "gui OpsxShell DOWN" fi } usage() { cat <