@oj-module/cli 0.2.23 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/devkit/manual.md +7 -5
  2. package/devkit/module-dev-and-cdn-playground.md +10 -10
  3. package/package.json +4 -4
  4. package/shell-dist/assets/runtime.js +5 -5
  5. package/shell-dist/index.html +2 -2
  6. package/shell-dist/versions.json +3 -3
  7. package/src/__tests__/build-backend.test.ts +13 -13
  8. package/src/__tests__/cli.test.ts +225 -0
  9. package/src/__tests__/layout.test.ts +8 -5
  10. package/src/__tests__/new-scaffold.test.ts +59 -0
  11. package/src/__tests__/usage.test.ts +45 -47
  12. package/src/build.ts +4 -4
  13. package/src/bundle.ts +9 -9
  14. package/src/cli.ts +265 -0
  15. package/src/contract/check.ts +12 -5
  16. package/src/dev.ts +17 -0
  17. package/src/index.ts +31 -211
  18. package/src/init.ts +2 -4
  19. package/src/keygen.ts +0 -19
  20. package/src/layout.ts +11 -11
  21. package/src/lifecycle/__tests__/authoring.test.ts +3 -14
  22. package/src/lifecycle/__tests__/bundle.test.ts +24 -24
  23. package/src/lifecycle/__tests__/install.test.ts +0 -23
  24. package/src/lifecycle/__tests__/new.test.ts +1 -1
  25. package/src/lifecycle/__tests__/smoke.test.ts +2 -2
  26. package/src/market/__tests__/bootstrap.test.ts +24 -0
  27. package/src/market/__tests__/setup.test.ts +8 -1
  28. package/src/market/bootstrap.ts +10 -8
  29. package/src/market/cdn-layout.ts +3 -3
  30. package/src/market/command.ts +0 -23
  31. package/src/market/setup.ts +14 -2
  32. package/src/market/trial.ts +3 -3
  33. package/src/market/walkthrough.ts +7 -6
  34. package/src/new.ts +21 -3
  35. package/src/preview-config.ts +2 -2
  36. package/src/preview.ts +5 -4
  37. package/src/publish.ts +0 -22
  38. package/src/sign.ts +0 -22
  39. package/src/usage.ts +482 -210
  40. package/src/vendor.ts +5 -1
  41. package/templates/README.md +3 -3
  42. package/templates/cdn/README.md +6 -6
  43. package/templates/config.yaml +5 -4
  44. package/templates/init.include.yaml +38 -2
  45. package/templates/seed.mysql.sql +2 -1
  46. package/templates/seed.pg.sql +2 -1
  47. package/templates/src/_platform/api/market/api.ts +11 -8
  48. package/templates/src/_platform/api/seed.sql +2 -1
  49. package/templates/src/_platform/api/solutions/api.ts +2 -2
  50. package/templates/src/system/web/pages/users/roles.tsx +3 -1
  51. package/templates/tests/lifecycle/market-api.spec.ts +35 -5
  52. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  53. package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
  54. package/vendor/host-versions.json +3 -3
  55. package/src/args.ts +0 -172
  56. package/src/lifecycle/__tests__/args.test.ts +0 -20
package/src/usage.ts CHANGED
@@ -1,222 +1,494 @@
1
1
  /**
2
- * `ojm` 用法文本(抽为纯函数供测试断言;index.ts 仅负责打印与退出码)。
2
+ * `ojm` 帮助文本与命令注册表(双语 zh-CN / en-US)。
3
+ *
4
+ * 单注册表两处消费(DRY):
5
+ * - cli.ts 遍历注册表做 cac command/option 注册(描述按调用语言选取);
6
+ * - 本模块渲染顶层分组帮助(usageText)与 per-command 详解段(cli.ts help 回调注入)。
3
7
  */
4
8
  import process from "node:process";
9
+ import cac from "cac";
5
10
 
6
- export function usageText(): string {
7
- return `@oj-module/cli
8
-
9
- 用法:
10
- ojm --version | -V
11
- 显示 cli 包版本
12
- ojm init [dir] [--yes]
13
- 前后端一体化工程脚手架(幂等补缺)
14
- ojm dev [port]
15
- 启动开发服务器(/api 反代 oj + 模块重建 + SSE 刷新)
16
- 开发模式快捷键(仅 TTY 终端,类 Vite):
17
- r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开
18
- q 退出(后端 oj 一并终止)
19
- ojm build [--bundle [name]] [--out <dir>]
20
- 构建后端(oj build)与模块产物(含全站合并)
21
- --bundle 方案包构建:契约校验(fail-fast)→ 确定性 tgz
22
- → dist/<name>-<version>.tgz + sha256(市场安装器的上游产物)
23
- --bundle <name> 只打这一份方案(无参 = 全部用户方案,框架自带自动排除)
24
- --out <dir> --bundle 输出目录(缺省 dist/)
25
- ojm new <name> [--layout container|none] [--api]
26
- 脚手架内生成用户方案骨架(src/<name>/{api,web} + solution.yaml;
27
- module-root 扫描自动收录,无需登记 web.config.ts)
28
- ojm preview [port]
29
- 生产形态预览(migrate → oj serve 单服务:API 与静态 SPA 同进程,深链回落)
30
- 静态由 oj web 挂载直出;ojm 不起第二个服务、不做反代
31
- ojm api [dir] [--check] [--docs] [--exempt <path>]
32
- 契约代码生成(client / routes / openapi / stub)
33
- --check 四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)
34
- --exempt <path> 指定豁免清单,覆盖默认 .ojm-api-exempt.json(工程根;历史布局在 api/ 下)
35
- --docs 出自包含文档站(单文件离线可看)
36
- ojm info
37
- 输出版本矩阵与模块清单(报障用,US-7)
38
- ojm vendor [tag] [--force]
39
- 下载/重装 oj vendor(经 npm 包 @oj-bin/oj;tag 缺省取最新,形如 v0.1.13)
40
- ojm merge <out.json> <in1.json> [in2.json ...]
41
- 合并多团队清单(R12)
42
- ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>]
43
- [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]
44
- 安装方案包到目标站点(P1-13):本地 tgz 走 file 上传(<name>-<version>.tgz 文件名推导
45
- 身份,可 --name/--version 覆盖);name[@version] 走 CDN 市场源。登录 → 提交 → 轮询任务
46
- 至终态并打印结果(失败附原因 + 指引,--locale 切双语)
47
- ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]
48
- 启用/禁用已装方案(D12 四处同写:锁 / sidecar / sys_solution_route / modules.json)
49
- ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]
50
- 卸载已装方案(P1-15a:四处同删 + 权限级联清理 + 版本目录移除;站点至少保留一个已锁模块)
51
- ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]
52
- 发布方信任库管理(P2-2/3,站点 .solutions/trusted-keys.yaml):
53
- ls <key_id 前缀?> 列出已信任 key(含 sha256 指纹分组展示)
54
- add <key_id> <pub> 新增信任(64 hex 公钥;首次绑定不可改)
55
- revoke <key_id> 吊销(阻断新装/升级,不动已装方案)
56
- confirm --task <id> TOFU 确认:把 needs_trust 终态待确认 key 入库
57
- 站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS
58
- ojm keygen --id <publisher.id> [--out <dir>] [--yes]
59
- 生成 Ed25519 签名密钥对(P2-1):私钥默认 ~/.ojm/keys/<id>/ 永不出目录(--out 指到
60
- 工程树内须 --yes 显式确认——私钥入仓即泄露);打印公钥 hex 供 ojm trust add / 模板回填
61
- ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]
62
- detached 侧车签名(P2-4):产出 <tgz>.sig(ojm install 自动探测随包上传);
63
- 私钥缺省取 ~/.ojm/keys/<id>/private.pem(--key 指定 pem 时须同时 --id);
64
- --check 离线自检签名(不必靠宿主装失败发现签错)
65
- 站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS
66
- (缺省 http://127.0.0.1:9778;凭据只走内存不落盘)
67
- ojm publish <dir> [--id <publisher.id>] [--key <pem>] [--check]
68
- 索引发布(P3-5/6/7):重算 <dir>/bundles.yaml + manifests.yaml(确定性
69
- stringify,消弭格式漂移)→ 对两份索引各产出 detached 侧车签名 <kind>.yaml.sig
70
- (与 archive .sig 同构,公钥取 ~/.ojm/keys/<id>/private.pem,缺省 --id 推导)→ 写回
71
- <dir>。CDN 侧 resolveCdn/index 拉取索引时校验签名(篡改/不符即拒,G4 fail-closed)。
72
- --check 离线自检两份索引签名
73
- ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]
74
- 把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由
75
- config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin
76
- must_change=1),会自动改密换全权 token 后再写入。
77
- ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]
78
- 把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz
79
- → 包级 detached 签名 <name>-<version>.tgz.sig。**不碰索引**(D-1:索引由
80
- build-index 扫描重建,多人发布才不会互相覆盖)。
81
- ojm market build-index [--id <key>] [--key <pem>] [--check]
82
- 扫描 cdn/*.tgz、提取包内 manifest,全量重建 bundles.yaml + manifests.yaml 并签名。
83
- 结果只取决于目录内容:连续执行字节一致。
84
- ojm market verify [--file <tgz>] [--rebuild-index]
85
- 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 /
86
- 索引验签 / manifest 结构(含 C2-31 desc===summary、各版本 0001 迁移一致)分列输出。
87
- --rebuild-index 额外比对「扫描重建的索引」与盘上索引,检出落了包却没重建的漂移。
88
- 注意:结论恒带边界声明——校验通过只说明来源与完整性,不构成内容安全性背书。
89
- ojm market trial [--fixture <name>@<version>] [--keep]
90
- 在一次性隔离站点(随机端口 + 一次性 DB/密钥/凭据/信任库/CDN)里跑完整安装,
91
- 跑完即毁;--keep 保留现场供排查。用于确认「这个包到底能不能装」,不污染你的工程。
92
- ojm test|exec|openapi|migrate|schema [args...]
93
- 透传给 oj 同名子命令(自动补 -c <project>/config.yaml;其余参数原样转发)
94
- 例:ojm test --anonymous / ojm migrate --db report / ojm openapi --check
95
- `;
96
- }
11
+ export type Lang = "zh-CN" | "en-US";
97
12
 
98
13
  /**
99
- * 单命令帮助文本(供 `ojm <cmd> --help` / `ojm help <cmd>` 打印)。
100
- * key 可为「命令」或「命令 子命令」(market 的子命令)。查不到具体子命令时回退到命令级。
101
- * 内容与 usageText 同源,但按命令拆分以便独立打印。
14
+ * CLI 帮助语言决议:OJM_LANG 显式指定 > LANG/LC_ALL/LC_MESSAGES 前缀探测 > 缺省 zh-CN。
15
+ * (与 ojm install --locale 无关——那是站点侧终态文案语言。)
102
16
  */
103
- const COMMAND_HELP: Record<string, string> = {
104
- "init": `ojm init [dir] [--yes]
105
- 前后端一体化工程脚手架(幂等补缺:重跑只补缺失文件,永不覆盖已有文件)`,
106
- "dev": `ojm dev [port]
107
- 启动开发服务器(/api 反代 oj + 模块重建 + SSE 刷新)
108
- 开发模式快捷键(仅 TTY 终端,类 Vite):
109
- r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开
110
- q 退出(后端 oj 一并终止)
111
- port 缺省 5174(oj 后端端口以 config.yaml 的 server.port 为准)`,
112
- "build": `ojm build [--bundle [name]] [--out <dir>]
113
- 构建后端(oj build)与模块产物(含全站合并)
114
- --bundle 方案包构建:契约校验(fail-fast,两段式全绿才写盘)→ 确定性 tgz
115
- → dist/<name>-<version>.tgz + sha256(市场安装器的上游产物)
116
- --bundle <name> / --bundle=<name> 只打这一份方案;无参 = 全部用户方案
117
- (src/<name>/solution.yaml;框架自带方案自动排除并逐条日志)
118
- --out <dir> --bundle 输出目录(缺省 dist/)`,
119
- "new": `ojm new <name> [--layout container|none] [--api]
120
- 脚手架内生成用户方案骨架:src/<name>/solution.yaml(契约,build --bundle 入口)
121
- + web(entry.ts 示例路由/占位页/双语 locales)+ 可选 api(oj 七键 manifest + ping 示例)
122
- 守卫:非 ojm 工程 / 历史布局 / 非法名 / 保留名 / 框架自带名 / 重名一律拒绝(零写入)
123
- module-root 布局以 src/<name>/web/entry.ts 扫描收录,无需登记 web.config.ts`,
124
- "preview": `ojm preview [port]
125
- 生产形态预览(migrate → oj serve 单服务:API 与静态 SPA 同进程,深链回落)
126
- 静态由 oj web 挂载直出;ojm 不起第二个服务、不做反代
127
- port 缺省取 config 的 server.port`,
128
- "api": `ojm api [dir] [--check] [--docs] [--exempt <path>]
129
- 契约代码生成(client / routes / openapi / stub)
130
- --check 四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)
131
- --exempt <path> 指定豁免清单,覆盖默认 .ojm-api-exempt.json(工程根;历史布局在 api/ 下)
132
- --docs 出自包含文档站(单文件离线可看)
133
- dir 缺省当前目录;显式目录须先存在`,
134
- "info": `ojm info [--all]
135
- 输出版本矩阵与模块清单(报障用)
136
- --all 展开框架自带模块(默认折叠为一行计数;名单见 src/.ojm-builtin.json)`,
137
- "vendor": `ojm vendor [tag] [--force]
138
- 下载/重装 oj vendor(经 npm 包 @oj-bin/oj;tag 缺省取最新,形如 v0.1.13)
139
- --force 即使已存在也重新下载`,
140
- "merge": `ojm merge <out.json> <in1.json> [in2.json ...]
141
- 合并多团队清单(清单先后 = 接管优先级)`,
142
- "install": `ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]
143
- 安装方案包到目标站点:本地 tgz 走 file 上传(<name>-<version>.tgz 文件名推导身份,可 --name/--version 覆盖);name[@version] 走 CDN 市场源
144
- 登录 → 提交 → 轮询任务至终态并打印结果(失败附原因 + 指引,--locale 切双语)
145
- 站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS`,
146
- "enable": `ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]
147
- 启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)
148
- 站点寻址同上`,
149
- "disable": `ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]
150
- 启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)
151
- 站点寻址同上`,
152
- "uninstall": `ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]
153
- 卸载已装方案(四处同删 + 权限级联清理 + 版本目录移除;站点至少保留一个已锁模块)
154
- 站点寻址同上`,
155
- "trust": `ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]
156
- 发布方信任库管理(站点 .solutions/trusted-keys.yaml):
157
- ls <key_id 前缀?> 列出已信任 key(含 sha256 指纹分组展示)
158
- add <key_id> <pub> 新增信任(64 hex 公钥;首次绑定不可改)
159
- revoke <key_id> 吊销(阻断新装/升级,不动已装方案)
160
- confirm --task <id> TOFU 确认:把 needs_trust 终态待确认 key 入库
161
- 站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS`,
162
- "trust ls": `ojm trust ls [key_id 前缀?] [--url <u>] [--user <u>] [--pass <p>]
163
- 列出已信任 key(含 sha256 指纹分组展示)`,
164
- "trust add": `ojm trust add <key_id> <pub> [--url <u>] [--user <u>] [--pass <p>]
165
- 新增信任(64 hex 公钥;首次绑定不可改)`,
166
- "trust revoke": `ojm trust revoke <key_id> [--url <u>] [--user <u>] [--pass <p>]
167
- 吊销(阻断新装/升级,不动已装方案)`,
168
- "trust confirm": `ojm trust confirm --task <id> [--url <u>] [--user <u>] [--pass <p>]
169
- TOFU 确认:把 needs_trust 终态待确认 key 入库`,
170
- "keygen": `ojm keygen --id <publisher.id> [--out <dir>] [--yes]
171
- 生成 Ed25519 签名密钥对:私钥默认 ~/.ojm/keys/<id>/ 永不出目录(--out 指到工程树内须 --yes 显式确认——私钥入仓即泄露);打印公钥 hex 供 ojm trust add / 模板回填`,
172
- "sign": `ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]
173
- detached 侧车签名:产出 <tgz>.sig(ojm install 自动探测随包上传)
174
- 私钥缺省取 ~/.ojm/keys/<id>/private.pem(--key 指定 pem 时须同时 --id)
175
- --check 离线自检签名(不必靠宿主装失败发现签错)`,
176
- "publish": `ojm publish <dir> [--id <key>] [--key <pem>] [--check]
177
- 索引发布:重算 <dir>/bundles.yaml + manifests.yaml(确定性 stringify,消弭格式漂移)→ 对两份索引各产出 detached 侧车签名 <kind>.yaml.sig(与 archive .sig 同构,公钥取 ~/.ojm/keys/<id>/private.pem,缺省 --id 推导)→ 写回 <dir>
178
- --check 离线自检两份索引签名`,
179
- "market": `ojm market <subcommand> [opts]
180
- 本地模拟 CDN 与市场链路子命令:setup / publish / build-index / verify / trial
181
- 详见各子命令帮助:ojm market setup --help 等`,
182
- "market setup": `ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]
183
- 把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由 config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入`,
184
- "market publish": `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]
185
- 把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级 detached 签名 <name>-<version>.tgz.sig
186
- 不碰索引(索引由 build-index 扫描重建,多人发布才不会互相覆盖)`,
187
- "market build-index": `ojm market build-index [--id <key>] [--key <pem>] [--check]
188
- 扫描 cdn/*.tgz、提取包内 manifest,全量重建 bundles.yaml + manifests.yaml 并签名
189
- 结果只取决于目录内容:连续执行字节一致`,
190
- "market verify": `ojm market verify [--file <tgz>] [--rebuild-index]
191
- 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出
192
- --rebuild-index 额外比对「扫描重建的索引」与盘上索引,检出落了包却没重建的漂移
193
- 注意:结论恒带边界声明——校验通过只说明来源与完整性,不构成内容安全性背书`,
194
- "market trial": `ojm market trial [--fixture <name>@<version>] [--keep]
195
- 在一次性隔离站点(随机端口 + 一次性 DB/密钥/凭据/信任库/CDN)里跑完整安装,跑完即毁;--keep 保留现场供排查。用于确认「这个包到底能不能装」,不污染你的工程`,
196
- };
197
-
198
- /** 取单命令帮助;返回 undefined 表示未知命令。 */
199
- export function getCommandHelp(command: string, subcommand?: string): string | undefined {
200
- if (subcommand) {
201
- const specific = COMMAND_HELP[`${command} ${subcommand}`];
202
- if (specific)
203
- return specific;
204
- }
205
- return COMMAND_HELP[command];
17
+ export function resolveLang(env: NodeJS.ProcessEnv = process.env): Lang {
18
+ const explicit = env.OJM_LANG?.toLowerCase();
19
+ if (explicit === "en" || explicit === "en-us")
20
+ return "en-US";
21
+ if (explicit === "zh" || explicit === "zh-cn")
22
+ return "zh-CN";
23
+ const sys = (env.LC_ALL ?? env.LC_MESSAGES ?? env.LANG ?? "").toLowerCase();
24
+ return sys.startsWith("en") ? "en-US" : "zh-CN";
25
+ }
26
+
27
+ function pick(b: { zh: string, en: string }, lang: Lang): string {
28
+ return lang === "zh-CN" ? b.zh : b.en;
29
+ }
30
+
31
+ export interface HelpOption { raw: string, zh: string, en: string }
32
+
33
+ /** 子命令条目:详解(--help 注入)+ short(分组索引一行,可选) */
34
+ export interface SubHelp { zh: string, en: string, short?: { zh: string, en: string } }
35
+
36
+ export interface HelpEntry {
37
+ /** action 分发键(cli.ts 的 switch 用) */
38
+ key: string
39
+ /** cac 注册名(含参数签名,如 "init [dir]" / "market [sub] [...rest]") */
40
+ name: string
41
+ zh: string
42
+ en: string
43
+ options?: HelpOption[]
44
+ /** --help 末尾追加的详解段 */
45
+ detail?: { zh: string, en: string }
46
+ /** 子命令详解(market / trust:--help 按匹配后 args[0] 注入对应条目) */
47
+ subs?: Record<string, SubHelp>
48
+ /** 放行未知选项(oj 透传五命令 / market 子命令自带解析器,raw 转发) */
49
+ allowUnknownOptions?: boolean
50
+ }
51
+
52
+ export interface HelpGroup {
53
+ key: string
54
+ commands: HelpEntry[]
55
+ zh: string
56
+ en: string
57
+ }
58
+
59
+ /** 站点寻址三件套(生命周期命令共用) */
60
+ const SITE_OPTIONS: HelpOption[] = [
61
+ { raw: "--url <url>", zh: "站点地址(缺省 http://127.0.0.1:9778)", en: "Site URL (default http://127.0.0.1:9778)" },
62
+ { raw: "--user <user>", zh: "登录账号(或环境变量 OJM_USER)", en: "Login username (or env OJM_USER)" },
63
+ { raw: "--pass <pass>", zh: "登录密码(或环境变量 OJM_PASS;只走内存不落盘)", en: "Login password (or env OJM_PASS; memory-only, never persisted)" },
64
+ ];
65
+
66
+ const OJ_PASSTHROUGH = ["test", "exec", "openapi", "migrate", "schema"];
67
+
68
+ export const HELP_GROUPS: HelpGroup[] = [
69
+ {
70
+ key: "scaffold",
71
+ zh: "工程脚手架",
72
+ en: "Project scaffolding",
73
+ commands: [
74
+ {
75
+ key: "init",
76
+ name: "init [dir]",
77
+ zh: "前后端一体化工程脚手架(幂等补缺)",
78
+ en: "Full-stack project scaffolding (idempotent, fills gaps only)",
79
+ options: [
80
+ { raw: "--yes", zh: "非交互:缺省回答一律 yes", en: "Non-interactive: default all answers to yes" },
81
+ ],
82
+ detail: {
83
+ zh: "ojm init [dir] [--yes]\n 前后端一体化工程脚手架(幂等补缺:重跑只补缺失文件,永不覆盖已有文件)",
84
+ en: "ojm init [dir] [--yes]\n Full-stack project scaffolding (idempotent: re-runs only fill missing files, never overwrite existing ones)",
85
+ },
86
+ },
87
+ {
88
+ key: "new",
89
+ name: "new [name] [...extra]",
90
+ zh: "脚手架内生成用户方案骨架(src/<name>/{api,web} + solution.yaml)",
91
+ en: "Generate a solution skeleton inside the project (src/<name>/{api,web} + solution.yaml)",
92
+ options: [
93
+ { raw: "--layout <layout>", zh: "页面布局 container|none", en: "Page layout container|none" },
94
+ { raw: "--api", zh: "同时生成 api 骨架(oj manifest + ping 示例)", en: "Also generate the api skeleton (oj manifest + ping sample)" },
95
+ ],
96
+ detail: {
97
+ zh: "ojm new <name> [--layout container|none] [--api]\n 守卫:非 ojm 工程 / 历史布局 / 非法名 / 保留名 / 框架自带名 / 重名一律拒绝(零写入)\n module-root 布局以 src/<name>/web/entry.ts 扫描收录,无需登记 web.config.ts",
98
+ en: "ojm new <name> [--layout container|none] [--api]\n Guards: rejects non-ojm projects / legacy layouts / illegal names / reserved names / builtin names / duplicates (zero writes)\n module-root layouts are collected by scanning src/<name>/web/entry.ts — no web.config.ts registration needed",
99
+ },
100
+ },
101
+ ],
102
+ },
103
+ {
104
+ key: "dev",
105
+ zh: "开发与构建",
106
+ en: "Development & build",
107
+ commands: [
108
+ {
109
+ key: "dev",
110
+ name: "dev [port]",
111
+ zh: "启动开发服务器(/api 反代 oj + 模块重建 + SSE 刷新)",
112
+ en: "Start dev server (/api proxy to oj + module rebuild + SSE refresh)",
113
+ detail: {
114
+ zh: "ojm dev [port]\n 开发模式快捷键(仅 TTY 终端,类 Vite):\n r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开\n q 退出(后端 oj 一并终止)\n port 缺省 5174(oj 后端端口以 config.yaml 的 server.port 为准)",
115
+ en: "ojm dev [port]\n Dev-mode shortcuts (TTY only, Vite-like):\n r force rebuild + hot reload h show help c clear screen o open browser\n q quit (terminates the oj backend as well)\n port defaults to 5174 (the oj backend port comes from config.yaml server.port)",
116
+ },
117
+ },
118
+ {
119
+ key: "build",
120
+ name: "build",
121
+ zh: "构建后端(oj build)与模块产物;--bundle 打确定性方案包",
122
+ en: "Build backend (oj build) and module artifacts; --bundle builds a deterministic solution bundle",
123
+ options: [
124
+ { raw: "--bundle [name]", zh: "方案包构建:契约校验(fail-fast)→ 确定性 tgz + sha256;带值只打该方案,无值 = 全部用户方案", en: "Solution bundle: contract check (fail-fast) → deterministic tgz + sha256; with a value builds only that solution, bare flag builds all user solutions" },
125
+ { raw: "--out <dir>", zh: "--bundle 输出目录(缺省 dist/)", en: "Output directory for --bundle (default dist/)" },
126
+ ],
127
+ detail: {
128
+ zh: "ojm build [--bundle [name]] [--out <dir>]\n 构建后端(oj build)与模块产物(含全站合并)\n --bundle 产出 dist/<name>-<version>.tgz + sha256(市场安装器的上游产物)",
129
+ en: "ojm build [--bundle [name]] [--out <dir>]\n Builds the backend (oj build) and module artifacts (including the site-wide merge)\n --bundle produces dist/<name>-<version>.tgz + sha256 (upstream artifact for the marketplace installer)",
130
+ },
131
+ },
132
+ {
133
+ key: "preview",
134
+ name: "preview [port]",
135
+ zh: "生产形态预览(migrate → oj serve 单服务,深链回落)",
136
+ en: "Production-shape preview (migrate → oj serve single service, deep-link fallback)",
137
+ detail: {
138
+ zh: "ojm preview [port]\n 静态由 oj web 挂载直出;ojm 不起第二个服务、不做反代\n port 缺省取 config 的 server.port",
139
+ en: "ojm preview [port]\n Static files are served directly by the oj web mount; ojm starts no second service and does no proxying\n port defaults to the config server.port",
140
+ },
141
+ },
142
+ {
143
+ key: "api",
144
+ name: "api [dir]",
145
+ zh: "契约代码生成(client / routes / openapi / stub);--check 四重对账",
146
+ en: "Contract code generation (client / routes / openapi / stub); --check runs the four-way reconciliation",
147
+ options: [
148
+ { raw: "--check", zh: "四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)", en: "Four-way reconciliation (generated sync / bidirectional routes / routes.js / WS route auth)" },
149
+ { raw: "--exempt <path>", zh: "豁免清单,覆盖默认 .ojm-api-exempt.json(工程根)", en: "Exemption list, overrides the default .ojm-api-exempt.json (project root)" },
150
+ { raw: "--docs", zh: "出自包含文档站(单文件离线可看)", en: "Generate a self-contained docs site (single file, works offline)" },
151
+ ],
152
+ detail: {
153
+ zh: "ojm api [dir] [--check] [--docs] [--exempt <path>]\n dir 缺省当前目录;显式目录须先存在",
154
+ en: "ojm api [dir] [--check] [--docs] [--exempt <path>]\n dir defaults to cwd; an explicit directory must exist",
155
+ },
156
+ },
157
+ ],
158
+ },
159
+ {
160
+ key: "diagnostics",
161
+ zh: "清单与诊断",
162
+ en: "Manifest & diagnostics",
163
+ commands: [
164
+ {
165
+ key: "info",
166
+ name: "info",
167
+ zh: "版本矩阵与模块清单(报障用)",
168
+ en: "Version matrix and module list (for bug reports)",
169
+ options: [
170
+ { raw: "--all", zh: "展开框架自带模块(默认折叠为一行计数)", en: "Expand builtin modules (collapsed to a one-line count by default)" },
171
+ ],
172
+ detail: {
173
+ zh: "ojm info [--all]\n 输出版本矩阵与模块清单(报障用,US-7)",
174
+ en: "ojm info [--all]\n Prints the version matrix and module list (for bug reports, US-7)",
175
+ },
176
+ },
177
+ {
178
+ key: "merge",
179
+ name: "merge [out] [...inputs]",
180
+ zh: "合并多团队清单(清单先后 = 接管优先级)",
181
+ en: "Merge team manifests (earlier manifest wins takeover priority)",
182
+ detail: {
183
+ zh: "ojm merge <out.json> <in1.json> [in2.json ...]\n 合并多团队清单(R12;同名模块显式拒绝)",
184
+ en: "ojm merge <out.json> <in1.json> [in2.json ...]\n Merge team manifests (R12; duplicate module names are rejected explicitly)",
185
+ },
186
+ },
187
+ {
188
+ key: "vendor",
189
+ name: "vendor [tag]",
190
+ zh: "下载/重装 oj vendor(经 npm 包 @oj-bin/oj)",
191
+ en: "Download/reinstall the oj vendor (via the npm package @oj-bin/oj)",
192
+ options: [
193
+ { raw: "--force", zh: "即使已存在也重新下载", en: "Re-download even if already present" },
194
+ ],
195
+ detail: {
196
+ zh: "ojm vendor [tag] [--force]\n tag 缺省取最新,形如 v0.1.13",
197
+ en: "ojm vendor [tag] [--force]\n tag defaults to latest, e.g. v0.1.13",
198
+ },
199
+ },
200
+ ],
201
+ },
202
+ {
203
+ key: "market-lifecycle",
204
+ zh: "方案市场(站点侧生命周期)",
205
+ en: "Marketplace (site-side lifecycle)",
206
+ commands: [
207
+ {
208
+ key: "install",
209
+ name: "install [target]",
210
+ zh: "安装方案包到目标站点(file 上传 / CDN 市场源)",
211
+ en: "Install a solution bundle to the target site (file upload / CDN marketplace source)",
212
+ options: [
213
+ ...SITE_OPTIONS,
214
+ { raw: "--name <name>", zh: "覆盖文件名推导的方案名", en: "Override the solution name derived from the file name" },
215
+ { raw: "--version <version>", zh: "覆盖文件名推导的版本", en: "Override the version derived from the file name" },
216
+ { raw: "--locale <locale>", zh: "终态文案语言 zh-CN|en-US", en: "Language of the final status message zh-CN|en-US" },
217
+ ],
218
+ detail: {
219
+ zh: "ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]\n 本地 tgz 走 file 上传(<name>-<version>.tgz 文件名推导身份,可 --name/--version 覆盖);name[@version] 走 CDN 市场源\n 登录 → 提交 → 轮询任务至终态并打印结果(失败附原因 + 指引)\n 站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS",
220
+ en: "ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]\n Local tgz goes through file upload (identity derived from the <name>-<version>.tgz file name, overridable via --name/--version); name[@version] goes through the CDN marketplace\n Login → submit → poll the task to a terminal state and print the result (failures include the reason + guidance)\n Site addressing: --url/--user/--pass, or env vars OJM_URL / OJM_USER / OJM_PASS",
221
+ },
222
+ },
223
+ {
224
+ key: "enable",
225
+ name: "enable [name]",
226
+ zh: "启用已装方案(四处同写)",
227
+ en: "Enable an installed solution (writes to four places)",
228
+ options: SITE_OPTIONS,
229
+ detail: {
230
+ zh: "ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]\n 启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)\n 站点寻址同上",
231
+ en: "ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]\n Enable/disable an installed solution (four synchronized writes: lock / sidecar / route manifest / module manifest)\n Site addressing: same as above",
232
+ },
233
+ },
234
+ {
235
+ key: "disable",
236
+ name: "disable [name]",
237
+ zh: "禁用已装方案(四处同写)",
238
+ en: "Disable an installed solution (writes to four places)",
239
+ options: SITE_OPTIONS,
240
+ },
241
+ {
242
+ key: "uninstall",
243
+ name: "uninstall [name]",
244
+ zh: "卸载已装方案(四处同删 + 权限级联清理)",
245
+ en: "Uninstall a solution (four synchronized removals + permission cascade cleanup)",
246
+ options: SITE_OPTIONS,
247
+ detail: {
248
+ zh: "ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]\n 四处同删 + 权限级联清理 + 版本目录移除;站点至少保留一个已锁模块\n 站点寻址同上",
249
+ en: "ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]\n Four synchronized removals + permission cascade cleanup + version directory removal; a site keeps at least one locked module\n Site addressing: same as above",
250
+ },
251
+ },
252
+ {
253
+ key: "trust",
254
+ name: "trust [sub] [keyId] [pub]",
255
+ zh: "发布方信任库管理(TOFU 确认)",
256
+ en: "Publisher trust store management (TOFU confirmation)",
257
+ options: [
258
+ ...SITE_OPTIONS,
259
+ { raw: "--task <id>", zh: "TOFU 确认:needs_trust 终态任务 id", en: "TOFU confirm: task id of a needs_trust terminal state" },
260
+ ],
261
+ detail: {
262
+ zh: "ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]\n 发布方信任库管理(站点 .solutions/trusted-keys.yaml)",
263
+ en: "ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]\n Publisher trust store management (site-side .solutions/trusted-keys.yaml)",
264
+ },
265
+ subs: {
266
+ ls: {
267
+ short: { zh: "列出已信任 key(含 sha256 指纹分组展示)", en: "List trusted keys (grouped sha256 fingerprints)" },
268
+ zh: "ojm trust ls [key_id 前缀?]\n 列出已信任 key(含 sha256 指纹分组展示)",
269
+ en: "ojm trust ls [key_id prefix?]\n List trusted keys (grouped display with sha256 fingerprints)",
270
+ },
271
+ add: {
272
+ short: { zh: "新增信任(64 hex 公钥;首次绑定不可改)", en: "Add trust (64-hex public key; first binding immutable)" },
273
+ zh: "ojm trust add <key_id> <pub>\n 新增信任(64 hex 公钥;首次绑定不可改)",
274
+ en: "ojm trust add <key_id> <pub>\n Add trust (64-hex public key; the first binding is immutable)",
275
+ },
276
+ revoke: {
277
+ short: { zh: "吊销(阻断新装/升级,不动已装方案)", en: "Revoke (blocks new installs/upgrades, installed untouched)" },
278
+ zh: "ojm trust revoke <key_id>\n 吊销(阻断新装/升级,不动已装方案)",
279
+ en: "ojm trust revoke <key_id>\n Revoke (blocks new installs/upgrades, leaves installed solutions untouched)",
280
+ },
281
+ confirm: {
282
+ short: { zh: "TOFU 确认:待确认 key 入库", en: "TOFU confirm: import the pending key" },
283
+ zh: "ojm trust confirm --task <id>\n TOFU 确认:把 needs_trust 终态待确认 key 入库",
284
+ en: "ojm trust confirm --task <id>\n TOFU confirm: import the key pending from a needs_trust terminal state",
285
+ },
286
+ },
287
+ },
288
+ ],
289
+ },
290
+ {
291
+ key: "signing",
292
+ zh: "发布与签名(作者侧)",
293
+ en: "Publishing & signing (author side)",
294
+ commands: [
295
+ {
296
+ key: "keygen",
297
+ name: "keygen",
298
+ zh: "生成 Ed25519 签名密钥对",
299
+ en: "Generate an Ed25519 signing key pair",
300
+ options: [
301
+ { raw: "--id <id>", zh: "发布方 key id(形如 publisher.key)", en: "Publisher key id (e.g. publisher.key)" },
302
+ { raw: "--out <dir>", zh: "输出目录(缺省 ~/.ojm/keys/<id>/;指到工程树内须 --yes)", en: "Output directory (default ~/.ojm/keys/<id>/; pointing inside the project tree requires --yes)" },
303
+ { raw: "--yes", zh: "确认私钥落盘到工程树内(私钥入仓即泄露)", en: "Acknowledge writing the private key inside the project tree (a private key in the repo is leaked)" },
304
+ ],
305
+ detail: {
306
+ zh: "ojm keygen --id <publisher.id> [--out <dir>] [--yes]\n 私钥默认 ~/.ojm/keys/<id>/ 永不出目录;打印公钥 hex 供 ojm trust add / 模板回填",
307
+ en: "ojm keygen --id <publisher.id> [--out <dir>] [--yes]\n The private key stays under ~/.ojm/keys/<id>/ by default; prints the public key hex for ojm trust add / template backfill",
308
+ },
309
+ },
310
+ {
311
+ key: "sign",
312
+ name: "sign [file]",
313
+ zh: "detached 侧车签名(产出 <tgz>.sig)",
314
+ en: "Detached sidecar signing (produces <tgz>.sig)",
315
+ options: [
316
+ { raw: "--id <id>", zh: "发布方 key id", en: "Publisher key id" },
317
+ { raw: "--key <pem>", zh: "指定私钥 pem(须同时 --id)", en: "Explicit private key pem (requires --id as well)" },
318
+ { raw: "--check", zh: "离线自检签名", en: "Offline signature self-check" },
319
+ ],
320
+ detail: {
321
+ zh: "ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]\n detached 侧车签名:产出 <tgz>.sig(ojm install 自动探测随包上传);私钥缺省取 ~/.ojm/keys/<id>/private.pem",
322
+ en: "ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]\n Detached sidecar signature: produces <tgz>.sig (ojm install auto-detects and uploads it with the bundle); the private key defaults to ~/.ojm/keys/<id>/private.pem",
323
+ },
324
+ },
325
+ {
326
+ key: "publish",
327
+ name: "publish [dir]",
328
+ zh: "索引发布(重算 bundles/manifests + 侧车签名)",
329
+ en: "Index publishing (recompute bundles/manifests + sidecar signatures)",
330
+ options: [
331
+ { raw: "--id <id>", zh: "发布方 key id", en: "Publisher key id" },
332
+ { raw: "--key <pem>", zh: "指定私钥 pem", en: "Explicit private key pem" },
333
+ { raw: "--check", zh: "离线自检两份索引签名", en: "Offline self-check of both index signatures" },
334
+ ],
335
+ detail: {
336
+ zh: "ojm publish <dir> [--id <key>] [--key <pem>] [--check]\n 重算 <dir>/bundles.yaml + manifests.yaml(确定性 stringify,消弭格式漂移)→ 各产出 detached 侧车签名 <kind>.yaml.sig → 写回 <dir>",
337
+ en: "ojm publish <dir> [--id <key>] [--key <pem>] [--check]\n Recompute <dir>/bundles.yaml + manifests.yaml (deterministic stringify to eliminate format drift) → produce a detached sidecar signature <kind>.yaml.sig for each → write back to <dir>",
338
+ },
339
+ },
340
+ ],
341
+ },
342
+ {
343
+ key: "market-cdn",
344
+ zh: "本地市场模拟 CDN",
345
+ en: "Local marketplace mock CDN",
346
+ commands: [
347
+ {
348
+ key: "market",
349
+ name: "market [sub] [...rest]",
350
+ zh: "本地模拟 CDN 与市场链路(setup / publish / build-index / verify / trial)",
351
+ en: "Local mock CDN and marketplace pipeline (setup / publish / build-index / verify / trial)",
352
+ allowUnknownOptions: true,
353
+ options: [
354
+ { raw: "--url <url>", zh: "站点地址", en: "Site URL" },
355
+ { raw: "--user <user>", zh: "登录账号", en: "Login username" },
356
+ { raw: "--pass <pass>", zh: "登录密码", en: "Login password" },
357
+ { raw: "--new-pass <pass>", zh: "首次改密期的新密码(setup)", en: "New password during the forced-reset period (setup)" },
358
+ { raw: "--fixture <name@version>", zh: "合成 fixture 方案包(publish / trial)", en: "Synthesize a fixture bundle (publish / trial)" },
359
+ { raw: "--real <project>", zh: "走真实方案工程发包(publish)", en: "Publish from a real solution project (publish)" },
360
+ { raw: "--id <key>", zh: "签名 key id", en: "Signing key id" },
361
+ { raw: "--key <pem>", zh: "签名私钥 pem", en: "Signing private key pem" },
362
+ { raw: "--rebuild-index", zh: "发包后顺带重建索引(publish)", en: "Rebuild the index after publishing (publish)" },
363
+ { raw: "--check", zh: "离线自检签名", en: "Offline signature self-check" },
364
+ ],
365
+ subs: {
366
+ "setup": {
367
+ short: { zh: "framework.market_cdn_url 指向外部真 CDN(脚手架缺省 /cdn 免 setup)", en: "Point framework.market_cdn_url at an external CDN (scaffold defaults to /cdn, no setup needed)" },
368
+ zh: "ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]\n 把系统设置 framework.market_cdn_url 改写为绝对地址(外部真 CDN 场景;脚手架 seed 缺省 /cdn = 本站点自供,免 setup)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入",
369
+ en: "ojm market setup [--url <site>] [--user <u> --pass <p>] [--new-pass <p>]\n Rewrite the framework.market_cdn_url system setting to an absolute URL (external CDN scenario; the scaffold seed defaults to /cdn = site-served, no setup needed). On first run, if the account is in the forced password-reset period (seed admin must_change=1), it resets the password to obtain a full-privilege token before writing",
370
+ },
371
+ "publish": {
372
+ short: { zh: "方案包落进 cdn/ + 包级签名(不碰索引)", en: "Drop a bundle into cdn/ + package-level signature (index untouched)" },
373
+ zh: "ojm market publish [--fixture <name@version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]\n 把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级 detached 签名 <name>-<version>.tgz.sig。不碰索引(索引由 build-index 扫描重建,多人发布才不会互相覆盖)",
374
+ en: "ojm market publish [--fixture <name@version>] [--real <project>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]\n Drop a solution bundle into cdn/: synthesize a fixture (or --real via ojm build --bundle) → deterministic tgz → package-level detached signature <name>-<version>.tgz.sig. Does not touch the index (the index is rebuilt by build-index so concurrent publishers don't overwrite each other)",
375
+ },
376
+ "build-index": {
377
+ short: { zh: "扫描重建 bundles.yaml + manifests.yaml 并签名", en: "Rebuild bundles.yaml + manifests.yaml from a scan and sign" },
378
+ zh: "ojm market build-index [--id <key>] [--key <pem>] [--check]\n 扫描 cdn/*.tgz、提取包内 manifest,全量重建 bundles.yaml + manifests.yaml 并签名。结果只取决于目录内容:连续执行字节一致",
379
+ en: "ojm market build-index [--id <key>] [--key <pem>] [--check]\n Scan cdn/*.tgz, extract each bundle's manifest, fully rebuild bundles.yaml + manifests.yaml and sign them. The result depends only on directory content: consecutive runs are byte-identical",
380
+ },
381
+ "verify": {
382
+ short: { zh: "离线验包(sha256 / 验签 / 索引 / manifest 结构)", en: "Offline bundle verification (sha256 / signatures / index / manifest)" },
383
+ zh: "ojm market verify [--file <tgz>] [--rebuild-index]\n 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出。--rebuild-index 额外比对「扫描重建的索引」与盘上索引,检出落了包却没重建的漂移。注意:校验通过只说明来源与完整性,不构成内容安全性背书",
384
+ en: "ojm market verify [--file <tgz>] [--rebuild-index]\n Offline bundle verification (no site, no install, no credentials): sha256 integrity / package-level Ed25519 signature / index signature / manifest structure in separate sections. --rebuild-index additionally compares the index rebuilt from a scan against the one on disk, detecting drift where a bundle was dropped but the index was not rebuilt. Note: passing only proves origin and integrity — it is not an endorsement of content safety",
385
+ },
386
+ "trial": {
387
+ short: { zh: "一次性隔离站点试装,跑完即毁", en: "Full install trial in a throwaway isolated site" },
388
+ zh: "ojm market trial [--fixture <name@version>] [--keep]\n 在一次性隔离站点(随机端口 + 一次性 DB/密钥/凭据/信任库/CDN)里跑完整安装,跑完即毁;--keep 保留现场供排查",
389
+ en: "ojm market trial [--fixture <name@version>] [--keep]\n Run a full install in a throwaway isolated site (random port + throwaway DB/keys/credentials/trust store/CDN), destroyed right after; --keep preserves the scene for debugging",
390
+ },
391
+ "walkthrough": {
392
+ short: { zh: "隔离站点跑全套市场操作并出 AC 表", en: "Run the full marketplace operation set in an isolated site, print an AC table" },
393
+ zh: "ojm market walkthrough [--only <op>] [--strict] [--keep]\n 隔离站点跑全套市场操作并出 AC 表",
394
+ en: "ojm market walkthrough [--only <op>] [--strict] [--keep]\n Run the full marketplace operation set in an isolated site and print an AC table",
395
+ },
396
+ },
397
+ },
398
+ ],
399
+ },
400
+ {
401
+ key: "oj-passthrough",
402
+ zh: "oj 子命令透传",
403
+ en: "oj subcommand passthrough",
404
+ commands: OJ_PASSTHROUGH.map(sub => ({
405
+ key: "oj",
406
+ name: sub,
407
+ zh: `透传给 oj ${sub}(自动补 -c <project>/config.yaml)`,
408
+ en: `Passthrough to oj ${sub} (auto-appends -c <project>/config.yaml)`,
409
+ allowUnknownOptions: true,
410
+ detail: {
411
+ zh: `ojm ${sub} [args...]\n 透传给 oj 同名子命令(自动补 -c <project>/config.yaml;其余参数原样转发)`,
412
+ en: `ojm ${sub} [args...]\n Passthrough to the oj subcommand of the same name (auto-appends -c <project>/config.yaml; all other arguments are forwarded as-is)`,
413
+ },
414
+ })),
415
+ },
416
+ ];
417
+
418
+ export const COMMANDS: HelpEntry[] = HELP_GROUPS.flatMap(g => g.commands);
419
+
420
+ export function findCommand(rawName: string): HelpEntry | undefined {
421
+ return COMMANDS.find(c => c.name === rawName);
422
+ }
423
+
424
+ /** 首位置参数是否是已知命令(cac 只按首段匹配,子命令如 verify 不算独立命令)。 */
425
+ export function isKnownCommand(name: string): boolean {
426
+ return COMMANDS.some(c => c.name.split(" ")[0] === name);
206
427
  }
207
428
 
429
+ interface HelpSection { title?: string, body: string }
430
+
208
431
  /**
209
- * 解析「帮助请求」:`ojm <cmd> --help` / `ojm <cmd> -h` / `ojm help <cmd>` 三种形态。
210
- * 返回 null 表示本次调用不是帮助请求;否则带出要查的命令与子命令(可能没有)。
211
- * 抽为纯逻辑(不碰 process.exit、不读模块外状态)便于单测。
432
+ * 借 cac 的 Commands 分节排版(命令名对齐 + 描述列)渲染一组命令行。
433
+ * cac.outputHelp 只走 console.log:临时静默,section 经 help 回调捕获后自行拼接。
212
434
  */
213
- export function resolveHelpRequest(argv: string[] = process.argv.slice(2)): { cmd?: string, sub?: string } | null {
214
- const helpRequested = argv.some(a => a === "--help" || a === "-h") || argv[0] === "help";
215
- if (!helpRequested)
216
- return null;
217
- const positional = argv.filter(a => a !== "--help" && a !== "-h" && !a.startsWith("-"));
218
- if (positional[0] === "help")
219
- positional.shift();
220
- const [cmd, sub] = positional;
221
- return { cmd, sub };
435
+ function renderGroupBody(lang: Lang, entries: HelpEntry[]): string {
436
+ const cli = cac("ojm");
437
+ for (const e of entries) {
438
+ // 带子命令的命令(market):索引按子命令展开一行一条,父命令本身不上索引
439
+ if (e.subs) {
440
+ const prefix = e.name.split(" ")[0];
441
+ for (const [sub, s] of Object.entries(e.subs)) {
442
+ const desc = s.short ? pick(s.short, lang) : pick(s, lang).split("\n")[0];
443
+ cli.command(`ojm ${prefix} ${sub}`, desc);
444
+ }
445
+ continue;
446
+ }
447
+ // 索引不展示守卫用 variadic(如 new 的 [...extra]),只显示主签名
448
+ cli.command(`ojm ${e.name.replace(/\s*\[\.\.\.\w+\]$/, "")}`, pick(e, lang));
449
+ }
450
+ let sections: HelpSection[] = [];
451
+ cli.help((s) => {
452
+ sections = s;
453
+ return s;
454
+ });
455
+ const log = console.log;
456
+ console.log = () => {};
457
+ try {
458
+ cli.outputHelp();
459
+ }
460
+ finally {
461
+ console.log = log;
462
+ }
463
+ return sections.find(s => s.title === "Commands")?.body ?? "";
464
+ }
465
+
466
+ function sectionsToString(sections: HelpSection[]): string {
467
+ return sections.map(s => (s.title ? `${s.title}:\n${s.body}` : s.body)).join("\n\n");
468
+ }
469
+
470
+ /** 顶层帮助 sections(usageText 与 cli.ts 的全局 help 回调同源)。 */
471
+ export function topHelpSections(lang: Lang): HelpSection[] {
472
+ return [
473
+ { title: lang === "zh-CN" ? "用法" : "Usage", body: "ojm <command> [options]" },
474
+ ...HELP_GROUPS.map(g => ({ title: pick(g, lang), body: renderGroupBody(lang, g.commands) })),
475
+ {
476
+ body: [
477
+ `ojm --version | -V ${lang === "zh-CN" ? "显示 cli 包版本" : "Show the cli package version"}`,
478
+ `ojm <cmd> --help ${lang === "zh-CN" ? "单命令详解(如 ojm market setup --help)" : "Per-command details (e.g. ojm market setup --help)"}`,
479
+ ].join("\n"),
480
+ },
481
+ ];
482
+ }
483
+
484
+ export function usageText(lang: Lang = resolveLang()): string {
485
+ return sectionsToString(topHelpSections(lang));
486
+ }
487
+
488
+ /** 单个分组的帮助文本(bare `ojm market` 等场景)。 */
489
+ export function groupHelpText(groupKey: string, lang: Lang): string | undefined {
490
+ const g = HELP_GROUPS.find(x => x.key === groupKey);
491
+ if (!g)
492
+ return undefined;
493
+ return `${pick(g, lang)}:\n${renderGroupBody(lang, g.commands)}`;
222
494
  }