@oj-module/cli 0.2.23 → 0.2.25

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 (55) 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 +4 -3
  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 +4 -4
  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/templates/README.md +3 -3
  41. package/templates/cdn/README.md +6 -6
  42. package/templates/config.yaml +5 -4
  43. package/templates/init.include.yaml +1 -1
  44. package/templates/seed.mysql.sql +2 -1
  45. package/templates/seed.pg.sql +2 -1
  46. package/templates/src/_platform/api/market/api.ts +11 -8
  47. package/templates/src/_platform/api/seed.sql +2 -1
  48. package/templates/src/_platform/api/solutions/api.ts +2 -2
  49. package/templates/src/system/web/pages/users/roles.tsx +3 -1
  50. package/templates/tests/lifecycle/market-api.spec.ts +35 -5
  51. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  52. package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
  53. package/vendor/host-versions.json +3 -3
  54. package/src/args.ts +0 -172
  55. package/src/lifecycle/__tests__/args.test.ts +0 -20
@@ -7,7 +7,7 @@
7
7
  ```bash
8
8
  pnpm install
9
9
  pnpm dev # 开发:前端 devServer + oj 后端(api/src 保存即热更)
10
- pnpm build # 构建:oj build + 前端全站合并到 web/dist
10
+ pnpm build # 构建:oj build + 前端全站合并到 dist/web
11
11
  pnpm preview # 预览:oj migrate + server(release 产物)
12
12
  ```
13
13
 
@@ -35,9 +35,9 @@ pnpm exec ojm api # 重新生成 web/src/*/__client(dev
35
35
  ```
36
36
  bin/ oj 二进制(init 解压,不入库)
37
37
  api/src/ 后端模块源码(目录镜像路由:api/src/<模块>/<路径>/api.ts)
38
- api/dist/ oj build 产物
38
+ dist/api/ oj build 产物
39
39
  web/src/ 前端模块源码
40
- web/dist/ 构建产物(完整站点)
40
+ dist/web/ 构建产物(完整站点)
41
41
  ```
42
42
 
43
43
  ## 注意
@@ -1,8 +1,8 @@
1
1
  # cdn/ —— 本地模拟市场源
2
2
 
3
- 本目录是**本地模拟 CDN**:`ojm market publish` 把分发包落在这里,站点通过 `api/config.yaml` 里的挂载
4
- (`- prefix: /cdn` → `web: ../cdn`)把它直出成分发源,市场页按系统设置 `framework.market_cdn_url`
5
- 指向的地址读取。
3
+ 本目录是**本地模拟 CDN**:`ojm market publish` 把分发包落在这里,站点通过工程根 `config.yaml` 里的挂载
4
+ (`- prefix: /cdn` → `web: ./cdn`)把它直出成分发源。市场按系统设置 `framework.market_cdn_url` 读取,
5
+ **seed 缺省值即相对路径 `/cdn`(本站点自供),新工程免 `ojm market setup`**;指向外部真 CDN 时才改设置。
6
6
 
7
7
  > **目录缺失会让站点起不来**:`oj serve` 启动即校验挂载目录,缺失就报错退出。
8
8
  > 所以本 README 进模板、进发布包、进新工程——它的存在就是目录存在的理由,**不要删**。
@@ -23,14 +23,14 @@
23
23
 
24
24
  ## 生产怎么切出去
25
25
 
26
- 1. 删掉 `api/config.yaml` 的 `mounts` 里这条(它就是本地挂载的全部):
26
+ 1. 删掉工程根 `config.yaml` 的 `mounts` 里这条(它就是本地挂载的全部):
27
27
 
28
28
  ```yaml
29
29
  - prefix: /cdn
30
- web: ../cdn
30
+ web: ./cdn
31
31
  ```
32
32
 
33
- 2. 把真 CDN(对象存储 / nginx)的地址写进系统设置 `framework.market_cdn_url`;
33
+ 2. 把真 CDN(对象存储 / nginx)的**绝对地址**写进系统设置 `framework.market_cdn_url`;
34
34
  命令行等价写法是 `ojm market setup --url <地址>`。
35
35
  3. 索引(`bundles.yaml` / `manifests.yaml`)由 CDN 侧维护:`ojm market build-index` 在本地产出后自行上传。
36
36
 
@@ -30,19 +30,20 @@
30
30
  # **这里没有 api 挂载是有意的**:本工程是 `src/<name>/{api,web}` 布局,模块分散在
31
31
  # `src/<name>/api` 下,而 mounts 的 api 项按 `<目录>/<模块>/manifest.yaml` 单层解析、
32
32
  # 不会递归(实测报 `module 'x' missing manifest.yaml`)。源码形态由 `ojm dev` 用
33
- # `--module-root src` 供给,产物形态由 `ojm preview` 用 `--api-path api/dist` 供给;
33
+ # `--module-root src` 供给,产物形态由 `ojm preview` 用 `--api-path dist/api` 供给;
34
34
  # 两者与 mounts 的 api 项同时出现还会报「module declared in multiple places」。
35
35
  # 手写部署(不经 ojm)时才需要自己加回一条 `- prefix: /api` + `api: dist`。
36
36
  mounts:
37
37
  # 本地模拟 CDN:直出工程根 cdn/(内容见 cdn/README.md)。
38
- # 红线:生产请删掉这一条挂载,并把 system 设置 framework.market_cdn_url 指向真 CDN。
38
+ # seed 的 framework.market_cdn_url 缺省即 /cdn(本站点自供),新工程免 market setup。
39
+ # 红线:生产请删掉这一条挂载,并把 system 设置 framework.market_cdn_url 指向真 CDN(绝对地址)。
39
40
  # 不得给此挂载设 spa——缺文件必须返回 404,回落 index.html 会让市场解析崩。
40
41
  - prefix: /cdn
41
42
  web: ./cdn
42
43
  # 生产若由 oj 直出前端(前后端同进程,即 ojm preview 的生产形态),
43
- # 取消下一行注释并先 build 出 web/dist(preview 会自动注入等价挂载,无需手改):
44
+ # 取消下一行注释并先 build 出 dist/web(preview 会自动注入等价挂载,无需手改):
44
45
  # - prefix: "/"
45
- # web: "./web/dist" # 相对 config.yaml → 工程根/web/dist
46
+ # web: "./dist/web" # 相对 config.yaml → 工程根/dist/web
46
47
  # spa: true
47
48
  # 嵌套 handler 组(oj v0.1.64)无需配置开关,组织方式见 api.ts 顶部注释。
48
49
 
@@ -8,7 +8,7 @@
8
8
  # 2. 模板自运行专属文件——templates 本身也是一个可 `pnpm dev` 的工程,
9
9
  # 它的 package.json / node_modules / bin / lock 绝不能泄漏进用户工程;
10
10
  # package.json 由 init 现场生成(钉死真实版本,不带 workspace:/catalog:)。
11
- # 3. 构建与运行期产物——api/dist、web/dist、logs、签发的私钥与 sqlite;
11
+ # 3. 构建与运行期产物——dist/api、dist/web、logs、签发的私钥与 sqlite;
12
12
  # 模板跑过 build/dev 后会留在本地,既不进用户工程,更不该让所有工程
13
13
  # 共享同一把私钥。
14
14
  # 4. 模板自用元数据——本清单自身。
@@ -1,5 +1,6 @@
1
1
  -- 系统设置默认值(PRD §4.3;注释内禁英文分号)
2
2
  INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
3
- INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
3
+ -- 系统设置默认值(PRD §4.3;注释内禁英文分号;framework.market_cdn_url 缺省 /cdn = 本站点自供,免 setup)
4
+ INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0);
4
5
  INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
5
6
  INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
@@ -1,5 +1,6 @@
1
1
  -- 系统设置默认值(PRD §4.3;注释内禁英文分号)
2
2
  INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0) ON CONFLICT DO NOTHING;
3
- INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0) ON CONFLICT DO NOTHING;
3
+ -- 系统设置默认值(PRD §4.3;注释内禁英文分号;framework.market_cdn_url 缺省 /cdn = 本站点自供,免 setup)
4
+ INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0) ON CONFLICT DO NOTHING;
4
5
  INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0) ON CONFLICT DO NOTHING;
5
6
  INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0) ON CONFLICT DO NOTHING;
@@ -87,7 +87,7 @@ export interface MarketEnv {
87
87
 
88
88
  /**
89
89
  * 宿主 versions.json 布局推导(B-11):与 manifests.yaml 同层(release 根 = `sharedRoot`),
90
- * 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`api/dist` vs `web/dist`)。
90
+ * 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`dist/api` vs `dist/web`)。
91
91
  */
92
92
  export function versionsJsonPathOf(releaseRoot: string): string {
93
93
  const trimmed = releaseRoot.replace(/\/+$/, "");
@@ -132,21 +132,22 @@ async function hostVersionsOf(env: MarketEnv): Promise<Record<string, string> |
132
132
  /**
133
133
  * oj serve release 布局生产缺省(P1-16 B3/B4;全字段可覆盖)。
134
134
  *
135
- * 两处根在 `src/<name>/{api,web}` 布局下分流,且都相对 fs jail 根(= config.yaml 所在目录):
136
- * release 根 `api/dist` —— 版本目录 / manifests.yaml / versions.json / migrations(后端产物)
137
- * 站点根 `web/dist` —— modules/ + modules.json(前端产物,安装器 upsert 的落点)
135
+ * 两处根在 `src/<name>/{api,web}` 布局下分流(2026-10-22 起收进工程根 dist/),
136
+ * 且都相对 fs jail 根(= config.yaml 所在目录):
137
+ * release 根 `dist/api` —— 版本目录 / manifests.yaml / versions.json / migrations(后端产物)
138
+ * 站点根 `dist/web` —— modules/ + modules.json(前端产物,安装器 upsert 的落点)
138
139
  * 暂存 / 任务 / 状态 / 信任库 / db 仍在 jail 根下:它们是安装器私有面,不属任何产物根。
139
140
  */
140
141
  export function marketEnv(): MarketEnv {
141
142
  return {
142
- sharedRoot: "api/dist",
143
+ sharedRoot: "dist/api",
143
144
  staging: "modules/.staging",
144
145
  taskDir: "modules/.install-tasks",
145
146
  statePath: "modules/.solutions/state.yaml",
146
147
  trustPath: ".solutions/trusted-keys.yaml",
147
148
  dbPath: "db.sqlite",
148
- siteModulesDir: "web/dist/modules",
149
- modulesJsonPath: "web/dist/modules.json",
149
+ siteModulesDir: "dist/web/modules",
150
+ modulesJsonPath: "dist/web/modules.json",
150
151
  apiPrefix: "/api",
151
152
  ledgerHeadOf: journalHeadOf,
152
153
  };
@@ -162,7 +163,9 @@ function fail(status: number, msg: string): never {
162
163
  }
163
164
 
164
165
  function fetchOf(env: MarketEnv): FetchInitFace {
165
- return env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace;
166
+ // 相对基址(feature:/cdn = 本站点自供)按请求源补全同源绝对 URL;绝对 http(s) 直通。
167
+ // 无请求上下文(无 Host)不猜原点、原样透传;runInstall 侧的 withSiteOrigin 包裹幂等。
168
+ return withSiteOrigin(env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace);
166
169
  }
167
170
 
168
171
  function execOf(env: MarketEnv): ExecFace {
@@ -23,8 +23,9 @@ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM
23
23
  INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor';
24
24
  INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer';
25
25
  -- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;framework.market_cdn_url 安全组仅 superadmin 可改)
26
+ -- framework.market_cdn_url 缺省 /cdn = 本站点自供(config.yaml 的 /cdn 挂载直出工程根 cdn/),脚手架免 setup;外部真 CDN 才改指绝对地址
26
27
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
27
- INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
28
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0);
28
29
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
29
30
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
30
31
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_base_dn', 'ldap', 'ldap', 'string', '', '', 'system:setting.ldap_base_dn', 0, 0);
@@ -37,9 +37,9 @@ export type { SolutionRouteRow } from "../_lifecycle/verify-checks";
37
37
  */
38
38
  export function solutionsEnv(): SolutionsEnv {
39
39
  return {
40
- sharedRoot: "api/dist",
40
+ sharedRoot: "dist/api",
41
41
  statePath: "modules/.solutions/state.yaml",
42
- modulesJsonPath: "web/dist/modules.json",
42
+ modulesJsonPath: "dist/web/modules.json",
43
43
  apiPrefix: "/api",
44
44
  };
45
45
  }
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
7
7
  import { useNavigate, useParams } from "react-router";
8
8
 
9
9
  import { systemRolesGet, systemUsersGet, systemUsersRolesPut } from "../../__client/api";
10
- import { isForbidden } from "../errors";
10
+ import { explainError, isForbidden } from "../errors";
11
11
  import { ForbiddenPanel } from "../forbidden";
12
12
 
13
13
  /**
@@ -36,6 +36,8 @@ export default function UserRolesPage() {
36
36
  queryClient.invalidateQueries({ queryKey: ["system", "users"] });
37
37
  navigate("/system/users");
38
38
  },
39
+ // 403 全局 toast 已被请求层抑制,页面须自行反馈(admin 非 superadmin 手输 URL 的场景)
40
+ onError: e => message.error(explainError(e, t)),
39
41
  });
40
42
 
41
43
  const user = usersQuery.data?.find(u => String(u.id) === uid);
@@ -331,6 +331,36 @@ describe("POST install(cdn:Form B 索引解析属市场侧,安装器不拼
331
331
  });
332
332
  });
333
333
 
334
+ describe("相对基址(feature:/cdn = 本站点自供,按请求源补全同源绝对 URL)", () => {
335
+ const REL = "/cdn";
336
+ const HOST = "127.0.0.1:9778";
337
+ const relEnv = () => makeEnv({ cdnBase: async () => REL });
338
+
339
+ it("index:索引与 .sig/manifests 拉取按请求 Host 补全为同源绝对 URL", async () => {
340
+ const api = createMarketApi(relEnv());
341
+ g.http = { user: { id: 1 }, headers: { host: HOST } };
342
+ await api.index.get();
343
+ expect(last?.status).toBe(200);
344
+ expect(fetchCalls).toContain(`http://${HOST}/cdn/bundles.yaml`);
345
+ expect(fetchCalls).toContain(`http://${HOST}/cdn/manifests.yaml`);
346
+ });
347
+
348
+ it("install:archive 分发包同样补全同源绝对 URL", async () => {
349
+ const api = createMarketApi(relEnv());
350
+ g.http = { user: { id: 1 }, body: { name: "demo-solution", version: "0.1.0", source: "cdn" }, headers: { host: HOST } };
351
+ await api.install.post();
352
+ expect(last?.status).toBe(200);
353
+ expect(fetchCalls).toContain(`http://${HOST}/cdn/demo-solution-0.1.0.tgz`);
354
+ });
355
+
356
+ it("无请求上下文(无 Host):不猜原点,原样透传相对 URL", async () => {
357
+ const api = createMarketApi(relEnv());
358
+ g.http = { user: { id: 1 } };
359
+ await api.index.get();
360
+ expect(fetchCalls).toContain("/cdn/bundles.yaml");
361
+ });
362
+ });
363
+
334
364
  describe("POST install/cancel(P3-12 端点接线,U5/P3-8 令牌的消费面)", () => {
335
365
  it("无进行中安装 → 404(取消只对 running 态有效,任务终态后信号已注销)", async () => {
336
366
  const api = createMarketApi(makeEnv());
@@ -858,12 +888,12 @@ describe("hostVersions 缺省接线(B-11:C2-09 比对真源 = <dist>/version
858
888
  });
859
889
 
860
890
  describe("P1-16 接线:marketEnv 生产口径 / exec opts 转发 / operator 通道 / 账本真源 / index 即对账入口", () => {
861
- it("marketEnv() 缺省 = 布局 v2 生产口径:版本目录与 versions.json 在 release 根 api/dist,站点 modules 在 web/dist;暂存/任务/sidecar 留 jail 根", () => {
891
+ it("marketEnv() 缺省 = 布局 v3 生产口径:版本目录与 versions.json 在 release 根 dist/api,站点 modules 在 dist/web;暂存/任务/sidecar 留 jail 根", () => {
862
892
  const env = marketEnv();
863
- expect(env.sharedRoot).toBe("api/dist");
864
- expect(env.siteModulesDir).toBe("web/dist/modules");
865
- expect(env.modulesJsonPath).toBe("web/dist/modules.json");
866
- expect(versionsJsonPathOf(env.sharedRoot)).toBe("api/dist/versions.json");
893
+ expect(env.sharedRoot).toBe("dist/api");
894
+ expect(env.siteModulesDir).toBe("dist/web/modules");
895
+ expect(env.modulesJsonPath).toBe("dist/web/modules.json");
896
+ expect(versionsJsonPathOf(env.sharedRoot)).toBe("dist/api/versions.json");
867
897
  expect(env.staging).toBe("modules/.staging");
868
898
  expect(env.taskDir).toBe("modules/.install-tasks");
869
899
  expect(env.statePath).toBe("modules/.solutions/state.yaml");
@@ -278,7 +278,7 @@ function setHttp(over: Record<string, unknown>) {
278
278
  }
279
279
 
280
280
  function makeEnv() {
281
- // modulesJsonPath 钉进临时目录(readFile 失败 → 不打 web 项):默认值 `web/dist/modules.json`
281
+ // modulesJsonPath 钉进临时目录(readFile 失败 → 不打 web 项):默认值 `dist/web/modules.json`
282
282
  // 是相对 cwd 的,会把模板本机构建产物卷进用例,全绿用例随「模板 build 过没有」摇摆。
283
283
  // P4-2 两条 integrity 用例显式覆盖它。
284
284
  return { ...solutionsEnv(), sharedRoot: root, statePath: join(root, "state.yaml"), modulesJsonPath: join(root, "modules.json") };
@@ -128,7 +128,7 @@ function setHttp(over: Record<string, unknown> = {}) {
128
128
 
129
129
  function makeEnv() {
130
130
  // modulesJsonPath 同 verify-checks.spec:钉进临时目录(缺文件 → 跳过 web 项),
131
- // 不吃模板本机 `web/dist/modules.json`。
131
+ // 不吃模板本机 `dist/web/modules.json`。
132
132
  return { ...solutionsEnv(), sharedRoot: root, statePath: join(root, "state.yaml"), modulesJsonPath: join(root, "modules.json"), verifyReportsDir: reportsDir };
133
133
  }
134
134
 
@@ -8,9 +8,9 @@
8
8
  "react-router": "7.18.2",
9
9
  "react-router/dom": "7.18.2",
10
10
  "@tanstack/react-query": "5.102.8",
11
- "@oj-module/runtime": "0.2.23",
12
- "@oj-module/runtime/contract": "0.2.23",
13
- "@oj-module/runtime/contract/errors": "0.2.23",
11
+ "@oj-module/runtime": "0.2.25",
12
+ "@oj-module/runtime/contract": "0.2.25",
13
+ "@oj-module/runtime/contract/errors": "0.2.25",
14
14
  "@plane/kit": "0.1.20",
15
15
  "@plane/editor": "0.1.20",
16
16
  "antd": "6.6.1",
package/src/args.ts DELETED
@@ -1,172 +0,0 @@
1
- /**
2
- * 子命令参数解析(纯函数,可测)。
3
- *
4
- * 约定:位置参数取第一个不以 `--` 开头的参数;`--xxx` 一律是开关/选项。
5
- * 缺陷背景:`ojm init --yes` 曾把 argv[3](即 `--yes`)当目标目录。
6
- */
7
-
8
- export function parseInitArgs(argv: string[]): { dest: string, yes: boolean } {
9
- const yes = argv.includes("--yes");
10
- const dest = argv.find(a => !a.startsWith("--")) ?? "";
11
- return { dest, yes };
12
- }
13
-
14
- /**
15
- * `ojm api [dir] [--check] [--docs] [--exempt <path>]`:位置参数为项目目录
16
- * (缺省 cwd),与 parseInitArgs 同一约定(docs/prd/202609032019-ojm-api-cwd.md)。
17
- * 注意:`--exempt` 的「值」不得以 `--` 开头,否则会被误判为 dir——故逐项扫描,
18
- * 消费掉 `--exempt` 紧随的值,避免被 dir 解析吞噬(P6 同款坑)。
19
- */
20
- export function parseApiArgs(argv: string[]): { dir: string, check: boolean, docs: boolean, exempt?: string } {
21
- const flags = new Set<string>();
22
- const positionals: string[] = [];
23
- let exempt: string | undefined;
24
- for (let i = 0; i < argv.length; i++) {
25
- const a = argv[i];
26
- if (a === "--exempt") {
27
- exempt = argv[i + 1];
28
- i++; // 跳过紧随的值,不被当 dir
29
- continue;
30
- }
31
- if (a.startsWith("--")) {
32
- flags.add(a);
33
- continue;
34
- }
35
- positionals.push(a);
36
- }
37
- const dir = positionals[0] ?? "";
38
- return { dir, check: flags.has("--check"), docs: flags.has("--docs"), exempt };
39
- }
40
-
41
- /**
42
- * `ojm build [--bundle] [--out <dir>]`。
43
- * `--bundle`:方案包构建(契约校验 → 确定性 tgz + sha256)。bundle 级契约位置:优先工程根
44
- * `solution.yaml`、或 `src/<模块>/solution.yaml`(模块级方案,契约与所打包模块同位);旧方案回退
45
- * `manifest.yaml`(同两处位置)。与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工
46
- * (devkit 手册「打包与分发」)。
47
- * `--out` 的值不得以 `--` 开头(同 parseApiArgs 的消费式扫描,P6 同款坑)。
48
- */
49
- export function parseBuildArgs(argv: string[]): { bundle: boolean, bundleName?: string, out?: string } {
50
- const flags = new Set<string>();
51
- let out: string | undefined;
52
- let bundleName: string | undefined;
53
- for (let i = 0; i < argv.length; i++) {
54
- const a = argv[i];
55
- if (a === "--out") {
56
- out = argv[i + 1];
57
- i++; // 消费紧随的值,不被当 flag
58
- continue;
59
- }
60
- // --bundle <name> / --bundle=<name>:只打这一份方案(无值 = 全部用户方案)
61
- if (a === "--bundle") {
62
- flags.add(a);
63
- const next = argv[i + 1];
64
- if (next !== undefined && !next.startsWith("--")) {
65
- bundleName = next;
66
- i++;
67
- }
68
- continue;
69
- }
70
- if (a.startsWith("--bundle=")) {
71
- flags.add("--bundle");
72
- bundleName = a.slice("--bundle=".length);
73
- continue;
74
- }
75
- if (a.startsWith("--")) {
76
- flags.add(a);
77
- }
78
- }
79
- return { bundle: flags.has("--bundle"), bundleName, out };
80
- }
81
-
82
- /**
83
- * `ojm new <name> [--layout container|none] [--api]`:新方案骨架生成参数。
84
- * name 为首个非 flag 位置参数(必填,由调用方判空——非交互/CI 友好)。
85
- */
86
- export function parseNewArgs(argv: string[]): { name?: string, layout?: "container" | "none", api?: boolean } {
87
- let name: string | undefined;
88
- let layout: "container" | "none" | undefined;
89
- let api = false;
90
- for (let i = 0; i < argv.length; i++) {
91
- const a = argv[i];
92
- if (a === "--layout") {
93
- const v = argv[i + 1];
94
- if (v !== "container" && v !== "none")
95
- throw new Error(`[ojm] --layout 只接受 container | none(实际:${v})`);
96
- layout = v;
97
- i++;
98
- continue;
99
- }
100
- if (a === "--api") {
101
- api = true;
102
- continue;
103
- }
104
- if (a !== undefined && !a.startsWith("--")) {
105
- if (name !== undefined)
106
- throw new Error(`[ojm] ojm new 只接受一个方案名(多余参数:${a})`);
107
- name = a;
108
- continue;
109
- }
110
- }
111
- return { name, layout, api };
112
- }
113
-
114
- /**
115
- * `ojm vendor [tag] [--force]`:下载/更新 oj vendor(npm 包 @oj-bin/oj)。
116
- * tag 缺省追 npm latest;形如 `v0.1.13` 或 `0.1.13`(自动补 v 前缀,装包时去掉)。
117
- * 非法 tag 形状即报错(防误把目录当版本)。
118
- */
119
- export function parseVendorArgs(argv: string[]): { force: boolean, tag?: string } {
120
- const tag = argv.find(a => !a.startsWith("--") && a !== "--");
121
- if (tag !== undefined && !/^v?\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(tag)) {
122
- throw new Error(`[ojm] 非法版本号:${tag}(形如 v0.1.11 或 0.1.11)`);
123
- }
124
- return {
125
- force: argv.includes("--force"),
126
- tag: tag === undefined ? undefined : (tag.startsWith("v") ? tag : `v${tag}`),
127
- };
128
- }
129
-
130
- export interface LifecycleArgs {
131
- positionals: string[]
132
- url?: string
133
- user?: string
134
- pass?: string
135
- locale?: string
136
- name?: string
137
- version?: string
138
- task?: string
139
- }
140
-
141
- /**
142
- * `ojm install|enable|disable` 共用解析:位置参数(目标/方案名)+ 值式 flag。
143
- * 站点寻址三件套 --url/--user/--pass(环境变量 OJM_URL/OJM_USER/OJM_PASS 兜底,见 src/site.ts);
144
- * install 专属 --name/--version(文件名身份覆盖)与 --locale(终态文案语言);
145
- * trust 专属 --task(TOFU 确认目标任务 id,P2-3)。
146
- * 值式 flag 逐项消费紧随的值,不被当位置参数(P6 同款坑,parseApiArgs 同纪律)。
147
- */
148
- export function parseLifecycleArgs(argv: string[]): LifecycleArgs {
149
- const valueFlags = new Set(["--url", "--user", "--pass", "--locale", "--name", "--version", "--task"]);
150
- const values: Record<string, string> = {};
151
- const positionals: string[] = [];
152
- for (let i = 0; i < argv.length; i++) {
153
- const a = argv[i]!;
154
- if (valueFlags.has(a)) {
155
- values[a] = argv[i + 1] ?? "";
156
- i++; // 消费紧随的值
157
- continue;
158
- }
159
- if (!a.startsWith("--"))
160
- positionals.push(a);
161
- }
162
- return {
163
- positionals,
164
- url: values["--url"],
165
- user: values["--user"],
166
- pass: values["--pass"],
167
- locale: values["--locale"],
168
- name: values["--name"],
169
- version: values["--version"],
170
- task: values["--task"],
171
- };
172
- }
@@ -1,20 +0,0 @@
1
- // BDD:ojm new 参数解析(统一审查 F8——多余位置参数须显式拒绝,不能静默丢弃)。
2
- // 静默丢弃的恶果:ojm new crm erp(手滑/粘贴)只建 crm,用户以为两份都建了。
3
- import { describe, expect, it } from "vitest";
4
- import { parseNewArgs } from "../../args";
5
-
6
- describe("parseNewArgs", () => {
7
- it("name + 旗标组合", () => {
8
- expect(parseNewArgs(["crm"])).toEqual({ name: "crm", layout: undefined, api: false });
9
- expect(parseNewArgs(["crm", "--layout", "none", "--api"])).toEqual({ name: "crm", layout: "none", api: true });
10
- expect(parseNewArgs([])).toEqual({ name: undefined, layout: undefined, api: false });
11
- });
12
-
13
- it("多余位置参数 → 报错而非静默丢弃", () => {
14
- expect(() => parseNewArgs(["crm", "erp"])).toThrow(/只接受一个方案名/);
15
- });
16
-
17
- it("非法 --layout 值 → 报错", () => {
18
- expect(() => parseNewArgs(["crm", "--layout", "sidebar"])).toThrow(/container \| none/);
19
- });
20
- });