From ffbeff116247d589201b743969e24bfa262e9263 Mon Sep 17 00:00:00 2001 From: Norvera <279165889+NorveraFlorent@users.noreply.github.com> Date: Sat, 30 May 2026 14:39:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20install/sync=20=E9=80=92=E5=BD=92?= =?UTF-8?q?=E6=8B=B7=20hub-server=20=E5=AD=90=E7=9B=AE=E5=BD=95=EF=BC=8C?= =?UTF-8?q?=E6=A0=B9=E6=B2=BB=E6=BC=8F=E6=8B=B7=20routes/=20=E8=87=B4=20Hu?= =?UTF-8?q?b=20100%=20=E5=B4=A9=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 为什么 install/sync 用非递归的 cpDir 拷 hub-server,只拷顶层 + 显式 channels/, 静默跳过 routes/。endpoints.ts 启动时 import './routes/health.js',运行时 缺该目录 → Hub 启动 100% 崩,KeepAlive 陷崩溃循环刷爆 stderr。根因是 endpoints.ts 拆分路由引入 routes/ 子目录后,installer 的硬编码拷贝清单没跟上。 ## 改了什么 - copyFilteredTree 增加 skipNames 参数(与 cpDir 对齐)。 - install/sync 的 hub-server 拷贝从非递归 cpDir 换成递归 copyFilteredTree, 排除 channels/(仍走原有 clean + chmod 700 路径),sync 额外保留 SYNC_SKIP。 自动覆盖 routes/ 及未来任何新增子目录,根治"加子目录忘改 installer"的回归。 - doctor 增加 "Hub routes/ deployed" 校验(检 routes/health.ts),避免漏拷后 仍报 All checks passed(#38 建议,credit AmberCXX)。 ## 没改什么 - channels/ 的 clean + chmod 700 安全路径不动。 - sync 的 SYNC_SKIP 用户配置保护语义不变。 ## 怎么验 - 临时 HOME 跑 doctor:缺 routes/ → ✗ 并给修复提示;补上 → ✓。 - 复刻补丁后的递归拷贝跑真实 hub-server:routes/ 6 文件全落、channels/ 正确排除。 - bun hub-test-harness/harness.ts:8/8 通过(无回归)。 Co-Authored-By: Claude Opus 4.8 --- cli.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cli.ts b/cli.ts index 2b57e1d..62d8dab 100755 --- a/cli.ts +++ b/cli.ts @@ -96,7 +96,10 @@ function installCmd(): void { cleanDirContents(HUB_DIR, new Set(HUB_INSTALL_PRESERVE_ENTRIES)); cleanDirContents(CHANNELS_RUNTIME); const serverSrc = path.join(packageRoot, "hub-server"); - cpDir(serverSrc, HUB_DIR, [".ts", ".json", ".lock"]); + // 递归拷 hub-server(含 routes/ 及未来任何子目录)——非递归 cpDir 会静默跳过 + // routes/,导致 endpoints.ts import './routes/health.js' 时 100% 崩(#35)。 + // channels/ 单独走下面的 clean + chmod 700 路径,故这里排除避免重复拷。 + copyFilteredTree(serverSrc, HUB_DIR, [".ts", ".json", ".lock"], new Set(["channels"])); cpDir(path.join(serverSrc, "channels"), CHANNELS_RUNTIME, [".ts"]); // Security (redteam B3): chmod 700 CHANNELS_RUNTIME——防其他 user-level 进程 // 写入恶意 plugin。fs.watch 已默认关闭(hub-server/channel-loader.ts), @@ -313,8 +316,10 @@ function syncCmd(): void { 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); + // 递归拷(含 routes/ 及未来任何子目录)——非递归会漏 routes/ 致 Hub 启动崩(#35)。 + // SYNC_SKIP 防止 hub-server/ 下同名 .json 静默覆盖用户运行时配置(hub-config.json 等); + // channels/ 单独 clean + 拷,故一并排除。 + copyFilteredTree(serverSrc, HUB_DIR, [".ts", ".json", ".lock"], new Set([...SYNC_SKIP, "channels"])); cleanDirContents(CHANNELS_RUNTIME); cpDir(path.join(serverSrc, "channels"), CHANNELS_RUNTIME, [".ts"]); @@ -445,6 +450,9 @@ async function doctorCmd(): Promise { check("Bun installed", which("bun") !== null, "https://bun.sh"); check("Hub server runtime", fs.existsSync(path.join(HUB_DIR, "hub.ts")), "跑 forge-hub install"); + // routes/ 是 endpoints.ts 启动时 import 的子目录,漏拷则 Hub 100% 崩(#35)。 + // 单独校验避免 install/sync 漏拷后 doctor 仍报 All checks passed。 + check("Hub routes/ deployed", fs.existsSync(path.join(HUB_DIR, "routes", "health.ts")), "routes/ 漏拷——跑 forge-hub install/sync 重新部署"); check("Hub client runtime", fs.existsSync(path.join(HUB_CLIENT_RUNTIME, "hub-channel.ts")), "跑 forge-hub install"); check("LaunchAgent plist", os.platform() !== "darwin" || fs.existsSync(LAUNCHD_PLIST), "Mac 上跑 forge-hub install"); check("MCP registered", isMcpRegistered(), "Hub channel 没在 ~/.claude.json"); @@ -524,15 +532,16 @@ export function cleanDirContents(dir: string, preserve = new Set()): voi } } -function copyFilteredTree(src: string, dst: string, exts: string[]): void { +function copyFilteredTree(src: string, dst: string, exts: string[], skipNames = new Set()): void { if (!fs.existsSync(src)) die(`source 不存在: ${src}`); fs.mkdirSync(dst, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue; + if (skipNames.has(entry.name)) continue; const sp = path.join(src, entry.name); const dp = path.join(dst, entry.name); if (entry.isDirectory()) { - copyFilteredTree(sp, dp, exts); + copyFilteredTree(sp, dp, exts, skipNames); continue; } if (entry.isFile() && exts.some((ext) => entry.name.endsWith(ext))) {