Skip to content

refactor(cli): 拆 17 个 handle* 到 commands/<name>.ts,index.ts 改为 dispatcher#76

Merged
lizhiyao merged 8 commits intodevelopfrom
refactor/cli-command-modularization
May 4, 2026
Merged

refactor(cli): 拆 17 个 handle* 到 commands/<name>.ts,index.ts 改为 dispatcher#76
lizhiyao merged 8 commits intodevelopfrom
refactor/cli-command-modularization

Conversation

@lizhiyao
Copy link
Copy Markdown
Owner

@lizhiyao lizhiyao commented May 4, 2026

Summary

  • src/cli/index.ts 1748 → 99 行,纯 dispatcher(domain/command 路由 + i18n + 顶层错误处理)
  • 17 个内联 handle* 函数全部抽到 src/cli/commands/<name>.ts,每个文件 export 一个 execute(argv: string[])
  • 共享类型 / helper(EvalResult / ReportServer / requireEvaluationReport / parseLastWindow)下沉到 src/cli/commands/_shared.ts

用户影响

无。纯代码组织重构:

  • CLI 命令、flag、输出、exit code 全部不变
  • 测量学不变量未触碰:judge prompt、Report schema、五层评分管道、Bootstrap CI、Krippendorff α、length-debias toggle 全部未改
  • yarn lint && yarn build && yarn test 全绿(1062/1062)

迁移说明

无。对外 CLI 表面无变化。后续添加新命令的工序:

  1. 新建 src/cli/commands/<name>.ts,export async function execute(argv)
  2. src/cli/index.ts 加一行 import + 一个 case

文件清单

src/cli/commands/
  _shared.ts            # 共享类型 + helper
  run.ts  doctor.ts  report.ts  init.ts  gate.ts  gen-samples.ts
  evolve.ts  diff.ts  gold.ts  debias-validate.ts  saturation.ts
  verdict.ts  diagnose.ts  failures.ts  analyze.ts

🤖 Generated with Claude Code

…cher

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/cli/commands/report.ts Outdated
if (values.dev && !process.env.__OMK_DEV_CHILD) {
const { spawn } = await import('node:child_process');
const { fileURLToPath } = await import('node:url');
const cliPath: string = fileURLToPath(import.meta.url);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

P2: --dev 分支里 cliPath 现在会指向 commands/report.ts 对应的编译产物,而不是 CLI 入口。child args 会变成类似 node --watch-path ... dist/src/cli/commands/report.js bench report ...;这个 module 只 export execute,没有 top-level main(),所以子进程会直接退出,omk bench report --dev 不会启动 report server。这里需要恢复为真正的 CLI entrypoint(例如从 command module resolve 到 ../index.js,或使用当前 process.argv[1]),最好再补一个覆盖 --dev spawn argv 的回归测试。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

已修,commit 2a1f4f1

const cliPath = resolve(fileURLToPath(import.meta.url), '..', '..', 'index.js');

import.meta.url 指 commands/report.js,往上两层到 cli/index.js — 这是真 CLI 入口,child 跑那里的 main()。

补了 test/cli/dev-spawn-argv.test.ts,mock spawn 断言 child argv 的 entrypoint 是 cli/index.js 不是 commands/report.js — 以后有人「简化」回 fileURLToPath(import.meta.url) 直接 fail。

附带观察(不在本 PR 修):libDir = resolve(cliPath, '..', 'lib') 算出来是 dist/src/cli/lib,这个目录不存在,是重构前就有的。--dev 启动不会 crash(node --watch-path 对不存在路径静默),但 hot reload 应该一直没在工作。可以另开 fix PR 处理(建议改成 dist/src/)。

@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

补充建议(不阻塞):

  1. bench report --dev 修复后建议补一个定向回归测试,断言 spawn 出去的 entrypoint 是 CLI index.js 而不是 commands/report.js。现有 test/cli.test.ts 只覆盖 help / dry-run,抓不到这类 child process argv 回归。

  2. 这次拆成 17 个 commands/<name>.ts 后,后续可以加一个轻量 dispatcher smoke test 或命令 registry 测试,覆盖 index.ts 里每个公开命令都能路由到对应 module。这样以后新增/移动 command 时,不容易出现文件存在但 switch 没接上的问题。

除前面那条 P2 外,我没有再看到需要阻塞合并的行为问题。

CR P2 修:commands/report.ts 里 import.meta.url 指向 commands/report.js,那个
module 只 export execute 没 main(),spawn 出去的 child 立刻退出,--dev 静默坏
掉。改成 resolve(url, '..', '..', 'index.js') 算回 CLI 入口。

补两层测试:
- dev-spawn-argv.test.ts:mock spawn,断言 child argv 的 entrypoint 是
  cli/index.js,不是 commands/report.js
- dispatcher-routing.test.ts:遍历 commands/*.ts,断言每个 module 都被
  index.ts import,防止以后加文件忘接 case

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

建议两条都 follow up 了,commit 2a1f4f1

  1. --dev spawn 回归:test/cli/dev-spawn-argv.test.ts,vi.mock spawn,断言 child argv 第三项(cliPath)匹配 cli/index.js$,且不含 commands/
  2. dispatcher smoke:test/cli/dispatcher-routing.test.ts,遍历 commands/*.ts(除 _shared),断言每个 module 都被 index.ts import。这层只验 import 行,不验 case 名(analyze/doctor 走 domain 分支不在 switch 里),但够兜「文件存在但忘接」这种回归。

P2 inline thread 里也回了 — 顺带看到 libDir 指向不存在的 dist/src/cli/lib 是重构前的旧 bug,--dev 的 hot reload 一直没在工作。建议另开 fix PR,不混本次 refactor。

附带发现:原 libDir = resolve(cliPath, '..', 'lib') = dist/src/cli/lib,那个
目录不存在,node --watch-path 对不存在路径静默,--dev 启动后 hot reload 一直
没在工作。

改成 resolve(cliPath, '..', '..') = dist/src/,覆盖整个编译产物根 (server /
renderer / eval-core 等 report server 依赖的 module 都在那)。

测试加一条断言:libDir 不能以 /lib 结尾,防止有人「恢复」回旧值。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

顺手把 libDir 也修了,commit bb2f777

const libDir: string = resolve(cliPath, '..', '..');  // dist/src/

旧值 dist/src/cli/lib 不存在,--dev 启动不会 crash 但 hot reload 一直没在工作。改成 dist/src/ 覆盖整个编译产物根(server / renderer / eval-core 都在那,report server 依赖它们)。

dev-spawn-argv 测试加了一条断言 libDir 不以 /lib 结尾,防止以后被「恢复」。

index.ts 99 → 43 行,原来 17 个 case 的 switch 换成 BENCH_COMMANDS /
DOMAIN_COMMANDS 两张 Record 表。新增子命令只要在 registry 里加一行,不再
回 index.ts 改 switch + import 两处。

dispatcher smoke test 同步改成验「文件 ⊆ registry」+「每个 entry 有 execute」,
比之前扫 import 行更直接,registry 自己漏注册也能抓到。

CommandModule 形状暂时只 { execute },--help 集中化 / process.exit 收敛走
follow-up。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

按短期路线把 registry 也并进来了,commit 26e4b07

  • src/cli/commands/registry.ts — 两张 Record<string, CommandModule>BENCH_COMMANDS(13 个 bench 子命令)+ DOMAIN_COMMANDS(analyze / doctor)
  • src/cli/index.ts 99 → 43 行,17-case switch 换成查表。新增子命令只在 registry 加一行,不再回 index.ts 改两处
  • dispatcher-routing.test.ts 改成验「commands 文件 ⊆ registry 注册」+「每个 entry 有 execute 函数」,比之前扫 import 行更直接。registry 自己漏注册也能抓到

CommandModule 形状先只 { execute }--help 集中化和 process.exit 收敛走 follow-up——前者要把 15 处 if (argv.includes('--help')) 抽出来加 helpKey,后者动到所有 handler 改 exit code → throw / return,工作量都大,单独排期更稳。

lizhiyao and others added 2 commits May 4, 2026 23:05
CommandModule 加 helpKey: CliMessageKey 字段,index.ts dispatcher 在调
execute 前扫 --help / -h,命中就 print + exit 0。15 个 command 文件里 11 处
inline `if (argv.includes('--help'))` 全部删除。

顺手修两个旧 bug:

1. `omk bench evolve --help` / `omk bench init --help` 之前会被
   parseArgsStrictOrExit 当 unknown option 报错(它俩没像 run/report 那样
   inline 拦 --help)。dispatcher 集中处理后任何子命令都响应。
2. `omk bench gate --some-flag --help` 之前只在 argv[0] 位置看 --help,中间
   位置会进 parseArgs 报错。dispatcher 扫整个 argv,任何位置都响应。

回归测试覆盖三条改进。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
新加 src/cli/cli-exit.ts 暴露 CliExit Error 类。command 文件 / parse-strict /
_shared 里 ~62 处 process.exit(N) 替换为 throw new CliExit(N)。index.ts main()
顶层 .catch 把 CliExit 转回 process.exit(code),其他错误打印 stack + exit 1。

唯一保留 process.exit 的地方:bench report --dev 子进程 spawn 的 exit listener
(commands/report.ts) — 那是 async callback, 不在 main 调用栈,throw 出去
没人 catch。注释里写明原因。

收益:execute() 单测可以直接 try/catch 命中 CliExit 拿 exit code,不再 kill
测试进程。补 test/cli/cli-exit.test.ts 演示三条:verdict 缺 reportId →
CliExit(1) / gold 缺子命令 → CliExit(1) / parseArgsStrictOrExit 未知 flag →
CliExit(2)。

行为不变:CLI 用户感知层面所有 exit code 完全一致,bench / doctor / verdict
等的 0/1/2 语义全部保留。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

短期路线最后两件事都做完,分两个 commit:

b9d8c43--help 集中化 + helpKey 元数据

  • CommandModulehelpKey: CliMessageKey
  • dispatcher (index.ts) 在调 execute 前扫 --help / -h,命中就 print + exit 0
  • 删 11 处 inline if (argv.includes('--help'))
  • 顺手修两个旧 bug:
    • bench evolve --help / bench init --help 之前会被 parseArgsStrictOrExit 当 unknown option 报错(这俩没 inline 拦),现在响应 main help
    • bench gate --foo --help 之前只查 argv[0],中间位置的 --help 会被 parseArgs 拒,现在任何位置都响应

11f6c84process.exitthrow CliExit

  • 新加 src/cli/cli-exit.ts 暴露 CliExit Error 类
  • 命令文件 / parse-strict / _shared 里 ~62 处 process.exit(N) 替换为 throw new CliExit(N)
  • index.ts main().catch 兜底:CliExit → process.exit(code),其他 throw 打印 stack + exit 1
  • 唯一保留 process.exit 的:bench report --dev 子进程 spawn 的 exit listener(async callback,不在 main 调用栈)
  • execute() 现在可以单测直接 try/catch 命中 CliExit 拿 code,不 kill 测试进程
  • test/cli/cli-exit.test.ts 演示三条 path

CLI 用户感知 exit code 完全一致(0/1/2 全部保留)。1071/1071 测试全绿。

Comment thread src/cli/commands/gate.ts
throw new CliExit(0);
}
throw new CliExit(1);
} catch (err: unknown) {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

P1: 这里会把上面正常的 throw new CliExit(0) / throw new CliExit(1) 全部吞掉并改成 exit 1。process.exit(0) 改成 CliExit 后不再会绕过 catch,所以 gate 的 dry-run、PROGRESS、SOLO PASS 路径都会进入这个 catch,打印 Error: CliExit(0),最终失败。omk bench gate && deploy 会因此在通过时也挡住。这里需要先 if (err instanceof CliExit) throw err;,只把真实运行错误包装成 CliExit(1),并补一个 gate pass/dry-run 的 exit-code 回归测试。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

P1 + P3 都修了,commit f908ac6

gate / run / evolve / doctor 的 catch 全部加 if (err instanceof CliExit) throw err;,只把真正运行时错误包成 CliExit(1)。gate 是真 bug——verdict 路径的 CliExit(0) 之前会被 catch 改成 CliExit(1),PROGRESS / SOLO PASS / dry-run 全 exit 1,omk bench gate && deploy 在 PASS 时也会挡住部署。

parseJudgeModelsArgOrExit 改成 throw new CliExit(2)(之前漏改)。

回归测试两条:

  • test/cli.test.tsbench gate --dry-run 退 0(反例:旧代码会被 catch 改成 exit 1)
  • test/cli/cli-exit.test.tsparseJudgeModelsArgOrExit('claude')CliExit(2)

1073/1073 全绿。

@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

补充一个 P3(一致性,不阻塞):

CliExit 收敛还漏了 src/cli/parse-run-config.ts 里的 parseJudgeModelsArgOrExit(),现在它仍然直接 process.exit(2)。所以 bench run --judge-models badbench evolve --judge-models badbench failures --judge-models badbench debias-validate ... --judge-models bad 这些路径在直接单测 execute() 时仍会 kill 测试进程,和本 PR 「execute() 可以 try/catch CliExit」的目标不完全一致。

建议把这个 helper 也改成 throw new CliExit(2),并补一条 invalid --judge-modelsCliExit(2) 测试。CLI 用户侧 exit code 不变。

gate / run / evolve / doctor 的 try/catch 之前会把 CliExit 一起当 generic
错误吞掉,再 throw CliExit(1)。gate 这条最严重 — verdict 路径 dry-run /
PROGRESS / SOLO PASS 都 throw CliExit(0),全部被 catch 改成 exit 1,
`omk bench gate && deploy` 在 PASS 时也会挡住部署。

四个 catch 都加 `if (err instanceof CliExit) throw err;`,只把真实运行时
错误包成 CliExit(1)。

parseJudgeModelsArgOrExit 之前漏改,仍直接 process.exit(2)。改成
throw new CliExit(2),让 bench run / evolve / failures / debias-validate
在带非法 --judge-models 单测时也不会 kill 测试进程。

回归测试:
- bench gate --dry-run 退 0 (反例:之前会被 generic catch 改成 exit 1)
- parseJudgeModelsArgOrExit('claude') 抛 CliExit(2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lizhiyao
Copy link
Copy Markdown
Owner Author

lizhiyao commented May 4, 2026

复查最新 head f908ac6:前面两条已解决。

  • P1 gateCliExit(0/1) 被 catch 吞掉的问题已修:gate / run / evolve / doctor 的 catch 都先透传 CliExit
  • P3 parseJudgeModelsArgOrExit() 仍直接 process.exit(2) 的一致性问题已修:现在改为 throw new CliExit(2),并补了 invalid --judge-models 测试。
  • bench gate --dry-run exits 0 回归测试覆盖了这次最关键的 gate pass-through 场景。

我本地跑了定向测试:yarn test test/cli.test.ts test/cli/cli-exit.test.ts test/cli/dev-spawn-argv.test.ts test/cli/dispatcher-routing.test.ts,21/21 pass。GitHub CI Node 22 / 24 也都 pass。

本轮没有新的阻塞发现。

自审 PR diff 时发现两类残留:

1. gen-samples / gold 的 catch 也是「catch + throw CliExit(1) without
   instanceof guard」,跟 gate / run / evolve / doctor 同性质。lib 调用
   (generateSamples / initGoldDataset)目前不抛 CliExit,所以不是真 bug,
   但加 guard 保持 6 个 catch 行为一致。
2. 拆 commands 之后,parse-run-config.ts 注释 / layer-gates.ts 注释 /
   README.md / README.zh.md 里仍有 handleRun / handleGate 旧名引用。
   改成 commands/run.ts / commands/gate.ts。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lizhiyao lizhiyao merged commit ee6a202 into develop May 4, 2026
2 checks passed
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.

1 participant