Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -436,10 +485,11 @@ async function doctorCmd(): Promise<void> {

// ── helpers ─────────────────────────────────────────────────────────────────

function cpDir(src: string, dst: string, exts: string[]): void {
function cpDir(src: string, dst: string, exts: string[], skipNames = new Set<string>()): 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);
Expand Down Expand Up @@ -680,6 +730,9 @@ if (import.meta.main) {
case "install":
installCmd();
break;
case "sync":
syncCmd();
break;
case "uninstall":
uninstallCmd();
break;
Expand All @@ -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 显示此帮助
Expand Down
1 change: 1 addition & 0 deletions 维护地图.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。
Loading