@oj-module/cli 0.2.18 → 0.2.23

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 (345) hide show
  1. package/devkit/README.md +4 -4
  2. package/devkit/SKILL.md +41 -37
  3. package/devkit/framework-dev.md +6 -5
  4. package/devkit/manual.md +352 -168
  5. package/devkit/menu-rules.md +3 -3
  6. package/devkit/module-dev-and-cdn-playground.md +31 -22
  7. package/devkit/sql-builder-missing-capabilities-pr.md +11 -6
  8. package/package.json +2 -2
  9. package/shell-dist/assets/runtime.js +59 -34
  10. package/shell-dist/index.html +2 -2
  11. package/shell-dist/versions.json +3 -3
  12. package/src/__tests__/build-backend.test.ts +54 -0
  13. package/src/__tests__/init-packagejson.test.ts +29 -0
  14. package/src/__tests__/layout.test.ts +176 -0
  15. package/src/__tests__/module-list.test.ts +66 -0
  16. package/src/__tests__/oj-caps.test.ts +114 -0
  17. package/src/alias-paths.ts +102 -0
  18. package/src/args.ts +54 -4
  19. package/src/build.ts +41 -9
  20. package/src/builtin.ts +93 -0
  21. package/src/bundle.ts +254 -38
  22. package/src/config.ts +82 -9
  23. package/src/contract/__tests__/market-client.integration.test.ts +14 -14
  24. package/src/contract/check.ts +56 -28
  25. package/src/contract/emit-stub.ts +21 -9
  26. package/src/contract/handler-scan.ts +19 -10
  27. package/src/contract/run.ts +14 -4
  28. package/src/dev.ts +80 -9
  29. package/src/index.ts +27 -7
  30. package/src/info.ts +69 -10
  31. package/src/init.ts +114 -28
  32. package/src/layout.ts +282 -24
  33. package/src/lifecycle/__tests__/args.test.ts +20 -0
  34. package/src/lifecycle/__tests__/builtin.test.ts +97 -0
  35. package/src/lifecycle/__tests__/bundle.test.ts +184 -6
  36. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  37. package/src/lifecycle/__tests__/new.test.ts +129 -0
  38. package/src/lifecycle/__tests__/smoke.test.ts +2 -1
  39. package/src/manifest-contract.ts +16 -3
  40. package/src/market/__tests__/bootstrap.test.ts +17 -9
  41. package/src/market/bootstrap.ts +30 -17
  42. package/src/market/publish.ts +10 -2
  43. package/src/new.ts +208 -0
  44. package/src/oj-caps.ts +133 -0
  45. package/src/oj.ts +27 -4
  46. package/src/preview-config.ts +1 -1
  47. package/src/preview.ts +2 -1
  48. package/src/site.ts +2 -2
  49. package/src/usage.ts +24 -12
  50. package/src/vendor.ts +5 -1
  51. package/templates/{api/config.yaml → config.yaml} +13 -8
  52. package/templates/global.d.ts +7 -6
  53. package/templates/init.include.yaml +41 -36
  54. package/templates/package.json +3 -1
  55. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
  56. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
  57. package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +4 -5
  58. package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
  59. package/templates/src/_platform/api/seed.mysql.sql +16 -0
  60. package/templates/src/_platform/api/seed.pg.sql +16 -0
  61. package/templates/{api/src/_platform → src/_platform/api}/seed.sql +16 -5
  62. package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
  63. package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
  64. package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
  65. package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
  66. package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
  67. package/templates/src/auth/api/manifest.yaml +6 -0
  68. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
  69. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
  70. package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
  71. package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +2 -2
  72. package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
  73. package/templates/src/oj-market/web/components/InstallDialog.tsx +194 -0
  74. package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +7 -3
  75. package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +18 -7
  76. package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +29 -12
  77. package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
  78. package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
  79. package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
  80. package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
  81. package/templates/src/system/api/manifest.yaml +6 -0
  82. package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
  83. package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
  84. package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
  85. package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
  86. package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
  87. package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
  88. package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
  89. package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
  90. package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
  91. package/templates/{web/src/system → src/system/web}/locales/en-US.json +1 -1
  92. package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +1 -1
  93. package/templates/src/tsconfig.json +16 -0
  94. package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
  95. package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
  96. package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
  97. package/templates/tests/fixture.ts +434 -0
  98. package/templates/{api/tests → tests}/guard.spec.ts +1 -1
  99. package/templates/{api/tests → tests}/l1-builder.test.ts +2 -2
  100. package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
  101. package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
  102. package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
  103. package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
  104. package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
  105. package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
  106. package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
  107. package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
  108. package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
  109. package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
  110. package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
  111. package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
  112. package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
  113. package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
  114. package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
  115. package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
  116. package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
  117. package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
  118. package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
  119. package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
  120. package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
  121. package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
  122. package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
  123. package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
  124. package/templates/{api/tests → tests}/rbac-seed.spec.ts +13 -11
  125. package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
  126. package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
  127. package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
  128. package/templates/tests/seed-role-accounts.spec.ts +87 -0
  129. package/templates/tsconfig.json +5 -1
  130. package/templates/tsconfig.paths.json +19 -0
  131. package/templates/types/oj-modules.d.ts +17 -0
  132. package/templates/vitest.config.ts +22 -0
  133. package/templates/web.config.ts +23 -13
  134. package/vendor/host-versions.json +3 -3
  135. package/templates/api/src/_platform/seed.mysql.sql +0 -8
  136. package/templates/api/src/_platform/seed.pg.sql +0 -8
  137. package/templates/api/src/auth/manifest.yaml +0 -5
  138. package/templates/api/src/system/manifest.yaml +0 -5
  139. package/templates/api/tests/fixture.ts +0 -58
  140. package/templates/api/tsconfig.json +0 -8
  141. package/templates/api/vitest.config.ts +0 -12
  142. package/templates/logs/task-events.jsonl +0 -0
  143. package/templates/web/src/oj-market/components/InstallDialog.tsx +0 -112
  144. /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
  145. /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
  146. /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
  147. /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
  148. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
  149. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
  150. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
  151. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
  152. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
  153. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
  154. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
  155. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
  156. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
  157. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
  158. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
  159. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
  160. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
  161. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
  162. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
  163. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
  164. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
  165. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
  166. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
  167. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
  168. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
  169. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
  170. /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
  171. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
  172. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
  173. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
  174. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
  175. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
  176. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
  177. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
  178. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
  179. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
  180. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
  181. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
  182. /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
  183. /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
  184. /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
  185. /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
  186. /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
  187. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
  188. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
  189. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
  190. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  191. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
  192. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  193. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
  194. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
  195. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
  196. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
  197. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
  198. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
  199. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  200. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
  201. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
  202. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  203. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  204. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
  205. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
  206. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
  207. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
  208. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  209. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
  210. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
  211. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  212. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
  213. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
  214. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
  215. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
  216. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
  217. /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
  218. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
  219. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
  220. /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
  221. /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
  222. /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
  223. /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
  224. /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
  225. /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
  226. /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
  227. /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
  228. /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
  229. /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
  230. /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
  231. /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
  232. /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
  233. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
  234. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
  235. /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
  236. /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
  237. /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
  238. /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
  239. /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
  240. /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
  241. /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
  242. /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
  243. /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
  244. /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
  245. /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
  246. /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
  247. /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
  248. /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
  249. /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
  250. /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
  251. /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
  252. /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
  253. /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
  254. /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
  255. /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
  256. /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
  257. /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
  258. /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
  259. /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
  260. /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
  261. /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
  262. /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
  263. /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
  264. /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
  265. /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
  266. /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
  267. /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
  268. /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
  269. /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
  270. /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
  271. /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
  272. /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
  273. /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
  274. /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
  275. /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
  276. /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
  277. /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
  278. /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
  279. /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
  280. /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
  281. /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
  282. /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
  283. /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  284. /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
  285. /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
  286. /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
  287. /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
  288. /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
  289. /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
  290. /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
  291. /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
  292. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
  293. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
  294. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
  295. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
  296. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
  297. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
  298. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
  299. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
  300. /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
  301. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
  302. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
  303. /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
  304. /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
  305. /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
  306. /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
  307. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
  308. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
  309. /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
  310. /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
  311. /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
  312. /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
  313. /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
  314. /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
  315. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
  316. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
  317. /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
  318. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
  319. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
  320. /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
  321. /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
  322. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
  323. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
  324. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
  325. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
  326. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
  327. /package/templates/{web/src → src}/styles.css +0 -0
  328. /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
  329. /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
  330. /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
  331. /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
  332. /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
  333. /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
  334. /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
  335. /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
  336. /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
  337. /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
  338. /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
  339. /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
  340. /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
  341. /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
  342. /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
  343. /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
  344. /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
  345. /package/templates/{api/tests → tests}/rbac-tables.spec.ts +0 -0
@@ -9,8 +9,8 @@ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
9
9
  // 不加载 @oj-module/runtime 重依赖(antd / pro-components 在 node 环境无法 import),仅桩 request、
10
10
  // 透传真实 zod(供生成 client 的 DEV 响应校验走真实路径)。ContractApiError 由轻量子路径提供。
11
11
  import { beforeEach, describe, expect, it, vi } from "vitest";
12
- import { bindRequest } from "../../../templates/web/src/_platform/__client/api";
13
- import { httpMarketClient } from "../../../templates/web/src/oj-market/api/defaultClient";
12
+ import { bindRequest } from "../../../templates/src/_platform/web/__client/api";
13
+ import { httpMarketClient } from "../../../templates/src/oj-market/web/api/defaultClient";
14
14
 
15
15
  // index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
16
16
  const { requestImpl } = vi.hoisted(() => ({
@@ -63,7 +63,7 @@ function mockRequest(responder: (c: Call) => { status?: number, data?: unknown }
63
63
 
64
64
  // 让 index() 直连的 request 与生成 client 的 bindRequest 指向同一 mock,行为一致。
65
65
  // 桥接:占位方法零参签名 ↔ ScopedRequestLike 真实签名,经 any 双向打通(测试桩,无运行时差异)。
66
- function useMock(m: ReturnType<typeof mockRequest>) {
66
+ function bindMock(m: ReturnType<typeof mockRequest>) {
67
67
  bindRequest(m.req);
68
68
  const req = m.req as unknown as Record<keyof typeof requestImpl, () => never>;
69
69
  for (const k of Object.keys(requestImpl) as Array<keyof typeof requestImpl>)
@@ -75,12 +75,12 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
75
75
 
76
76
  beforeEach(() => {
77
77
  mock = mockRequest(() => ({ data: {} }));
78
- useMock(mock);
78
+ bindMock(mock);
79
79
  });
80
80
 
81
81
  it("meta() → GET _platform/market/meta,返回 { apiVersion }", async () => {
82
82
  mock = mockRequest(() => ({ data: { apiVersion: "1.2.3" } }));
83
- useMock(mock);
83
+ bindMock(mock);
84
84
  const r = await httpMarketClient.meta();
85
85
  expect(mock.calls).toEqual([{ method: "get", url: "_platform/market/meta", opts: undefined }]);
86
86
  expect(r).toEqual({ apiVersion: "1.2.3" });
@@ -89,7 +89,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
89
89
  it("index() → GET _platform/market/index,返回 IndexRow[]", async () => {
90
90
  const rows = [{ name: "a", latest: "1.0", installed: "1.0", enabled: true, upgradable: false }];
91
91
  mock = mockRequest(() => ({ data: rows }));
92
- useMock(mock);
92
+ bindMock(mock);
93
93
  const r = await httpMarketClient.index();
94
94
  expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/index" });
95
95
  expect(r).toEqual(rows);
@@ -97,7 +97,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
97
97
 
98
98
  it("index() 遇 503(未配置市场地址)→ [] 空态,不抛(PRD §5.3)", async () => {
99
99
  mock = mockRequest(c => (c.url === "_platform/market/index" ? { status: 503 } : { data: {} }));
100
- useMock(mock);
100
+ bindMock(mock);
101
101
  const r = await httpMarketClient.index();
102
102
  expect(r).toEqual([]);
103
103
  // 503 是配置态而非故障:silentError 抑制全局 toast + retry:0 不重试(原
@@ -108,7 +108,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
108
108
 
109
109
  it("install(input) → POST _platform/market/install,body 含 name/version/source", async () => {
110
110
  mock = mockRequest(() => ({ data: { task_id: "t-1" } }));
111
- useMock(mock);
111
+ bindMock(mock);
112
112
  const r = await httpMarketClient.install({ name: "alpha", version: "2.0", source: "file" });
113
113
  expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/install" });
114
114
  expect(mock.calls[0].opts?.json).toEqual({ name: "alpha", version: "2.0", source: "file" });
@@ -117,7 +117,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
117
117
 
118
118
  it("install(input) 缺省 version/source → null / cdn 兜底", async () => {
119
119
  mock = mockRequest(() => ({ data: { task_id: "t-2" } }));
120
- useMock(mock);
120
+ bindMock(mock);
121
121
  await httpMarketClient.install({ name: "beta" });
122
122
  expect(mock.calls[0].opts?.json).toEqual({ name: "beta", version: null, source: "cdn" });
123
123
  });
@@ -125,7 +125,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
125
125
  it("task(id) → GET _platform/market/task? 带 id", async () => {
126
126
  const rec = { id: "t-1", name: "a", version: "1.0", status: "done", stage: "done", progress: 100, message: "ok", stages: [], startedAt: 1 };
127
127
  mock = mockRequest(() => ({ data: rec }));
128
- useMock(mock);
128
+ bindMock(mock);
129
129
  const r = await httpMarketClient.task("t-1");
130
130
  expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/task" });
131
131
  expect(mock.calls[0].opts?.searchParams).toEqual({ id: "t-1" });
@@ -134,7 +134,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
134
134
 
135
135
  it("enable(input) → POST _platform/market/enable,body {name,enabled}", async () => {
136
136
  mock = mockRequest(() => ({ data: { ok: true, message: "done" } }));
137
- useMock(mock);
137
+ bindMock(mock);
138
138
  const r = await httpMarketClient.enable({ name: "a", enabled: false });
139
139
  expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/enable" });
140
140
  expect(mock.calls[0].opts?.json).toEqual({ name: "a", enabled: false });
@@ -144,7 +144,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
144
144
  it("history() → GET _platform/market/history,返回 MarketHistory[]", async () => {
145
145
  const hist = [{ id: 1, solution: "a", version: "1.0", action: "install", status: "ok", operator: null, message: null, created_at: 1 }];
146
146
  mock = mockRequest(() => ({ data: hist }));
147
- useMock(mock);
147
+ bindMock(mock);
148
148
  const r = await httpMarketClient.history();
149
149
  expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/history" });
150
150
  expect(r).toEqual(hist);
@@ -152,7 +152,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
152
152
 
153
153
  it("uninstallImpact(name) → GET _platform/market/uninstall-impact? 带 name", async () => {
154
154
  mock = mockRequest(() => ({ data: { name: "a", affectedRoles: 2, affectedUsers: 5 } }));
155
- useMock(mock);
155
+ bindMock(mock);
156
156
  const r = await httpMarketClient.uninstallImpact("a");
157
157
  expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/uninstall-impact" });
158
158
  expect(mock.calls[0].opts?.searchParams).toEqual({ name: "a" });
@@ -161,7 +161,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
161
161
 
162
162
  it("uninstall(name) → POST _platform/market/uninstall,body {name}", async () => {
163
163
  mock = mockRequest(() => ({ data: { ok: true, message: "removed" } }));
164
- useMock(mock);
164
+ bindMock(mock);
165
165
  const r = await httpMarketClient.uninstall("a");
166
166
  expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/uninstall" });
167
167
  expect(mock.calls[0].opts?.json).toEqual({ name: "a" });
@@ -1,9 +1,10 @@
1
+ import type { ApiModuleRoot } from "../layout";
1
2
  import type { HandlerRow } from "./handler-scan";
2
3
  import type { IrEndpoint } from "./ir";
3
4
  import type { OpenApiDoc } from "./openapi-ir";
4
5
  import { existsSync, readdirSync, readFileSync } from "node:fs";
5
6
  import { dirname, join, relative, sep } from "node:path";
6
- import { resolveConfigPath } from "../layout";
7
+ import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, resolveConfigPath, tryResolveLayout, webSrcOf } from "../layout";
7
8
  import { readOjAnonymousPaths } from "../oj-config";
8
9
  import { emitClient } from "./emit-client";
9
10
  import { planStubWrites } from "./emit-stub";
@@ -47,9 +48,12 @@ export interface ApiExemption {
47
48
  }
48
49
 
49
50
  /**
50
- * 加载豁免清单:默认 `<cwd>/api/.ojm-api-exempt.json`(新名),缺失时回退旧名
51
- * `api/.ram-api-exempt.json`(R4:存量工程豁免不因改名失效);`opts.exempt` 为
52
- * 绝对/相对路径时覆盖默认。文件缺失或 JSON 损坏 → 空豁免 `{}`(绝不抛错)。
51
+ * 加载豁免清单:按布局找位置,`opts.exempt` 为绝对/相对路径时覆盖默认。
52
+ * 候选顺序(第一个存在的胜出):
53
+ * 1. `<cwd>/.ojm-api-exempt.json` —— module-root 布局(config.yaml 在工程根)
54
+ * 2. `<cwd>/api/.ojm-api-exempt.json` —— 历史布局(api/ 即工程根)
55
+ * 3. `<cwd>/api/.ram-api-exempt.json` —— R4:存量工程旧名不因改名失效
56
+ * 文件缺失或 JSON 损坏 → 空豁免 `{}`(绝不抛错)。
53
57
  * 反常识坑:清单是严格 JSON.parse——写 `//` 注释会解析失败、静默回退空豁免
54
58
  * (豁免全失效且无报错);说明文字请放 `_comment` 字段(本加载器只读
55
59
  * modules/paths,多余字段天然忽略)。
@@ -70,18 +74,29 @@ export function loadExemption(opts: { cwd: string, exempt?: string }): ApiExempt
70
74
  }
71
75
  }
72
76
 
73
- /** 豁免清单文件名(新名优先;旧名只读回退)。`init` 只产新名。 */
74
- const EXEMPT_FILE = "api/.ojm-api-exempt.json";
75
- const EXEMPT_FILE_LEGACY = "api/.ram-api-exempt.json";
77
+ /**
78
+ * 豁免清单候选路径(按优先级;`init` 只产第一条)。
79
+ *
80
+ * 位置由布局决定而不是常量钉死:module-root 布局 config.yaml 在工程根、随行的
81
+ * `.ojm-api-exempt.json` 也在根;历史布局两者都在 `api/` 下。写死 `api/` 会让新
82
+ * 布局工程的豁免**静默失效**——`loadExemption` 缺文件即回退空豁免,`ojm api --check`
83
+ * 会突然对 auth/web 报一堆「有 handler 无契约」。
84
+ */
85
+ const EXEMPT_FILES = [
86
+ ".ojm-api-exempt.json",
87
+ "api/.ojm-api-exempt.json",
88
+ "api/.ram-api-exempt.json", // R4:旧名只读回退
89
+ ] as const;
76
90
 
77
91
  function resolveExemptPath(cwd: string, exempt?: string): string {
78
92
  if (exempt)
79
93
  return exempt;
80
- const next = join(cwd, EXEMPT_FILE);
81
- if (existsSync(next))
82
- return next;
83
- const legacy = join(cwd, EXEMPT_FILE_LEGACY);
84
- return existsSync(legacy) ? legacy : next;
94
+ for (const rel of EXEMPT_FILES) {
95
+ const candidate = join(cwd, rel);
96
+ if (existsSync(candidate))
97
+ return candidate;
98
+ }
99
+ return join(cwd, EXEMPT_FILES[0]);
85
100
  }
86
101
 
87
102
  /** 路由身份串(如 "order/list"、"auth/login")的模块段 = 首段 */
@@ -130,15 +145,18 @@ function walkWsFiles(dir: string, out: string[] = []): string[] {
130
145
  * 只 warn 不 error:带 Bearer/cookie 的受保护 WS 是正当用法。
131
146
  * 读不到 config 直接不判——缺配置时做不出有依据的断言。
132
147
  */
133
- function checkWsAnonymous(wsFiles: string[], apiSrcDir: string, configPath: string): CheckViolation[] {
148
+ function checkWsAnonymous(roots: readonly ApiModuleRoot[], configPath: string): CheckViolation[] {
149
+ const wsFiles = roots.flatMap(root => walkWsFiles(root.dir).map(file => ({ file, root })));
134
150
  if (!existsSync(configPath) || wsFiles.length === 0)
135
151
  return [];
136
152
  const anonymous = readOjAnonymousPaths(configPath);
137
153
  if (anonymous.length === 0)
138
154
  return [];
139
155
  return wsFiles
140
- .map((file) => {
141
- const relDir = relative(apiSrcDir, dirname(file)).split(sep).join("/");
156
+ .map(({ file, root }) => {
157
+ const relDir = [root.name, relative(root.dir, dirname(file)).split(sep).filter(Boolean).join("/")]
158
+ .filter(Boolean)
159
+ .join("/");
142
160
  return { file, path: `/${relDir}/ws` };
143
161
  })
144
162
  .filter(({ path }) => !anonymous.includes(path))
@@ -146,7 +164,7 @@ function checkWsAnonymous(wsFiles: string[], apiSrcDir: string, configPath: stri
146
164
  level: "warn" as const,
147
165
  kind: "ws-anonymous-missing" as const,
148
166
  filePath: file,
149
- message: `[ojm-api] WS 路由 ${path} 不在 auth.anonymous_paths——oj 0.1.30 起 WS 握手过鉴权守卫,未带凭据的客户端会握手 401(表现为「文件在却连不上」)。若该 WS 本就要求鉴权(客户端带 Bearer/Cookie)可忽略本提示;否则请在 api/config.yaml 的 auth.anonymous_paths 补上 ${path}。`,
167
+ message: `[ojm-api] WS 路由 ${path} 不在 auth.anonymous_paths——oj 0.1.30 起 WS 握手过鉴权守卫,未带凭据的客户端会握手 401(表现为「文件在却连不上」)。若该 WS 本就要求鉴权(客户端带 Bearer/Cookie)可忽略本提示;否则请在 config.yaml 的 auth.anonymous_paths 补上 ${path}。`,
150
168
  }));
151
169
  }
152
170
 
@@ -279,18 +297,29 @@ async function checkFromOpenapi(openapiPath: string, cwd: string, _exemption: Ap
279
297
  const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as OpenApiDoc;
280
298
  const ir = irFromOpenapi(doc);
281
299
  const violations: CheckViolation[] = [];
300
+ const layout = tryResolveLayout(cwd);
301
+ // 契约工具的扫描单元是「模块 api 根」而不是整棵源码树:module-root 布局里
302
+ // `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/api.ts` 不能被当 handler。
303
+ const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
304
+ const clientDirOf = (module: string) =>
305
+ join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
282
306
  for (const [module, eps] of groupIrByModule(ir)) {
283
307
  const client = emitClient(eps, { target: "module", module });
284
- const clientDir = join(cwd, "web/src", module, "__client");
308
+ const clientDir = clientDirOf(module);
285
309
  for (const [file, content] of [["api.ts", client["api.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
286
310
  const p = join(clientDir, file);
287
311
  if (!existsSync(p) || readFileSync(p, "utf8") !== content)
288
312
  violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 生成物过期或缺失:${relative(cwd, p)}——请重跑 ojm api 使其与 openapi.json 同步。`, filePath: p });
289
313
  }
290
314
  }
291
- const apiSrcDir = join(cwd, "api/src");
315
+ const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
292
316
  if (existsSync(apiSrcDir)) {
293
- const stubWrites = await planStubWrites(ir, { apiSrcDir, implemented: implementedRouteKeys(apiSrcDir) });
317
+ const stubWrites = await planStubWrites(ir, {
318
+ apiSrcDir,
319
+ // stub 落模块的 api 侧:module-root 布局是 `<src>/<m>/api`,不是 `<src>/<m>`
320
+ apiRootOf: layout ? (m: string) => apiSrcOf(layout, m) : undefined,
321
+ implemented: implementedRouteKeys(roots),
322
+ });
294
323
  for (const w of stubWrites) {
295
324
  if (w.action === "create")
296
325
  violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] stub 缺失:${relative(cwd, w.filePath)}——请重跑 ojm api 生成。`, filePath: w.filePath });
@@ -318,19 +347,18 @@ export async function checkApi(opts: { cwd: string, exempt?: string }): Promise<
318
347
  throw new Error(`[ojm-api] ${opts.cwd} 下未发现 openapi.json——请先运行 oj v0.1.73 的 \`oj openapi -c config -d api/src -b /api -o openapi.json\` 生成(OpenAPI 3.1,含全部模块)。\`ojm api --check\` 的单一事实源即 openapi.json,不再消费旧 contract.ts。`);
319
348
  }
320
349
 
321
- // ② route 双向对账(仅 uni-dev——oj 树存在才有意义)
322
- if (existsSync(join(opts.cwd, "api/src"))) {
323
- const apiSrcDir = join(opts.cwd, "api/src");
324
- const handlers = walkApiFiles(apiSrcDir).flatMap(f => scanHandlerFile(f, apiSrcDir));
350
+ // ② route 双向对账(仅后端树存在才有意义)
351
+ const layout = tryResolveLayout(opts.cwd);
352
+ const apiSrcDir = layout ? apiModulesSrc(layout) : join(opts.cwd, "api/src");
353
+ const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(opts.cwd);
354
+ if (existsSync(apiSrcDir)) {
355
+ const handlers = roots.flatMap(root =>
356
+ walkApiFiles(root.dir).flatMap(f => scanHandlerFile(f, root.dir, root.name)));
325
357
  violations.push(...reconcileRoutes(uniDevIr, handlers, exemption));
326
358
  }
327
359
 
328
360
  // ④ WS 路由鉴权对账(oj 0.1.30 起握手过鉴权守卫)
329
- violations.push(...checkWsAnonymous(
330
- walkWsFiles(join(opts.cwd, "api/src")),
331
- join(opts.cwd, "api/src"),
332
- resolveConfigPath(opts.cwd),
333
- ));
361
+ violations.push(...checkWsAnonymous(roots, resolveConfigPath(opts.cwd)));
334
362
 
335
363
  // ③ routes.js diff
336
364
  const distDir = join(opts.cwd, "api/dist");
@@ -2,7 +2,7 @@ import type { OjSchemaDiag } from "./emit-oj-schema";
2
2
  import type { IrEndpoint } from "./ir";
3
3
  import { createHash } from "node:crypto";
4
4
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import { dirname } from "node:path";
5
+ import { dirname, join } from "node:path";
6
6
  import { emitOjSchemaLine } from "./emit-oj-schema";
7
7
  import { splitRoute, wirePrefixOf } from "./ir";
8
8
  import { jsonSchemaToSample } from "./json-schema-sample";
@@ -32,8 +32,14 @@ export interface StubWrite {
32
32
  }
33
33
 
34
34
  export interface PlanStubOptions {
35
- /** oj api 源码根(如 "api/src") */
35
+ /** oj api 源码根(如 "api/src");仅用于未提供 `apiRootOf` 时的路径拼接 */
36
36
  apiSrcDir: string
37
+ /**
38
+ * 模块名 → 该模块 api 侧目录。module-root 布局传 `(m) => apiSrcOf(layout, m)`
39
+ * (`<src>/<m>/api`)——stub 必须落进模块的 api 侧,串到 `<src>/<m>/` 会被 oj 当成
40
+ * 模块根下的普通文件;历史布局缺省即 `<apiSrcDir>/<m>`。
41
+ */
42
+ apiRootOf?: (module: string) => string
37
43
  /** 读已有文件(不存在返回 undefined);缺省真实 fs */
38
44
  readFile?: (path: string) => string | undefined
39
45
  /** 格式化注入缝(测试/特殊工程);缺省恒等——模板本身天然 lint-clean(F6) */
@@ -119,7 +125,9 @@ export async function planStubWrites(ir: IrEndpoint[], opts: PlanStubOptions): P
119
125
 
120
126
  // 按(模块段 + 目录镜像)分组 → 一个 api.ts
121
127
  // 已在别处(模块根嵌套组 / 其他文件)实现的端点不进 stub 计划。
122
- const groups = new Map<string, IrEndpoint[]>();
128
+ // 键保留模块与目录两段:落盘路径要经 `apiRootOf` 定位模块 api 根(module-root 布局
129
+ // 多一层 `api`),不能只拼成一条相对串。
130
+ const groups = new Map<string, { module: string, dir: string, endpoints: IrEndpoint[] }>();
123
131
  for (const ep of ir) {
124
132
  if (opts.implemented) {
125
133
  const ojFullAbs = (ep.apiPrefix + ep.route).replace(/\/{2,}/g, "/");
@@ -127,16 +135,20 @@ export async function planStubWrites(ir: IrEndpoint[], opts: PlanStubOptions): P
127
135
  if ([ojFullAbs, contractAbs].some(a => opts.implemented!.has(`${ep.method} ${a}`)))
128
136
  continue;
129
137
  }
138
+ const module = ep.apiPrefix.replace(/^\//, "");
130
139
  const { dir } = splitRoute(ep.route);
131
- const rel = [ep.apiPrefix.replace(/^\//, ""), dir, "api.ts"].filter(Boolean).join("/");
132
- const list = groups.get(rel) ?? [];
133
- list.push(ep);
134
- groups.set(rel, list);
140
+ const key = `${module}\u0000${dir}`;
141
+ const group = groups.get(key);
142
+ if (group)
143
+ group.endpoints.push(ep);
144
+ else
145
+ groups.set(key, { module, dir, endpoints: [ep] });
135
146
  }
136
147
 
137
148
  const writes: StubWrite[] = [];
138
- for (const [rel, endpoints] of [...groups.entries()].sort()) {
139
- const filePath = `${opts.apiSrcDir}/${rel}`;
149
+ for (const [, { module, dir, endpoints }] of [...groups.entries()].sort()) {
150
+ const apiRoot = opts.apiRootOf ? opts.apiRootOf(module) : join(opts.apiSrcDir, module);
151
+ const filePath = join(apiRoot, dir, "api.ts");
140
152
  // 模板天然 lint-clean(F6)→ 直接哈希发射内容;注入缝仅供测试/特殊工程对齐
141
153
  const body = await fix(emitStubBody(endpoints, opts.diag), filePath);
142
154
  const fingerprint = `// ojm-api:stub ${endpoints.map(e => e.name).sort().join(",")} sha256:${hashContent(body)}\n`;
@@ -1,3 +1,4 @@
1
+ import type { ApiModuleRoot } from "../layout";
1
2
  import type { IrEndpoint } from "./ir";
2
3
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
4
  import { dirname, join, relative, sep } from "node:path";
@@ -205,13 +206,19 @@ export function handlerAbs(h: HandlerRow): string {
205
206
  return h.tail?.startsWith("/") ? h.tail : `/${h.full}`;
206
207
  }
207
208
 
208
- export function scanHandlerFile(filePath: string, apiSrcDir: string): HandlerRow[] {
209
+ /**
210
+ * `apiDir` 是**模块的 api 侧目录**(module-root → `src/<name>/api`,历史 → `api/src/<name>`),
211
+ * `moduleName` 补进路由镜像的首段。两个参数而不是「扫描根 + 相对路径」,是因为 module-root
212
+ * 布局在扫描根与 handler 目录之间多一层 `api`,而它不属于路由(见 layout.apiModuleRoots)。
213
+ */
214
+ export function scanHandlerFile(filePath: string, apiDir: string, moduleName = ""): HandlerRow[] {
209
215
  const text = readFileSync(filePath, "utf8");
210
216
  const sf = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
211
217
  const routes = collectRouteDecls(sf);
212
218
  const decls = collectObjectDecls(sf);
213
219
  const callDecls = collectCallDecls(sf);
214
- const relDir = relative(apiSrcDir, dirname(filePath)).split(sep).join("/");
220
+ const inner = relative(apiDir, dirname(filePath)).split(sep).filter(Boolean).join("/");
221
+ const relDir = [moduleName, inner].filter(Boolean).join("/");
215
222
  const rows: HandlerRow[] = [];
216
223
  const seen = new Set<ts.Node>();
217
224
 
@@ -312,15 +319,17 @@ export function walkApiFiles(dir: string, out: string[] = []): string[] {
312
319
  * emit-stub 据此跳过「已在别处(模块根嵌套组 / 其他文件)实现」的端点——否则 DDD
313
320
  * reshape 后镜像 stub 与真实 handler 双写同路由,oj 路由表出现重复注册。
314
321
  */
315
- export function implementedRouteKeys(apiSrcDir: string): Set<string> {
322
+ export function implementedRouteKeys(roots: readonly ApiModuleRoot[]): Set<string> {
316
323
  const keys = new Set<string>();
317
- for (const f of walkApiFiles(apiSrcDir)) {
318
- const text = readFileSync(f, "utf8");
319
- // stub 镜像文件不算实现——它们覆盖的路由需照常报「stub 缺失/待更新」
320
- if (/^\/\/ (?:ojm|ram)-api:stub /.test(text))
321
- continue;
322
- for (const row of scanHandlerFile(f, apiSrcDir))
323
- keys.add(pathKey(row.method, handlerAbs(row)));
324
+ for (const root of roots) {
325
+ for (const f of walkApiFiles(root.dir)) {
326
+ const text = readFileSync(f, "utf8");
327
+ // stub 镜像文件不算实现——它们覆盖的路由需照常报「stub 缺失/待更新」
328
+ if (/^\/\/ (?:ojm|ram)-api:stub /.test(text))
329
+ continue;
330
+ for (const row of scanHandlerFile(f, root.dir, root.name))
331
+ keys.add(pathKey(row.method, handlerAbs(row)));
332
+ }
324
333
  }
325
334
  return keys;
326
335
  }
@@ -8,6 +8,7 @@ import { dirname, join } from "node:path";
8
8
  import process from "node:process";
9
9
  import { promisify } from "node:util";
10
10
  import { stringify as stringifyYaml } from "yaml";
11
+ import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, tryResolveLayout, webSrcOf } from "../layout";
11
12
  import { emitClient } from "./emit-client";
12
13
  import { applyStubWrites, planStubWrites } from "./emit-stub";
13
14
  import { implementedRouteKeys } from "./handler-scan";
@@ -81,19 +82,27 @@ async function runFromOpenapi(openapiPath: string, cwd: string, result: RunResul
81
82
  const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as OpenApiDoc;
82
83
  const ir = irFromOpenapi(doc, { rawPaths });
83
84
  const byModule = groupIrByModule(ir);
85
+ const layout = tryResolveLayout(cwd);
84
86
  result.contracts += byModule.size;
85
87
  for (const [module, eps] of byModule) {
86
88
  const client = emitClient(eps, { target: "module", module });
87
- const clientDir = join(cwd, "web/src", module, "__client");
89
+ const clientDir = join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
88
90
  writeIfChanged(join(clientDir, "api.ts"), client["api.ts"], result);
89
91
  writeIfChanged(join(clientDir, "api.schemas.ts"), client["api.schemas.ts"], result);
90
92
  }
91
93
  // stub:oj 目录镜像树(人碰过的文件永不写——plan 阶段已按指纹判好)。
92
94
  // 已实现键全量扫一遍传入:嵌套组(模块根单文件)实现的端点不再生成镜像 stub。
93
- const apiSrcDir = join(cwd, "api/src");
95
+ const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
96
+ const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
94
97
  if (existsSync(apiSrcDir)) {
95
98
  const diag: OjSchemaDiag = { warnings: [] };
96
- const writes = await planStubWrites(ir, { apiSrcDir, diag, implemented: implementedRouteKeys(apiSrcDir) });
99
+ const writes = await planStubWrites(ir, {
100
+ apiSrcDir,
101
+ // stub 落模块的 api 侧:module-root 布局是 `<src>/<m>/api`,不是 `<src>/<m>`
102
+ apiRootOf: layout ? (m: string) => apiSrcOf(layout, m) : undefined,
103
+ diag,
104
+ implemented: implementedRouteKeys(roots),
105
+ });
97
106
  const counts = applyStubWrites(writes);
98
107
  result.stubs.created += counts.created;
99
108
  result.stubs.updated += counts.updated;
@@ -126,7 +135,8 @@ export async function runApiDocs(cwd: string, opts: { redocBin?: string, fetchJs
126
135
  }
127
136
  const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as { paths?: Record<string, Record<string, unknown>> };
128
137
  const paths = doc.paths ?? {};
129
- const isUniDev = existsSync(join(cwd, "api/src"));
138
+ const layout = tryResolveLayout(cwd);
139
+ const isUniDev = existsSync(layout ? apiModulesSrc(layout) : join(cwd, "api/src"));
130
140
 
131
141
  const outDir = isUniDev ? join(cwd, "api/docs") : join(cwd, "docs/api");
132
142
  mkdirSync(outDir, { recursive: true });
package/src/dev.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - 布局探测 → layout.ts
11
11
  * - 工程 mock 约定 → dev-mock.ts
12
12
  *
13
- * 两种形态(按 api/config.yaml 是否存在自动判定):
13
+ * 两种形态(按 config.yaml 是否存在自动判定):
14
14
  * 全栈 /api/* 反代 oj(oj 自带热更,ojm 不重启它),模块源码变更 → 重建 → SSE 刷新
15
15
  * 纯前端 /api/* 走工程 mock(mock/*.mock.mjs 约定,未命中 404),行为与历史版本一致
16
16
  *
@@ -28,6 +28,7 @@ import http from "node:http";
28
28
  import { join, resolve } from "node:path";
29
29
  import process from "node:process";
30
30
  import readline from "node:readline";
31
+ import { syncAliasPaths } from "./alias-paths";
31
32
  import { buildModules } from "./build";
32
33
  import { loadContractMocks, resolveMock } from "./contract/mock";
33
34
  import { loadProjectMocks, mockStatusCode } from "./dev-mock";
@@ -35,6 +36,7 @@ import { createReloadHub, proxyApi, proxyWsUpgrade, sseScript } from "./dev-prox
35
36
  import { loadHtmlTransform } from "./html-transform";
36
37
  import { resolveConfigPath, resolveLayout } from "./layout";
37
38
  import { startOj } from "./oj";
39
+ import { requireModuleRootCapability } from "./oj-caps";
38
40
  import { ensureDevCert } from "./oj-cert";
39
41
  import { readOjApiPrefix } from "./oj-config";
40
42
  import { createStaticHandler, decodeReqPath, listenOnFreePort } from "./static-handler";
@@ -48,8 +50,8 @@ export interface DevOptions {
48
50
  shellDist?: string
49
51
  /** 注入重建函数(测试);默认 buildModules(不合并全站,产物只写模块) */
50
52
  buildModulesFn?: (projectRoot: string) => Promise<unknown>
51
- /** 注入桩 oj(测试);默认 startOj;纯前端工程不调用 */
52
- ojStarter?: (configPath: string, base: string, apiSrc: string) => OjProcess
53
+ /** 注入桩 oj(测试);默认 startOj;纯前端工程不调用。第三参为目录(→ --api-path)或已拼好的旗标数组(→ --module-root) */
54
+ ojStarter?: (configPath: string, base: string, apiSource: string | string[]) => OjProcess
53
55
  /** 强制纯前端形态(不起 oj,/api 走 mock) */
54
56
  frontendOnly?: boolean
55
57
  /** 启用 Vite 风格快捷键(默认仅 TTY 启用;测试可强制开) */
@@ -70,6 +72,10 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
70
72
  console.log("[ojm] 构建本地模块…");
71
73
  await build(projectRoot);
72
74
 
75
+ // 别名投影跟着模块清单走:dev 是「新增模块 → 立刻写 #/<新模块>/…」的主循环,
76
+ // 不刷新 paths 的话编辑器要把这条 import 报成 any/TS2307,直到下次 build。内容未变不写盘。
77
+ syncAliasPaths(projectRoot);
78
+
73
79
  // 工程 mock(可选约定 mock/*.mock.mjs):纯前端形态的 /api 边界
74
80
  const mocks: MockRoute[] = await loadProjectMocks(projectRoot);
75
81
  // 契约 mock(AC-D14):手写精确匹配优先,契约 pattern 兜底示例值;装载失败不崩 dev
@@ -84,17 +90,26 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
84
90
  // htmlTransform(web.config.ts,可选)先于起进程校验:配置坏了不该留下 oj 孤儿
85
91
  const transform = await loadHtmlTransform(projectRoot);
86
92
 
87
- // 2) oj 后端(工程有 api/config.yaml 时全栈形态;桩可注入)
93
+ // 2) oj 后端(工程有 config.yaml 时全栈形态;桩可注入)
88
94
  const configPath = resolveConfigPath(projectRoot);
89
95
  let oj: OjProcess | undefined;
90
96
  let ojTarget: string | undefined;
91
97
  // 反代前缀跟随 config 的 mounts:(api 挂载 prefix;F2:改前缀后硬编码 /api 会静默失效)
92
98
  let apiBase = "/api";
93
99
  if (!opts.frontendOnly && existsSync(configPath)) {
100
+ // dev 读源码树(保存即热更,不经产物):module-root 布局给 `--module-root <root>/src`
101
+ // (oj 递归发现 manifest.yaml),历史布局给 `--api-path <root>/api/src`——两者互斥。
102
+ const moduleRoot = layout.kind === "module-root";
103
+ if (moduleRoot)
104
+ requireModuleRootCapability(projectRoot);
105
+ const apiSource: string | string[] = moduleRoot && layout.moduleRoot
106
+ ? ["--module-root", layout.moduleRoot]
107
+ : resolve(projectRoot, "api/src");
94
108
  apiBase = readOjApiPrefix(configPath);
95
109
  await ensureDevCert(configPath);
96
- const starter = opts.ojStarter ?? ((cfg: string, b: string, apiSrc: string) => startOj(cfg, b, apiSrc));
97
- oj = starter(configPath, apiBase, resolve(projectRoot, "api/src"));
110
+ const fallbackStarter = (cfg: string, b: string, apiSrc: string | string[]) => startOj(cfg, b, apiSrc);
111
+ const starter: NonNullable<DevOptions["ojStarter"]> = opts.ojStarter ?? fallbackStarter;
112
+ oj = starter(configPath, apiBase, apiSource);
98
113
  try {
99
114
  await oj.ready; // 失败人话报错(stderr 尾部),fail-fast
100
115
  }
@@ -210,7 +225,7 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
210
225
  if (ojTarget)
211
226
  console.log(`[ojm] oj 后端已就绪:${apiBase} → ${ojTarget}(api.ts 保存即热更)`);
212
227
  else
213
- console.log("[ojm] 纯前端形态(无 api/config.yaml):/api 由 mock/ 提供");
228
+ console.log("[ojm] 纯前端形态(无 config.yaml):/api 由 mock/ 提供");
214
229
 
215
230
  console.log("[ojm] 宿主来自 @oj-module/cli 内置 shell-dist(importmap 单例),模块来自本地 dist/");
216
231
 
@@ -279,6 +294,8 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
279
294
  // 仅 TTY 默认启用(管道/CI 下不抢 stdin);测试可经 keymap + inputStream 强制。
280
295
  const keymapEnabled = opts.keymap ?? (process.stdin.isTTY ?? false);
281
296
  if (keymapEnabled) {
297
+ const footerText = "快捷键:r 热更新 · h 帮助 · c 清屏 · o 打开浏览器 · q 退出";
298
+ const footer = pinFooter(`[ojm] ${footerText}`);
282
299
  setupDevKeymap({
283
300
  server,
284
301
  oj,
@@ -286,12 +303,15 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
286
303
  port: actualPort,
287
304
  input: opts.inputStream,
288
305
  openBrowser: opts.openBrowser,
306
+ clearScreen: footer ? () => footer.clear() : undefined,
289
307
  onReload: () => {
290
308
  void trigger();
291
309
  },
292
310
  onQuit: shutdown,
293
311
  });
294
- console.log("[ojm] 快捷键:r 热更新 · h 帮助 · c 清屏 · o 打开浏览器 · q 退出");
312
+ // 非 TTY(pinFooter 拒绝)回退一次性打印,行为与历史一致
313
+ if (!footer)
314
+ console.log(`[ojm] ${footerText}`);
295
315
  }
296
316
 
297
317
  return server;
@@ -320,6 +340,52 @@ export async function shutdownDev(
320
340
  setTimeout(exit, 1500, 0).unref();
321
341
  }
322
342
 
343
+ /**
344
+ * 快捷键提示常驻底栏:DECSTBM 把滚动区设为 1..rows-1,末行留给提示。
345
+ * 之后本进程 console.log 与 oj 透传(均经 process.stdout.write)只在滚动区内滚动,
346
+ * 末行由终端保持,无需逐条重绘。非 TTY / 行数不足返回 undefined(调用方回退一次性打印)。
347
+ */
348
+ export interface PinnedFooter {
349
+ /** 清屏:ED(\x1b[2J)保留滚动区(RIS \x1bc 会复位滚动区,禁用),随后重绘提示 */
350
+ clear: () => void
351
+ /** 恢复全屏滚动区并落光标到底行(进程退出/shell 接管前调用);幂等 */
352
+ stop: () => void
353
+ }
354
+
355
+ export function pinFooter(text: string, stream: NodeJS.WriteStream = process.stdout): PinnedFooter | undefined {
356
+ if (!stream.isTTY || (stream.rows ?? 0) < 3)
357
+ return undefined;
358
+ const write = (s: string) => {
359
+ stream.write(s);
360
+ };
361
+ const render = () => {
362
+ const rows = stream.rows || 24;
363
+ write(`\x1B[1;${rows - 1}r`); // 滚动区:首行 ~ 倒数第二行(DECSTBM 会把光标送回 1;1)
364
+ write(`\x1B[${rows};1H\x1B[2K${text}`); // 末行清行后画常驻提示
365
+ write(`\x1B[${rows - 1};1H`); // 光标回滚动区底行,后续输出照常滚动
366
+ };
367
+ render();
368
+ const onResize = () => render();
369
+ let stopped = false;
370
+ const stop = () => {
371
+ if (stopped)
372
+ return;
373
+ stopped = true;
374
+ process.removeListener("SIGWINCH", onResize);
375
+ process.removeListener("exit", stop);
376
+ write("\x1B[r\x1B[999;1H\n"); // 恢复全屏滚动区,光标夹到底行避免 shell 提示符悬空
377
+ };
378
+ process.on("SIGWINCH", onResize);
379
+ process.on("exit", stop);
380
+ return {
381
+ clear: () => {
382
+ write("\x1B[2J");
383
+ render();
384
+ },
385
+ stop,
386
+ };
387
+ }
388
+
323
389
  /**
324
390
  * Vite 风格 dev 快捷键(默认仅在 TTY 启用,见 devServer 内的 keymapEnabled 判定)。
325
391
  * r 强制重建并热更新(复用 trigger)
@@ -341,6 +407,8 @@ export interface DevKeymapDeps {
341
407
  */
342
408
  input?: NodeJS.ReadableStream & { setRawMode?: (mode: boolean) => unknown }
343
409
  openBrowser?: (url: string) => void
410
+ /** 清屏实现(默认 \x1Bc);devServer 注入底栏版以保住滚动区 */
411
+ clearScreen?: () => void
344
412
  onReload: () => void
345
413
  onQuit: () => void
346
414
  log?: (msg: string) => void
@@ -389,7 +457,10 @@ export function setupDevKeymap(deps: DevKeymapDeps): void {
389
457
  help();
390
458
  }
391
459
  else if (k === "c") {
392
- process.stdout.write("\x1Bc");
460
+ if (deps.clearScreen)
461
+ deps.clearScreen();
462
+ else
463
+ process.stdout.write("\x1Bc");
393
464
  }
394
465
  else if (k === "o") {
395
466
  open(url);