diff --git a/cli.ts b/cli.ts index 987064c..c71a498 100755 --- a/cli.ts +++ b/cli.ts @@ -47,6 +47,10 @@ export const HUB_INSTALL_PRESERVE_ENTRIES = [ ".DS_Store", ]; +// 用户运行时配置——sync 时绝不允许从源码 cpDir 覆盖。 +// (install 走 cleanDirContents preserve set,sync 不 clean 但要防同名 .json 静默覆盖。) +const SYNC_SKIP = new Set(["hub-config.json", "lock-phrase.json", "lock.json"]); + // 找包根目录(从 cli.ts 的位置反推) const PKG_ROOT = path.dirname(fileURLToPath(import.meta.url)); @@ -282,6 +286,51 @@ function installCmd(): void { `); } +// ── sync ──────────────────────────────────────────────────────────────────── + +function syncCmd(): void { + log("🔄 Forge Hub sync\n"); + + // 1. Re-stage package snapshot from current source + const packageRoot = stagePackageRuntime(); + const serverSrc = path.join(packageRoot, "hub-server"); + + // 2. Copy hub-server .ts/.json/.lock files to runtime + // SYNC_SKIP 防止 hub-server/ 下未来若出现同名 .json 静默覆盖用户运行时配置(hub-config.json 等)。 + cpDir(serverSrc, HUB_DIR, [".ts", ".json", ".lock"], SYNC_SKIP); + cleanDirContents(CHANNELS_RUNTIME); + cpDir(path.join(serverSrc, "channels"), CHANNELS_RUNTIME, [".ts"]); + + // Security: maintain 700 permissions + try { + fs.chmodSync(HUB_DIR, 0o700); + fs.chmodSync(CHANNELS_RUNTIME, 0o700); + } catch (err) { + console.warn(`⚠️ chmod 700 失败: ${String(err)}`); + } + log("✓ hub-server runtime synced(channels/ 已更新)"); + + // 3. Restart hub via launchctl + // 复用 installCmd 的 bootout + bootstrap 模式(kickstart -k -p 不是合法 launchctl 语法; + // bootstrap 第一参数是 domain 不是完整 label)。 + if (os.platform() === "darwin") { + const uid = os.userInfo().uid; + const domain = `gui/${uid}`; + const label = `${domain}/com.forge-hub`; + try { + execFileSync("launchctl", ["bootout", label], { stdio: "ignore" }); + } catch { /* might not be bootstrapped */ } + try { + execFileSync("launchctl", ["bootstrap", domain, LAUNCHD_PLIST], { stdio: "inherit" }); + log("✓ Hub 已重启"); + } catch { + log(`⚠️ 无法重启 Hub。手动执行:launchctl bootout ${label} && launchctl bootstrap ${domain} ${LAUNCHD_PLIST}`); + } + } + + log("\n✅ Sync 完成。hub-server 运行时已对齐源码。"); +} + // ── uninstall ─────────────────────────────────────────────────────────────── function uninstallCmd(): void { @@ -436,10 +485,11 @@ async function doctorCmd(): Promise { // ── helpers ───────────────────────────────────────────────────────────────── -function cpDir(src: string, dst: string, exts: string[]): void { +function cpDir(src: string, dst: string, exts: string[], skipNames = new Set()): void { if (!fs.existsSync(src)) die(`source 不存在: ${src}`); fs.mkdirSync(dst, { recursive: true }); for (const f of fs.readdirSync(src)) { + if (skipNames.has(f)) continue; const sp = path.join(src, f); const dp = path.join(dst, f); const stat = fs.statSync(sp); @@ -680,6 +730,9 @@ if (import.meta.main) { case "install": installCmd(); break; + case "sync": + syncCmd(); + break; case "uninstall": uninstallCmd(); break; @@ -696,6 +749,7 @@ USAGE: COMMANDS: install 一键部署到 ~/.forge-hub/ + ~/.claude/channels/hub/ + launchd + MCP 注册 + sync 仅同步 hub-server 运行时文件(git pull 后跑这个,不重装 deps/dashboard) uninstall 反向操作(保留 state) doctor 诊断 install 状态 + connectivity --help 显示此帮助 diff --git "a/\347\273\264\346\212\244\345\234\260\345\233\276.md" "b/\347\273\264\346\212\244\345\234\260\345\233\276.md" index ec0e4ad..c2c0868 100644 --- "a/\347\273\264\346\212\244\345\234\260\345\233\276.md" +++ "b/\347\273\264\346\212\244\345\234\260\345\233\276.md" @@ -158,3 +158,4 @@ Claude Code permission_request - 安全相关失败默认 fail closed;如果不能 fail closed,文档和日志必须讲清代价。 - 安装和运行时路径不要靠记忆,查 [部署.md](部署.md) 和 [运行时状态.md](运行时状态.md)。 - Preview / experimental 可以探索,但不要让默认安装路径变复杂。 +- **源码更新后跑 `forge-hub sync` 对齐运行时**。git pull / 切换分支后 `~/.forge-hub/` 文件不会自动更新,源码与运行时版本不一致会导致通道插件报错。`forge-hub sync` 只同步 hub-server 源文件 + 重启 Hub,不重装 deps/dashboard/plist/MCP。