From 42b498edc8dcae964e20335d6b4676a5f87faeb6 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Fri, 19 Jun 2026 14:24:22 +0800 Subject: [PATCH 1/9] feat(skill): add upgrade helper functions to shared.mjs --- packages/cli-box-skill/installer/shared.mjs | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/packages/cli-box-skill/installer/shared.mjs b/packages/cli-box-skill/installer/shared.mjs index 824d2e8..14779a7 100644 --- a/packages/cli-box-skill/installer/shared.mjs +++ b/packages/cli-box-skill/installer/shared.mjs @@ -4,6 +4,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { createRequire } from "node:module"; +import { execSync } from "node:child_process"; +import http from "node:http"; const require = createRequire(import.meta.url); @@ -45,6 +47,8 @@ export const HARNESS_TARGETS = { export const HARNESS_IDS = Object.keys(HARNESS_TARGETS); +const CLI_BOX_DIR = path.join(os.homedir(), ".cli-box"); + // Accepts an array of tokens or a comma/space-separated string. // Returns the resolved list of harness ids. Throws on unknown tokens. export function parseTargets(input) { @@ -147,3 +151,155 @@ export function ensureBinaries({ home = os.homedir() } = {}) { } return { ok: true, linked, binDir }; } + +// ── Upgrade helpers ──────────────────────────────────────────────────────── + +/** Read ~/.cli-box/daemon.json. Returns {port, pid, started_at} or null. */ +export function readDaemonInfo() { + const p = path.join(CLI_BOX_DIR, "daemon.json"); + if (!safeExists(p)) return null; + try { + return JSON.parse(fs.readFileSync(p, "utf8")); + } catch { + return null; + } +} + +/** Read ~/.cli-box/electron.json. Returns {pid} or null. */ +export function readElectronInfo() { + const p = path.join(CLI_BOX_DIR, "electron.json"); + if (!safeExists(p)) return null; + try { + return JSON.parse(fs.readFileSync(p, "utf8")); + } catch { + return null; + } +} + +/** Check if a process with the given PID is alive. */ +export function isProcessAlive(pid) { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +/** HTTP GET to localhost:{port}{urlPath}. Returns parsed JSON. */ +export function httpGet(port, urlPath) { + return new Promise((resolve, reject) => { + const req = http.get(`http://127.0.0.1:${port}${urlPath}`, (res) => { + let body = ""; + res.on("data", (c) => (body += c)); + res.on("end", () => { + try { + resolve(JSON.parse(body)); + } catch { + resolve(body); + } + }); + }); + req.on("error", reject); + req.setTimeout(5000, () => { + req.destroy(); + reject(new Error("Request timeout")); + }); + }); +} + +/** HTTP POST to localhost:{port}{urlPath}. Returns parsed JSON. */ +export function httpPost(port, urlPath) { + return new Promise((resolve, reject) => { + const req = http.request( + `http://127.0.0.1:${port}${urlPath}`, + { method: "POST" }, + (res) => { + let body = ""; + res.on("data", (c) => (body += c)); + res.on("end", () => { + try { + resolve(JSON.parse(body)); + } catch { + resolve(body); + } + }); + } + ); + req.on("error", reject); + req.setTimeout(5000, () => { + req.destroy(); + reject(new Error("Request timeout")); + }); + req.end(); + }); +} + +/** List running sandboxes from the daemon. Returns array of sandbox objects. */ +export async function listRunningSandboxes(port) { + try { + const data = await httpGet(port, "/instances"); + return Array.isArray(data) ? data : []; + } catch { + return []; + } +} + +/** Close a single sandbox by ID. Returns true on success. */ +export async function closeSandbox(port, id) { + try { + await httpPost(port, `/box/${id}/close`); + return true; + } catch { + return false; + } +} + +/** Send shutdown signal to the daemon. */ +export async function shutdownDaemon(port) { + try { + await httpPost(port, "/shutdown"); + } catch { + // Daemon may already be shutting down + } +} + +/** Kill Electron process if running. Removes electron.json. */ +export function killElectron() { + const info = readElectronInfo(); + if (!info || !info.pid) return; + if (!isProcessAlive(info.pid)) { + // Clean up stale file + try { fs.unlinkSync(path.join(CLI_BOX_DIR, "electron.json")); } catch {} + return; + } + try { + process.kill(info.pid, "SIGTERM"); + // Wait briefly for graceful shutdown + const deadline = Date.now() + 3000; + while (Date.now() < deadline && isProcessAlive(info.pid)) { + execSync("sleep 0.2"); + } + if (isProcessAlive(info.pid)) { + process.kill(info.pid, "SIGKILL"); + } + } catch { + // Permission error or already dead + } + try { fs.unlinkSync(path.join(CLI_BOX_DIR, "electron.json")); } catch {} +} + +/** Poll until a process exits. Returns true if it exited, false on timeout. */ +export function waitForProcessExit(pid, timeoutMs = 10000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!isProcessAlive(pid)) return true; + execSync("sleep 0.3"); + } + return false; +} + +/** Run npm install -g . Throws on failure. */ +export function npmInstall(pkg) { + execSync(`npm install -g ${pkg}`, { stdio: "inherit" }); +} From 16ad931ee0de3421bef773efc89bad81db8e919b Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Fri, 19 Jun 2026 14:26:28 +0800 Subject: [PATCH 2/9] feat(skill): add upgrade subcommand to cli-box-skill --- packages/cli-box-skill/installer/cli.mjs | 120 +++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/packages/cli-box-skill/installer/cli.mjs b/packages/cli-box-skill/installer/cli.mjs index 4daf45f..179a314 100644 --- a/packages/cli-box-skill/installer/cli.mjs +++ b/packages/cli-box-skill/installer/cli.mjs @@ -7,6 +7,9 @@ // cli-box-skill install --no-tui claude # non-interactive explicit import { Command } from "commander"; import * as clack from "@clack/prompts"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { HARNESS_IDS, HARNESS_TARGETS, @@ -14,6 +17,14 @@ import { detectHarnesses, installSkillToTargets, ensureBinaries, + readDaemonInfo, + isProcessAlive, + listRunningSandboxes, + closeSandbox, + shutdownDaemon, + killElectron, + waitForProcessExit, + npmInstall, } from "./shared.mjs"; const isTTY = Boolean(process.stdin.isTTY); @@ -79,6 +90,101 @@ async function runInstall(targets, opts) { process.exit(okCount > 0 ? 0 : 1); } +const CLI_BOX_DIR = path.join(os.homedir(), ".cli-box"); + +async function runUpgrade(targetVersion, opts = {}) { + const version = targetVersion || "latest"; + const pkg = `cli-box-skill@${version}`; + + // 1. Check daemon + const daemon = readDaemonInfo(); + if (daemon && isProcessAlive(daemon.pid)) { + console.log(` ℹ Daemon running (PID ${daemon.pid}, port ${daemon.port})`); + + // 2. List sandboxes + const sandboxes = await listRunningSandboxes(daemon.port); + if (sandboxes.length > 0) { + console.log(`\n ⚠ ${sandboxes.length} sandbox(es) running:`); + for (const sb of sandboxes) { + const id = sb.id || sb.instance_id || "unknown"; + const title = sb.title || sb.command || ""; + console.log(` • ${id} ${title}`); + } + console.log( + "\n Upgrade requires stopping all sandboxes and the daemon." + ); + + if (isTTY) { + const confirm = await clack.confirm({ + message: "Close all sandboxes and proceed with upgrade?", + initialValue: false, + }); + if (clack.isCancel(confirm) || !confirm) { + console.log("Cancelled."); + process.exit(0); + } + } else if (!opts.yes) { + console.error( + "Non-interactive shell. Use --yes to confirm, or close sandboxes manually first." + ); + process.exit(1); + } + + // 3. Close all sandboxes + console.log("\n Closing sandboxes..."); + for (const sb of sandboxes) { + const id = sb.id || sb.instance_id; + const ok = await closeSandbox(daemon.port, id); + console.log(ok ? ` ✓ Closed ${id}` : ` ⚠ Failed to close ${id}`); + } + + // 4. Shutdown daemon + console.log(" Shutting down daemon..."); + await shutdownDaemon(daemon.port); + if (!waitForProcessExit(daemon.pid, 10000)) { + console.warn(" ⚠ Daemon did not exit in time, force killing..."); + try { + process.kill(daemon.pid, "SIGKILL"); + } catch {} + } + // Clean up daemon.json + try { + const daemonJson = path.join(CLI_BOX_DIR, "daemon.json"); + fs.unlinkSync(daemonJson); + } catch {} + console.log(" ✓ Daemon stopped"); + + // 5. Kill Electron + console.log(" Stopping Electron..."); + killElectron(); + console.log(" ✓ Electron stopped"); + } else { + // No sandboxes, just shutdown daemon + console.log(" No sandboxes running. Shutting down daemon..."); + await shutdownDaemon(daemon.port); + waitForProcessExit(daemon.pid, 10000); + killElectron(); + console.log(" ✓ Daemon stopped"); + } + } else { + console.log(" ℹ No daemon running"); + // Still try to kill stale Electron + killElectron(); + } + + // 6. npm install + console.log(`\n Installing ${pkg}...`); + try { + npmInstall(pkg); + console.log(`\n ✓ cli-box upgraded to ${version}.`); + console.log(" Run 'cli-box start' to begin."); + } catch (e) { + console.error(`\n ✗ npm install failed: ${e.message}`); + console.error(` Try manually: npm install -g ${pkg}`); + process.exit(1); + } +} + const program = new Command(); program .name("cli-box-skill") @@ -98,4 +204,18 @@ program } }); +program + .command("upgrade") + .description("Upgrade cli-box to a new version (stops running sandboxes)") + .argument("[version]", "target version (default: latest)") + .option("--yes", "Skip confirmation prompt") + .action(async (version, opts) => { + try { + await runUpgrade(version, opts); + } catch (e) { + console.error(`✗ ${e.message}`); + process.exit(1); + } + }); + program.parseAsync(process.argv); From 89d6d8f28055fe4cf3f2f54708eeed66972a7f9d Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Fri, 19 Jun 2026 14:27:34 +0800 Subject: [PATCH 3/9] docs(skill): add upgrade instructions to SKILL.md --- packages/cli-box-skill/skill/SKILL.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/cli-box-skill/skill/SKILL.md b/packages/cli-box-skill/skill/SKILL.md index 6f7c367..ce00c5a 100644 --- a/packages/cli-box-skill/skill/SKILL.md +++ b/packages/cli-box-skill/skill/SKILL.md @@ -27,6 +27,23 @@ Choose Claude Code, OpenCode, and/or OpenClaw. Or non-interactively: npx cli-box-skill install claude # claude | opencode | openclaw | all ``` +## Upgrade + +Upgrade to the latest version (stops running sandboxes first): + +```bash +cli-box-skill upgrade +``` + +Upgrade to a specific version: + +```bash +cli-box-skill upgrade 0.3.0 +``` + +> **Note:** `upgrade` does NOT overwrite your SKILL.md. Use `npx cli-box-skill install` +> if you want to reset SKILL.md to the bundled version. + ## Quick Start ```bash From 9e6cd3fba611e609b0718e1aea793a258ca96390 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Fri, 19 Jun 2026 14:28:20 +0800 Subject: [PATCH 4/9] docs: add upgrade instructions to README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index c69d44f..d7aaedb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ | **npm** | `npx cli-box-skill install` | Choose harness(es); binaries + skill installed | | **Shell** | `bash <(curl -fsSL https://raw.githubusercontent.com/Shadow-Azure/cli-box/main/packages/cli-box-skill/skill/install.sh) claude` | Downloads to `~/.cli-box/bin/`, installs skill | | **Manual** | [GitHub Releases](https://github.com/Shadow-Azure/cli-box/releases) | Download and extract manually | +| **Upgrade** | `cli-box-skill upgrade` | Stops sandboxes, upgrades package, preserves SKILL.md | ### For Humans @@ -53,6 +54,15 @@ https://raw.githubusercontent.com/Shadow-Azure/cli-box/main/docs/guide/installat npx cli-box-skill install claude # claude | opencode | openclaw | all ``` +### Upgrade + +```bash +cli-box-skill upgrade # latest +cli-box-skill upgrade 0.3.0 # specific version +``` + +Stops running sandboxes and daemon before upgrading. Your SKILL.md is preserved. + ### Add to PATH ```bash From 6c9323e7ab536478f31a4c744af40dbebebe58f0 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Sat, 20 Jun 2026 21:09:42 +0800 Subject: [PATCH 5/9] ci: add upgrade flow test workflow --- .github/workflows/test-upgrade.yml | 188 +++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 .github/workflows/test-upgrade.yml diff --git a/.github/workflows/test-upgrade.yml b/.github/workflows/test-upgrade.yml new file mode 100644 index 0000000..304e8c4 --- /dev/null +++ b/.github/workflows/test-upgrade.yml @@ -0,0 +1,188 @@ +# cli-box-skill upgrade 命令测试 +# 验证升级流程在隔离环境中正常工作 +# +# 测试场景: +# 1. 无 daemon 运行时的升级(CI 环境) +# 2. npm install 后二进制重新链接 +# 3. SKILL.md 保留验证 +# 4. cli-box-skill 命令可用性 + +name: Test Upgrade + +on: + pull_request: + paths: + - 'packages/cli-box-skill/**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + # ==================== 升级流程测试 (macOS) ==================== + upgrade-test: + name: Upgrade Flow Test + runs-on: macos-latest + timeout-minutes: 10 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + # 1. 先安装当前 npm 上的 latest 版本(模拟用户现有环境) + - name: 安装当前 latest 版本 + run: npm install -g cli-box-skill@latest + + - name: 验证 latest 安装 + run: | + echo "=== 验证 cli-box-skill 命令 ===" + cli-box-skill --help + echo "" + echo "=== 验证二进制链接 ===" + ls -la ~/.cli-box/bin/cli-box || echo "⚠ cli-box binary not found" + ls -la ~/.cli-box/bin/cli-box-daemon || echo "⚠ cli-box-daemon binary not found" + + # 2. 模拟用户自定义 SKILL.md(写入自定义内容) + - name: 模拟用户自定义 SKILL.md + run: | + mkdir -p ~/.claude/skills/cli-box + echo "# Custom SKILL.md (user modified)" > ~/.claude/skills/cli-box/SKILL.md + echo "Custom content that should be preserved" >> ~/.claude/skills/cli-box/SKILL.md + echo "" + echo "=== 自定义 SKILL.md 内容 ===" + cat ~/.claude/skills/cli-box/SKILL.md + + # 3. 从 PR 分支打包并安装(模拟升级前的包状态) + - name: 从 PR 分支打包 + working-directory: packages/cli-box-skill + run: | + npm pack + ls -la cli-box-skill-*.tgz + + # 4. 运行 upgrade 命令(无 daemon 场景) + - name: 运行 upgrade 命令 + working-directory: packages/cli-box-skill + run: | + echo "=== 运行 upgrade(无 daemon)===" + node installer/cli.mjs upgrade --yes 2>&1 | tee /tmp/upgrade-output.log + + # 5. 验证升级结果 + - name: 验证升级结果 + run: | + echo "============================================" + echo " 升级结果验证" + echo "============================================" + echo "" + + # 检查二进制是否重新链接 + echo "=== 二进制链接检查 ===" + if [ -L ~/.cli-box/bin/cli-box ]; then + echo "✓ cli-box is a symlink" + ls -la ~/.cli-box/bin/cli-box + else + echo "⚠ cli-box is NOT a symlink" + fi + + if [ -L ~/.cli-box/bin/cli-box-daemon ]; then + echo "✓ cli-box-daemon is a symlink" + ls -la ~/.cli-box/bin/cli-box-daemon + else + echo "⚠ cli-box-daemon is NOT a symlink" + fi + + echo "" + + # 检查 SKILL.md 是否保留 + echo "=== SKILL.md 保留检查 ===" + if [ -f ~/.claude/skills/cli-box/SKILL.md ]; then + CONTENT=$(cat ~/.claude/skills/cli-box/SKILL.md) + if echo "$CONTENT" | grep -q "Custom content that should be preserved"; then + echo "✓ SKILL.md preserved (custom content intact)" + else + echo "✗ SKILL.md was overwritten!" + echo "Expected: 'Custom content that should be preserved'" + echo "Got:" + cat ~/.claude/skills/cli-box/SKILL.md + exit 1 + fi + else + echo "⚠ SKILL.md not found at ~/.claude/skills/cli-box/SKILL.md" + fi + + echo "" + + # 检查 cli-box-skill 命令是否可用 + echo "=== cli-box-skill 命令检查 ===" + cli-box-skill --help 2>&1 | head -5 + + - name: 上传升级日志 + if: always() + uses: actions/upload-artifact@v4 + with: + name: upgrade-output + path: /tmp/upgrade-output.log + retention-days: 7 + + # ==================== 单元测试 ==================== + unit-test: + name: cli-box-skill Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: 安装依赖 + working-directory: packages/cli-box-skill + run: npm install + + - name: 运行单元测试 + working-directory: packages/cli-box-skill + run: node --test + + # ==================== 门禁结果汇总 ==================== + upgrade-gate: + name: Upgrade Test Gate + runs-on: ubuntu-latest + needs: [upgrade-test, unit-test] + if: always() + + steps: + - name: 检查结果 + run: | + echo "============================================" + echo " cli-box-skill 升级测试结果" + echo "============================================" + echo "" + + UPGRADE="${{ needs.upgrade-test.result }}" + UNIT="${{ needs.unit-test.result }}" + + echo "| 检查项 | 状态 |" + echo "|-------|------|" + echo "| 升级流程测试 | $UPGRADE |" + echo "| 单元测试 | $UNIT |" + echo "" + + if [[ "$UPGRADE" == "success" && "$UNIT" == "success" ]]; then + echo "✅ 所有升级测试通过!" + exit 0 + else + echo "❌ 升级测试未通过" + exit 1 + fi From e6d29c12108f8116d0e0439b9c59e16d59a77f2c Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Sat, 20 Jun 2026 21:20:56 +0800 Subject: [PATCH 6/9] fix: remove duplicate execSync import from merge --- packages/cli-box-skill/installer/shared.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli-box-skill/installer/shared.mjs b/packages/cli-box-skill/installer/shared.mjs index a74aa2e..f794437 100644 --- a/packages/cli-box-skill/installer/shared.mjs +++ b/packages/cli-box-skill/installer/shared.mjs @@ -5,7 +5,6 @@ import os from "node:os"; import path from "node:path"; import { execSync } from "node:child_process"; import { createRequire } from "node:module"; -import { execSync } from "node:child_process"; import http from "node:http"; const require = createRequire(import.meta.url); From 322a2e62f1122aac60aba0a75e851e8c10b1f868 Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Sat, 20 Jun 2026 22:39:14 +0800 Subject: [PATCH 7/9] ci: integrate test-upgrade into CI gate + add publish simulation - Add workflow_call trigger to test-upgrade.yml for reusable workflow - Add publish-sim job: validates tarball file list and package structure - Wire test-upgrade as a required check in ci.yml gate-result - Include upgrade test status in gate summary and PR comment --- .github/workflows/ci.yml | 13 +++++- .github/workflows/test-upgrade.yml | 71 +++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c877d0c..ab3f05f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -419,11 +419,16 @@ jobs: - name: 运行 test.sh run: bash test.sh + # ==================== 升级流程测试 ==================== + upgrade-test: + name: 升级测试 + uses: ./.github/workflows/test-upgrade.yml + # ==================== 门禁结果汇总 ==================== gate-result: name: 门禁结果 runs-on: ubuntu-latest - needs: [rust-fmt, rust-clippy, rust-test, frontend-test, e2e-test, unified-test, security] + needs: [rust-fmt, rust-clippy, rust-test, frontend-test, e2e-test, unified-test, security, upgrade-test] if: always() steps: @@ -457,6 +462,7 @@ jobs: E2E_TEST="${{ needs.e2e-test.result }}" UNIFIED_TEST="${{ needs.unified-test.result }}" SECURITY_RESULT="${{ needs.security.result }}" + UPGRADE_TEST="${{ needs.upgrade-test.result }}" echo "| 检查项 | 状态 |" echo "|-------|------|" @@ -467,6 +473,7 @@ jobs: echo "| Playwright E2E | $E2E_TEST |" echo "| 统一测试 (test.sh) | $UNIFIED_TEST |" echo "| 安全检查 | $SECURITY_RESULT |" + echo "| 升级测试 | $UPGRADE_TEST |" echo "" if [ -f coverage-artifacts/rust-coverage-summary.md ]; then @@ -487,7 +494,8 @@ jobs: "$FRONTEND_TEST" == "success" && \ "$E2E_TEST" == "success" && \ "$UNIFIED_TEST" == "success" && \ - "$SECURITY_RESULT" == "success" ]]; then + "$SECURITY_RESULT" == "success" && \ + "$UPGRADE_TEST" == "success" ]]; then echo "✅ 所有门禁检查通过!" exit 0 else @@ -510,6 +518,7 @@ jobs: 'Playwright E2E': '${{ needs.e2e-test.result }}', '统一测试 (test.sh)': '${{ needs.unified-test.result }}', '安全检查': '${{ needs.security.result }}', + '升级测试': '${{ needs.upgrade-test.result }}', }; const statusEmoji = (status) => status === 'success' ? '✅' : (status === 'skipped' ? '⏭️' : '❌'); diff --git a/.github/workflows/test-upgrade.yml b/.github/workflows/test-upgrade.yml index 304e8c4..f9de9a6 100644 --- a/.github/workflows/test-upgrade.yml +++ b/.github/workflows/test-upgrade.yml @@ -13,6 +13,7 @@ on: pull_request: paths: - 'packages/cli-box-skill/**' + workflow_call: workflow_dispatch: permissions: @@ -132,6 +133,70 @@ jobs: path: /tmp/upgrade-output.log retention-days: 7 + # ==================== 发布模拟验证 ==================== + publish-sim: + name: Publish Simulation + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: npm pack + working-directory: packages/cli-box-skill + run: | + npm pack + ls -la cli-box-skill-*.tgz + + - name: 验证 tarball 文件清单 + working-directory: packages/cli-box-skill + run: | + TARBALL=$(ls cli-box-skill-*.tgz) + echo "=== 验证 $TARBALL ===" + tar tzf "$TARBALL" | sort + + # 必须包含的关键文件 + REQUIRED=("package/postinstall.mjs" "package/installer/cli.mjs" "package/installer/shared.mjs" "package/bin/cli-box-wrapper.js" "package/skill/SKILL.md") + for f in "${REQUIRED[@]}"; do + if tar tzf "$TARBALL" | grep -q "$f"; then + echo "✓ $f" + else + echo "✗ $f MISSING" + exit 1 + fi + done + echo "" + echo "✅ tarball 文件清单验证通过" + + - name: 验证包结构 + working-directory: packages/cli-box-skill + run: | + echo "=== package.json 字段检查 ===" + node -e " + const pkg = require('./package.json'); + const checks = [ + ['name', pkg.name === 'cli-box-skill'], + ['bin.cli-box', !!pkg.bin?.['cli-box']], + ['bin.cli-box-skill', !!pkg.bin?.['cli-box-skill']], + ['files includes postinstall.mjs', pkg.files?.includes('postinstall.mjs')], + ['files includes installer/', pkg.files?.some(f => f.startsWith('installer/'))], + ['optionalDependencies.cli-box-darwin-arm64', !!pkg.optionalDependencies?.['cli-box-darwin-arm64']], + ]; + let ok = true; + for (const [name, pass] of checks) { + console.log(pass ? '✓ ' + name : '✗ ' + name); + if (!pass) ok = false; + } + if (!ok) process.exit(1); + console.log('\n✅ 包结构验证通过'); + " + # ==================== 单元测试 ==================== unit-test: name: cli-box-skill Unit Tests @@ -159,7 +224,7 @@ jobs: upgrade-gate: name: Upgrade Test Gate runs-on: ubuntu-latest - needs: [upgrade-test, unit-test] + needs: [publish-sim, upgrade-test, unit-test] if: always() steps: @@ -170,16 +235,18 @@ jobs: echo "============================================" echo "" + PUBLISH="${{ needs.publish-sim.result }}" UPGRADE="${{ needs.upgrade-test.result }}" UNIT="${{ needs.unit-test.result }}" echo "| 检查项 | 状态 |" echo "|-------|------|" + echo "| 发布模拟验证 | $PUBLISH |" echo "| 升级流程测试 | $UPGRADE |" echo "| 单元测试 | $UNIT |" echo "" - if [[ "$UPGRADE" == "success" && "$UNIT" == "success" ]]; then + if [[ "$PUBLISH" == "success" && "$UPGRADE" == "success" && "$UNIT" == "success" ]]; then echo "✅ 所有升级测试通过!" exit 0 else From 050efcdc81882bcd0550290103af9955b38fbd5a Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Sat, 20 Jun 2026 22:48:23 +0800 Subject: [PATCH 8/9] ci: inline upgrade test jobs into CI gate Reusable workflow via uses: doesn't expose jobs to needs context. Inline publish-sim, skill-upgrade-flow, skill-unit-test directly into ci.yml so gate-result can check their status. Remove workflow_call from test-upgrade.yml (not needed for inline). Keep test-upgrade.yml as standalone workflow for PR/manual triggers. --- .github/workflows/ci.yml | 208 +++++++++++++++++++++++++++-- .github/workflows/test-upgrade.yml | 1 - 2 files changed, 199 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab3f05f..99773e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -419,16 +419,198 @@ jobs: - name: 运行 test.sh run: bash test.sh - # ==================== 升级流程测试 ==================== - upgrade-test: - name: 升级测试 - uses: ./.github/workflows/test-upgrade.yml + # ==================== 发布模拟验证 ==================== + publish-sim: + name: 发布模拟验证 + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: npm pack + working-directory: packages/cli-box-skill + run: | + npm pack + ls -la cli-box-skill-*.tgz + + - name: 验证 tarball 文件清单 + working-directory: packages/cli-box-skill + run: | + TARBALL=$(ls cli-box-skill-*.tgz) + echo "=== 验证 $TARBALL ===" + tar tzf "$TARBALL" | sort + + REQUIRED=("package/postinstall.mjs" "package/installer/cli.mjs" "package/installer/shared.mjs" "package/bin/cli-box-wrapper.js" "package/skill/SKILL.md") + for f in "${REQUIRED[@]}"; do + if tar tzf "$TARBALL" | grep -q "$f"; then + echo "✓ $f" + else + echo "✗ $f MISSING" + exit 1 + fi + done + echo "" + echo "✅ tarball 文件清单验证通过" + + - name: 验证包结构 + working-directory: packages/cli-box-skill + run: | + echo "=== package.json 字段检查 ===" + node -e " + const pkg = require('./package.json'); + const checks = [ + ['name', pkg.name === 'cli-box-skill'], + ['bin.cli-box', !!pkg.bin?.['cli-box']], + ['bin.cli-box-skill', !!pkg.bin?.['cli-box-skill']], + ['files includes postinstall.mjs', pkg.files?.includes('postinstall.mjs')], + ['files includes installer/', pkg.files?.some(f => f.startsWith('installer/'))], + ['optionalDependencies.cli-box-darwin-arm64', !!pkg.optionalDependencies?.['cli-box-darwin-arm64']], + ]; + let ok = true; + for (const [name, pass] of checks) { + console.log(pass ? '✓ ' + name : '✗ ' + name); + if (!pass) ok = false; + } + if (!ok) process.exit(1); + console.log('\n✅ 包结构验证通过'); + " + + # ==================== 升级流程测试 (macOS) ==================== + skill-upgrade-flow: + name: 升级流程测试 + runs-on: macos-latest + timeout-minutes: 10 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: 安装当前 latest 版本 + run: npm install -g cli-box-skill@latest + + - name: 验证 latest 安装 + run: | + echo "=== 验证 cli-box-skill 命令 ===" + cli-box-skill --help + echo "" + echo "=== 验证二进制链接 ===" + ls -la ~/.cli-box/bin/cli-box || echo "⚠ cli-box binary not found" + ls -la ~/.cli-box/bin/cli-box-daemon || echo "⚠ cli-box-daemon binary not found" + + - name: 模拟用户自定义 SKILL.md + run: | + mkdir -p ~/.claude/skills/cli-box + echo "# Custom SKILL.md (user modified)" > ~/.claude/skills/cli-box/SKILL.md + echo "Custom content that should be preserved" >> ~/.claude/skills/cli-box/SKILL.md + echo "" + echo "=== 自定义 SKILL.md 内容 ===" + cat ~/.claude/skills/cli-box/SKILL.md + + - name: 从 PR 分支打包 + working-directory: packages/cli-box-skill + run: | + npm pack + ls -la cli-box-skill-*.tgz + + - name: 运行 upgrade 命令 + working-directory: packages/cli-box-skill + run: | + echo "=== 运行 upgrade(无 daemon)===" + node installer/cli.mjs upgrade --yes 2>&1 | tee /tmp/upgrade-output.log + + - name: 验证升级结果 + run: | + echo "============================================" + echo " 升级结果验证" + echo "============================================" + echo "" + + echo "=== 二进制链接检查 ===" + if [ -L ~/.cli-box/bin/cli-box ]; then + echo "✓ cli-box is a symlink" + ls -la ~/.cli-box/bin/cli-box + else + echo "⚠ cli-box is NOT a symlink" + fi + + if [ -L ~/.cli-box/bin/cli-box-daemon ]; then + echo "✓ cli-box-daemon is a symlink" + ls -la ~/.cli-box/bin/cli-box-daemon + else + echo "⚠ cli-box-daemon is NOT a symlink" + fi + + echo "" + + echo "=== SKILL.md 保留检查 ===" + if [ -f ~/.claude/skills/cli-box/SKILL.md ]; then + CONTENT=$(cat ~/.claude/skills/cli-box/SKILL.md) + if echo "$CONTENT" | grep -q "Custom content that should be preserved"; then + echo "✓ SKILL.md preserved (custom content intact)" + else + echo "✗ SKILL.md was overwritten!" + echo "Expected: 'Custom content that should be preserved'" + echo "Got:" + cat ~/.claude/skills/cli-box/SKILL.md + exit 1 + fi + else + echo "⚠ SKILL.md not found at ~/.claude/skills/cli-box/SKILL.md" + fi + + echo "" + + echo "=== cli-box-skill 命令检查 ===" + cli-box-skill --help 2>&1 | head -5 + + - name: 上传升级日志 + if: always() + uses: actions/upload-artifact@v4 + with: + name: upgrade-output + path: /tmp/upgrade-output.log + retention-days: 7 + + # ==================== cli-box-skill 单元测试 ==================== + skill-unit-test: + name: cli-box-skill 单元测试 + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 安装 Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: 安装依赖 + working-directory: packages/cli-box-skill + run: npm install + + - name: 运行单元测试 + working-directory: packages/cli-box-skill + run: node --test # ==================== 门禁结果汇总 ==================== gate-result: name: 门禁结果 runs-on: ubuntu-latest - needs: [rust-fmt, rust-clippy, rust-test, frontend-test, e2e-test, unified-test, security, upgrade-test] + needs: [rust-fmt, rust-clippy, rust-test, frontend-test, e2e-test, unified-test, security, publish-sim, skill-upgrade-flow, skill-unit-test] if: always() steps: @@ -462,7 +644,9 @@ jobs: E2E_TEST="${{ needs.e2e-test.result }}" UNIFIED_TEST="${{ needs.unified-test.result }}" SECURITY_RESULT="${{ needs.security.result }}" - UPGRADE_TEST="${{ needs.upgrade-test.result }}" + PUBLISH_SIM="${{ needs.publish-sim.result }}" + UPGRADE_FLOW="${{ needs.skill-upgrade-flow.result }}" + SKILL_UNIT="${{ needs.skill-unit-test.result }}" echo "| 检查项 | 状态 |" echo "|-------|------|" @@ -473,7 +657,9 @@ jobs: echo "| Playwright E2E | $E2E_TEST |" echo "| 统一测试 (test.sh) | $UNIFIED_TEST |" echo "| 安全检查 | $SECURITY_RESULT |" - echo "| 升级测试 | $UPGRADE_TEST |" + echo "| 发布模拟验证 | $PUBLISH_SIM |" + echo "| 升级流程测试 | $UPGRADE_FLOW |" + echo "| cli-box-skill 单元测试 | $SKILL_UNIT |" echo "" if [ -f coverage-artifacts/rust-coverage-summary.md ]; then @@ -495,7 +681,9 @@ jobs: "$E2E_TEST" == "success" && \ "$UNIFIED_TEST" == "success" && \ "$SECURITY_RESULT" == "success" && \ - "$UPGRADE_TEST" == "success" ]]; then + "$PUBLISH_SIM" == "success" && \ + "$UPGRADE_FLOW" == "success" && \ + "$SKILL_UNIT" == "success" ]]; then echo "✅ 所有门禁检查通过!" exit 0 else @@ -518,7 +706,9 @@ jobs: 'Playwright E2E': '${{ needs.e2e-test.result }}', '统一测试 (test.sh)': '${{ needs.unified-test.result }}', '安全检查': '${{ needs.security.result }}', - '升级测试': '${{ needs.upgrade-test.result }}', + '发布模拟验证': '${{ needs.publish-sim.result }}', + '升级流程测试': '${{ needs.skill-upgrade-flow.result }}', + 'cli-box-skill 单元测试': '${{ needs.skill-unit-test.result }}', }; const statusEmoji = (status) => status === 'success' ? '✅' : (status === 'skipped' ? '⏭️' : '❌'); diff --git a/.github/workflows/test-upgrade.yml b/.github/workflows/test-upgrade.yml index f9de9a6..24db544 100644 --- a/.github/workflows/test-upgrade.yml +++ b/.github/workflows/test-upgrade.yml @@ -13,7 +13,6 @@ on: pull_request: paths: - 'packages/cli-box-skill/**' - workflow_call: workflow_dispatch: permissions: From 923196b53e8ce3f4e0ea809e56eb779a3c0be1dd Mon Sep 17 00:00:00 2001 From: ZN-Ice Date: Sat, 20 Jun 2026 23:03:07 +0800 Subject: [PATCH 9/9] docs: fix upgrade example version to 0.2.9 0.3.0 doesn't exist; use 0.2.9 (next release after current 0.2.8) as the example version in upgrade command docs. --- README.md | 2 +- packages/cli-box-skill/skill/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7aaedb..9b1d326 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ npx cli-box-skill install claude # claude | opencode | openclaw | all ```bash cli-box-skill upgrade # latest -cli-box-skill upgrade 0.3.0 # specific version +cli-box-skill upgrade 0.2.9 # specific version ``` Stops running sandboxes and daemon before upgrading. Your SKILL.md is preserved. diff --git a/packages/cli-box-skill/skill/SKILL.md b/packages/cli-box-skill/skill/SKILL.md index ce00c5a..2f9155e 100644 --- a/packages/cli-box-skill/skill/SKILL.md +++ b/packages/cli-box-skill/skill/SKILL.md @@ -38,7 +38,7 @@ cli-box-skill upgrade Upgrade to a specific version: ```bash -cli-box-skill upgrade 0.3.0 +cli-box-skill upgrade 0.2.9 ``` > **Note:** `upgrade` does NOT overwrite your SKILL.md. Use `npx cli-box-skill install`