@ultimat3/cli 20.2.1 → 22.0.0

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 (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
@@ -0,0 +1,21 @@
1
+ // `x doctor`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-doctor.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
7
+ export const DEFAULT_DOCTOR_PORT = 3000;
8
+
9
+ export const doctorSpec: CommandSpec = {
10
+ name: 'doctor',
11
+ summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
12
+ usage: 'x doctor [--port 3000] [--json]',
13
+ flags: [
14
+ {
15
+ name: 'port',
16
+ type: 'string',
17
+ summary: 'port to test',
18
+ default: String(DEFAULT_DOCTOR_PORT),
19
+ },
20
+ ],
21
+ };
package/src/cmd-doctor.ts CHANGED
@@ -19,9 +19,9 @@ import {
19
19
  } from '@ultimat3/db';
20
20
  import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
21
21
  import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
22
+ import { DEFAULT_DOCTOR_PORT, doctorSpec } from './cmd-doctor-spec';
22
23
  import type { CliCommand, CommandContext } from './command';
23
24
  import { checkMigrationSnapshots } from './db-snapshot';
24
- import { syncPortFor } from './dev-sync';
25
25
  import type { OfflineFallbackFact } from './doctor-offline';
26
26
  import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
27
27
  import { intFlagOr, PORT_RANGE, portPairAfter } from './flag-number';
@@ -31,6 +31,7 @@ import type { CommandResult, Finding } from './output';
31
31
  import { findingFrom } from './output';
32
32
  import type { ParsedArgs } from './parse';
33
33
  import { portFree } from './port-probe';
34
+ import { syncPortFor } from './role-sync';
34
35
  import { checkMigrationDrift } from './schema-drift';
35
36
 
36
37
  /**
@@ -129,9 +130,6 @@ export const embeddedDatabaseFinding = (fact: EmbeddedDatabase): Finding | undef
129
130
  /** The file `x doctor` reports missing, and the one the reader creates. */
130
131
  export const ENV_DEVELOPMENT = '.env.development';
131
132
 
132
- /** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
133
- const DEFAULT_DOCTOR_PORT = 3000;
134
-
135
133
  /**
136
134
  * Both ports `x dev` binds, each labelled with the role that wants it. `x dev --port 3999` printed
137
135
  * `web listening on 3999`, then died on 4000 as `X_CLI_UNEXPECTED` with a caught `Error` rendered
@@ -144,7 +142,7 @@ const DEFAULT_DOCTOR_PORT = 3000;
144
142
  * N+1 is still not a runnable command. It did not move both until 2026-09 — the line was
145
143
  * `neighbouringPort(probe.port)`, which for the sync finding IS the port the finding is about, and
146
144
  * a docblock claiming otherwise is how it survived. `portPairAfter` is the one reader of that rule
147
- * and `dev-sync.ts`'s own refusal shares it.
145
+ * and `role-sync.ts`'s own refusal shares it.
148
146
  *
149
147
  * The sync port is `syncPortFor`, never `neighbouringPort` again: that helper answers 65534 for a
150
148
  * web port of 65535 — BELOW the web port, and a port `x dev` never binds — where the boot refuses
@@ -400,19 +398,7 @@ export function probeFor(cwd: string, bunVersion: string, port: number): DoctorP
400
398
  }
401
399
 
402
400
  export const doctorCommand: CliCommand = {
403
- spec: {
404
- name: 'doctor',
405
- summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
406
- usage: 'x doctor [--port 3000] [--json]',
407
- flags: [
408
- {
409
- name: 'port',
410
- type: 'string',
411
- summary: 'port to test',
412
- default: String(DEFAULT_DOCTOR_PORT),
413
- },
414
- ],
415
- },
401
+ spec: doctorSpec,
416
402
  async run(ctx: CommandContext): Promise<CommandResult> {
417
403
  const port = doctorPort(ctx.args);
418
404
  const findings = await runDoctor(probeFor(ctx.cwd, ctx.bunVersion, port));
@@ -0,0 +1,18 @@
1
+ // `x env`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-env.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import { ENV_SCHEMA_EXPORT } from './app-env';
5
+ import { APP_CONFIG_FILE } from './app-root';
6
+ import type { CommandSpec } from './parse';
7
+
8
+ export const envSpec: CommandSpec = {
9
+ name: 'env',
10
+ summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
11
+ usage: 'x env [check|example] [--json]',
12
+ requiresApp: true,
13
+ subcommands: ['check', 'example'],
14
+ // The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
15
+ // framework already tells its reader to run.
16
+ defaultSubcommand: 'check',
17
+ flags: [],
18
+ };
package/src/cmd-env.ts CHANGED
@@ -5,8 +5,9 @@
5
5
  // Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
6
6
  import { join } from 'node:path';
7
7
  import { checkEnv, ENV_EXAMPLE_PATH, ERROR_DOCS_URL, maskedEnvValues } from '@ultimat3/core';
8
- import { ENV_SCHEMA_EXPORT, envExampleFor, loadEnvSchema } from './app-env';
9
- import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
8
+ import { envExampleFor, loadEnvSchema } from './app-env';
9
+ import { requireAppRoot } from './app-root';
10
+ import { envSpec } from './cmd-env-spec';
10
11
  import type { CliCommand, CommandContext } from './command';
11
12
  import { EnvSchemaMissingError } from './errors';
12
13
  import { msg } from './messages';
@@ -73,17 +74,7 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
73
74
  }
74
75
 
75
76
  export const envCommand: CliCommand = {
76
- spec: {
77
- name: 'env',
78
- summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
79
- usage: 'x env [check|example] [--json]',
80
- requiresApp: true,
81
- subcommands: ['check', 'example'],
82
- // The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
83
- // framework already tells its reader to run.
84
- defaultSubcommand: 'check',
85
- flags: [],
86
- },
77
+ spec: envSpec,
87
78
  async run(ctx: CommandContext): Promise<CommandResult> {
88
79
  // `subcommand`, never `positionals[0]`: the parser has already lifted a declared subcommand
89
80
  // out of the positionals, so reading the array here matches nothing and every invocation
@@ -0,0 +1,23 @@
1
+ // `x errors`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-errors.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const ERRORS_SUBCOMMANDS = ['explain', 'list'] as const;
7
+
8
+ export const errorsSpec: CommandSpec = {
9
+ name: 'errors',
10
+ summary: 'an X_* code, explained: cause, runnable fix, docs URL',
11
+ usage: 'x errors [explain <CODE>|list] [--json]',
12
+ subcommands: ERRORS_SUBCOMMANDS,
13
+ // `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
14
+ // which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
15
+ // to a caller who meant to explain one — see `MissingPositionalError`'s own note.
16
+ defaultSubcommand: 'explain',
17
+ // `x errors X_PERMISSION_UNKNOWN` is the form every reader tries first — `x help` prints
18
+ // `errors an X_* code, explained`, which reads as exactly that — and it answered
19
+ // `X_CLI_UNKNOWN_COMMAND … fix: x help`, which leads back to the line that suggested it.
20
+ // Safe to declare here and nowhere else so far: the only thing that is not `explain` or
21
+ // `list` in this slot is a code, and a near miss of either is still refused (#F16).
22
+ defaultSubcommandTakesPositional: true,
23
+ };
package/src/cmd-errors.ts CHANGED
@@ -5,6 +5,7 @@ import { nearestName, singleLine } from '@ultimat3/core';
5
5
  // a plausible-sounding explanation an agent would then act on.
6
6
 
7
7
  import type { ErrorExplanation } from '@ultimat3/mcp';
8
+ import { errorsSpec } from './cmd-errors-spec';
8
9
  import type { CliCommand, CommandContext } from './command';
9
10
  import type { ErrorCatalog } from './error-catalog';
10
11
  import { loadErrorCatalog } from './error-catalog';
@@ -14,7 +15,7 @@ import { explainErrorCode, explainEveryErrorCode } from './mcp-errors';
14
15
  import { msg } from './messages';
15
16
  import type { CommandResult, JsonValue } from './output';
16
17
 
17
- export const ERRORS_SUBCOMMANDS = ['explain', 'list'] as const;
18
+ export { ERRORS_SUBCOMMANDS } from './cmd-errors-spec';
18
19
 
19
20
  /**
20
21
  * `site` is the throw site as DATA, and it is why the `fix:` for a code whose fix is built at run
@@ -85,22 +86,7 @@ function listAll(catalog: ErrorCatalog): CommandResult {
85
86
  }
86
87
 
87
88
  export const errorsCommand: CliCommand = {
88
- spec: {
89
- name: 'errors',
90
- summary: 'an X_* code, explained: cause, runnable fix, docs URL',
91
- usage: 'x errors [explain <CODE>|list] [--json]',
92
- subcommands: ERRORS_SUBCOMMANDS,
93
- // `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
94
- // which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
95
- // to a caller who meant to explain one — see `MissingPositionalError`'s own note.
96
- defaultSubcommand: 'explain',
97
- // `x errors X_PERMISSION_UNKNOWN` is the form every reader tries first — `x help` prints
98
- // `errors an X_* code, explained`, which reads as exactly that — and it answered
99
- // `X_CLI_UNKNOWN_COMMAND … fix: x help`, which leads back to the line that suggested it.
100
- // Safe to declare here and nowhere else so far: the only thing that is not `explain` or
101
- // `list` in this slot is a code, and a near miss of either is still refused (#F16).
102
- defaultSubcommandTakesPositional: true,
103
- },
89
+ spec: errorsSpec,
104
90
  // `async` is load-bearing: a synchronous throw would escape every caller that awaits the
105
91
  // promise this signature promises, including the dispatcher's own error path.
106
92
  async run(ctx: CommandContext): Promise<CommandResult> {
@@ -0,0 +1,18 @@
1
+ // `x fix`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-fix.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const FIX_SUBCOMMANDS = ['boundary'] as const;
7
+
8
+ export const fixSpec: CommandSpec = {
9
+ name: 'fix',
10
+ // Says "plan" in the one line `x help` prints. The name is kept — five packages' `fix:` lines
11
+ // cite `x fix boundary <file>` and renaming a shipped command breaks every one of them — so
12
+ // the honest move is to stop the summary from promising a repair the command never performs.
13
+ summary: 'plan the minimal cut for an import that crossed a surface boundary (never rewrites)',
14
+ usage: 'x fix boundary <file> [--json]',
15
+ requiresApp: true,
16
+ subcommands: FIX_SUBCOMMANDS,
17
+ defaultSubcommand: 'boundary',
18
+ };
package/src/cmd-fix.ts CHANGED
@@ -8,16 +8,17 @@ import { appImportGraph, readAppSources } from './app-boundaries';
8
8
  import { requireAppRoot } from './app-root';
9
9
  import type { BoundaryCut } from './boundary-cuts';
10
10
  import { planBoundaryCuts } from './boundary-cuts';
11
+ import { fixSpec } from './cmd-fix-spec';
11
12
  import type { CliCommand, CommandContext } from './command';
12
13
  import { BadFlagError, FixTargetUnknownError, MissingPositionalError } from './errors';
13
14
  import { msg } from './messages';
14
15
  import type { CommandResult, Finding, JsonValue } from './output';
15
16
 
17
+ export { FIX_SUBCOMMANDS } from './cmd-fix-spec';
18
+
16
19
  export type { BoundaryCut };
17
20
  export { planBoundaryCuts };
18
21
 
19
- export const FIX_SUBCOMMANDS = ['boundary'] as const;
20
-
21
22
  /**
22
23
  * Accept either an app-root-relative path or a suffix that matches exactly one scanned file —
23
24
  * an agent copying the path out of a `fix:` line has the short form
@@ -88,17 +89,7 @@ const editCount = (cuts: readonly BoundaryCut[]): number =>
88
89
  new Set(cuts.map((cut) => JSON.stringify([cut.edge.from, cut.edge.to]))).size;
89
90
 
90
91
  export const fixCommand: CliCommand = {
91
- spec: {
92
- name: 'fix',
93
- // Says "plan" in the one line `x help` prints. The name is kept — five packages' `fix:` lines
94
- // cite `x fix boundary <file>` and renaming a shipped command breaks every one of them — so
95
- // the honest move is to stop the summary from promising a repair the command never performs.
96
- summary: 'plan the minimal cut for an import that crossed a surface boundary (never rewrites)',
97
- usage: 'x fix boundary <file> [--json]',
98
- requiresApp: true,
99
- subcommands: FIX_SUBCOMMANDS,
100
- defaultSubcommand: 'boundary',
101
- },
92
+ spec: fixSpec,
102
93
  async run(ctx: CommandContext): Promise<CommandResult> {
103
94
  const root = requireAppRoot('fix', ctx.cwd).dir;
104
95
  // Refused before the scan, never defaulted to `''`: an empty string reached `resolveTarget` as
@@ -0,0 +1,31 @@
1
+ // `x generate`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-generate.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import { GENERATORS } from './generate-kinds';
5
+ import type { CommandSpec } from './parse';
6
+
7
+ export const generateSpec: CommandSpec = {
8
+ name: 'g',
9
+ aliases: ['generate'],
10
+ summary: 'scaffold a primitive with its passing test',
11
+ // Projected from `GENERATORS`, never restated: the literal that used to live here had already
12
+ // drifted — it omitted `backfill` — and a usage line that can disagree with the list it
13
+ // describes is exactly the second source of truth axiom 2 forbids.
14
+ usage: `x g ${GENERATORS.join('|')} <name> [--feature f]`,
15
+ // Declared from the SAME constant `readKind` validates against: without it `fix-command.ts`
16
+ // has no set to judge the word after `x g`, and two shipped `@ultimat3/admin` fix lines said
17
+ // `x g migration` — a generator that has never existed — straight through the `errors` gate.
18
+ positionalChoices: GENERATORS,
19
+ requiresApp: true,
20
+ flags: [
21
+ { name: 'feature', type: 'string', summary: 'feature slice to write into' },
22
+ { name: 'surface', type: 'string', summary: 'site | app', default: 'app' },
23
+ { name: 'live', type: 'boolean', summary: 'subscribable query' },
24
+ { name: 'admin', type: 'boolean', summary: 'resource: also emit the admin override' },
25
+ { name: 'locales', type: 'string', summary: 'comma-separated locales, default en' },
26
+ { name: 'at', type: 'string', summary: 'island, admin:page: directory to write into' },
27
+ { name: 'permission', type: 'string', summary: 'admin:page: the permission it needs' },
28
+ { name: 'force', type: 'boolean', summary: 'overwrite existing files' },
29
+ { name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
30
+ ],
31
+ };
@@ -4,19 +4,27 @@
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
6
  import { MANIFEST_FILENAME } from '@ultimat3/manifest';
7
- import { appManifest, writeAppManifest } from './app-manifest';
7
+ import { registerGeneratedPrimitives } from './api-registration';
8
+ import { writeAppArtifacts } from './app-artifacts';
9
+ import { appManifest } from './app-manifest';
8
10
  import { requireAppRoot } from './app-root';
11
+ import { generateSpec } from './cmd-generate-spec';
9
12
  import type { CliCommand, CommandContext } from './command';
13
+ import { invocationOf } from './command';
14
+ import { assertFeatureExists } from './generate-feature';
10
15
  import { generate, sliceDir } from './generate-files';
16
+ import { grantGeneratedPermissions } from './generate-grants';
11
17
  import type { Generator } from './generate-kinds';
12
- import { GENERATORS, readKind, readName, readPermission, readSurface } from './generate-kinds';
18
+ import { readFeature, readKind, readName, readPermission, readSurface } from './generate-kinds';
13
19
  import { containedPath, writeFiles } from './generate-write';
14
20
  import { resolveCatalogModule } from './i18n-audit';
15
21
  import { syncI18nIndex } from './i18n-index';
22
+ import { reproducedFlags } from './invocation-flags';
16
23
  import { msg } from './messages';
17
24
  import type { CommandResult, Finding } from './output';
18
25
  import { flagBool, flagList, flagString } from './parse';
19
- import { resolveLocales } from './templates';
26
+ import { quoteArg } from './shell-quote';
27
+ import { kebab, resolveLocales } from './templates';
20
28
 
21
29
  // One import path for the generator, unchanged by the split: `index.ts`, `x new` and the scaffold
22
30
  // fixture reach the kinds, the pure file list and the writer through this module, and a second path
@@ -29,36 +37,12 @@ export type { WriteReport } from './generate-write';
29
37
  export { dedupe, writeFiles } from './generate-write';
30
38
 
31
39
  export const generateCommand: CliCommand = {
32
- spec: {
33
- name: 'g',
34
- aliases: ['generate'],
35
- summary: 'scaffold a primitive with its passing test',
36
- // Projected from `GENERATORS`, never restated: the literal that used to live here had already
37
- // drifted — it omitted `backfill` — and a usage line that can disagree with the list it
38
- // describes is exactly the second source of truth axiom 2 forbids.
39
- usage: `x g ${GENERATORS.join('|')} <name> [--feature f]`,
40
- // Declared from the SAME constant `readKind` validates against: without it `fix-command.ts`
41
- // has no set to judge the word after `x g`, and two shipped `@ultimat3/admin` fix lines said
42
- // `x g migration` — a generator that has never existed — straight through the `errors` gate.
43
- positionalChoices: GENERATORS,
44
- requiresApp: true,
45
- flags: [
46
- { name: 'feature', type: 'string', summary: 'feature slice to write into' },
47
- { name: 'surface', type: 'string', summary: 'site | app', default: 'app' },
48
- { name: 'live', type: 'boolean', summary: 'subscribable query' },
49
- { name: 'admin', type: 'boolean', summary: 'resource: also emit the admin override' },
50
- { name: 'locales', type: 'string', summary: 'comma-separated locales, default en' },
51
- { name: 'at', type: 'string', summary: 'island, admin:page: directory to write into' },
52
- { name: 'permission', type: 'string', summary: 'admin:page: the permission it needs' },
53
- { name: 'force', type: 'boolean', summary: 'overwrite existing files' },
54
- { name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
55
- ],
56
- },
40
+ spec: generateSpec,
57
41
  async run(ctx: CommandContext): Promise<CommandResult> {
58
42
  const root = requireAppRoot('g', ctx.cwd).dir;
59
43
  const kind = readKind(ctx.args.positionals[0]);
60
44
  const name = readName(ctx.args.positionals[1], kind);
61
- const featureFlag = flagString(ctx.args, 'feature');
45
+ const featureFlag = readFeature(flagString(ctx.args, 'feature'), kind);
62
46
  // Both flags are resolved before a single file is planned: a bad surface or a locale that is
63
47
  // really a path fails here, with nothing written and nothing to undo.
64
48
  const surface = readSurface(flagString(ctx.args, 'surface'), kind, name);
@@ -72,7 +56,9 @@ export const generateCommand: CliCommand = {
72
56
  // imports `useT()` from is a fact about THIS app, and `generate` is a pure function.
73
57
  const catalogModule = await resolveCatalogModule(root);
74
58
  // Read for the same reason: which errors the slice declares is written on THIS app's disk.
75
- const slice = sliceDir(surface, featureFlag ?? name);
59
+ const slice = sliceDir(surface, kebab(featureFlag ?? name));
60
+ // A named slice that is not there is refused, never invented (X_FEATURE_UNKNOWN).
61
+ assertFeatureExists(root, kind, featureFlag, slice);
76
62
  const sliceErrors = await readSliceErrors(root, kind, slice);
77
63
  // Same reason again: whether `job`/`task` may assume the tenant-scoped shape is a fact about
78
64
  // THIS feature's own `entity.ts`/`repo.ts`, not a default the template gets to assume.
@@ -102,16 +88,27 @@ export const generateCommand: CliCommand = {
102
88
  lines: files.map((file) => msg('cli.file.added', { path: file.path })),
103
89
  };
104
90
  }
105
- const report = await writeFiles(
106
- root,
107
- files,
108
- flagBool(ctx.args, 'force'),
109
- `x g ${kind} ${name}`,
110
- );
91
+ // The caller's own invocation, EVERY flag it set included: without `--feature` the fix wrote
92
+ // a second slice beside the one that conflicted.
93
+ const invocation = [
94
+ invocationOf(ctx, 'g'),
95
+ kind,
96
+ quoteArg(name),
97
+ ...reproducedFlags(generateCommand.spec, ctx.args),
98
+ ].join(' ');
99
+ const report = await writeFiles(root, files, flagBool(ctx.args, 'force'), invocation);
100
+ // The two edits a generated primitive needs outside its own slice, performed rather than left
101
+ // as findings: a declared permission granted to a role, and a job listed in `defineApi`.
102
+ // Before the manifest load below, so the projection sees both.
103
+ const edited = [
104
+ ...(await grantGeneratedPermissions(root, report.written)),
105
+ ...(await registerGeneratedPrimitives(root, report.written)),
106
+ ];
111
107
  // A locale's catalog existing on disk and the app being able to select it are two different
112
108
  // facts — see `syncI18nIndex`. Runs before the manifest load below so a route or resource
113
109
  // this same invocation just wrote never gets projected against a stale catalog registration.
114
- if (report.written.length > 0) await syncI18nIndex(root);
110
+ const indexSync =
111
+ report.written.length > 0 ? await syncI18nIndex(root) : { registered: true, findings: [] };
115
112
  // Facts, not prose: every `x g` run leaves the route/action/entity/job/policy table current,
116
113
  // the same guarantee `x manifest` makes on its own — an agent reading it after `x g` never
117
114
  // sees a resource that exists on disk but not in the manifest.
@@ -124,18 +121,23 @@ export const generateCommand: CliCommand = {
124
121
  // partial load would replace the compatibility contract with a subset of the app. The scaffold
125
122
  // stays on disk — only the projection is withheld, and the load failures travel as findings.
126
123
  const loadFailures: Finding[] = [];
124
+ // `openapi.json` rides with it (`writeAppArtifacts`): refreshing one contract and not the other
125
+ // made this command's own output fail the `contract-diff` step.
126
+ const artifacts: string[] = [];
127
127
  if (report.written.length > 0 && existsSync(containedPath(root, MANIFEST_FILENAME))) {
128
128
  const { manifest, findings } = await appManifest(root);
129
129
  if (findings.length === 0) {
130
- await writeAppManifest(root, manifest);
130
+ artifacts.push(
131
+ ...(await writeAppArtifacts(root, manifest, { openapi: true, onlyExisting: true })),
132
+ );
131
133
  buildId = manifest.buildId;
132
134
  } else loadFailures.push(...findings);
133
135
  }
134
- const findings = [...report.conflicts, ...loadFailures];
136
+ const findings = [...report.conflicts, ...indexSync.findings, ...loadFailures];
135
137
  // One list behind all three renderings. The manifest was printed as a `+` line while the count
136
138
  // beside it came from `report.written` alone, so `x g island` said "wrote 2 file(s)" over three
137
139
  // lines — and `--json` carried the shorter list, which is the drift `--json` exists to prevent.
138
- const written = [...report.written, ...(buildId === undefined ? [] : [MANIFEST_FILENAME])];
140
+ const written = [...report.written, ...edited, ...artifacts];
139
141
  return {
140
142
  ok: findings.length === 0,
141
143
  command: 'g',
@@ -166,7 +168,7 @@ async function readSliceErrors(
166
168
  }
167
169
 
168
170
  /**
169
- * `job` and `task` only: the slice's `entity.ts`/`repo.ts` as they stand on disk, absent when the
171
+ * `job`, `task`, `action` and `mutator`: the slice's `entity.ts`/`repo.ts` as they stand on disk, absent when the
170
172
  * generator's kind is neither or the file does not exist yet. `readSliceErrors`'s reason —
171
173
  * whichever generator reads it decides on THIS app's disk, not on a default the template assumes.
172
174
  */
@@ -176,7 +178,9 @@ async function readSliceFile(
176
178
  slice: string,
177
179
  name: 'entity.ts' | 'repo.ts',
178
180
  ): Promise<string | undefined> {
179
- if (kind !== 'job' && kind !== 'task') return undefined;
181
+ if (kind !== 'job' && kind !== 'task' && kind !== 'action' && kind !== 'mutator') {
182
+ return undefined;
183
+ }
180
184
  const file = containedPath(root, `${slice}/${name}`);
181
185
  return existsSync(file) ? await Bun.file(file).text() : undefined;
182
186
  }
@@ -0,0 +1,16 @@
1
+ // `x i18n`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-i18n.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const I18N_SUBCOMMANDS = ['check', 'add', 'sync'] as const;
7
+
8
+ export const i18nSpec: CommandSpec = {
9
+ name: 'i18n',
10
+ summary: 'catalogs: add a locale, sync keys, check for gaps',
11
+ usage: 'x i18n [check|add <locale>|sync <locale>] [--json]',
12
+ requiresApp: true,
13
+ subcommands: I18N_SUBCOMMANDS,
14
+ // The bare `x i18n` audits; `add` and `sync` write catalogs and must be asked for.
15
+ defaultSubcommand: 'check',
16
+ };
package/src/cmd-i18n.ts CHANGED
@@ -13,6 +13,7 @@ import type { Catalog } from '@ultimat3/i18n';
13
13
  import { auditCatalogs, catalogKeys } from '@ultimat3/i18n';
14
14
  import { loadApp } from './app-load';
15
15
  import { requireAppRoot } from './app-root';
16
+ import { i18nSpec } from './cmd-i18n-spec';
16
17
  import type { CliCommand, CommandContext } from './command';
17
18
  import { BadFlagError, CatalogExistsError, MissingPositionalError } from './errors';
18
19
  import {
@@ -36,7 +37,7 @@ import type { CommandResult, Finding, JsonValue } from './output';
36
37
  import { renderTable } from './table';
37
38
  import { catalogPath, resolveLocales } from './templates/locales';
38
39
 
39
- export const I18N_SUBCOMMANDS = ['check', 'add', 'sync'] as const;
40
+ export { I18N_SUBCOMMANDS } from './cmd-i18n-spec';
40
41
 
41
42
  /** `ExtractReport` is plain JSON by construction — same idiom as `cmd-registries.ts`'s `asJson`. */
42
43
  const asJson = (value: object): Record<string, JsonValue> => value as Record<string, JsonValue>;
@@ -192,7 +193,7 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
192
193
  // answered `X_CATALOG_UNREGISTERED` with a fix naming an edit that had already been made. Same
193
194
  // writer `x g --locales` already uses, so a catalog on disk and a selectable locale can never be
194
195
  // two different sets (#F4).
195
- const registered = await syncI18nIndex(root);
196
+ const { registered, findings: indexFindings } = await syncI18nIndex(root);
196
197
 
197
198
  const keys = catalogKeys(seeded).length;
198
199
  return {
@@ -202,7 +203,7 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
202
203
  ok: true,
203
204
  command: 'i18n',
204
205
  summary: msg('cli.i18n.added', { locale, keys, from: from ?? locale }),
205
- findings: app.findings,
206
+ findings: [...indexFindings, ...app.findings],
206
207
  // `registered` is false only for an app with no `packages/i18n` at all — a fact a caller has
207
208
  // to be able to read, because it is the one case where the locale is on disk and unselectable.
208
209
  data: { locale, from: from ?? locale, keys, path, registered },
@@ -270,7 +271,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
270
271
  // hand-created file, a `git merge` — is exactly the unregistered locale the gate refuses, and
271
272
  // this is the command its `fix:` names. Re-deriving an index that is already correct writes the
272
273
  // same bytes.
273
- const registered = await syncI18nIndex(root);
274
+ const { registered, findings: indexFindings } = await syncI18nIndex(root);
274
275
 
275
276
  const total = catalogKeys(merged).length;
276
277
  return {
@@ -282,7 +283,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
282
283
  // The keys themselves, raw — `runCheck` lists gaps the same way, because a key is a value an
283
284
  // author copies and never prose the catalog owns.
284
285
  lines: seeded ? added.map((key) => ` ${key}`) : [],
285
- findings: app.findings,
286
+ findings: [...indexFindings, ...app.findings],
286
287
  data: {
287
288
  locale,
288
289
  from: from ?? locale,
@@ -298,15 +299,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
298
299
  }
299
300
 
300
301
  export const i18nCommand: CliCommand = {
301
- spec: {
302
- name: 'i18n',
303
- summary: 'catalogs: add a locale, sync keys, check for gaps',
304
- usage: 'x i18n [check|add <locale>|sync <locale>] [--json]',
305
- requiresApp: true,
306
- subcommands: I18N_SUBCOMMANDS,
307
- // The bare `x i18n` audits; `add` and `sync` write catalogs and must be asked for.
308
- defaultSubcommand: 'check',
309
- },
302
+ spec: i18nSpec,
310
303
  async run(ctx: CommandContext): Promise<CommandResult> {
311
304
  const root = requireAppRoot('i18n', ctx.cwd).dir;
312
305
  const sub = ctx.args.subcommand ?? 'check';
@@ -0,0 +1,56 @@
1
+ // `x jobs`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-jobs.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const JOBS_SUBCOMMANDS = ['ls', 'show', 'retry', 'cancel', 'drain'] as const;
7
+
8
+ /**
9
+ * The drivers a drain may move work ONTO — every one of them durable, and that is the whole rule.
10
+ * Closed, and read three ways: the flag summary, the refusal, and the `memory` case below.
11
+ */
12
+ export const DRAIN_TARGETS = ['redis', 'nats'] as const;
13
+
14
+ export const jobsSpec: CommandSpec = {
15
+ name: 'jobs',
16
+ summary: 'list, show, retry, cancel and drain the job queue',
17
+ usage:
18
+ 'x jobs [ls|show <id>|retry <id>|cancel <id>|drain --to <driver>] [--queue q] [--state s] [--limit n] [--from-step name] [--reason text] [--to driver] [--dry-run] [--json]',
19
+ requiresApp: true,
20
+ subcommands: JOBS_SUBCOMMANDS,
21
+ // The bare `x jobs` lists; it never retries, cancels or drains anything.
22
+ defaultSubcommand: 'ls',
23
+ flags: [
24
+ { name: 'queue', type: 'string', summary: 'filter by queue name' },
25
+ { name: 'state', type: 'string', summary: 'filter by job state' },
26
+ { name: 'limit', type: 'string', summary: 'max rows to return' },
27
+ { name: 'name', type: 'string', summary: 'filter by job name' },
28
+ // Each of these is read by ONE subcommand — `retryJob`, `cancelJob`, `runDrain` — and says
29
+ // so in its own summary. The scope is what makes the parser refuse it anywhere else instead
30
+ // of accepting it and ignoring it: `x db gen --dry-run` parsed and wrote the migration.
31
+ {
32
+ name: 'from-step',
33
+ type: 'string',
34
+ summary: 'retry: drop this step so it re-executes',
35
+ subcommands: ['retry'],
36
+ },
37
+ {
38
+ name: 'reason',
39
+ type: 'string',
40
+ summary: 'cancel: why, recorded on the job',
41
+ subcommands: ['cancel'],
42
+ },
43
+ {
44
+ name: 'to',
45
+ type: 'string',
46
+ summary: `drain: target driver — ${DRAIN_TARGETS.join(', ')}`,
47
+ subcommands: ['drain'],
48
+ },
49
+ {
50
+ name: 'dry-run',
51
+ type: 'boolean',
52
+ summary: 'drain: report the plan, move nothing',
53
+ subcommands: ['drain'],
54
+ },
55
+ ],
56
+ };