@oj-module/cli 0.2.69 → 0.2.71

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 (147) hide show
  1. package/devkit/README.md +10 -8
  2. package/devkit/framework-dev.md +1 -1
  3. package/devkit/manual.md +45 -19
  4. package/devkit/module-dev-and-cdn-playground.md +20 -14
  5. package/devkit/publish-walkthrough.md +132 -43
  6. package/devkit/wecom-customer-reauth-guide.md +110 -0
  7. package/package.json +2 -2
  8. package/shell-dist/assets/{runtime.zrv2zhj9j0.js → runtime.7x99hmm020.js} +2 -2
  9. package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
  10. package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
  11. package/shell-dist/index.html +4 -4
  12. package/shell-dist/versions.json +3 -3
  13. package/src/__tests__/list.test.ts +7 -7
  14. package/src/__tests__/oj-health-host.test.ts +19 -0
  15. package/src/__tests__/site-tenant-header.test.ts +2 -2
  16. package/src/__tests__/templates-builtin-list.test.ts +47 -0
  17. package/src/__tests__/templates-schema-columns.test.ts +11 -2
  18. package/src/__tests__/templates-tenant-scope.test.ts +2 -3
  19. package/src/__tests__/users-roles-display.test.ts +84 -0
  20. package/src/bundle.ts +1 -1
  21. package/src/contract/__tests__/market-client.integration.test.ts +23 -22
  22. package/src/enable.ts +2 -2
  23. package/src/init.ts +45 -0
  24. package/src/install.ts +5 -5
  25. package/src/lifecycle/__tests__/install.test.ts +29 -29
  26. package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
  27. package/src/lifecycle/__tests__/publish.test.ts +8 -10
  28. package/src/lifecycle/__tests__/trust.test.ts +14 -14
  29. package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
  30. package/src/list.ts +3 -3
  31. package/src/market/__tests__/bootstrap.test.ts +24 -7
  32. package/src/market/__tests__/failure-playbook.test.ts +0 -1
  33. package/src/market/__tests__/remote-push.test.ts +228 -0
  34. package/src/market/__tests__/verify.test.ts +45 -207
  35. package/src/market/bootstrap.ts +12 -7
  36. package/src/market/command.ts +33 -35
  37. package/src/market/db-seed.ts +129 -0
  38. package/src/market/failure-playbook.ts +3 -9
  39. package/src/market/fixture.ts +2 -6
  40. package/src/market/publish.ts +145 -89
  41. package/src/market/remote-push.ts +236 -0
  42. package/src/market/trial.ts +2 -2
  43. package/src/market/verify.ts +17 -270
  44. package/src/market/walkthrough.ts +38 -63
  45. package/src/oj.ts +13 -2
  46. package/src/preview.ts +5 -3
  47. package/src/publish.ts +3 -2
  48. package/src/scaffold-guard.ts +4 -0
  49. package/src/site.ts +1 -1
  50. package/src/trust.ts +8 -8
  51. package/src/uninstall.ts +2 -2
  52. package/src/usage.ts +23 -21
  53. package/templates/config.yaml +22 -3
  54. package/templates/init.include.yaml +16 -11
  55. package/templates/openapi.json +3948 -3655
  56. package/templates/src/.ojm-builtin.json +13 -0
  57. package/templates/src/README.md +5 -2
  58. package/templates/src/app/api/_platform/README.md +3 -2
  59. package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
  60. package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
  61. package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
  62. package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
  63. package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
  64. package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
  65. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
  66. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
  67. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
  68. package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
  69. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
  70. package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
  71. package/templates/src/app/api/_platform/manifest.yaml +1 -2
  72. package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
  73. package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
  74. package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
  75. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
  76. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
  77. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
  78. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
  79. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
  80. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
  81. package/templates/src/app/api/_platform/schema.yaml +8 -11
  82. package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
  83. package/templates/src/app/api/_platform/seed.pg.sql +1 -1
  84. package/templates/src/app/api/_platform/seed.sql +1 -1
  85. package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
  86. package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -114
  87. package/templates/src/app/web/_platform/__client/index.ts +1 -333
  88. package/templates/src/app/web/system/locales/en-US.json +15 -1
  89. package/templates/src/app/web/system/locales/zh-CN.json +15 -1
  90. package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
  91. package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
  92. package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
  93. package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
  94. package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
  95. package/templates/src/app/web/system/pages/users/index.tsx +12 -2
  96. package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
  97. package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
  98. package/templates/src/app/web/system/routes.ts +18 -1
  99. package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
  100. package/templates/src/oj-market/api/backfill.ts +221 -0
  101. package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
  102. package/templates/src/oj-market/api/cdn/api.ts +312 -0
  103. package/templates/src/oj-market/api/cdn/shared.ts +104 -0
  104. package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
  105. package/templates/src/oj-market/api/manifest.yaml +8 -0
  106. package/templates/src/oj-market/solution.yaml +21 -0
  107. package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
  108. package/templates/src/oj-market/web/__client/index.ts +509 -0
  109. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
  110. package/templates/src/oj-market/web/api/client.ts +6 -6
  111. package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
  112. package/templates/src/oj-market/web/entry.ts +5 -3
  113. package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
  114. package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
  115. package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
  116. package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
  117. package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
  118. package/templates/src/oj-market/web/routes.ts +12 -0
  119. package/templates/src/oj-market/web/types.ts +14 -8
  120. package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
  121. package/templates/tests/lifecycle/backfill.spec.ts +392 -0
  122. package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
  123. package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
  124. package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
  125. package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
  126. package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
  127. package/templates/tests/lifecycle/installer.spec.ts +36 -0
  128. package/templates/tests/lifecycle/market-api.spec.ts +448 -803
  129. package/templates/tests/lifecycle/preflight.spec.ts +18 -0
  130. package/templates/tests/lifecycle/trust.spec.ts +2 -2
  131. package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
  132. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  133. package/templates/tests/solutions-entry.spec.ts +55 -0
  134. package/templates/tsconfig.paths.json +1 -0
  135. package/templates/web.config.ts +6 -6
  136. package/vendor/host-versions.json +3 -3
  137. package/bin/oj +0 -4
  138. package/src/market/__tests__/build-index.test.ts +0 -641
  139. package/src/market/__tests__/sharding.test.ts +0 -90
  140. package/src/market/__tests__/yank-ledger.test.ts +0 -60
  141. package/src/market/build-index.ts +0 -526
  142. package/src/market/index-builder.ts +0 -149
  143. package/src/market/sharding.ts +0 -94
  144. package/src/market/yank-ledger.ts +0 -91
  145. package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
  146. package/templates/src/app/api/_platform/market/index-source.ts +0 -112
  147. package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
package/src/enable.ts CHANGED
@@ -1,4 +1,4 @@
1
- // ojm enable/disable(P1-13,D8):生命周期开关瘦客户端——POST /api/_platform/market/enable
1
+ // ojm enable/disable(P1-13,D8):生命周期开关瘦客户端——POST /api/oj-market/enable
2
2
  // 触发站点侧 setEnabled 四处同写(锁/sidecar/route/modules.json,D12)。本地零状态,纯转发。
3
3
  import process from "node:process";
4
4
  import { callApi, login, resolveSite } from "./site";
@@ -21,7 +21,7 @@ export async function setEnabledCommand(name: string, enabled: boolean, opts: En
21
21
  const fetchFn = opts.fetchFn ?? globalThis.fetch;
22
22
  const { base, user, pass } = resolveSite(opts);
23
23
  const token = await login(fetchFn, base, user, pass);
24
- const result = await callApi<{ ok: boolean, message: string }>(fetchFn, base, token, "POST", "/api/_platform/market/enable", { name: name.trim(), enabled });
24
+ const result = await callApi<{ ok: boolean, message: string }>(fetchFn, base, token, "POST", "/api/oj-market/enable", { name: name.trim(), enabled });
25
25
  console.log(`[ojm] ${result.message}`);
26
26
  if (!result.ok)
27
27
  process.exitCode = 1;
package/src/init.ts CHANGED
@@ -38,6 +38,8 @@ export interface InitOptions {
38
38
  ojInstaller?: (binDir: string) => Promise<void>
39
39
  /** 注入桩密封(测试);缺省真跑 bin/oj secret keygen/seal */
40
40
  seal?: Sealer
41
+ /** 随站第一方 bundle 源目录(测试注入);缺省 <cliRoot>/shell-dist/bundles(prepack 同步面) */
42
+ bundlesDir?: string
41
43
  }
42
44
 
43
45
  interface InitReport {
@@ -149,6 +151,12 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
149
151
  else if (builtinStatus === "skipped")
150
152
  report.skipped.push(`${BUILTIN_LIST_FILE}(已存在,不覆盖用户手改)`);
151
153
 
154
+ // 1c) 随站第一方 bundle 落位(Phase B T3b,仅 --yes 自动化路径;交互式零动作——
155
+ // 决策 #1 两路径并存):tgz+sig 拷入 cdn/ 并写 bundles.json 意图标记,
156
+ // 站点经 oj-market index 端点对账收敛(上拉不下拉,方案 §5)。
157
+ // bundle 由发布流水线 built+signed 后收进 shell-dist/bundles(scripts/sync-bundles.mjs)。
158
+ placeBundledSolutions(destDir, opts, report, cliRoot);
159
+
152
160
  // 1b) tsconfig.paths.json:把 oj 别名 `#/<模块名>/…` 投影成 TS 的 paths。
153
161
  // 必须**现场生成**而不是随模板拷贝——模板兼作 demo 工程(多出 demo/home 两个
154
162
  // 模块),而用户工程只有白名单里的那些;照拷会让 paths 指向不存在的目录。
@@ -277,6 +285,43 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
277
285
  }
278
286
 
279
287
  /** 默认密封器:bin/oj secret keygen + seal(失败返回 null,不抛) */
288
+ /**
289
+ * 随站第一方 bundle 落位(Phase B T3b):`ojm init --yes` 把发布流水线收进
290
+ * shell-dist/bundles 的 <name>-<version>.tgz(+sig) 拷入工程 cdn/,并写
291
+ * bundles.json 意图标记(站点启动后经 oj-market index 端点对账收敛——上拉不下拉)。
292
+ * 仅 --yes 自动化路径执行(决策 #1:--yes = 无人值守授权;交互式保持手动口径);
293
+ * bundle 源缺失是「发布未内置 bundle」的合法态(如源码仓直跑)——warning 不失败。
294
+ */
295
+ function placeBundledSolutions(destDir: string, opts: InitOptions, report: InitReport, cliRoot: string): void {
296
+ if (!opts.yes)
297
+ return;
298
+ const srcDir = opts.bundlesDir ?? path.join(cliRoot, "shell-dist", "bundles");
299
+ if (!fs.existsSync(srcDir)) {
300
+ report.warnings.push("随站方案包缺失(shell-dist/bundles 不在——发布流水线未内置 bundle):市场保持出厂未装,superadmin 可经 system 域「方案安装」首装");
301
+ return;
302
+ }
303
+ const files = fs.readdirSync(srcDir).filter(f => f.endsWith(".tgz")).sort();
304
+ if (files.length === 0) {
305
+ report.warnings.push("随站方案包目录为空(shell-dist/bundles 无 tgz):市场保持出厂未装");
306
+ return;
307
+ }
308
+ const cdnDir = path.join(destDir, "cdn");
309
+ fs.mkdirSync(cdnDir, { recursive: true });
310
+ const entries: { name: string, version: string, file: string }[] = [];
311
+ for (const file of files) {
312
+ fs.copyFileSync(path.join(srcDir, file), path.join(cdnDir, file));
313
+ const sig = `${file}.sig`;
314
+ if (fs.existsSync(path.join(srcDir, sig)))
315
+ fs.copyFileSync(path.join(srcDir, sig), path.join(cdnDir, sig));
316
+ // name/version 自文件名解析(bundle 命名纪律 <name>-<version>.tgz,同 download 段正名口径);
317
+ // version 以数字开头取最后一个连字符切分(name 本身可含连字符,如 oj-market)
318
+ const m = file.match(/^(.+)-(\d[0-9A-Za-z.+-]*)\.tgz$/);
319
+ entries.push({ name: m?.[1] ?? file.replace(/\.tgz$/, ""), version: m?.[2] ?? "", file });
320
+ }
321
+ fs.writeFileSync(path.join(cdnDir, "bundles.json"), `${JSON.stringify({ solutions: entries }, null, "\t")}\n`);
322
+ report.created.push(`cdn/ 随站方案包 ${files.length} 个 + 意图标记 bundles.json`);
323
+ }
324
+
280
325
  const defaultSealer: Sealer = async (destDir, plainSecret) => {
281
326
  const ojBin = resolveOjBin(destDir);
282
327
  if (!fs.existsSync(ojBin))
package/src/install.ts CHANGED
@@ -2,7 +2,7 @@ import type { TaskRecord } from "./site";
2
2
  // ojm install(P1-13,D6):方案包安装瘦客户端。目标两种形态:
3
3
  // · 本地 tgz 路径(<name>-<version>.tgz 文件名推导身份,--name/--version 可覆盖)→ file 源 multipart 上传;
4
4
  // · name[@version] → cdn 源(站点侧 Form B 索引解析,安装器不拼 URL——市场侧职责,api.ts 同纪律)。
5
- // 流程:登录 → POST /api/_platform/market/install → 轮询 task?id= 至终态 → 渲染。
5
+ // 流程:登录 → POST /api/oj-market/install → 轮询 task?id= 至终态 → 渲染。
6
6
  // 终态渲染(契约附录 A 同表取用,R28/DRY):失败/partial 打 error.reason + retryable 标注 +
7
7
  // hintKey 对应双语指引(--locale zh-CN|en-US,缺省 zh-CN;文案真源为契约附录 A 表)。
8
8
  import fs from "node:fs";
@@ -285,11 +285,11 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
285
285
  if (opts.name !== undefined && opts.name !== slug)
286
286
  throw new UsageError(`[ojm] --name「${opts.name}」与 registry 目标推导的物理名「${slug}」不一致——registry 源由目标推导身份,请二选一`);
287
287
  // 旧站点拒:meta apiVersion < 0.2.0 不支持 registry 源(REG-R9② 升版反噬 F4 的人话面)
288
- const meta = await callApi<{ apiVersion?: string }>(fetchFn, base, token, "GET", "/api/_platform/market/meta");
288
+ const meta = await callApi<{ apiVersion?: string }>(fetchFn, base, token, "GET", "/api/oj-market/meta");
289
289
  if (!isSiteApiAtLeast(meta.apiVersion, "0.2.0"))
290
290
  throw new Error(`[ojm] 目标站点平台 API 版本过低(需 ≥ 0.2.0 才支持 --registry 源安装,实为 ${meta.apiVersion ?? "未知"})——请升级目标站点的 oj-module,或改用 CDN/file 源`);
291
291
  const registryOrigin = originOf(src.url) ?? src.url;
292
- response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/_platform/market/install${forceQ}`, {
292
+ response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/oj-market/install${forceQ}`, {
293
293
  name: slug,
294
294
  version: opts.version ?? version,
295
295
  source: "registry",
@@ -321,7 +321,7 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
321
321
  else {
322
322
  console.log(`[ojm] 未发现签名侧车(${path.basename(sigPath)})——宿主验签硬 fail-closed,未签名包将被拒(sig_missing);签名方法见 devkit「签名与信任」`);
323
323
  }
324
- const res = await fetchFn(`${base}/api/_platform/market/install${forceQ}`, {
324
+ const res = await fetchFn(`${base}/api/oj-market/install${forceQ}`, {
325
325
  method: "POST",
326
326
  headers: { authorization: `Bearer ${token}`, ...tenantHeaders() },
327
327
  body: form,
@@ -331,7 +331,7 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
331
331
  else {
332
332
  if (opts.name !== undefined && opts.name !== parsed.name)
333
333
  throw new Error(`[ojm] --name「${opts.name}」与目标「${parsed.name}」不一致——请二选一`);
334
- response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/_platform/market/install${forceQ}`, {
334
+ response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/oj-market/install${forceQ}`, {
335
335
  name: parsed.name,
336
336
  version: opts.version ?? parsed.version,
337
337
  source: "cdn",
@@ -96,7 +96,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
96
96
  sleepLog.length = 0;
97
97
  let n = 0;
98
98
  const { fetchFn } = fetchStub({
99
- "/api/_platform/market/task": () => ok(n++ < 2 ? rec({ status: "running", progress: 30 * n }) : rec()),
99
+ "/api/oj-market/task": () => ok(n++ < 2 ? rec({ status: "running", progress: 30 * n }) : rec()),
100
100
  });
101
101
  const r = await pollTask(fetchFn, "http://x", "tok", "t1", { sleep });
102
102
  expect(r.status).toBe("done");
@@ -107,7 +107,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
107
107
  sleepLog.length = 0;
108
108
  let n = 0;
109
109
  const { fetchFn } = fetchStub({
110
- "/api/_platform/market/task": () => ok(rec({ status: n++ < 5 ? "running" : "done", progress: 10 + n })),
110
+ "/api/oj-market/task": () => ok(rec({ status: n++ < 5 ? "running" : "done", progress: 10 + n })),
111
111
  });
112
112
  const r = await pollTask(fetchFn, "http://x", "tok", "t1", { sleep, timeoutMs: 20_000 });
113
113
  expect(r.status).toBe("done");
@@ -117,7 +117,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
117
117
  it("超时报错:总等待预算耗尽即人话退出", async () => {
118
118
  sleepLog.length = 0;
119
119
  const { fetchFn } = fetchStub({
120
- "/api/_platform/market/task": () => ok(rec({ status: "running" })),
120
+ "/api/oj-market/task": () => ok(rec({ status: "running" })),
121
121
  });
122
122
  await expect(pollTask(fetchFn, "http://x", "tok", "t1", { sleep, timeoutMs: 1000 })).rejects.toThrow(/超时/);
123
123
  expect(sleepLog).toEqual([800]);
@@ -125,7 +125,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
125
125
 
126
126
  it("查询参数形态:task?id=<id>(非路径段,P1-12 裁决)", async () => {
127
127
  const { fetchFn, calls } = fetchStub({
128
- "/api/_platform/market/task": () => ok(rec()),
128
+ "/api/oj-market/task": () => ok(rec()),
129
129
  });
130
130
  await pollTask(fetchFn, "http://x", "tok", "t1", { sleep });
131
131
  expect(calls[0]?.url).toContain("task?id=t1");
@@ -233,7 +233,7 @@ describe("schemaVersion 旧记录兼容(P2-10 升版纪律读方侧:高版
233
233
 
234
234
  it("pollTask:高版本记录原样返回不炸(轮询消费面兼容)", async () => {
235
235
  const { fetchFn } = fetchStub({
236
- "/api/_platform/market/task": () => ok(rec({ status: "done", schemaVersion: 99 })),
236
+ "/api/oj-market/task": () => ok(rec({ status: "done", schemaVersion: 99 })),
237
237
  });
238
238
  const r = await pollTask(fetchFn, "http://x", "tok", "t1", { sleep: async () => {} });
239
239
  expect(r.status).toBe("done");
@@ -245,8 +245,8 @@ describe("schemaVersion 旧记录兼容(P2-10 升版纪律读方侧:高版
245
245
  writeFileSync(tgz, "fake-tgz-bytes");
246
246
  const { fetchFn } = fetchStub({
247
247
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
248
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
249
- "/api/_platform/market/task": () => ok(rec({ status: "done", schemaVersion: 99 })),
248
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
249
+ "/api/oj-market/task": () => ok(rec({ status: "done", schemaVersion: 99 })),
250
250
  });
251
251
  const exitBackup = process.exitCode;
252
252
  process.exitCode = 0;
@@ -351,8 +351,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
351
351
  writeFileSync(tgz, "fake-tgz-bytes");
352
352
  const { fetchFn, calls } = fetchStub({
353
353
  "/api/auth/login": () => ok({ access_token: "tok-1", refresh_token: "r", expires_in: 1 }),
354
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
355
- "/api/_platform/market/task": () => ok(rec()),
354
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
355
+ "/api/oj-market/task": () => ok(rec()),
356
356
  });
357
357
  await installCommand(tgz, { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
358
358
  // 登录体
@@ -378,8 +378,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
378
378
  writeFileSync(tgz, "fake-tgz-bytes");
379
379
  const { fetchFn } = fetchStub({
380
380
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
381
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
382
- "/api/_platform/market/task": () => ok(rec({
381
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
382
+ "/api/oj-market/task": () => ok(rec({
383
383
  status: "failed",
384
384
  stage: "migrating",
385
385
  error: { reason: "迁移失败", retryable: true, hintKey: "lifecycle.stage.migrating.hint" },
@@ -403,8 +403,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
403
403
  writeFileSync(`${tgz}.sig`, JSON.stringify({ alg: "Ed25519", key_id: "test.publisher", public_key: "a".repeat(64), value: "AAAA", canon: "v2", registry_id: "test.registry" }));
404
404
  const { fetchFn, calls } = fetchStub({
405
405
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
406
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
407
- "/api/_platform/market/task": () => ok(rec()),
406
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
407
+ "/api/oj-market/task": () => ok(rec()),
408
408
  });
409
409
  await installCommand(tgz, { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
410
410
  const form = calls[1]?.init?.body as FormData;
@@ -418,8 +418,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
418
418
  writeFileSync(tgz, "fake-tgz-bytes");
419
419
  const { fetchFn, calls } = fetchStub({
420
420
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
421
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
422
- "/api/_platform/market/task": () => ok(rec()),
421
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
422
+ "/api/oj-market/task": () => ok(rec()),
423
423
  });
424
424
  await installCommand(tgz, { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
425
425
  const form = calls[1]?.init?.body as FormData;
@@ -441,8 +441,8 @@ describe("installCommand · cdn 源(JSON 体 name@version)", () => {
441
441
  it("source=cdn 的 JSON 体 + 版本缺省 null", async () => {
442
442
  const { fetchFn, calls } = fetchStub({
443
443
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
444
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
445
- "/api/_platform/market/task": () => ok(rec()),
444
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
445
+ "/api/oj-market/task": () => ok(rec()),
446
446
  });
447
447
  await installCommand("demo-solution", { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
448
448
  const installCall = calls[1]!;
@@ -453,8 +453,8 @@ describe("installCommand · cdn 源(JSON 体 name@version)", () => {
453
453
  it("@version 透传", async () => {
454
454
  const { fetchFn, calls } = fetchStub({
455
455
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
456
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
457
- "/api/_platform/market/task": () => ok(rec()),
456
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
457
+ "/api/oj-market/task": () => ok(rec()),
458
458
  });
459
459
  await installCommand("demo-solution@0.2.0", { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
460
460
  expect(JSON.parse(String(calls[1]?.init?.body)).version).toBe("0.2.0");
@@ -465,11 +465,11 @@ describe("setEnabledCommand · enable/disable 瘦客户端(D8)", () => {
465
465
  it("pOST enable {name, enabled} → 打印服务端 message;ok:false 非零退出", async () => {
466
466
  const { fetchFn, calls } = fetchStub({
467
467
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
468
- "/api/_platform/market/enable": () => ok({ ok: true, message: "启用完成:四处同写" }),
468
+ "/api/oj-market/enable": () => ok({ ok: true, message: "启用完成:四处同写" }),
469
469
  });
470
470
  await setEnabledCommand("demo-solution", true, { siteUrl: "http://x", user: "u", pass: "p", fetchFn });
471
471
  const call = calls[1]!;
472
- expect(call.url).toContain("/api/_platform/market/enable");
472
+ expect(call.url).toContain("/api/oj-market/enable");
473
473
  expect(JSON.parse(String(call.init?.body))).toEqual({ name: "demo-solution", enabled: true });
474
474
  expect(logs.join("\n")).toContain("启用完成:四处同写");
475
475
 
@@ -478,7 +478,7 @@ describe("setEnabledCommand · enable/disable 瘦客户端(D8)", () => {
478
478
  try {
479
479
  const { fetchFn: fetchFn2 } = fetchStub({
480
480
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
481
- "/api/_platform/market/enable": () => ok({ ok: false, message: "禁用失败:「x」未安装" }),
481
+ "/api/oj-market/enable": () => ok({ ok: false, message: "禁用失败:「x」未安装" }),
482
482
  });
483
483
  await setEnabledCommand("x", false, { siteUrl: "http://x", user: "u", pass: "p", fetchFn: fetchFn2 });
484
484
  expect(process.exitCode).toBe(1);
@@ -521,13 +521,13 @@ describe("s5-2② REG-R9 install 源切换:registry(CLI 归一 + 旧站点
521
521
  it("installCommand registry:新站点(apiVersion 0.2.0)POST source=registry + registry=origin(slug 归一)", async () => {
522
522
  const { fetchFn, calls } = fetchStub({
523
523
  "/api/auth/login": () => ok({ access_token: "tok" }),
524
- "/api/_platform/market/meta": () => ok({ apiVersion: "0.2.0" }),
525
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
526
- "/api/_platform/market/task": () => ok(rec()),
524
+ "/api/oj-market/meta": () => ok({ apiVersion: "0.2.0" }),
525
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
526
+ "/api/oj-market/task": () => ok(rec()),
527
527
  });
528
528
  const r = await installCommand("@acme/demo", { siteUrl: "http://site", user: "u", pass: "p", registry: "https://r.example.com/", fetchFn });
529
529
  expect(r.status).toBe("done");
530
- const call = calls.find(c => c.url.includes("/api/_platform/market/install"));
530
+ const call = calls.find(c => c.url.includes("/api/oj-market/install"));
531
531
  expect(call).toBeDefined();
532
532
  const body = JSON.parse(String(call!.init!.body));
533
533
  expect(body).toMatchObject({ name: "acme-demo", source: "registry", registry: "https://r.example.com" });
@@ -537,12 +537,12 @@ describe("s5-2② REG-R9 install 源切换:registry(CLI 归一 + 旧站点
537
537
  it("installCommand registry:旧站点(apiVersion 0.1.0)→ 抛「站点版本过低」且不 POST install", async () => {
538
538
  const { fetchFn, calls } = fetchStub({
539
539
  "/api/auth/login": () => ok({ access_token: "tok" }),
540
- "/api/_platform/market/meta": () => ok({ apiVersion: "0.1.0" }),
541
- "/api/_platform/market/install": () => ok({ task_id: "t1" }),
540
+ "/api/oj-market/meta": () => ok({ apiVersion: "0.1.0" }),
541
+ "/api/oj-market/install": () => ok({ task_id: "t1" }),
542
542
  });
543
543
  await expect(installCommand("@acme/demo", { siteUrl: "http://site", user: "u", pass: "p", registry: "https://r.example.com", fetchFn }))
544
544
  .rejects
545
545
  .toThrow(/版本过低/);
546
- expect(calls.some(c => c.url.includes("/api/_platform/market/install"))).toBe(false);
546
+ expect(calls.some(c => c.url.includes("/api/oj-market/install"))).toBe(false);
547
547
  });
548
548
  });
@@ -0,0 +1,87 @@
1
+ import { execFileSync } from "node:child_process";
2
+ // BDD:oj-market 第一方方案包(Phase B T1)——市场 UI 以**纯 web 方案包**形态分发:
3
+ // 契约 src/oj-market/solution.yaml(web-only:api 编排面留在平台,随脚手架内置不随包),
4
+ // `ojm build --bundle oj-market` 产出 dist/oj-market-0.2.0.tgz(+ 发包人 ojm sign 平台签名)。
5
+ // 版本基线 0.2.0(20261106-2122 拍板);entry.ts 版本与契约版本同源漂移由本 spec 钉住。
6
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
11
+ import { parse } from "yaml";
12
+ import { buildBundle } from "../../bundle";
13
+ import { validateContract } from "../../manifest-contract";
14
+
15
+ const TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "../../../templates");
16
+ const CONTRACT = join(TEMPLATES, "src/oj-market/solution.yaml");
17
+
18
+ /** tar 成员清单(与 bundle.test 同原语) */
19
+ function tarList(tgz: string): string[] {
20
+ const out = execFileSync("tar", ["-tzf", tgz], { encoding: "utf-8" });
21
+ return out.split("\n").filter(Boolean);
22
+ }
23
+
24
+ describe("oj-market 方案包契约(T1)", () => {
25
+ it("src/oj-market/solution.yaml 在位:name=oj-market、version=0.2.0、web-only(无 api 段)、契约零违规", () => {
26
+ expect(existsSync(CONTRACT)).toBe(true);
27
+ const m = parse(readFileSync(CONTRACT, "utf8")) as Record<string, unknown>;
28
+ expect(m).toMatchObject({ name: "oj-market", version: "0.2.0" });
29
+ expect(m.api).toBeUndefined(); // 安装编排面留在平台(src/oj-market/api 内置不随包)
30
+ const web = m.web as Record<string, unknown>;
31
+ expect(web.entry).toBe("modules/oj-market/0.2.0/entry.js");
32
+ expect(validateContract(m)).toEqual([]);
33
+ });
34
+
35
+ it("模板工程 builtin 名单不含 oj-market(第一方打包放行;用户工程名单由 init 按白名单派生仍含——两侧语义分歧是本用例钉住的裁决)", () => {
36
+ const p = join(TEMPLATES, "src/.ojm-builtin.json");
37
+ const list = JSON.parse(readFileSync(p, "utf8")) as { modules: string[] };
38
+ expect(list.modules).not.toContain("oj-market");
39
+ });
40
+
41
+ it("版本同源:web entry.ts 模块版本 = 契约版本(0.2.0 基线,双写漂移即红)", async () => {
42
+ const { readModuleDefinition } = await import("../../build/module-def");
43
+ const def = await readModuleDefinition(join(TEMPLATES, "src/oj-market/web/entry.ts"), TEMPLATES);
44
+ expect(def.version).toBe("0.2.0");
45
+ });
46
+ });
47
+
48
+ describe("oj-market 方案包组装(T1,scratch 工程钉布局)", () => {
49
+ let project: string;
50
+ let out: string;
51
+
52
+ beforeEach(() => {
53
+ project = mkdtempSync(join(tmpdir(), "ojm-market-bundle-"));
54
+ out = mkdtempSync(join(tmpdir(), "ojm-market-out-"));
55
+ // 契约原样入 scratch(唯一真源,勿在测试里另写一份)
56
+ mkdirSync(join(project, "src/oj-market"), { recursive: true });
57
+ cpSync(CONTRACT, join(project, "src/oj-market/solution.yaml"));
58
+ // 模块标记(布局探测)+ 无 category 路由的 entry 桩(derive 空 → 保留契约 routes)
59
+ mkdirSync(join(project, "src/oj-market/web/locales"), { recursive: true });
60
+ writeFileSync(join(project, "src/oj-market/web/entry.ts"), "export default { name: \"oj-market\", version: \"0.2.0\", routes: [] };\n");
61
+ writeFileSync(join(project, "src/oj-market/web/locales/zh-CN.json"), JSON.stringify({ menu: { main: "方案市场" } }));
62
+ writeFileSync(join(project, "src/oj-market/web/locales/en-US.json"), JSON.stringify({ menu: { main: "Market" } }));
63
+ // 站点构建产物桩(版本段 === 0.2.0)
64
+ mkdirSync(join(project, "dist/web/modules/oj-market/0.2.0"), { recursive: true });
65
+ writeFileSync(join(project, "dist/web/modules/oj-market/0.2.0/entry.js"), "export default {};\n");
66
+ });
67
+
68
+ afterEach(() => {
69
+ rmSync(project, { recursive: true, force: true });
70
+ rmSync(out, { recursive: true, force: true });
71
+ });
72
+
73
+ it("buildBundle --bundle oj-market:纯 web 包(manifest + web/modules + 双语 locales,无 api/ 成员)", async () => {
74
+ const [r] = await buildBundle(project, { out, name: "oj-market" });
75
+ expect(r).toMatchObject({ name: "oj-market", version: "0.2.0" });
76
+ expect(r!.sha256).toMatch(/^[a-f0-9]{64}$/);
77
+ const names = tarList(r!.tgz);
78
+ expect(names).toContain("oj-market-0.2.0/manifest.yaml");
79
+ expect(names).toContain("oj-market-0.2.0/web/modules/oj-market/0.2.0/entry.js");
80
+ expect(names).toContain("oj-market-0.2.0/web/modules/oj-market/locales/zh-CN.json");
81
+ expect(names).toContain("oj-market-0.2.0/web/modules/oj-market/locales/en-US.json");
82
+ expect(names.some(n => n.includes("/api/"))).toBe(false);
83
+ // 确定性:同输入两次打包哈希一致(C2-32)
84
+ const [r2] = await buildBundle(project, { out, name: "oj-market" });
85
+ expect(r2!.sha256).toBe(r!.sha256);
86
+ });
87
+ });
@@ -205,17 +205,15 @@ describe("ojm publish 索引重算 + 签名(P3-5/6/7,REG-CS5 三级化)",
205
205
  it("索引写入只有 writeIndexFile 一条路径(源码级不变量,grep 可证)", () => {
206
206
  const srcOf = (rel: string) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
207
207
  const countOf = (text: string, re: RegExp) => text.split("\n").filter(line => re.test(line)).length;
208
- // 生成侧:publish.ts 里唯一的 fs.writeFileSync 就在 writeIndexFile 体内(tmp+rename);
209
- // build-index.ts 零直写——索引文件全部经唯一写入口
208
+ // 生成侧:publish.ts 里唯一的 fs.writeFileSync 就在 writeIndexFile 体内(tmp+rename)
210
209
  expect(countOf(srcOf("../../publish.ts"), /fs\.writeFileSync/)).toBe(1);
211
210
  expect(countOf(srcOf("../../publish.ts"), /appendFileSync/)).toBe(0);
212
- expect(countOf(srcOf("../../market/build-index.ts"), /fs\.writeFileSync|writeFileSync\(/)).toBe(0);
213
- expect(countOf(srcOf("../../market/build-index.ts"), /appendFileSync/)).toBe(0);
214
- // 全仓其余直写只剩 walkthrough 的负向演练:往 catalog 追加重码标记(drill 本身),
215
- // 且回抄恢复走 writeIndexFile——除这一行 deliberate tamper 外没有第二个索引直写点
216
- const walkthrough = srcOf("../../market/walkthrough.ts");
217
- expect(countOf(walkthrough, /appendFileSync/)).toBe(1);
218
- expect(countOf(walkthrough, /# tampered/)).toBe(1);
219
- expect(countOf(walkthrough, /writeIndexFile\(catalog/)).toBe(1);
211
+ // A6:市场索引机制整建制退役——build-index/index-builder/sharding/yank-ledger 已删,
212
+ // 全仓不再有第二个索引写者(registry-* 文件面留档,唯一写入口仍是 writeIndexFile)
213
+ for (const rel of ["../../market/build-index.ts", "../../market/index-builder.ts", "../../market/sharding.ts", "../../market/yank-ledger.ts"])
214
+ expect(existsSync(fileURLToPath(new URL(rel, import.meta.url)))).toBe(false);
215
+ // walkthrough 的索引 tamper drill 随 neg-badindex 退役——不再有任何索引直写点
216
+ expect(countOf(srcOf("../../market/walkthrough.ts"), /appendFileSync/)).toBe(0);
217
+ expect(countOf(srcOf("../../market/walkthrough.ts"), /writeIndexFile/)).toBe(0);
220
218
  });
221
219
  });
@@ -1,6 +1,6 @@
1
1
  import process from "node:process";
2
2
  // BDD:ojm trust 瘦客户端(P2-2,C2-38/39)——纯转发(uninstall.test.ts 同范式):
3
- // 登录 → GET/POST/DELETE /api/_platform/market/trust* → 打印人话;fetch 全 stub 不真发网络。
3
+ // 登录 → GET/POST/DELETE /api/oj-market/trust* → 打印人话;fetch 全 stub 不真发网络。
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { formatFingerprint, trustCommand } from "../../trust";
6
6
 
@@ -44,10 +44,10 @@ describe("trustCommand · ls(P2-2)", () => {
44
44
  it("登录后 GET /market/trust → 逐行打印 key_id + 分组指纹", async () => {
45
45
  const { fetchFn, calls } = fetchStub({
46
46
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
47
- "/api/_platform/market/trust": () => ok({ keys: [KEY_A], note: null }),
47
+ "/api/oj-market/trust": () => ok({ keys: [KEY_A], note: null }),
48
48
  });
49
49
  await trustCommand("ls", [], { ...SITE, fetchFn });
50
- expect(calls[1]?.url).toContain("/api/_platform/market/trust");
50
+ expect(calls[1]?.url).toContain("/api/oj-market/trust");
51
51
  expect(calls[1]?.init?.method).toBe("GET");
52
52
  expect((calls[1]?.init?.headers as Record<string, string>).authorization).toBe("Bearer tok");
53
53
  const out = logs.join("\n");
@@ -58,7 +58,7 @@ describe("trustCommand · ls(P2-2)", () => {
58
58
  it("空库 → 可读空态;note 在场(.bak 回落)→ 先打注意行", async () => {
59
59
  const { fetchFn } = fetchStub({
60
60
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
61
- "/api/_platform/market/trust": () => ok({ keys: [], note: "x 损坏,已自动回落 x.bak" }),
61
+ "/api/oj-market/trust": () => ok({ keys: [], note: "x 损坏,已自动回落 x.bak" }),
62
62
  });
63
63
  await trustCommand("ls", [], { ...SITE, fetchFn });
64
64
  expect(logs.join("\n")).toContain("已自动回落");
@@ -69,7 +69,7 @@ describe("trustCommand · add / revoke(P2-2)", () => {
69
69
  it("add:POST {key_id, public_key};added / exists_same 两种回显", async () => {
70
70
  const added = fetchStub({
71
71
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
72
- "/api/_platform/market/trust/key": () => ok({ result: "added" }),
72
+ "/api/oj-market/trust/key": () => ok({ result: "added" }),
73
73
  });
74
74
  await trustCommand("add", ["ojm.test", "a".repeat(64)], { ...SITE, fetchFn: added.fetchFn });
75
75
  const call = added.calls[1]!;
@@ -79,7 +79,7 @@ describe("trustCommand · add / revoke(P2-2)", () => {
79
79
 
80
80
  const idem = fetchStub({
81
81
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
82
- "/api/_platform/market/trust/key": () => ok({ result: "exists_same" }),
82
+ "/api/oj-market/trust/key": () => ok({ result: "exists_same" }),
83
83
  });
84
84
  await trustCommand("add", ["ojm.test", "a".repeat(64)], { ...SITE, fetchFn: idem.fetchFn });
85
85
  expect(logs.join("\n")).toContain("无需重复添加");
@@ -97,18 +97,18 @@ describe("trustCommand · add / revoke(P2-2)", () => {
97
97
  try {
98
98
  const yes = fetchStub({
99
99
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
100
- "/api/_platform/market/trust/key": () => ok({ revoked: true }),
100
+ "/api/oj-market/trust/key": () => ok({ revoked: true }),
101
101
  });
102
102
  await trustCommand("revoke", ["ojm.test"], { ...SITE, fetchFn: yes.fetchFn });
103
103
  const call = yes.calls[1]!;
104
104
  expect(call.init?.method).toBe("DELETE");
105
- expect(call.url).toContain("/api/_platform/market/trust/key?key_id=ojm.test");
105
+ expect(call.url).toContain("/api/oj-market/trust/key?key_id=ojm.test");
106
106
  expect(process.exitCode).toBe(0);
107
107
 
108
108
  process.exitCode = 0;
109
109
  const no = fetchStub({
110
110
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
111
- "/api/_platform/market/trust/key": () => ok({ revoked: false }),
111
+ "/api/oj-market/trust/key": () => ok({ revoked: false }),
112
112
  });
113
113
  await trustCommand("revoke", ["nope.key"], { ...SITE, fetchFn: no.fetchFn });
114
114
  expect(process.exitCode).toBe(1);
@@ -138,14 +138,14 @@ describe("trustCommand · confirm(P2-3 TOFU 确认链)", () => {
138
138
  it("先展示指纹再调端点:GET task → 打印 key_id + 分组指纹 → POST /market/trust {task_id}", async () => {
139
139
  const { fetchFn, calls } = fetchStub({
140
140
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
141
- "/api/_platform/market/task": () => ok(needsTrustTask),
142
- "/api/_platform/market/trust": () => ok({ result: "added", key_id: "stranger.pub" }),
141
+ "/api/oj-market/task": () => ok(needsTrustTask),
142
+ "/api/oj-market/trust": () => ok({ result: "added", key_id: "stranger.pub" }),
143
143
  });
144
144
  await trustCommand("confirm", [], { ...SITE, task: "install-1-abc", fetchFn });
145
145
  // 调用序:登录 → GET task → POST trust
146
- expect(calls[1]?.url).toContain("/api/_platform/market/task?id=install-1-abc");
146
+ expect(calls[1]?.url).toContain("/api/oj-market/task?id=install-1-abc");
147
147
  expect(calls[2]?.init?.method).toBe("POST");
148
- expect(calls[2]?.url).toContain("/api/_platform/market/trust");
148
+ expect(calls[2]?.url).toContain("/api/oj-market/trust");
149
149
  expect(JSON.parse(String(calls[2]?.init?.body))).toEqual({ task_id: "install-1-abc" });
150
150
  const out = logs.join("\n");
151
151
  expect(out).toContain("stranger.pub");
@@ -157,7 +157,7 @@ describe("trustCommand · confirm(P2-3 TOFU 确认链)", () => {
157
157
  it("任务非 needs_trust → 本地可读报错(不发确认请求)", async () => {
158
158
  const { fetchFn, calls } = fetchStub({
159
159
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
160
- "/api/_platform/market/task": () => ok({ ...needsTrustTask, status: "done", trust: undefined }),
160
+ "/api/oj-market/task": () => ok({ ...needsTrustTask, status: "done", trust: undefined }),
161
161
  });
162
162
  await expect(trustCommand("confirm", [], { ...SITE, task: "install-1-abc", fetchFn })).rejects.toThrow(/不在待信任确认状态/);
163
163
  expect(calls).toHaveLength(2); // 登录 + GET task,无 POST
@@ -1,6 +1,6 @@
1
1
  import process from "node:process";
2
2
  // BDD:ojm uninstall 瘦客户端(P1-15a 缺口 B,D6/D8 同款)——纯转发:
3
- // 登录 → POST /api/_platform/market/uninstall {name} → 打印服务端人话 message;
3
+ // 登录 → POST /api/oj-market/uninstall {name} → 打印服务端人话 message;
4
4
  // ok:false(未装/空锁拒启)置 exitCode 1。fetch 全 stub 不真发网络(install.test.ts 同范式)。
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { uninstallCommand } from "../../uninstall";
@@ -42,13 +42,13 @@ describe("uninstallCommand · 卸载瘦客户端(P1-15a 缺口 B)", () => {
42
42
  it("登录后 POST uninstall {name}(Bearer 注入)→ 打印服务端 message", async () => {
43
43
  const { fetchFn, calls } = fetchStub({
44
44
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
45
- "/api/_platform/market/uninstall": () => ok({ ok: true, message: "卸载完成:四处同删(demo-solution@0.1.0)" }),
45
+ "/api/oj-market/uninstall": () => ok({ ok: true, message: "卸载完成:四处同删(demo-solution@0.1.0)" }),
46
46
  });
47
47
  await uninstallCommand("demo-solution", { siteUrl: "http://x", user: "u", pass: "p", fetchFn });
48
48
  // 登录体 + uninstall JSON 体 + Bearer
49
49
  expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ username: "u", password: "p" });
50
50
  const call = calls[1]!;
51
- expect(call.url).toContain("/api/_platform/market/uninstall");
51
+ expect(call.url).toContain("/api/oj-market/uninstall");
52
52
  expect(call.init?.method).toBe("POST");
53
53
  expect((call.init?.headers as Record<string, string>).authorization).toBe("Bearer tok");
54
54
  expect(JSON.parse(String(call.init?.body))).toEqual({ name: "demo-solution" });
@@ -58,7 +58,7 @@ describe("uninstallCommand · 卸载瘦客户端(P1-15a 缺口 B)", () => {
58
58
  it("ok:false(域逻辑拒绝)→ message 照打 + exitCode 1", async () => {
59
59
  const { fetchFn } = fetchStub({
60
60
  "/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
61
- "/api/_platform/market/uninstall": () => ok({ ok: false, message: "卸载失败:「x」未安装(锁与 sidecar 均无记录)" }),
61
+ "/api/oj-market/uninstall": () => ok({ ok: false, message: "卸载失败:「x」未安装(锁与 sidecar 均无记录)" }),
62
62
  });
63
63
  const exitBackup = process.exitCode;
64
64
  process.exitCode = 0;
package/src/list.ts CHANGED
@@ -1,10 +1,10 @@
1
- // `ojm list`(站点已装方案的可查面):登录 → GET /api/_platform/market/index → 只挑已装的打表。
1
+ // `ojm list`(站点已装方案的可查面):登录 → GET /api/oj-market/index → 只挑已装的打表。
2
2
  //
3
3
  // 为什么需要它:到 0.3.2 为止,CLI **没有任何一条命令能回答「这个站点上装了什么」**——
4
4
  // 装完只有一条终态文案,之后要核对版本/启用态得登站点翻市场页。运维与 CI 都需要一个
5
5
  // 可脚本化的清单(升级前看谁落后、禁用后确认状态、卸载前确认名字)。
6
6
  //
7
- // 为什么复用 `GET /_platform/market/index` 而不是新增端点:这一条已经把三件事聚合好了——
7
+ // 为什么复用 `GET /oj-market/index` 而不是新增端点:这一条已经把三件事聚合好了——
8
8
  // sidecar 里的 installed/enabled、索引里的 latest/available(算 upgradable)、信任库判定
9
9
  // 出的 trust_state。新增一个「只列已装」的端点等于让两个真相源各自漂移,而站点侧那边
10
10
  // 本来就要维护全量行(市场页在用)。
@@ -59,7 +59,7 @@ function asTrustState(raw: unknown): TrustState {
59
59
 
60
60
  /** 取一份已装清单(`--all` 时含未装)。不打印、不 exit 的纯函数层,便于 CI 直接在进程内用。 */
61
61
  export async function fetchInstalled(fetchFn: typeof fetch, base: string, token: string, all: boolean): Promise<SolutionRow[]> {
62
- const rows = await callApi<MarketIndexRow[]>(fetchFn, base, token, "GET", "/api/_platform/market/index");
62
+ const rows = await callApi<MarketIndexRow[]>(fetchFn, base, token, "GET", "/api/oj-market/index");
63
63
  const out: SolutionRow[] = [];
64
64
  for (const raw of rows) {
65
65
  const name = String(raw.name ?? "").trim();