@oj-module/cli 0.2.1 → 0.2.5

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 (231) hide show
  1. package/devkit/README.md +14 -3
  2. package/devkit/SKILL.md +16 -4
  3. package/devkit/framework-dev.md +8 -6
  4. package/devkit/manual.md +243 -23
  5. package/package.json +2 -2
  6. package/shell-dist/assets/runtime.js +143 -8
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +3 -3
  9. package/src/args.ts +67 -0
  10. package/src/build.ts +11 -3
  11. package/src/bundle.ts +197 -0
  12. package/src/contract/__tests__/handler-scan.test.ts +82 -0
  13. package/src/contract/check.ts +2 -1
  14. package/src/contract/handler-scan.ts +32 -0
  15. package/src/deterministic-tar.ts +120 -0
  16. package/src/dev.ts +4 -2
  17. package/src/enable.ts +28 -0
  18. package/src/index.ts +63 -2
  19. package/src/info.ts +3 -2
  20. package/src/init.ts +147 -14
  21. package/src/install.ts +268 -0
  22. package/src/keygen.ts +88 -0
  23. package/src/layout.ts +31 -0
  24. package/src/lifecycle/__tests__/authoring.test.ts +109 -0
  25. package/src/lifecycle/__tests__/bundle.test.ts +150 -0
  26. package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
  27. package/src/lifecycle/__tests__/fixtures/README.md +59 -0
  28. package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
  29. package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
  30. package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
  31. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
  32. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
  33. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
  36. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
  37. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
  38. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
  39. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
  40. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
  41. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
  42. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
  43. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
  44. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
  45. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
  46. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
  47. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
  48. package/src/lifecycle/__tests__/helpers.ts +78 -0
  49. package/src/lifecycle/__tests__/install.test.ts +514 -0
  50. package/src/lifecycle/__tests__/keygen.test.ts +61 -0
  51. package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
  52. package/src/lifecycle/__tests__/publish.test.ts +80 -0
  53. package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
  54. package/src/lifecycle/__tests__/sign.test.ts +131 -0
  55. package/src/lifecycle/__tests__/smoke.test.ts +61 -0
  56. package/src/lifecycle/__tests__/trust.test.ts +177 -0
  57. package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
  58. package/src/lifecycle/keygen.ts +40 -0
  59. package/src/lifecycle/sign.ts +74 -0
  60. package/src/manifest-contract.ts +696 -0
  61. package/src/oj-cert.ts +19 -0
  62. package/src/oj-cmd.ts +3 -3
  63. package/src/preview.ts +2 -2
  64. package/src/publish.ts +150 -0
  65. package/src/sign.ts +128 -0
  66. package/src/site.ts +146 -0
  67. package/src/trust.ts +116 -0
  68. package/src/uninstall.ts +29 -0
  69. package/src/usage.ts +35 -1
  70. package/templates/README.md +46 -7
  71. package/templates/api/config.yaml +47 -0
  72. package/templates/api/seed.mysql.sql +1 -1
  73. package/templates/api/seed.pg.sql +1 -1
  74. package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
  75. package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
  76. package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
  77. package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
  78. package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
  79. package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
  80. package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
  81. package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
  82. package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
  83. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
  84. package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
  85. package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
  86. package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
  87. package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
  88. package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
  89. package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
  90. package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
  91. package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
  92. package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
  93. package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
  94. package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
  95. package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
  96. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
  97. package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
  98. package/templates/api/src/_platform/_shared/service/access.ts +2 -2
  99. package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
  100. package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
  101. package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
  102. package/templates/api/src/_platform/manifest.yaml +1 -0
  103. package/templates/api/src/_platform/market/api.ts +883 -0
  104. package/templates/api/src/_platform/market/bootstrap.ts +39 -0
  105. package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
  106. package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
  107. package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
  108. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
  109. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
  110. package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
  111. package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
  112. package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
  113. package/templates/api/src/_platform/schema.yaml +21 -4
  114. package/templates/api/src/_platform/seed.sql +2 -2
  115. package/templates/api/src/_platform/solutions/api.ts +212 -0
  116. package/templates/api/src/demo/manifest.yaml +5 -0
  117. package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
  118. package/templates/api/src/demo/schema.yaml +11 -0
  119. package/templates/api/src/demo/seed.sql +5 -0
  120. package/templates/api/src/demo/todos/api.ts +59 -0
  121. package/templates/api/src/home/line/api.ts +57 -0
  122. package/templates/api/src/home/manifest.yaml +6 -0
  123. package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
  124. package/templates/api/src/home/pie/api.ts +40 -0
  125. package/templates/api/src/home/schema.yaml +14 -0
  126. package/templates/api/src/home/seed.sql +70 -0
  127. package/templates/api/src/system/oplog/api.ts +44 -2
  128. package/templates/api/tests/fixture.ts +16 -0
  129. package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
  130. package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
  131. package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
  132. package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
  133. package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
  134. package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
  135. package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
  136. package/templates/api/tests/lifecycle/register.spec.ts +194 -0
  137. package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
  138. package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
  139. package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
  140. package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
  141. package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
  142. package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
  143. package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
  144. package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
  145. package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
  146. package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
  147. package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
  148. package/templates/api/tests/rbac-settings.spec.ts +8 -0
  149. package/templates/init.include.yaml +56 -0
  150. package/templates/openapi.json +527 -0
  151. package/templates/package.json +52 -0
  152. package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
  153. package/templates/web/src/_platform/__client/api.ts +335 -0
  154. package/templates/web/src/about/entry.ts +42 -0
  155. package/templates/web/src/about/locales/en-US.json +16 -0
  156. package/templates/web/src/about/locales/zh-CN.json +16 -0
  157. package/templates/web/src/about/pages/constants.ts +20 -0
  158. package/templates/web/src/about/pages/index.tsx +135 -0
  159. package/templates/web/src/access/entry.ts +74 -0
  160. package/templates/web/src/access/locales/en-US.json +46 -0
  161. package/templates/web/src/access/locales/zh-CN.json +46 -0
  162. package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
  163. package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
  164. package/templates/web/src/access/pages/button-control/index.tsx +181 -0
  165. package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
  166. package/templates/web/src/access/pages/page-control/index.tsx +142 -0
  167. package/templates/web/src/demo/__client/api.schemas.ts +20 -0
  168. package/templates/web/src/demo/__client/api.ts +89 -0
  169. package/templates/web/src/exception/entry.ts +80 -0
  170. package/templates/web/src/exception/locales/en-US.json +13 -0
  171. package/templates/web/src/exception/locales/zh-CN.json +13 -0
  172. package/templates/web/src/exception/pages/403/index.tsx +30 -0
  173. package/templates/web/src/exception/pages/404/index.tsx +30 -0
  174. package/templates/web/src/exception/pages/500/index.tsx +30 -0
  175. package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
  176. package/templates/web/src/home/__client/api.schemas.ts +22 -0
  177. package/templates/web/src/home/__client/api.ts +113 -0
  178. package/templates/web/src/login/locales/en-US.json +6 -0
  179. package/templates/web/src/login/locales/zh-CN.json +6 -0
  180. package/templates/web/src/login/pages/login.tsx +67 -18
  181. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
  182. package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
  183. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
  184. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
  185. package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
  186. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
  187. package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
  188. package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
  189. package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
  190. package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
  191. package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
  192. package/templates/web/src/oj-market/api/client.ts +135 -0
  193. package/templates/web/src/oj-market/api/context.ts +11 -0
  194. package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
  195. package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
  196. package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
  197. package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
  198. package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
  199. package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
  200. package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
  201. package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
  202. package/templates/web/src/oj-market/entry.ts +27 -0
  203. package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
  204. package/templates/web/src/oj-market/locales/en-US.json +100 -0
  205. package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
  206. package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
  207. package/templates/web/src/oj-market/types.ts +84 -0
  208. package/templates/web/src/oj-market/utils/poll.ts +36 -0
  209. package/templates/web/src/oj-market/utils/semver.ts +14 -0
  210. package/templates/web/src/outside/entry.ts +80 -0
  211. package/templates/web/src/outside/locales/en-US.json +10 -0
  212. package/templates/web/src/outside/locales/zh-CN.json +10 -0
  213. package/templates/web/src/personal-center/entry.ts +1 -10
  214. package/templates/web/src/personal-center/locales/en-US.json +1 -5
  215. package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
  216. package/templates/web/src/route-nest/entry.ts +73 -0
  217. package/templates/web/src/route-nest/locales/en-US.json +9 -0
  218. package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
  219. package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
  220. package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
  221. package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
  222. package/templates/web/src/system/__client/api.schemas.ts +162 -0
  223. package/templates/web/src/system/__client/api.ts +382 -0
  224. package/templates/web/src/system/pages/oplog/index.tsx +1 -1
  225. package/templates/web/src/system/pages/roles/index.tsx +1 -0
  226. package/templates/web/src/system/pages/settings/index.tsx +1 -1
  227. package/templates/web/src/system/pages/users/index.tsx +1 -1
  228. package/templates/web.config.ts +32 -40
  229. package/vendor/host-versions.json +3 -3
  230. package/templates/pnpm-workspace.yaml +0 -14
  231. package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
@@ -0,0 +1,82 @@
1
+ // BDD:handler AST 扫描(P2-14 裁决:修 handler-scan 认 CallExpression 工厂形态——
2
+ // `export default createXxxApi(env)` 是调用表达式,此前不认 → route-unregistered 守卫对
3
+ // market/solutions 全族失效)。镜像 oj 运行时内省语义:oj 执行工厂拿到返回对象。
4
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
8
+ import { scanHandlerFile } from "../handler-scan";
9
+
10
+ let dir: string;
11
+ let apiSrc: string;
12
+
13
+ beforeEach(() => {
14
+ dir = mkdtempSync(join(tmpdir(), "ojm-scan-"));
15
+ apiSrc = join(dir, "api", "src", "_platform", "market");
16
+ mkdirSync(apiSrc, { recursive: true });
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ });
22
+
23
+ function scan(name: string, text: string) {
24
+ writeFileSync(join(apiSrc, name), text);
25
+ return scanHandlerFile(join(apiSrc, name), join(dir, "api", "src"));
26
+ }
27
+
28
+ describe("handler-scan · 工厂形态(P2-14:export default createXxxApi(env))", () => {
29
+ it("callExpression → 同名函数声明 return 对象字面量:动词键/嵌套组照常展开(dir 镜像起算)", () => {
30
+ const rows = scan("api.ts", `
31
+ import type { Env } from "./types";
32
+ export function createMarketApi(env: Env) {
33
+ return {
34
+ "install": {
35
+ async post() { return {}; },
36
+ },
37
+ "verify": {
38
+ async post() { return {}; },
39
+ last: {
40
+ async get() { return {}; },
41
+ },
42
+ },
43
+ };
44
+ }
45
+ export default createMarketApi({} as Env);
46
+ `);
47
+ expect(rows.map(r => `${r.method} ${r.full}`).sort()).toEqual([
48
+ "GET _platform/market/verify/last",
49
+ "POST _platform/market/install",
50
+ "POST _platform/market/verify",
51
+ ]);
52
+ });
53
+
54
+ it("无 return 对象 / 未知 callee / 点分 callee 不可解析 → 零行(不炸不猜)", () => {
55
+ expect(scan("a.ts", "export default createMissingApi({});")).toEqual([]);
56
+ expect(scan("b.ts", `
57
+ function createApi() { console.log("no return object"); }
58
+ export default createApi();
59
+ `)).toEqual([]);
60
+ expect(scan("c.ts", "export default (makeApi())({});")).toEqual([]);
61
+ });
62
+
63
+ it("既有形态回归:内联字面量与标识符 default 导出照常识别(P2-14 改动不破旧口径)", () => {
64
+ const inline = scan("inline.ts", `
65
+ export default {
66
+ hello: {
67
+ get() { return {}; },
68
+ },
69
+ };
70
+ `);
71
+ expect(inline.map(r => `${r.method} ${r.full}`)).toEqual(["GET _platform/market/hello"]);
72
+ const viaIdent = scan("ident.ts", `
73
+ const api = {
74
+ bye: {
75
+ del() { return {}; },
76
+ },
77
+ };
78
+ export default api;
79
+ `);
80
+ expect(viaIdent.map(r => `${r.method} ${r.full}`)).toEqual(["DELETE _platform/market/bye"]);
81
+ });
82
+ });
@@ -3,6 +3,7 @@ import type { IrEndpoint } from "./ir";
3
3
  import type { OpenApiDoc } from "./openapi-ir";
4
4
  import { existsSync, readdirSync, readFileSync } from "node:fs";
5
5
  import { dirname, join, relative, sep } from "node:path";
6
+ import { resolveConfigPath } from "../layout";
6
7
  import { readOjAnonymousPaths } from "../oj-config";
7
8
  import { emitClient } from "./emit-client";
8
9
  import { planStubWrites } from "./emit-stub";
@@ -328,7 +329,7 @@ export async function checkApi(opts: { cwd: string, exempt?: string }): Promise<
328
329
  violations.push(...checkWsAnonymous(
329
330
  walkWsFiles(join(opts.cwd, "api/src")),
330
331
  join(opts.cwd, "api/src"),
331
- join(opts.cwd, "api/config.yaml"),
332
+ resolveConfigPath(opts.cwd),
332
333
  ));
333
334
 
334
335
  // ③ routes.js diff
@@ -136,6 +136,29 @@ function defaultExportObject(sf: ts.SourceFile, decls: Map<string, ts.ObjectLite
136
136
  return undefined;
137
137
  }
138
138
 
139
+ /**
140
+ * 工厂形态(P2-14 裁决:修 handler-scan 认 CallExpression——`export default createXxxApi(env)`
141
+ * 是调用表达式,此前扫描器不认,route-unregistered 守卫对 market/solutions 全族失效):
142
+ * 解析同文件同名函数声明体内 return 的对象字面量(运行时 oj 内省执行工厂拿到该对象,静态
143
+ * 扫描镜像此语义)。支持多个 return;对象内标识符引用仍解析文件级顶层 const。
144
+ */
145
+ function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression): ts.ObjectLiteralExpression[] {
146
+ const callee = dottedNameOf(call.expression);
147
+ if (callee === undefined)
148
+ return [];
149
+ for (const stmt of sf.statements) {
150
+ if (!ts.isFunctionDeclaration(stmt) || stmt.name?.text !== callee || stmt.body === undefined)
151
+ continue;
152
+ const out: ts.ObjectLiteralExpression[] = [];
153
+ for (const st of stmt.body.statements) {
154
+ if (ts.isReturnStatement(st) && st.expression !== undefined && ts.isObjectLiteralExpression(st.expression))
155
+ out.push(st.expression);
156
+ }
157
+ return out;
158
+ }
159
+ return [];
160
+ }
161
+
139
162
  /** 相对路由按层前缀解析(绝对 `/` 开头即根级);与 oj 内省 resolve 同规则 */
140
163
  function resolveRoute(own: string, prefix: string): string {
141
164
  return own.startsWith("/") ? own : (prefix ? `${prefix}/${own}` : own);
@@ -227,6 +250,15 @@ export function scanHandlerFile(filePath: string, apiSrcDir: string): HandlerRow
227
250
  }
228
251
  walk(root, "", filePrefix, 1);
229
252
  }
253
+ else {
254
+ // P2-14:工厂形态(CallExpression)——组前缀从目录镜像起算(同内联字面量形态)
255
+ for (const stmt of sf.statements) {
256
+ if (ts.isExportAssignment(stmt) && !stmt.isExportEquals && ts.isCallExpression(stmt.expression)) {
257
+ for (const obj of factoryReturnObjects(sf, stmt.expression))
258
+ walk(obj, "", "", 1);
259
+ }
260
+ }
261
+ }
230
262
  return rows;
231
263
  }
232
264
 
@@ -0,0 +1,120 @@
1
+ // 确定性 tgz 打包(P1-11,契约 C2-32):固定 mtime=0、成员按路径字典序、权限 0644/0755、
2
+ // uid/gid=0 且无 owner 名——同输入两次打包 sha256 相等(可复现构建,发布留痕与校验的基础)。
3
+ // 打包自检(C2-30 构建列):symlink/硬链接/特殊文件成员一律拒绝(tar 忠实还原链接,
4
+ // 安装侧 list→validate→extract 三步守卫的前提是包内没有守卫绕过面)。
5
+ // 手写 ustar 而不用系统 tar:跨平台 tar 的 mtime/权限/排序旗标方言不一,确定性无法逐字保证;
6
+ // ustar 是 POSIX 最小形态,系统 tar -tvzf/-xzf(安装侧 extract 段的唯一消费面)原生可读。
7
+ import { Buffer } from "node:buffer";
8
+ import crypto from "node:crypto";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { gzipSync } from "node:zlib";
12
+
13
+ const BLOCK = 512;
14
+ const RECORD = 20 * BLOCK; // 常规记录长度:GNU tar 缺省 -b20,补齐以兼容最保守的读端
15
+
16
+ interface Entry {
17
+ relPath: string
18
+ absPath: string
19
+ type: "file" | "dir"
20
+ size: number
21
+ }
22
+
23
+ /**
24
+ * 把 `dir` 的内容打包为确定性 tgz 写到 `out`(父目录自动创建),返回 tgz 的 sha256(hex)。
25
+ * 归档成员 = dir 下的相对路径(含唯一的顶层目录时即单根布局)。
26
+ */
27
+ export async function createDeterministicTgz(dir: string, out: string): Promise<string> {
28
+ if (!fs.statSync(dir).isDirectory()) {
29
+ throw new Error(`[ojm] 确定性打包失败:${dir} 不是目录`);
30
+ }
31
+ const entries = walk(dir);
32
+ const body: Buffer[] = [];
33
+ for (const e of entries) {
34
+ const isDir = e.type === "dir";
35
+ const header = ustarHeader(e);
36
+ body.push(header);
37
+ if (!isDir) {
38
+ const content = fs.readFileSync(e.absPath);
39
+ body.push(content);
40
+ const pad = (BLOCK - (content.length % BLOCK)) % BLOCK;
41
+ if (pad > 0)
42
+ body.push(Buffer.alloc(pad));
43
+ }
44
+ }
45
+ body.push(Buffer.alloc(RECORD)); // 双零块 EOF(1024B)+ 补齐到整记录
46
+ // Node zlib gzip 缺省不写 MTIME/FNAME(头内时间戳恒 0、无原始文件名)——确定性由测试钉住
47
+ const tgz = gzipSync(Buffer.concat(body));
48
+ const sha256 = crypto.createHash("sha256").update(tgz).digest("hex");
49
+ fs.mkdirSync(path.dirname(out), { recursive: true });
50
+ fs.writeFileSync(out, tgz);
51
+ return sha256;
52
+ }
53
+
54
+ /** 深度收集全部成员并按 relPath 字典序排序(目录成员在子成员之前:`p` 是 `p/child` 的前缀) */
55
+ function walk(dir: string): Entry[] {
56
+ const out: Entry[] = [];
57
+ const visit = (absPath: string, relPath: string) => {
58
+ const stat = fs.lstatSync(absPath);
59
+ if (stat.isSymbolicLink()) {
60
+ throw new Error(`[ojm] 打包自检失败(C2-30):成员「${relPath}」是 symlink,禁入 tgz(tar 忠实还原链接,链接是安装守卫绕过面)——请改为实体文件`);
61
+ }
62
+ if (stat.isDirectory()) {
63
+ out.push({ relPath, absPath, type: "dir", size: 0 });
64
+ for (const e of fs.readdirSync(absPath, { withFileTypes: true }))
65
+ visit(path.join(absPath, e.name), `${relPath}/${e.name}`);
66
+ return;
67
+ }
68
+ if (stat.isFile()) {
69
+ out.push({ relPath, absPath, type: "file", size: stat.size });
70
+ return;
71
+ }
72
+ throw new Error(`[ojm] 打包自检失败(C2-30):成员「${relPath}」既非文件亦非目录(lstat=${stat.mode.toString(8)}),禁入 tgz`);
73
+ };
74
+ for (const e of fs.readdirSync(dir, { withFileTypes: true }))
75
+ visit(path.join(dir, e.name), e.name);
76
+ return out.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
77
+ }
78
+
79
+ /** 512B ustar 头(POSIX 最小形态):数字字段 NUL 结尾;uname/gname 空(无 owner 名);mtime=0 */
80
+ function ustarHeader(e: Entry): Buffer {
81
+ const fullName = e.type === "dir" ? `${e.relPath}/` : e.relPath;
82
+ const split = splitUstarName(fullName);
83
+ const h = Buffer.alloc(BLOCK);
84
+ h.write(split.name, 0, 100, "utf8");
85
+ if (split.prefix)
86
+ h.write(split.prefix, 345, 155, "utf8");
87
+ writeOctal(h, 100, 8, e.type === "dir" ? 0o755 : 0o644); // 权限固定:umask/源文件权限不泄漏
88
+ writeOctal(h, 108, 8, 0); // uid=0
89
+ writeOctal(h, 116, 8, 0); // gid=0
90
+ writeOctal(h, 124, 12, e.size);
91
+ writeOctal(h, 136, 12, 0); // mtime=0(确定性核心)
92
+ h.write("ustar\0", 257, 6, "binary");
93
+ h.write("00", 263, 2, "binary");
94
+ // uname/gname(265/297 各 32B)保持全零 = 无 owner 名;devmajor/devminor 全零(非常规设备)
95
+ h.write(e.type === "dir" ? "5" : "0", 156, 1, "binary"); // typeflag:目录/普通文件
96
+ h.write(" ".repeat(8), 148, 8, "binary"); // POSIX:chksum 参与求和时本字段按 8 个空格计
97
+ const sum = h.reduce((acc, byte) => acc + byte, 0);
98
+ h.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "binary"); // chksum:6 位八进制 + NUL + 空格
99
+ return h;
100
+ }
101
+
102
+ /** 八进制数字字段:值左补零到 size-1,NUL 结尾 */
103
+ function writeOctal(buf: Buffer, offset: number, size: number, value: number): void {
104
+ buf.write(`${value.toString(8).padStart(size - 1, "0")}\0`, offset, size, "binary");
105
+ }
106
+
107
+ /** ustar name(100)/prefix(155) 拆分;容量外拒绝并给可读错误(不用 pax 扩展头——其自带元数据是确定性反面) */
108
+ function splitUstarName(fullName: string): { name: string, prefix: string } {
109
+ if (fullName.length <= 100)
110
+ return { name: fullName, prefix: "" };
111
+ let cut = -1;
112
+ for (let i = fullName.indexOf("/"); i !== -1; i = fullName.indexOf("/", i + 1)) {
113
+ if (i <= 155 && fullName.length - i - 1 <= 100)
114
+ cut = i; // 取最后一个合法切点(确定性:fullName 相同则结果相同)
115
+ }
116
+ if (cut === -1) {
117
+ throw new Error(`[ojm] 打包自检失败:成员路径长度超出 ustar 容量(name ≤100 / prefix ≤155),且路径过长无法拆分:「${fullName}」(${fullName.length} 字符)——请缩短目录/文件名`);
118
+ }
119
+ return { name: fullName.slice(cut + 1), prefix: fullName.slice(0, cut) };
120
+ }
package/src/dev.ts CHANGED
@@ -33,8 +33,9 @@ import { loadContractMocks, resolveMock } from "./contract/mock";
33
33
  import { loadProjectMocks, mockStatusCode } from "./dev-mock";
34
34
  import { createReloadHub, proxyApi, proxyWsUpgrade, sseScript } from "./dev-proxy";
35
35
  import { loadHtmlTransform } from "./html-transform";
36
- import { resolveLayout } from "./layout";
36
+ import { resolveConfigPath, resolveLayout } from "./layout";
37
37
  import { startOj } from "./oj";
38
+ import { ensureDevCert } from "./oj-cert";
38
39
  import { readOjApiPrefix } from "./oj-config";
39
40
  import { createStaticHandler, decodeReqPath, listenOnFreePort } from "./static-handler";
40
41
  import { resolveShellDist } from "./versions";
@@ -84,13 +85,14 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
84
85
  const transform = await loadHtmlTransform(projectRoot);
85
86
 
86
87
  // 2) oj 后端(工程有 api/config.yaml 时全栈形态;桩可注入)
87
- const configPath = resolve(projectRoot, "api/config.yaml");
88
+ const configPath = resolveConfigPath(projectRoot);
88
89
  let oj: OjProcess | undefined;
89
90
  let ojTarget: string | undefined;
90
91
  // 反代前缀跟随 config 的 mounts:(api 挂载 prefix;F2:改前缀后硬编码 /api 会静默失效)
91
92
  let apiBase = "/api";
92
93
  if (!opts.frontendOnly && existsSync(configPath)) {
93
94
  apiBase = readOjApiPrefix(configPath);
95
+ await ensureDevCert(configPath);
94
96
  const starter = opts.ojStarter ?? ((cfg: string, b: string, apiSrc: string) => startOj(cfg, b, apiSrc));
95
97
  oj = starter(configPath, apiBase, resolve(projectRoot, "api/src"));
96
98
  try {
package/src/enable.ts ADDED
@@ -0,0 +1,28 @@
1
+ // ojm enable/disable(P1-13,D8):生命周期开关瘦客户端——POST /api/_platform/market/enable
2
+ // 触发站点侧 setEnabled 四处同写(锁/sidecar/route/modules.json,D12)。本地零状态,纯转发。
3
+ import process from "node:process";
4
+ import { callApi, login, resolveSite } from "./site";
5
+
6
+ export interface EnableOptions {
7
+ url?: string
8
+ user?: string
9
+ pass?: string
10
+ /** 测试注入;缺省 globalThis.fetch */
11
+ fetchFn?: typeof fetch
12
+ }
13
+
14
+ /**
15
+ * 启用/禁用命令体:登录 → POST enable {name, enabled} → 打印服务端人话 message。
16
+ * ok:false = 域逻辑拒绝(未装/空锁拒启/版本目录缺失),打印 message 并置 exitCode 1。
17
+ */
18
+ export async function setEnabledCommand(name: string, enabled: boolean, opts: EnableOptions = {}): Promise<void> {
19
+ if (name.trim() === "")
20
+ throw new Error("[ojm] 用法:ojm enable|disable <name>——name 为已安装方案名");
21
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
22
+ const { base, user, pass } = resolveSite(opts);
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 });
25
+ console.log(`[ojm] ${result.message}`);
26
+ if (!result.ok)
27
+ process.exitCode = 1;
28
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
- import { parseApiArgs, parseInitArgs, parseVendorArgs } from "./args";
4
+ import { parseApiArgs, parseBuildArgs, parseInitArgs, parseLifecycleArgs, parseVendorArgs } from "./args";
5
5
  import { buildBackend, buildModules } from "./build";
6
+ import { buildBundle } from "./bundle";
6
7
  import { devServer } from "./dev";
7
8
  import { mergeManifests, printInfo } from "./info";
8
9
  import { initProject } from "./init";
@@ -31,11 +32,18 @@ async function main() {
31
32
  }
32
33
 
33
34
  switch (command) {
34
- case "build":
35
+ case "build": {
36
+ const { bundle, out } = parseBuildArgs(process.argv.slice(3));
37
+ if (bundle) {
38
+ // 方案包构建(P1-11):契约校验 fail-fast → 确定性 tgz + sha256(消费方:市场安装器)
39
+ await buildBundle(projectRoot, { out });
40
+ break;
41
+ }
35
42
  // 设计 §5:后端 oj build(零 DB 副作用)→ 前端全站合并(仅 build 清场合并)
36
43
  await buildBackend(projectRoot);
37
44
  await buildModules(projectRoot, { mergeSite: true });
38
45
  break;
46
+ }
39
47
  case "init": {
40
48
  const { dest, yes } = parseInitArgs(process.argv.slice(3));
41
49
  await initProject(dest ? path.resolve(dest) : projectRoot, { yes });
@@ -100,6 +108,59 @@ async function main() {
100
108
  case "merge":
101
109
  await mergeManifests(process.argv[3] ?? "", process.argv.slice(4));
102
110
  break;
111
+ case "keygen": {
112
+ // 签名密钥对生成(P2-4,作者工具链):缺省 ~/.ojm/keys/<id>,工程树内落盘需 --yes
113
+ const { parseKeygenArgs } = await import("./keygen");
114
+ const { keygenCommand } = await import("./keygen");
115
+ const { id, out, yes } = parseKeygenArgs(process.argv.slice(3));
116
+ keygenCommand({ id, out, yes });
117
+ break;
118
+ }
119
+ case "sign": {
120
+ // detached 侧车签名(P2-4,G4 载体):ojm sign <tgz> [--id <publisher.id>] [--key <pem>] [--check]
121
+ const { parseSignArgs } = await import("./sign");
122
+ const { signCommand } = await import("./sign");
123
+ const { positionals, id, key, check } = parseSignArgs(process.argv.slice(3));
124
+ signCommand(positionals[0] ?? "", { id, key, check });
125
+ break;
126
+ }
127
+ case "install": {
128
+ // 方案包安装瘦客户端(P1-13,D6):file 上传 / CDN 市场源 → 轮询任务至终态
129
+ const { positionals, url, user, pass, locale, name, version } = parseLifecycleArgs(process.argv.slice(3));
130
+ const { installCommand } = await import("./install");
131
+ await installCommand(positionals[0] ?? "", { url, user, pass, locale, name, version });
132
+ break;
133
+ }
134
+ case "enable":
135
+ case "disable": {
136
+ // 生命周期开关瘦客户端(P1-13,D8):站点侧 setEnabled 四处同写
137
+ const { positionals, url, user, pass } = parseLifecycleArgs(process.argv.slice(3));
138
+ const { setEnabledCommand } = await import("./enable");
139
+ await setEnabledCommand(positionals[0] ?? "", command === "enable", { url, user, pass });
140
+ break;
141
+ }
142
+ case "uninstall": {
143
+ // 卸载瘦客户端(P1-15a,缺口 B):站点侧 runUninstall 四处同删 + 权限级联
144
+ const { positionals, url, user, pass } = parseLifecycleArgs(process.argv.slice(3));
145
+ const { uninstallCommand } = await import("./uninstall");
146
+ await uninstallCommand(positionals[0] ?? "", { url, user, pass });
147
+ break;
148
+ }
149
+ case "trust": {
150
+ // 信任库运维瘦客户端(P2-2,C2-38/39):ls / add <key_id> <pubkey_hex> / revoke <key_id> / confirm --task
151
+ const { positionals, url, user, pass, task } = parseLifecycleArgs(process.argv.slice(3));
152
+ const { trustCommand } = await import("./trust");
153
+ await trustCommand(positionals[0] ?? "", positionals.slice(1), { url, user, pass, task });
154
+ break;
155
+ }
156
+ case "publish": {
157
+ // 索引发布(P3-5/6/7,作者工具链):重算 bundles/manifests 索引 + detached .sig 签名写回
158
+ const { parsePublishArgs } = await import("./publish");
159
+ const { publishCommand } = await import("./publish");
160
+ const { positionals, id, key, check } = parsePublishArgs(process.argv.slice(3));
161
+ publishCommand(positionals[0] ?? "", { id, key, check });
162
+ break;
163
+ }
103
164
  default: {
104
165
  if (command && OJ_PASSTHROUGH.has(command)) {
105
166
  const { runOjSubcommand } = await import("./oj-cmd");
package/src/info.ts CHANGED
@@ -13,6 +13,7 @@ import fs from "node:fs";
13
13
  import http from "node:http";
14
14
  import path from "node:path";
15
15
  import { loadModulesConfig } from "./config";
16
+ import { resolveConfigPath } from "./layout";
16
17
  import { mergeModuleManifests } from "./manifest";
17
18
  import { readOjApiPrefix, readOjServerField } from "./oj-config";
18
19
  import { isSupportedOjVersion, MIN_OJ_VERSION, readLocalVersion, resolveOjBin } from "./vendor";
@@ -71,7 +72,7 @@ function probeOjHealth(base: string, port: number): Promise<Record<string, unkno
71
72
  /** CLI 默认观测:真跑 `bin/oj -V` 与真探 /health */
72
73
  export function realOjObservability(projectRoot: string): OjObservability {
73
74
  const ojBin = resolveOjBin(projectRoot);
74
- const configPath = path.join(projectRoot, "api/config.yaml");
75
+ const configPath = resolveConfigPath(projectRoot);
75
76
  return {
76
77
  ojVersion: () => (fs.existsSync(ojBin) ? probeOjVersion(ojBin) : null),
77
78
  ojHealth: () => {
@@ -120,7 +121,7 @@ export async function printInfo(projectRoot: string, oj: OjObservability = realO
120
121
 
121
122
  // ---- 后端(oj)观测段:工程有 config.yaml 或 bin/oj 才展示;观测不可达仅降级不失败 ----
122
123
  let backendBlock = "";
123
- const configPath = path.join(projectRoot, "api/config.yaml");
124
+ const configPath = resolveConfigPath(projectRoot);
124
125
  const ojBin = resolveOjBin(projectRoot);
125
126
  if (fs.existsSync(configPath) || fs.existsSync(ojBin)) {
126
127
  // 安装标记(ojm vendor/init 下载时写入);bin/oj 存在但无标记 → 未知