@oj-module/cli 0.2.2 → 0.2.6

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 +189 -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 +1 -1
  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 +62 -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 +12 -0
  223. package/templates/web/src/system/__client/api.ts +25 -1
  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
@@ -201,7 +201,8 @@ function mapAuthPayload(data) {
201
201
  };
202
202
  }
203
203
  function fetchLogin(data) {
204
- return request.post("auth/login", { json: data }).json().then((env) => mapAuthPayload(env.data));
204
+ const path = data.method === "ldap" ? "auth/ldap-login" : "auth/login";
205
+ return request.post(path, { json: data }).json().then((env) => mapAuthPayload(env.data));
205
206
  }
206
207
  function fetchLogout(data) {
207
208
  return request.post("auth/logout", { json: { refresh_token: data?.refreshToken ?? "" } }).json().then(() => {});
@@ -255,10 +256,10 @@ function getAppInfo() {
255
256
  return {
256
257
  "pkg": {
257
258
  "name": "@oj-module/runtime",
258
- "version": "0.2.2",
259
+ "version": "0.2.6",
259
260
  "license": "MIT"
260
261
  },
261
- "lastBuildTime": "2026-10-16 22:08:56"
262
+ "lastBuildTime": "2026-10-19 12:08:59"
262
263
  };
263
264
  }
264
265
  var init_get_app_info = __esmMin((() => {}));
@@ -8321,6 +8322,39 @@ function generateMenuItemsFromRoutes(routeList, parentPath = "/") {
8321
8322
  }
8322
8323
  var init_generate_menu_items_from_routes = __esmMin((() => {}));
8323
8324
  //#endregion
8325
+ //#region src/router/utils/merge-solution-categories.ts
8326
+ /**
8327
+ * 方案路由归并(纯函数,R21):按 path 把 sys_solution_route 行注记到已注册路由上。
8328
+ *
8329
+ * - 三值 category 原样落 handle.category(main=常规组、admin=管理组、hidden=不进菜单);
8330
+ * - hidden 与 enabled=0(禁用)复用既有 hideInMenu 通路——只过滤菜单层,路由仍可达
8331
+ * (禁用的路由摘除归装载层三处同写,D12);
8332
+ * - order 覆写为声明序:框架项保留 100 间隔带、方案项从 200 起编,共用一条序轴;
8333
+ * - 未匹配路由(框架/后端)原样保留(同一对象引用),入参不突变;
8334
+ * - solRoutes 为空(端点失败/未登录降级空表)时原样返回——降级不阻断,路由照常。
8335
+ *
8336
+ * title 不在此覆写:entry.ts 是模块元数据唯一来源(module-loader 既有惯例),
8337
+ * title_key 随行下发仅供深检对比。
8338
+ */
8339
+ function mergeSolutionCategories(routes, solRoutes) {
8340
+ if (solRoutes.length === 0) return routes;
8341
+ const byPath = new Map(solRoutes.map((r) => [r.path, r]));
8342
+ return routes.map((route) => {
8343
+ const row = route.path ? byPath.get(route.path) : void 0;
8344
+ if (!row) return route;
8345
+ return {
8346
+ ...route,
8347
+ handle: {
8348
+ ...route.handle,
8349
+ category: row.category,
8350
+ order: row.order,
8351
+ hideInMenu: row.category === "hidden" || !row.enabled || (route.handle.hideInMenu ?? false)
8352
+ }
8353
+ };
8354
+ });
8355
+ }
8356
+ var init_merge_solution_categories = __esmMin((() => {}));
8357
+ //#endregion
8324
8358
  //#region src/router/utils/resolve-login-route.ts
8325
8359
  /**
8326
8360
  * 内外 login 路由去重(P3,login 模块化):唯一合成入口(SRP/DRY),
@@ -8380,18 +8414,20 @@ var init_access = __esmMin((() => {
8380
8414
  init_ascending();
8381
8415
  init_flatten_routes();
8382
8416
  init_generate_menu_items_from_routes();
8417
+ init_merge_solution_categories();
8383
8418
  init_resolve_login_route();
8384
8419
  initialState$1 = {
8385
8420
  wholeMenus: generateMenuItemsFromRoutes(baseRoutes),
8386
8421
  routeList: baseRoutes,
8387
8422
  flatRouteList: flattenRoutes(baseRoutes),
8388
- isAccessChecked: false
8423
+ isAccessChecked: false,
8424
+ solutionRoutes: []
8389
8425
  };
8390
8426
  lastRegisteredRoutes = [];
8391
- useAccessStore = create((set) => ({
8427
+ useAccessStore = create((set, get) => ({
8392
8428
  ...initialState$1,
8393
8429
  setAccessStore: (routes) => {
8394
- const newRoutes = ascending([...resolveLoginRoute(baseRoutes, routes, router, rootRoute), ...routes]);
8430
+ const newRoutes = ascending([...resolveLoginRoute(baseRoutes, routes, router, rootRoute), ...mergeSolutionCategories(routes, get().solutionRoutes)]);
8395
8431
  router.patchRoutes(ROOT_ROUTE_ID, routes);
8396
8432
  const flatRouteList = flattenRoutes(newRoutes);
8397
8433
  const newState = {
@@ -8404,6 +8440,10 @@ var init_access = __esmMin((() => {
8404
8440
  set(() => newState);
8405
8441
  return newState;
8406
8442
  },
8443
+ setSolutionRoutes: (rows) => {
8444
+ set({ solutionRoutes: rows });
8445
+ if (lastRegisteredRoutes.length > 0) get().setAccessStore(lastRegisteredRoutes);
8446
+ },
8407
8447
  reset: () => {
8408
8448
  router._internalSetRoutes(rootRoute);
8409
8449
  set(initialState$1);
@@ -8739,7 +8779,10 @@ var init_request = __esmMin((() => {
8739
8779
  }
8740
8780
  }
8741
8781
  return refreshTokenAndRetry(request, options, refreshToken);
8742
- } else return handleErrorResponse(response);
8782
+ } else {
8783
+ if (options.silentError) return response;
8784
+ return handleErrorResponse(response);
8785
+ }
8743
8786
  }
8744
8787
  return response;
8745
8788
  }]
@@ -8777,11 +8820,13 @@ function fetchRbacOplog(query) {
8777
8820
  for (const [k, v] of Object.entries(query)) if (v != null && v !== "") sp.set(k, String(v));
8778
8821
  return get(`system/oplog?${sp.toString()}`);
8779
8822
  }
8823
+ var fetchRbacOplogPost = (body) => post("system/oplog", body);
8780
8824
  var fetchRbacSettings = () => get("system/settings");
8781
8825
  var fetchRbacUpdateSetting = (key, value) => put("system/settings", {
8782
8826
  key,
8783
8827
  value
8784
8828
  });
8829
+ var fetchSolutionRoutes = () => get("_platform/solutions/routes");
8785
8830
  function fetchRbacContacts(keyword, limit) {
8786
8831
  return get(`_platform/contacts?keyword=${encodeURIComponent(keyword)}${limit ? `&limit=${limit}` : ""}`);
8787
8832
  }
@@ -8827,6 +8872,46 @@ function groupByModule(items) {
8827
8872
  items
8828
8873
  }));
8829
8874
  }
8875
+ /**
8876
+ * 框架自有设置模块(seed.sql 现行集合;新增框架设置模块须同步)。
8877
+ * 方案设置经安装预检强制 `moduleOf(key)===name`(R24),落库 module 列即方案名——
8878
+ * 据此把 sys_setting 行二分为框架区(维持按 group 聚合的现状渲染)与方案区(按模块三层嵌套)。
8879
+ */
8880
+ var FRAMEWORK_SETTING_MODULES = /* @__PURE__ */ new Set([
8881
+ "framework",
8882
+ "market",
8883
+ "ldap",
8884
+ "sso"
8885
+ ]);
8886
+ /**
8887
+ * 设置行按模块三层嵌套分组(R20/R24,纯函数):框架卡在前(按 group 列聚合、保持服务端
8888
+ * `order by group, key` 次序),方案卡在后(module 名升序、组内按 group 列聚合)。
8889
+ * 不按 key 首段切组——框架 seed 存在无点号键(ldap_base_dn),首段不是其模块名;
8890
+ * module 列是落库的所有权真源。空行降级返回空数组。
8891
+ */
8892
+ function groupSettingsByModule(rows) {
8893
+ const framework = /* @__PURE__ */ new Map();
8894
+ const solutions = /* @__PURE__ */ new Map();
8895
+ for (const item of rows) if (FRAMEWORK_SETTING_MODULES.has(item.module)) (framework.get(item.group) ?? framework.set(item.group, []).get(item.group)).push(item);
8896
+ else {
8897
+ const groups = solutions.get(item.module) ?? solutions.set(item.module, /* @__PURE__ */ new Map()).get(item.module);
8898
+ (groups.get(item.group) ?? groups.set(item.group, []).get(item.group)).push(item);
8899
+ }
8900
+ const cards = [];
8901
+ for (const [group, items] of framework) cards.push({
8902
+ module: null,
8903
+ group,
8904
+ label_key: null,
8905
+ items
8906
+ });
8907
+ for (const module of [...solutions.keys()].sort()) for (const [group, items] of solutions.get(module)) cards.push({
8908
+ module,
8909
+ group,
8910
+ label_key: `${module}:settings.group.${group}`,
8911
+ items
8912
+ });
8913
+ return cards;
8914
+ }
8830
8915
  //#endregion
8831
8916
  //#region src/api/system/menu/index.ts
8832
8917
  init_api_provider();
@@ -9545,6 +9630,55 @@ function defineModule(definition) {
9545
9630
  return definition;
9546
9631
  }
9547
9632
  //#endregion
9633
+ //#region src/store/solution-routes.ts
9634
+ init_access();
9635
+ init_auth();
9636
+ var useSolutionRoutes = create((set) => ({
9637
+ routes: [],
9638
+ loaded: false,
9639
+ refresh: async () => {
9640
+ try {
9641
+ const routes = await fetchSolutionRoutes();
9642
+ set({
9643
+ routes,
9644
+ loaded: true
9645
+ });
9646
+ useAccessStore.getState().setSolutionRoutes(routes);
9647
+ } catch {
9648
+ set({
9649
+ routes: [],
9650
+ loaded: true
9651
+ });
9652
+ useAccessStore.getState().setSolutionRoutes([]);
9653
+ }
9654
+ },
9655
+ reset: () => {
9656
+ set({
9657
+ routes: [],
9658
+ loaded: false
9659
+ });
9660
+ useAccessStore.getState().setSolutionRoutes([]);
9661
+ }
9662
+ }));
9663
+ /**
9664
+ * token 就绪监视(R21):登录成功(token 翻转)强制重拉一次;登出清空行并解除归并。
9665
+ * 幂等(armed 防重),模块导入即武装——宿主链(shell 无 AuthGuard)同样生效。
9666
+ */
9667
+ var armed = false;
9668
+ function watchTokenReady() {
9669
+ if (armed) return;
9670
+ armed = true;
9671
+ let lastToken = useAuthStore.getState().token;
9672
+ if (lastToken) useSolutionRoutes.getState().refresh();
9673
+ useAuthStore.subscribe((state) => {
9674
+ if (state.token === lastToken) return;
9675
+ lastToken = state.token;
9676
+ if (state.token) useSolutionRoutes.getState().refresh();
9677
+ else useSolutionRoutes.getState().reset();
9678
+ });
9679
+ }
9680
+ watchTokenReady();
9681
+ //#endregion
9548
9682
  //#region src/utils/get-all-expanded-keys/index.ts
9549
9683
  function getAllExpandedKeys(data, fieldName = "key") {
9550
9684
  return data.flatMap((item) => [item[fieldName], ...item.children?.length ? getAllExpandedKeys(item.children, fieldName) : []]);
@@ -27594,10 +27728,11 @@ init_tabs();
27594
27728
  init_user();
27595
27729
  init_get_app_info();
27596
27730
  init_get_redirect_path();
27731
+ init_request();
27597
27732
  init_user$1();
27598
27733
  init_local_icons();
27599
27734
  init_ri();
27600
27735
  init_menu_icons();
27601
27736
  init_tree();
27602
27737
  //#endregion
27603
- export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, bindingDiff, canAssignRole, canDeleteRole, canEditRoleBindings, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchChangePassword, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLdapLogin, fetchLogin, fetchLoginMethods, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRbacAdminResetPassword, fetchRbacContactDetail, fetchRbacContacts, fetchRbacCreateRole, fetchRbacCreateUser, fetchRbacDeleteRole, fetchRbacOplog, fetchRbacPermissionList, fetchRbacRoleList, fetchRbacSetForbidden, fetchRbacSettings, fetchRbacUpdateBindings, fetchRbacUpdateSetting, fetchRbacUpdateUserRoles, fetchRbacUserList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, groupByModule, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, roleLabel, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useTabKey, useTabsStore, useUserStore, external_exports as z };
27738
+ export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, bindingDiff, canAssignRole, canDeleteRole, canEditRoleBindings, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchChangePassword, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLdapLogin, fetchLogin, fetchLoginMethods, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRbacAdminResetPassword, fetchRbacContactDetail, fetchRbacContacts, fetchRbacCreateRole, fetchRbacCreateUser, fetchRbacDeleteRole, fetchRbacOplog, fetchRbacOplogPost, fetchRbacPermissionList, fetchRbacRoleList, fetchRbacSetForbidden, fetchRbacSettings, fetchRbacUpdateBindings, fetchRbacUpdateSetting, fetchRbacUpdateUserRoles, fetchRbacUserList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchSolutionRoutes, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, groupByModule, groupSettingsByModule, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, request, roleLabel, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useSolutionRoutes, useTabKey, useTabsStore, useUserStore, external_exports as z };
@@ -5,9 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.ico" />
7
7
  <title>React Antd Admin · Shell</title>
8
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-ELLiP0aL4p7v4pSj08o8IQ=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-p4BgRegxexzGkBE95NQxKA=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
9
9
 
10
- <script type="importmap" nonce="ELLiP0aL4p7v4pSj08o8IQ==">{
10
+ <script type="importmap" nonce="p4BgRegxexzGkBE95NQxKA==">{
11
11
  "imports": {
12
12
  "react": "/assets/react.js",
13
13
  "react/jsx-runtime": "/assets/jsx-runtime.js",
@@ -7,9 +7,9 @@
7
7
  "react-router": "7.18.2",
8
8
  "react-router/dom": "7.18.2",
9
9
  "@tanstack/react-query": "5.102.8",
10
- "@oj-module/runtime": "0.2.2",
11
- "@oj-module/runtime/contract": "0.2.2",
12
- "@oj-module/runtime/contract/errors": "0.2.2",
10
+ "@oj-module/runtime": "0.2.6",
11
+ "@oj-module/runtime/contract": "0.2.6",
12
+ "@oj-module/runtime/contract/errors": "0.2.6",
13
13
  "@plane/kit": "0.1.20",
14
14
  "@plane/editor": "0.1.20",
15
15
  "antd": "6.6.1",
package/src/args.ts CHANGED
@@ -38,6 +38,29 @@ export function parseApiArgs(argv: string[]): { dir: string, check: boolean, doc
38
38
  return { dir, check: flags.has("--check"), docs: flags.has("--docs"), exempt };
39
39
  }
40
40
 
41
+ /**
42
+ * `ojm build [--bundle] [--out <dir>]`。
43
+ * `--bundle`:方案包构建(manifest.yaml 契约校验 → 确定性 tgz + sha256),
44
+ * 与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工(devkit 手册「打包与分发」)。
45
+ * `--out` 的值不得以 `--` 开头(同 parseApiArgs 的消费式扫描,P6 同款坑)。
46
+ */
47
+ export function parseBuildArgs(argv: string[]): { bundle: boolean, out?: string } {
48
+ const flags = new Set<string>();
49
+ let out: string | undefined;
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === "--out") {
53
+ out = argv[i + 1];
54
+ i++; // 消费紧随的值,不被当 flag
55
+ continue;
56
+ }
57
+ if (a.startsWith("--")) {
58
+ flags.add(a);
59
+ }
60
+ }
61
+ return { bundle: flags.has("--bundle"), out };
62
+ }
63
+
41
64
  /**
42
65
  * `ojm vendor [tag] [--force]`:下载/更新 oj vendor(npm 包 @oj-bin/oj)。
43
66
  * tag 缺省追 npm latest;形如 `v0.1.13` 或 `0.1.13`(自动补 v 前缀,装包时去掉)。
@@ -53,3 +76,47 @@ export function parseVendorArgs(argv: string[]): { force: boolean, tag?: string
53
76
  tag: tag === undefined ? undefined : (tag.startsWith("v") ? tag : `v${tag}`),
54
77
  };
55
78
  }
79
+
80
+ export interface LifecycleArgs {
81
+ positionals: string[]
82
+ url?: string
83
+ user?: string
84
+ pass?: string
85
+ locale?: string
86
+ name?: string
87
+ version?: string
88
+ task?: string
89
+ }
90
+
91
+ /**
92
+ * `ojm install|enable|disable` 共用解析:位置参数(目标/方案名)+ 值式 flag。
93
+ * 站点寻址三件套 --url/--user/--pass(环境变量 OJM_URL/OJM_USER/OJM_PASS 兜底,见 src/site.ts);
94
+ * install 专属 --name/--version(文件名身份覆盖)与 --locale(终态文案语言);
95
+ * trust 专属 --task(TOFU 确认目标任务 id,P2-3)。
96
+ * 值式 flag 逐项消费紧随的值,不被当位置参数(P6 同款坑,parseApiArgs 同纪律)。
97
+ */
98
+ export function parseLifecycleArgs(argv: string[]): LifecycleArgs {
99
+ const valueFlags = new Set(["--url", "--user", "--pass", "--locale", "--name", "--version", "--task"]);
100
+ const values: Record<string, string> = {};
101
+ const positionals: string[] = [];
102
+ for (let i = 0; i < argv.length; i++) {
103
+ const a = argv[i]!;
104
+ if (valueFlags.has(a)) {
105
+ values[a] = argv[i + 1] ?? "";
106
+ i++; // 消费紧随的值
107
+ continue;
108
+ }
109
+ if (!a.startsWith("--"))
110
+ positionals.push(a);
111
+ }
112
+ return {
113
+ positionals,
114
+ url: values["--url"],
115
+ user: values["--user"],
116
+ pass: values["--pass"],
117
+ locale: values["--locale"],
118
+ name: values["--name"],
119
+ version: values["--version"],
120
+ task: values["--task"],
121
+ };
122
+ }
package/src/build.ts CHANGED
@@ -10,7 +10,7 @@ import tailwindcss from "@tailwindcss/vite";
10
10
  import { build as esbuild } from "esbuild";
11
11
  import { build } from "vite";
12
12
  import { loadModulesConfig, resolveModuleEntry } from "./config";
13
- import { resolveLayout } from "./layout";
13
+ import { resolveConfigPath, resolveLayout } from "./layout";
14
14
  import { isSharedDep, SHARED_DEPS } from "./shared-deps";
15
15
  import { resolveOjBin } from "./vendor";
16
16
  import { checkSharedVersions, resolveShellDist } from "./versions";
@@ -72,6 +72,7 @@ export const fetchRbacUpdateUserRoles = _fn;
72
72
  export const fetchRbacAdminResetPassword = _fn;
73
73
  export const fetchRbacSetForbidden = _fn;
74
74
  export const fetchRbacOplog = _fn;
75
+ export const fetchRbacOplogPost = _fn;
75
76
  export const fetchRbacSettings = _fn;
76
77
  export const fetchRbacUpdateSetting = _fn;
77
78
  export const fetchRbacContacts = _fn;
@@ -79,12 +80,15 @@ export const fetchRbacContactDetail = _fn;
79
80
  export const fetchLoginMethods = _fn;
80
81
  export const fetchChangePassword = _fn;
81
82
  export const fetchLdapLogin = _fn;
83
+ // 方案路由归并(P1-14/R21 读端点 + 分组纯函数)
84
+ export const fetchSolutionRoutes = _fn;
82
85
  export const roleLabel = _fn;
83
86
  export const canEditRoleBindings = _fn;
84
87
  export const canDeleteRole = _fn;
85
88
  export const canAssignRole = _fn;
86
89
  export const bindingDiff = _fn;
87
90
  export const groupByModule = _fn;
91
+ export const groupSettingsByModule = _fn;
88
92
  // hooks 与权限常量
89
93
  export const useAccess = _fn;
90
94
  export const usePreferences = _fn;
@@ -97,6 +101,8 @@ export const AccessControlRoles = _obj;
97
101
  export const permissionPrefix = "permission:button";
98
102
  // store
99
103
  export const useAuthStore = _fn;
104
+ // 方案路由归并 store(token 就绪自动重拉,R21)
105
+ export const useSolutionRoutes = _fn;
100
106
  export const useUserStore = _fn;
101
107
  export const useTabsStore = _fn;
102
108
  // 页签 key 唯一正确取口(keepAlive 下 useLocation 是激活页位置,0.1.21)
@@ -148,6 +154,8 @@ export const unloadModule = async () => {};
148
154
  export const useSlotNodes = () => [];
149
155
  // AC-D15:zod re-export——stub 用可调用代理(契约 schema 不在元数据读取期求值,仅占位防 missing export)
150
156
  export const z = new Proxy({}, { get: () => _fn });
157
+ // R29:ky 实例 re-export(第一方市场编排面)——同 z,元数据读取期仅需可调用占位
158
+ export const request = new Proxy({}, { get: () => _fn });
151
159
  `;
152
160
 
153
161
  /**
@@ -492,9 +500,9 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
492
500
  const apiDist = path.join(projectRoot, "api/dist");
493
501
  console.log("[ojm] oj build(后端)…");
494
502
  // 必须显式给 -c:oj build 的 -c 缺省是 ./config.yaml(相对 CWD),而 config 在
495
- // api/ 下——漏传会静默读不到配置,连带 tasks 镜像目录回落默认 "tasks"、
503
+ // <project>/config/ 下——漏传会静默读不到配置,连带 tasks 镜像目录回落默认 "tasks"、
496
504
  // sql_guard 声明校验不生效(oj 只 warn 两行,极易被忽略)。
497
- const configPath = path.join(projectRoot, "api/config.yaml");
505
+ const configPath = resolveConfigPath(projectRoot);
498
506
  const configArgs = fs.existsSync(configPath) ? ["-c", configPath] : [];
499
507
  // --no-minify:oj 的 swc minify 当前会把产物压坏,必须绕开。实测两种坏法:
500
508
  // 1) `const rows = await db.query(...); rows[0]` → 压成 `await db.query(...)[0]`,
package/src/bundle.ts ADDED
@@ -0,0 +1,197 @@
1
+ // 方案包构建(P1-11,`ojm build --bundle`):manifest.yaml 契约校验(fail-fast 先于打包)
2
+ // → 组装 tgz 内布局(契约 §2.9)→ 确定性打包(C2-32)+ sha256。
3
+ // 组装映射(对齐 P1-9 staging copy 源一层定版,unpack 布局即安装器 extract 段消费面):
4
+ // <project>/manifest.yaml → <name>-<version>/manifest.yaml(Form A)
5
+ // <project>/api/dist/<name>-<version>/ → <name>-<version>/api/<name>-<version>/(oj build 产物原样,C2-31 分层核对)
6
+ // <project>/web/dist/modules/ → <name>-<version>/web/modules/(web.entry/css 存在性复核)
7
+ // 红线:modules.json 禁入包(C2-31——该清单由安装器在目标站点 upsert,防覆盖宿主清单);
8
+ // 文件面契约(tables↔schema.yaml、locales 双语齐备)构建期 fail-fast(契约 §4.1 构建列)。
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+
12
+ import { parse } from "yaml";
13
+ import { createDeterministicTgz } from "./deterministic-tar";
14
+ import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, referencedI18nKeys, validateContract } from "./manifest-contract";
15
+
16
+ export interface BundleBuildResult {
17
+ tgz: string
18
+ sha256: string
19
+ name: string
20
+ version: string
21
+ }
22
+
23
+ /**
24
+ * 从方案工程产出可安装 tgz + sha256。
25
+ * @param projectRoot 方案工程根(manifest.yaml 所在目录)
26
+ * @param opts 输出与选项
27
+ * @param opts.out 输出目录,缺省 `dist/`;产物 `<name>-<version>.tgz`
28
+ */
29
+ export async function buildBundle(projectRoot: string, opts: { out?: string } = {}): Promise<BundleBuildResult> {
30
+ // 1) 读入并校验契约(构建即校验,坏清单出不了包)
31
+ const manifestPath = path.join(projectRoot, "manifest.yaml");
32
+ if (!fs.existsSync(manifestPath)) {
33
+ throw new Error(`[ojm] 方案包构建失败:${projectRoot} 下缺少 manifest.yaml(bundle 级契约是打包入口,写法见 devkit 手册「打包与分发」)`);
34
+ }
35
+ const parsed = parseManifest(fs.readFileSync(manifestPath, "utf8"));
36
+ if (!parsed.ok) {
37
+ throw new Error(`[ojm] 方案包构建失败:${parsed.error}`);
38
+ }
39
+ const m = parsed.manifest;
40
+ const violations = validateContract(m);
41
+ if (violations.length > 0) {
42
+ throw new Error(`[ojm] 方案包构建失败:manifest.yaml 契约校验未通过(${violations.length} 项违规,全量列出):\n${violations.map(x => `- ${x}`).join("\n")}`);
43
+ }
44
+ const name = m.name as string;
45
+ const version = m.version as string;
46
+
47
+ // 2) 文件面契约(§4.1 构建列专属执行点;manifest 内在规则已在 validateContract 全量通过)
48
+ const fileViolations = [
49
+ ...checkTables(m, projectRoot, name),
50
+ ...checkLocales(m, projectRoot, name),
51
+ ];
52
+ if (fileViolations.length > 0) {
53
+ throw new Error(`[ojm] 方案包构建失败:文件面契约未通过(${fileViolations.length} 项,全量列出):\n${fileViolations.map(x => `- ${x}`).join("\n")}`);
54
+ }
55
+
56
+ // order <200 仅构建期 lint 警告不阻断(C2-17:框架项保留 <200 间隔带)
57
+ const routes = Array.isArray(m.routes) ? m.routes as Record<string, unknown>[] : [];
58
+ for (const r of routes) {
59
+ const order = r.order;
60
+ if (typeof order === "number" && order < 200) {
61
+ console.warn(`[ojm] ⚠️ route「${String(r.path)}」order=${order} <200(框架项保留间隔带,建议 ≥200;仅警告不阻断)`);
62
+ }
63
+ }
64
+
65
+ // 3) 组装 staging:<name>-<version>/ 单根
66
+ const staging = fs.mkdtempSync(path.join(projectRoot, ".ojm-bundle-"));
67
+ try {
68
+ const root = path.join(staging, `${name}-${version}`);
69
+ fs.mkdirSync(root);
70
+ fs.copyFileSync(manifestPath, path.join(root, "manifest.yaml"));
71
+
72
+ // api:oj build 产物原样入包(编译编排不在此重跑——作者先 oj build,本命令只校验与组装;
73
+ // 编排接线随 P1-15 方案脚手架定形)
74
+ if (m.api !== undefined) {
75
+ const built = path.join(projectRoot, "api/dist", `${name}-${version}`);
76
+ if (!fs.existsSync(built)) {
77
+ throw new Error(`[ojm] 方案包构建失败:缺少 oj build 产物 ${path.relative(projectRoot, built)}(manifest 声明了 api 段;先对 api/ 运行 oj build,产物原样入包)`);
78
+ }
79
+ const layoutViolations = checkModuleManifest(built, name, version, m.summary as string);
80
+ if (layoutViolations.length > 0) {
81
+ throw new Error(`[ojm] 方案包构建失败:分层核对未通过(C2-31):\n${layoutViolations.map(x => `- ${x}`).join("\n")}`);
82
+ }
83
+ fs.cpSync(built, path.join(root, "api", `${name}-${version}`), { recursive: true });
84
+ }
85
+
86
+ // web:站点构建模块产物入包(web 段缺省 = 纯后端方案,整段跳过)
87
+ if (m.web !== undefined) {
88
+ const webModules = path.join(projectRoot, "web/dist/modules");
89
+ if (!fs.existsSync(webModules)) {
90
+ throw new Error("[ojm] 方案包构建失败:缺少站点构建产物 web/dist/modules(manifest 声明了 web 段;先运行 ojm build 生成模块产物)");
91
+ }
92
+ fs.cpSync(webModules, path.join(root, "web", "modules"), { recursive: true });
93
+ const web = m.web as Record<string, unknown>;
94
+ for (const rel of [web.entry, ...(Array.isArray(web.css) ? web.css : [])]) {
95
+ if (typeof rel === "string" && !fs.existsSync(path.join(root, "web", rel))) {
96
+ throw new Error(`[ojm] 方案包构建失败:web 声明的产物不存在:${rel}(C2-27 成品存在性复核;请核对该文件是否已在本次构建产出)`);
97
+ }
98
+ }
99
+ }
100
+
101
+ // 红线自检:modules.json 禁入包任何位置(C2-31)+ 私钥/凭证禁入(P2-4 入包红线;
102
+ // 完整性自查发生在确定性打包前的 staging 上)
103
+ assertNoForbiddenMembers(root);
104
+
105
+ // 4) 确定性打包(C2-32;symlink 等成员守卫在打包原语内,C2-30)
106
+ const outDir = path.resolve(projectRoot, opts.out ?? "dist");
107
+ const tgzPath = path.join(outDir, `${name}-${version}.tgz`);
108
+ const sha256 = await createDeterministicTgz(staging, tgzPath);
109
+ const size = fs.statSync(tgzPath).size;
110
+ console.log(`[ojm] 方案包已生成 → ${tgzPath}`);
111
+ console.log(`[ojm] sha256:${sha256}(${size} 字节;确定性打包:同输入两次打包哈希一致,C2-32)`);
112
+ return { tgz: tgzPath, sha256, name, version };
113
+ }
114
+ finally {
115
+ fs.rmSync(staging, { recursive: true, force: true });
116
+ }
117
+ }
118
+
119
+ /** C2-31 分层核对:模块级 manifest 仅 oj 原生键,且 name/version/desc 与 bundle 级逐一相等(oj 启动强校验同源) */
120
+ function checkModuleManifest(built: string, name: string, version: string, summary: string): string[] {
121
+ const p = path.join(built, "manifest.yaml");
122
+ let doc: unknown;
123
+ try {
124
+ doc = parse(fs.readFileSync(p, "utf8"));
125
+ }
126
+ catch (e) {
127
+ return [`api/<name>-<version>/manifest.yaml 读取失败(${e instanceof Error ? e.message : String(e)})——oj 安装后要真实解析此文件`];
128
+ }
129
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
130
+ return ["api/<name>-<version>/manifest.yaml 必须是键值映射(oj 模块级,仅 oj 原生七键)"];
131
+ }
132
+ const mm = doc as Record<string, unknown>;
133
+ const out: string[] = [];
134
+ if (mm.name !== name) {
135
+ out.push(`模块级 name「${String(mm.name)}」必须 === bundle name「${name}」(oj 启动强校验 name == 目录名)`);
136
+ }
137
+ if (mm.version !== version) {
138
+ out.push(`模块级 version「${String(mm.version)}」必须 === bundle version「${version}」`);
139
+ }
140
+ if (mm.desc !== summary) {
141
+ out.push(`模块级 desc「${String(mm.desc)}」必须 === bundle summary「${summary}」(安装器写 desc 即取 summary)`);
142
+ }
143
+ return out;
144
+ }
145
+
146
+ /** api.tables ↔ schema.yaml 表集对照(C2-12a 构建列;schema 缺失/声明不一致均 fail-fast) */
147
+ function checkTables(m: Record<string, unknown>, projectRoot: string, name: string): string[] {
148
+ const api = m.api as Record<string, unknown> | undefined;
149
+ if (api === undefined || api.tables === undefined)
150
+ return [];
151
+ const schemaPath = path.join(projectRoot, "api/src", name, "schema.yaml");
152
+ if (!fs.existsSync(schemaPath)) {
153
+ return [`【C2-12a】api.tables 已声明但缺少 ${path.relative(projectRoot, schemaPath)},无法对照表集(oj 冗余校验同源)`];
154
+ }
155
+ return checkTablesAgainstSchema(m, fs.readFileSync(schemaPath, "utf8"));
156
+ }
157
+
158
+ /** locales 双语齐备(C2-25 构建列缺 key fail-fast;安装期信任构建产物不重复校验) */
159
+ function checkLocales(m: Record<string, unknown>, projectRoot: string, name: string): string[] {
160
+ if (referencedI18nKeys(m).length === 0)
161
+ return [];
162
+ const dir = path.join(projectRoot, "web/src", name, "locales");
163
+ const texts: Partial<Record<"zh-CN" | "en-US", string>> = {};
164
+ const out: string[] = [];
165
+ for (const lang of ["zh-CN", "en-US"] as const) {
166
+ const p = path.join(dir, `${lang}.json`);
167
+ if (!fs.existsSync(p)) {
168
+ out.push(`【C2-25】manifest 引用了 i18n key 但缺少 ${path.relative(projectRoot, p)}(双语齐备,D4)`);
169
+ continue;
170
+ }
171
+ texts[lang] = fs.readFileSync(p, "utf8");
172
+ }
173
+ return [...out, ...checkLocalesComplete(m, texts["zh-CN"] ?? "", texts["en-US"] ?? "")];
174
+ }
175
+
176
+ /** 私钥/凭证文件名红线(P2-4 入包自检):*.pem / *.key / .env / id_ed25519|id_rsa */
177
+ const FORBIDDEN_MEMBER = /\.(?:pem|key)$|^\.env$|^id_(?:ed25519|rsa)$/;
178
+
179
+ /** staging 全树扫描:modules.json 禁入(C2-31 红线)+ 私钥/凭证禁入(P2-4 红线) */
180
+ function assertNoForbiddenMembers(root: string): void {
181
+ const visit = (dir: string) => {
182
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
183
+ const p = path.join(dir, e.name);
184
+ if (e.isDirectory()) {
185
+ visit(p);
186
+ continue;
187
+ }
188
+ if (e.isFile() && e.name === "modules.json") {
189
+ throw new Error(`[ojm] 打包自检失败(C2-31):modules.json 禁入方案包(该清单由安装器在目标站点 upsert,防覆盖宿主清单)——位置:${p}`);
190
+ }
191
+ if (e.isFile() && FORBIDDEN_MEMBER.test(e.name)) {
192
+ throw new Error(`[ojm] 打包自检失败(P2-4 入包红线):私钥/凭证文件「${e.name}」禁入方案包(*.pem / *.key / .env / id_ed25519 / id_rsa)——签名私钥永不入包,签名经 detached .sig 侧车随包分发(包内只含产物与公钥)。位置:${p}`);
193
+ }
194
+ }
195
+ };
196
+ visit(root);
197
+ }