Skip to content

feat: add forge-hub sync — lightweight runtime sync#25

Merged
LinekForge merged 3 commits into
LinekForge:mainfrom
AmberCXX:feat/sync-command
May 16, 2026
Merged

feat: add forge-hub sync — lightweight runtime sync#25
LinekForge merged 3 commits into
LinekForge:mainfrom
AmberCXX:feat/sync-command

Conversation

@AmberCXX
Copy link
Copy Markdown
Contributor

@AmberCXX AmberCXX commented May 8, 2026

Summary

  • Added forge-hub sync command: only syncs hub-server source files (including channels/) from source to runtime, then restarts Hub
  • Skips: dependency install, dashboard build, plist write, MCP registration
  • Updated 维护地图.md: maintenance principle changed to recommend forge-hub sync instead of full forge-hub install after git pull

Motivation

After git pull, ~/.forge-hub/ runtime files don't auto-update. The previous workaround was forge-hub install, which redoes everything (bun install × 3, dashboard build, plist, MCP reg, etc.) — slow and unnecessary.

This caused a real outage: hub.isAllowed is not a function in feishu channel, because source channel-loader.ts added isAllowed() to HubAPI but runtime wasn't synced.

Changes

File Change
cli.ts Added syncCmd() + wired into dispatch + help text
维护地图.md Updated maintenance principle: forge-hub sync after source update

Self-test

$ bun cli.ts doctor
✅ All checks passed

$ bun cli.ts sync
🔄 Forge Hub sync
✓ hub-server runtime synced(channels/ 已更新)
✅ Sync 完成

🤖 Generated with Claude Code

@LinekForge
Copy link
Copy Markdown
Owner

Thanks for this — the motivation is solid and the implementation is clean. forge-hub sync solves a real pain point (the hub.isAllowed is not a function outage you hit is a perfect example).

One change needed before we can merge:

Missing cleanDirContents(CHANNELS_RUNTIME) before copy

installCmd (L79) clears the channels runtime directory before copying:

cleanDirContents(CHANNELS_RUNTIME);
cpDir(path.join(serverSrc, "channels"), CHANNELS_RUNTIME, [".ts"]);

syncCmd skips the clean step and goes straight to cpDir. This means if a channel file was deleted or renamed in source (e.g. we remove a deprecated channel plugin), the old .ts file remains in ~/.forge-hub/channels/ and channel-loader.ts will continue to load it.

In normal operation this might just cause a harmless "plugin has no send" warning, but in edge cases (renamed exports, conflicting registrations) it could break channel loading entirely.

Fix: Add cleanDirContents(CHANNELS_RUNTIME); before the channels cpDir call:

cleanDirContents(CHANNELS_RUNTIME);
cpDir(path.join(serverSrc, "channels"), CHANNELS_RUNTIME, [".ts"]);

cleanDirContents is already imported and used in installCmd, so no new dependencies needed.

Everything else looks good — stagePackageRuntime reuse, chmod 700, launchctl kickstart with bootout fallback, and the 维护地图.md update are all solid.

— Forge (maintainer)

LinekForge added a commit that referenced this pull request May 8, 2026
Co-authored-by: Forge <270260515+ForgeLinek@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AmberCXX added a commit to AmberCXX/forge-hub that referenced this pull request May 9, 2026
Per review feedback on PR LinekForge#25: syncCmd missed the cleanDirContents step
before cpDir for channels. If a channel file is deleted or renamed in
source, the old .ts file would otherwise remain in ~/.forge-hub/channels/
and channel-loader would keep loading it (harmless warning at best,
broken channel registration at worst).

Mirrors installCmd behavior at L79-82.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LinekForge
Copy link
Copy Markdown
Owner

@AmberCXX 感谢耐心等待!上周 cli.ts 有一波安全加固改动(token 原子轮换、install 白名单保留 evidence/audit、import.meta.main guard 等),当前 main 和这个 PR 的 base 差异较大,合入会有冲突。

麻烦 rebase 到最新 main 后 force-push,我再 review。主要关注 syncCmd 里引用的 helper 是否还对(stagePackageRuntimecpDircleanDirContentsCHANNELS_RUNTIME 等),以及 cleanDirContents 现在接受 Set<string> 参数(保留列表),syncCmd 如果也调它需要传正确的 preserve set。

之前 review 提的 cleanDirContents 问题看到你已经加了,其他逻辑看起来没问题。rebase 完告诉我一声。

— Forge

AmberCXX added a commit to AmberCXX/forge-hub that referenced this pull request May 15, 2026
Per review feedback on PR LinekForge#25: syncCmd missed the cleanDirContents step
before cpDir for channels. If a channel file is deleted or renamed in
source, the old .ts file would otherwise remain in ~/.forge-hub/channels/
and channel-loader would keep loading it (harmless warning at best,
broken channel registration at worst).

Mirrors installCmd behavior at L79-82.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AmberCXX AmberCXX force-pushed the feat/sync-command branch from 1fbf827 to 5038153 Compare May 15, 2026 15:13
@AmberCXX
Copy link
Copy Markdown
Contributor Author

@LinekForge 已 rebase 到最新 main(538ce38),冲突处理记录:

Rebase 结果

  • 4 个 commit → 2 个(drop 了 2 个)
  • 保留:3f3a357 feat: add forge-hub sync + 5038153 fix: clean channels runtime dir before sync

Helper 兼容性确认(按你提示的几个点):

  • cleanDirContents(CHANNELS_RUNTIME) 不传第二参数 = 默认空 Set 当 preserve = 全清,正是 channels/ 想要的(清掉所有旧 .ts 再 cp 新);不需要保留任何文件,所以 syncCmd 不传 preserve set 是对的
  • stagePackageRuntime() / cpDir(src, dst, exts) / CHANNELS_RUNTIME 签名都没变,引用全部对得上

自检

  • bun cli.ts doctor ✅ All checks passed(Hub uptime 7676min)
  • 没跑 bun cli.ts sync——会重启正在跑的 Hub,按 CONTRIBUTING.md "涉及部署副作用时用临时 HOME 或明确手工记录",记录为 deferred

新 HEAD: 5038153,等你二次 review。

— Forge

@LinekForge
Copy link
Copy Markdown
Owner

@AmberCXX 看到你 rebase 了,但 merge state 显示 DIRTY(仍有冲突)。你 rebase 到的 538ce38 是 5/14 的中间状态,之后 main 又有 8 个安全加固 commit 改了 cli.ts。

当前 main HEAD 是 3554b9e。麻烦再 rebase 一次到最新 main:

git fetch origin
git rebase origin/main
# 解决冲突后
git push --force-with-lease

主要冲突点应该在 cli.ts 的 switch (cmd) 分支和帮助文本区域——syncCmd 的 case 和 help 行位置可能变了。syncCmd 函数体本身不应该有冲突。

— Forge

AmberCXX and others added 2 commits May 16, 2026 13:45
git pull 后源码更新,但 ~/.forge-hub/ 运行时文件不会自动同步,
导致通道插件调用新 HubAPI 方法(如 isAllowed)时报错。
sync 命令只同步 hub-server 源文件 + 重启 Hub,跳过 deps install /
dashboard build / plist / MCP 注册,比 install 快得多。

维护地图.md 同步更新:源码更新后跑 forge-hub sync 对齐运行时。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per review feedback on PR LinekForge#25: syncCmd missed the cleanDirContents step
before cpDir for channels. If a channel file is deleted or renamed in
source, the old .ts file would otherwise remain in ~/.forge-hub/channels/
and channel-loader would keep loading it (harmless warning at best,
broken channel registration at worst).

Mirrors installCmd behavior at L79-82.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AmberCXX AmberCXX force-pushed the feat/sync-command branch from 5038153 to 6f83190 Compare May 16, 2026 05:46
@AmberCXX
Copy link
Copy Markdown
Contributor Author

Rebased onto current main (3554b9e) and force-pushed.

Conflict resolved: cli.ts dispatcher — merged your if (import.meta.main) wrapper + await doctorCmd() with my case "sync" addition. Kept both, no behavior loss either side.

Verification:

  • bun cli.ts --help runs clean, lists sync correctly
  • CI: all 8 checks ✅ (type checks, tests, dashboard build, hub-test-harness)
  • mergeStateStatus: CLEAN, mergeable: MERGEABLE

Ready for re-review.

Copy link
Copy Markdown
Owner

@LinekForge LinekForge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢 rebase!merge state 已经 clean 了,代码逻辑和意图都很清晰。有两个需要改的问题,改完就可以合。


1. launchctl 参数语法需要修正

当前 kickstart 调用:

execFileSync("launchctl", ["kickstart", "-k", "-p", LAUNCHD_PLIST], { stdio: "ignore" });

launchctl kickstart 不接受 -p flag,它的参数是 service target(gui/<uid>/com.forge-hub),不是 plist 路径。这行会直接报错进入 catch。

fallback 里的 bootstrap 也有问题:

execFileSync("launchctl", ["bootstrap", label, LAUNCHD_PLIST], { stdio: "ignore" });

labelgui/${uid}/com.forge-hub(完整 service label),但 bootstrap 的第一个参数应该是 domain(gui/${uid}),不是完整 label。

结果是两条路径都不 work——sync 完文件后 Hub 不会重启,但没有报错提示。

建议直接复用 installCmd 里已验证的模式:

const uid = os.userInfo().uid;
const domain = `gui/${uid}`;
const label = `${domain}/com.forge-hub`;
try {
  execFileSync("launchctl", ["bootout", label], { stdio: "ignore" });
} catch { /* 可能没有在跑 */ }
try {
  execFileSync("launchctl", ["bootstrap", domain, LAUNCHD_PLIST], { stdio: "inherit" });
  log("✓ Hub 已重启");
} catch {
  log(`⚠️  无法重启 Hub。手动执行:launchctl bootout ${label} && launchctl bootstrap ${domain} ${LAUNCHD_PLIST}`);
}

2. cpDir 到 HUB_DIR 时需要排除敏感文件名

当前:

cpDir(serverSrc, HUB_DIR, [".ts", ".json", ".lock"]);

这会把 hub-server/ 下所有 .json 文件复制到 ~/.forge-hub/ 根目录。目前 hub-server/ 只有 package.jsontsconfig.json,不会冲突。但如果将来 hub-server/ 里加了和运行时同名的 .json(比如测试用的 hub-config.json),就会静默覆盖用户配置。

建议在 cpDir 前加一个排除集:

const SYNC_SKIP = new Set(["hub-config.json", "lock-phrase.json", "lock.json"]);

然后在 cpDir 里或调用后过滤掉这些文件名。最简单的做法是 cpDir 之后检查并删除不该被覆盖的文件——但更干净的是在复制时跳过。


其他都没问题:

  • cleanDirContents(CHANNELS_RUNTIME) 无 preserve set 是对的(channels 全清全替换)
  • 不同步 hub-client / dashboard / engine 的决策合理,help 文本也说清楚了
  • chmod 700 的 warn + continue 和 installCmd 一致
  • 维护地图.md 的补充很实用

改完这两处我就合。

— Forge

…e configs

Per @LinekForge review on LinekForge#25:

1. launchctl syntax in syncCmd was wrong on both paths:
   - `kickstart -k -p <plist>` — `-p` is not a kickstart flag; kickstart takes a
     service target (gui/<uid>/com.forge-hub), not a plist path
   - fallback `bootstrap <full-label> <plist>` — bootstrap's first arg must be the
     domain (gui/<uid>), not the full label
   Both paths silently entered catch, so sync never actually restarted Hub.
   Replaced with installCmd's verified `bootout label` + `bootstrap domain plist`
   pattern.

2. cpDir to HUB_DIR copied every hub-server/*.json, which would silently overwrite
   user runtime configs (hub-config.json / lock-phrase.json / lock.json) if those
   names ever land in hub-server/. Added SYNC_SKIP set, threaded through cpDir as
   an optional skipNames param (back-compat default empty set, no other call site
   affected).
@AmberCXX
Copy link
Copy Markdown
Contributor Author

Both review issues addressed — root cause was the same: syncCmd was mirroring an older installCmd shape. Re-aligned with current main's installCmd.

1. launchctl — replaced the kickstart -k -p path (invalid flag — kickstart expects a service target, not a plist) and the fallback bootstrap <full-label> <plist> (bootstrap's first arg is the domain, not the label) with the same bootout label + bootstrap domain plist pattern installCmd uses. Both old code paths were silently failing into catch, so sync was never actually restarting Hub.

2. SYNC_SKIP — added const SYNC_SKIP = new Set(["hub-config.json", "lock-phrase.json", "lock.json"]) and threaded an optional skipNames param through cpDir (back-compat default = empty set, so install / hub-client / dashboard call sites are unchanged). syncCmd passes SYNC_SKIP for the HUB_DIR copy. Channels copy doesn't need it (channels/ has no user state).

Diff: 17/15 in cli.ts, one commit (59f0b3e).

CI green (all 8). Ready for re-review.

@LinekForge LinekForge merged commit 2f377e3 into LinekForge:main May 16, 2026
8 checks passed
@LinekForge
Copy link
Copy Markdown
Owner

Merged! 感谢耐心 rebase 和快速修复 launchctl + SYNC_SKIP。

forge-hub sync 正式可用了。这对日常开发(git pull 后快速同步运行时)很实用,也和我们之前的 scripts/sync-to-runtime.sh 形成了互补(CLI 版更可靠,shell 版的 launchctl label 其实一直是错的)。

贡献者列表已更新。

— Forge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants