@ultimat3/cli 21.0.0 → 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 (234) hide show
  1. package/CLAUDE.md +84 -1650
  2. package/README.md +40 -2
  3. package/package.json +30 -30
  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-load.ts +37 -5
  8. package/src/app-openapi.ts +27 -0
  9. package/src/app-permissions.ts +0 -0
  10. package/src/app-root.ts +1 -1
  11. package/src/async-pages.ts +32 -0
  12. package/src/bin.ts +7 -1
  13. package/src/boundary-findings.ts +36 -0
  14. package/src/browser-launcher-fake-html.ts +80 -0
  15. package/src/browser-launcher-fake.ts +165 -0
  16. package/src/browser-launcher-port.ts +159 -0
  17. package/src/browser-launcher.ts +26 -133
  18. package/src/budgets.ts +11 -1
  19. package/src/cdp-shot-a11y.ts +77 -0
  20. package/src/cdp-shot-clock.ts +14 -0
  21. package/src/cdp-shot-driver.ts +150 -0
  22. package/src/cdp-shot-element.ts +147 -0
  23. package/src/cdp-shot-errors.ts +62 -0
  24. package/src/cdp-shot-keys.ts +152 -0
  25. package/src/cdp-shot-page.ts +230 -0
  26. package/src/cdp-shot-watch.ts +241 -0
  27. package/src/cmd-affected-spec.ts +24 -0
  28. package/src/cmd-affected.ts +3 -19
  29. package/src/cmd-build-spec.ts +16 -0
  30. package/src/cmd-build.ts +46 -20
  31. package/src/cmd-ci-spec.ts +24 -0
  32. package/src/cmd-ci.ts +17 -21
  33. package/src/cmd-db-branch.ts +2 -2
  34. package/src/cmd-db-spec.ts +99 -0
  35. package/src/cmd-db.ts +15 -94
  36. package/src/cmd-deploy-helm.ts +136 -0
  37. package/src/cmd-deploy-spec.ts +40 -0
  38. package/src/cmd-deploy.ts +111 -43
  39. package/src/cmd-dev-spec.ts +25 -0
  40. package/src/cmd-dev.ts +55 -36
  41. package/src/cmd-docs-spec.ts +16 -0
  42. package/src/cmd-docs.ts +2 -11
  43. package/src/cmd-doctor-spec.ts +21 -0
  44. package/src/cmd-doctor.ts +4 -18
  45. package/src/cmd-env-spec.ts +18 -0
  46. package/src/cmd-env.ts +4 -13
  47. package/src/cmd-errors-spec.ts +23 -0
  48. package/src/cmd-errors.ts +3 -17
  49. package/src/cmd-fix-spec.ts +18 -0
  50. package/src/cmd-fix.ts +4 -13
  51. package/src/cmd-generate-spec.ts +31 -0
  52. package/src/cmd-generate.ts +46 -42
  53. package/src/cmd-i18n-spec.ts +16 -0
  54. package/src/cmd-i18n.ts +7 -14
  55. package/src/cmd-jobs-spec.ts +56 -0
  56. package/src/cmd-jobs.ts +3 -50
  57. package/src/cmd-manifest-spec.ts +15 -0
  58. package/src/cmd-manifest.ts +19 -20
  59. package/src/cmd-mcp-spec.ts +21 -0
  60. package/src/cmd-mcp.ts +2 -15
  61. package/src/cmd-new-spec.ts +33 -0
  62. package/src/cmd-new.ts +38 -33
  63. package/src/cmd-policy-spec.ts +13 -0
  64. package/src/cmd-policy.ts +2 -9
  65. package/src/cmd-pr-spec.ts +39 -0
  66. package/src/cmd-pr.ts +5 -65
  67. package/src/cmd-registries-spec.ts +32 -0
  68. package/src/cmd-registries.ts +4 -25
  69. package/src/cmd-routes-spec.ts +12 -0
  70. package/src/cmd-routes.ts +2 -7
  71. package/src/cmd-secrets-spec.ts +19 -0
  72. package/src/cmd-secrets.ts +22 -34
  73. package/src/cmd-shot-island.ts +12 -13
  74. package/src/cmd-shot-spec.ts +53 -0
  75. package/src/cmd-shot.ts +11 -60
  76. package/src/cmd-tasks-spec.ts +21 -0
  77. package/src/cmd-tasks.ts +2 -16
  78. package/src/cmd-test-spec.ts +54 -0
  79. package/src/cmd-test.ts +4 -49
  80. package/src/cmd-verify-spec.ts +28 -0
  81. package/src/cmd-verify.ts +3 -23
  82. package/src/db-seed.ts +4 -2
  83. package/src/dev-dashboard.ts +4 -4
  84. package/src/dev-lock.ts +18 -2
  85. package/src/dev-port.ts +21 -0
  86. package/src/dev-route-table.ts +3 -3
  87. package/src/dispatch.ts +12 -5
  88. package/src/document-styles.ts +1 -1
  89. package/src/drift.ts +4 -18
  90. package/src/error-catalog.ts +16 -9
  91. package/src/error-codes.ts +28 -30
  92. package/src/error-pages.ts +19 -7
  93. package/src/errors.ts +14 -0
  94. package/src/favicon.ts +2 -2
  95. package/src/fix-imports.ts +1 -1
  96. package/src/fix-scan.ts +2 -9
  97. package/src/flag-reads.ts +1 -2
  98. package/src/foreign-text.ts +36 -0
  99. package/src/framework-schema.ts +5 -6
  100. package/src/generate-feature.ts +42 -0
  101. package/src/generate-files.ts +7 -3
  102. package/src/generate-grants.ts +83 -0
  103. package/src/generate-kinds.ts +56 -4
  104. package/src/i18n-index.ts +59 -10
  105. package/src/icon-assets.ts +1 -1
  106. package/src/image-prepare.ts +14 -0
  107. package/src/import-scan.ts +63 -0
  108. package/src/index.ts +67 -348
  109. package/src/invocation-flags.ts +26 -0
  110. package/src/island-bundle.ts +6 -1
  111. package/src/island-capture.ts +6 -6
  112. package/src/island-harness-script.ts +6 -2
  113. package/src/island-realtime.ts +15 -8
  114. package/src/island-shot.ts +5 -0
  115. package/src/island-store.ts +131 -0
  116. package/src/island-verdict.ts +1 -1
  117. package/src/job-registration.ts +42 -0
  118. package/src/jobs-driver.ts +2 -2
  119. package/src/load-findings.ts +51 -0
  120. package/src/mcp-db-target.ts +1 -1
  121. package/src/mcp-errors.ts +28 -27
  122. package/src/mcp-host.ts +27 -12
  123. package/src/mcp-ui-diff.ts +27 -0
  124. package/src/mcp-ui-inspect.ts +4 -4
  125. package/src/mcp-ui-interact.ts +21 -15
  126. package/src/mcp-ui.ts +19 -15
  127. package/src/measure-database.ts +73 -0
  128. package/src/measure-paths.ts +74 -0
  129. package/src/measure-scope.ts +74 -0
  130. package/src/messages.ts +1 -3
  131. package/src/metrics-endpoint.ts +1 -1
  132. package/src/otlp-export.ts +1 -1
  133. package/src/output.ts +6 -0
  134. package/src/permission-grants.ts +86 -0
  135. package/src/prerender-out.ts +25 -0
  136. package/src/prerender.ts +121 -102
  137. package/src/pwa-artifacts.ts +3 -3
  138. package/src/reexport-manifest.ts +2 -1
  139. package/src/registry.ts +80 -56
  140. package/src/role-realtime.ts +36 -0
  141. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  142. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  143. package/src/role-start-types.ts +112 -0
  144. package/src/{dev-roles.ts → role-start.ts} +42 -115
  145. package/src/{dev-sync.ts → role-sync.ts} +18 -8
  146. package/src/root-env.ts +67 -0
  147. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  148. package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
  149. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  150. package/src/runtime-jobs.ts +87 -0
  151. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
  152. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  153. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  154. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  155. package/src/runtime-realtime.ts +55 -0
  156. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  157. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  158. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  159. package/src/scaffold-fixture.ts +28 -6
  160. package/src/scaffold-typecheck.ts +6 -3
  161. package/src/schema-drift.ts +7 -1
  162. package/src/script-csp.ts +5 -2
  163. package/src/secrets-rotation.ts +59 -0
  164. package/src/serve-boot.ts +192 -0
  165. package/src/serve-drain.ts +24 -0
  166. package/src/serve-entry.ts +6 -0
  167. package/src/serve-env.ts +116 -0
  168. package/src/serve-types.ts +55 -0
  169. package/src/serve.ts +44 -347
  170. package/src/shot-server.ts +2 -2
  171. package/src/shot-settle.ts +10 -1
  172. package/src/shot-theme.ts +3 -3
  173. package/src/shot-verdict.ts +16 -7
  174. package/src/signal-shred.ts +27 -0
  175. package/src/solid-loader.ts +26 -2
  176. package/src/static-report.ts +8 -1
  177. package/src/templates/action.ts +30 -16
  178. package/src/templates/entity.ts +12 -7
  179. package/src/templates/index.ts +1 -1
  180. package/src/templates/job.ts +10 -7
  181. package/src/templates/policy.ts +20 -2
  182. package/src/templates/resource-create.ts +127 -0
  183. package/src/templates/resource-form-island.ts +49 -12
  184. package/src/templates/resource.ts +10 -4
  185. package/src/templates/scaffold-app.ts +4 -1
  186. package/src/templates/scaffold-auth.ts +3 -1
  187. package/src/templates/scaffold-container-compose.ts +184 -0
  188. package/src/templates/scaffold-container.ts +24 -159
  189. package/src/templates/scaffold-dashboard-example.ts +2 -2
  190. package/src/templates/scaffold-db-package.ts +16 -3
  191. package/src/templates/scaffold-demo-org.ts +41 -0
  192. package/src/templates/scaffold-entries.ts +1 -1
  193. package/src/templates/scaffold-helm-templates.ts +66 -7
  194. package/src/templates/scaffold-helm.ts +27 -5
  195. package/src/templates/scaffold-i18n.ts +15 -10
  196. package/src/templates/scaffold-repo.ts +13 -10
  197. package/src/templates/scaffold-roles.ts +38 -10
  198. package/src/templates/slice-foundation.ts +1 -1
  199. package/src/templates/wrap.ts +4 -1
  200. package/src/test-passes.ts +2 -1
  201. package/src/test-workers.ts +26 -0
  202. package/src/ts-scan.ts +3 -6
  203. package/src/tsconfig-references.ts +1 -2
  204. package/src/verify-checks.ts +41 -39
  205. package/src/verify-e2e.ts +10 -7
  206. package/src/verify-run.ts +11 -1
  207. package/src/verify-step.ts +3 -3
  208. package/src/verify-tests.ts +1 -26
  209. package/src/verify-typecheck.ts +28 -0
  210. package/src/web-binding.ts +2 -2
  211. package/src/workspace-graph.ts +10 -33
  212. package/src/cdp-browser.ts +0 -94
  213. package/src/cdp-connection.ts +0 -247
  214. package/src/cdp-e2e-page.ts +0 -180
  215. package/src/cdp-e2e-session.ts +0 -199
  216. package/src/cdp-errors.ts +0 -56
  217. package/src/cdp-launch.ts +0 -193
  218. package/src/cdp-offline-script.ts +0 -73
  219. package/src/cdp-pipe.ts +0 -77
  220. package/src/e2e-app.ts +0 -103
  221. package/src/e2e-browser-handle.ts +0 -55
  222. package/src/e2e-dom-fixture.ts +0 -117
  223. package/src/e2e-driver.ts +0 -117
  224. package/src/e2e-errors.ts +0 -117
  225. package/src/e2e-evaluate.ts +0 -156
  226. package/src/e2e-locator.ts +0 -86
  227. package/src/e2e-page.ts +0 -153
  228. package/src/e2e-preload.ts +0 -64
  229. package/src/e2e-probe.ts +0 -23
  230. package/src/e2e-selection.ts +0 -182
  231. package/src/e2e-spawn.ts +0 -169
  232. package/src/measurement-actor.ts +0 -26
  233. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
  234. /package/src/{dev-render.ts → runtime-render.ts} +0 -0
@@ -0,0 +1,24 @@
1
+ // `x ci`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-ci.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ /** Log lines kept per failed job. Enough to hold a findings block, short enough to read. */
7
+ export const TAIL_LINES = 40;
8
+
9
+ export const ciSpec: CommandSpec = {
10
+ name: 'ci',
11
+ summary: 'the workflow runs for this branch, and the findings inside the failed steps log',
12
+ usage: 'x ci [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--json]',
13
+ flags: [
14
+ { name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
15
+ { name: 'branch', type: 'string', summary: 'branch to read runs for; this one by default' },
16
+ { name: 'run', type: 'string', summary: 'one run id, instead of this branch latest' },
17
+ {
18
+ name: 'tail',
19
+ type: 'string',
20
+ summary: `log lines kept per failed job (default ${TAIL_LINES})`,
21
+ },
22
+ { name: 'full', type: 'boolean', summary: 'the whole failed-step log, not the tail' },
23
+ ],
24
+ };
package/src/cmd-ci.ts CHANGED
@@ -9,14 +9,18 @@ import type { CiLogLine } from './ci-log';
9
9
  import { findingsFrom, jobsInLog, parseLogLines, tailOf } from './ci-log';
10
10
  import type { CiJob, CiRun } from './ci-runs';
11
11
  import { failedLog, isFailed, isRunning, latestPerWorkflow, listRuns, viewRun } from './ci-runs';
12
+ import { ciSpec, TAIL_LINES } from './cmd-ci-spec';
12
13
  import type { CliCommand, CommandContext } from './command';
13
14
  import { parseIntFlag } from './flag-number';
15
+ import { commentBlock } from './foreign-text';
14
16
  import type { GhRepo } from './gh-target';
15
17
  import { currentBranch, resolveRepo } from './gh-target';
16
18
  import { msg } from './messages';
17
19
  import type { CommandResult, Finding, JsonValue } from './output';
18
20
  import { flagBool, flagString } from './parse';
19
21
 
22
+ export { TAIL_LINES } from './cmd-ci-spec';
23
+
20
24
  /**
21
25
  * Every catalog key this command renders, declared — `msg()` answers `⟦key⟧` for a key nobody
22
26
  * added, which is loud in a terminal and SILENT to a build. `cmd-ci.test.ts` holds this list
@@ -34,9 +38,6 @@ export const CI_MESSAGE_KEYS = [
34
38
  'cli.ci.logs.empty',
35
39
  ] as const;
36
40
 
37
- /** Log lines kept per failed job. Enough to hold a findings block, short enough to read. */
38
- export const TAIL_LINES = 40;
39
-
40
41
  /** How far back a branch's run history is read before "the latest run of each workflow". */
41
42
  export const RUN_LOOKBACK = 20;
42
43
 
@@ -55,22 +56,7 @@ const RUN_FLAG = { name: 'run', command: 'ci', min: 1, example: 'x ci --run 3248
55
56
  const TAIL_FLAG = { name: 'tail', command: 'ci', min: 1, example: 'x ci --tail 80 --json' };
56
57
 
57
58
  export const ciCommand: CliCommand = {
58
- spec: {
59
- name: 'ci',
60
- summary: 'the workflow runs for this branch, and the findings inside the failed steps log',
61
- usage: 'x ci [--branch <name>] [--run <id>] [--repo owner/name] [--tail <n>] [--full] [--json]',
62
- flags: [
63
- { name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
64
- { name: 'branch', type: 'string', summary: 'branch to read runs for; this one by default' },
65
- { name: 'run', type: 'string', summary: 'one run id, instead of this branch latest' },
66
- {
67
- name: 'tail',
68
- type: 'string',
69
- summary: `log lines kept per failed job (default ${TAIL_LINES})`,
70
- },
71
- { name: 'full', type: 'boolean', summary: 'the whole failed-step log, not the tail' },
72
- ],
73
- },
59
+ spec: ciSpec,
74
60
  async run(ctx: CommandContext): Promise<CommandResult> {
75
61
  const repo = await resolveRepo(ctx, 'ci', flagString(ctx.args, 'repo'));
76
62
  const rawRun = flagString(ctx.args, 'run');
@@ -142,7 +128,14 @@ async function inspect(
142
128
  const pool: readonly CiLogLine[] = own.length > 0 ? own : lines;
143
129
  const job = failed.find((candidate) => candidate.name === name);
144
130
  for (const finding of findingsFrom(pool)) {
145
- findings.push(finding.at === undefined ? { ...finding, at: name } : finding);
131
+ findings.push({
132
+ ...finding,
133
+ at: finding.at ?? name,
134
+ // Fenced and tagged: the log is foreign text, and a `fix:` reconstructed from it is the
135
+ // log author's instruction, not this CLI's. `commentBlock` is `x pr review`'s fence.
136
+ fix: commentBlock(`ci-log ${name}`, [finding.fix]).join('\n'),
137
+ source: 'ci-log',
138
+ });
146
139
  }
147
140
  failures.push({
148
141
  job: name,
@@ -229,7 +222,8 @@ function runLines(entry: RunReport): readonly string[] {
229
222
  continue;
230
223
  }
231
224
  out.push(msg('cli.ci.tail', { job: failure.job }));
232
- for (const line of failure.tail) out.push(` | ${line}`);
225
+ for (const line of commentBlock(`ci-log ${failure.job}`, failure.tail))
226
+ out.push(` | ${line}`);
233
227
  }
234
228
  const jobs = entry.jobs;
235
229
  if (jobs !== undefined && jobs.length > failures.length) {
@@ -266,6 +260,8 @@ function runJson(entry: RunReport): JsonValue {
266
260
  job: failure.job,
267
261
  url: failure.url,
268
262
  failedSteps: [...failure.failedSteps],
263
+ // Raw lines for a machine, labelled as foreign: the log's author wrote every one.
264
+ tailSource: 'ci-log',
269
265
  tail: [...failure.tail],
270
266
  })),
271
267
  }),
@@ -23,12 +23,12 @@ import {
23
23
  previewUrl,
24
24
  } from './db-branch';
25
25
  import { stepFinding } from './db-finding';
26
- import type { DevServices } from './dev-services';
27
- import { resolveServices } from './dev-services';
28
26
  import { MissingPositionalError, UnknownCommandError } from './errors';
29
27
  import { msg } from './messages';
30
28
  import type { CommandResult, Finding } from './output';
31
29
  import { flagString } from './parse';
30
+ import type { DevServices } from './runtime-bindings';
31
+ import { resolveServices } from './runtime-bindings';
32
32
  import { portFromEnv } from './serve';
33
33
  import { renderTable } from './table';
34
34
 
@@ -0,0 +1,99 @@
1
+ // `x db`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-db.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import { BRANCH_SUBCOMMANDS } from './db-branch';
5
+ import type { CommandSpec } from './parse';
6
+
7
+ export const DB_SUBCOMMANDS = [
8
+ 'gen',
9
+ 'migrate',
10
+ 'reset',
11
+ 'seed',
12
+ 'studio',
13
+ 'branch',
14
+ 'backfill',
15
+ ] as const;
16
+
17
+ export const dbSpec: CommandSpec = {
18
+ name: 'db',
19
+ summary: 'gen, migrate, reset, seed, studio, branch, backfill',
20
+ usage:
21
+ 'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch ls | branch create <name> | branch drop <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
22
+ requiresApp: true,
23
+ subcommands: DB_SUBCOMMANDS,
24
+ // Declared from the constant `runBranchCommand` validates against, never a second literal: it
25
+ // is what lets the `errors` step resolve `x db branch ls` — a fix line three shipped errors
26
+ // hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
27
+ subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
28
+ // Each flag whose summary begins `<subcommand>:` declares that scope, and the parser refuses
29
+ // it anywhere else: `x db gen --dry-run` used to parse, reach `runGen` and WRITE the
30
+ // migration. `cmd-db.test.ts` pins summary and scope to the same fact.
31
+ flags: [
32
+ {
33
+ name: 'name',
34
+ type: 'string',
35
+ summary: 'migration, branch or seed name, or backfill to filter',
36
+ },
37
+ {
38
+ name: 'tier',
39
+ type: 'string',
40
+ summary: 'seed: which tier to run — reference or dev; also ULTIMATE_SEED_TIER',
41
+ subcommands: ['seed'],
42
+ },
43
+ {
44
+ name: 'dry-run',
45
+ type: 'boolean',
46
+ summary: 'seed: report what each seed would write, and write nothing',
47
+ subcommands: ['seed'],
48
+ },
49
+ {
50
+ name: 'list',
51
+ type: 'boolean',
52
+ summary: 'backfill: print the x_backfills ledger',
53
+ subcommands: ['backfill'],
54
+ },
55
+ {
56
+ name: 'pending',
57
+ type: 'boolean',
58
+ summary: 'backfill: declared minus completed; non-zero exit when anything is unswept',
59
+ subcommands: ['backfill'],
60
+ },
61
+ {
62
+ name: 'all',
63
+ type: 'boolean',
64
+ summary: 'backfill: every pending sweep, isolated per name',
65
+ subcommands: ['backfill'],
66
+ },
67
+ {
68
+ name: 'write',
69
+ type: 'boolean',
70
+ summary: 'backfill: enqueue the pass; dry run without it',
71
+ subcommands: ['backfill'],
72
+ },
73
+ {
74
+ name: 'force',
75
+ type: 'boolean',
76
+ summary: 'backfill: sweep a name the ledger records as completed, as a NEW ledger row',
77
+ subcommands: ['backfill'],
78
+ },
79
+ {
80
+ name: 'status',
81
+ type: 'string',
82
+ summary: 'backfill: filter by running, completed or failed',
83
+ subcommands: ['backfill'],
84
+ },
85
+ {
86
+ name: 'limit',
87
+ type: 'string',
88
+ summary: 'backfill: max ledger rows to return',
89
+ subcommands: ['backfill'],
90
+ },
91
+ // Declared because `X_MIGRATION_IRREVERSIBLE`'s own fix line names it. A `fix:` is copied
92
+ // and run verbatim, so a flag the parser refuses would make the error unfollowable.
93
+ {
94
+ name: 'allow-destructive',
95
+ type: 'boolean',
96
+ summary: 'let x db gen emit a drop whose down cannot restore the rows',
97
+ },
98
+ ],
99
+ };
package/src/cmd-db.ts CHANGED
@@ -17,9 +17,9 @@ import { postgresDriver } from '@ultimat3/entity';
17
17
  import { requireAppRoot } from './app-root';
18
18
  import { runBackfillCommand } from './cmd-db-backfill';
19
19
  import { runBranchCommand } from './cmd-db-branch';
20
+ import { DB_SUBCOMMANDS, dbSpec } from './cmd-db-spec';
20
21
  import { plannedSubcommand } from './cmd-planned';
21
22
  import type { CliCommand, CommandContext } from './command';
22
- import { BRANCH_SUBCOMMANDS } from './db-branch';
23
23
  import { stepFinding } from './db-finding';
24
24
  import { generateAppMigration, unrenderedJson, unrenderedLines } from './db-generate';
25
25
  import type { SeedPassRow } from './db-seed';
@@ -32,109 +32,20 @@ import {
32
32
  seedTotals,
33
33
  selectSeeds,
34
34
  } from './db-seed';
35
- import { resolveServices } from './dev-services';
35
+ import { DevAlreadyRunningError, liveDevLock } from './dev-lock';
36
36
  import { CliNotImplementedError, MissingSubcommandError, UnknownCommandError } from './errors';
37
37
  import { withJobDriver } from './jobs-driver';
38
38
  import { msg } from './messages';
39
39
  import type { CommandResult, Finding } from './output';
40
40
  import { findingFrom } from './output';
41
41
  import { flagBool, flagString } from './parse';
42
+ import { resolveServices } from './runtime-bindings';
42
43
  import { runMigrations } from './serve';
43
44
 
44
- export const DB_SUBCOMMANDS = [
45
- 'gen',
46
- 'migrate',
47
- 'reset',
48
- 'seed',
49
- 'studio',
50
- 'branch',
51
- 'backfill',
52
- ] as const;
45
+ export { DB_SUBCOMMANDS } from './cmd-db-spec';
53
46
 
54
47
  export const dbCommand: CliCommand = {
55
- spec: {
56
- name: 'db',
57
- summary: 'gen, migrate, reset, seed, studio, branch, backfill',
58
- usage:
59
- 'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch ls | branch create <name> | branch drop <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
60
- requiresApp: true,
61
- subcommands: DB_SUBCOMMANDS,
62
- // Declared from the constant `runBranchCommand` validates against, never a second literal: it
63
- // is what lets the `errors` step resolve `x db branch ls` — a fix line three shipped errors
64
- // hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
65
- subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
66
- // Each flag whose summary begins `<subcommand>:` declares that scope, and the parser refuses
67
- // it anywhere else: `x db gen --dry-run` used to parse, reach `runGen` and WRITE the
68
- // migration. `cmd-db.test.ts` pins summary and scope to the same fact.
69
- flags: [
70
- {
71
- name: 'name',
72
- type: 'string',
73
- summary: 'migration, branch or seed name, or backfill to filter',
74
- },
75
- {
76
- name: 'tier',
77
- type: 'string',
78
- summary: 'seed: which tier to run — reference or dev; also ULTIMATE_SEED_TIER',
79
- subcommands: ['seed'],
80
- },
81
- {
82
- name: 'dry-run',
83
- type: 'boolean',
84
- summary: 'seed: report what each seed would write, and write nothing',
85
- subcommands: ['seed'],
86
- },
87
- {
88
- name: 'list',
89
- type: 'boolean',
90
- summary: 'backfill: print the x_backfills ledger',
91
- subcommands: ['backfill'],
92
- },
93
- {
94
- name: 'pending',
95
- type: 'boolean',
96
- summary: 'backfill: declared minus completed; non-zero exit when anything is unswept',
97
- subcommands: ['backfill'],
98
- },
99
- {
100
- name: 'all',
101
- type: 'boolean',
102
- summary: 'backfill: every pending sweep, isolated per name',
103
- subcommands: ['backfill'],
104
- },
105
- {
106
- name: 'write',
107
- type: 'boolean',
108
- summary: 'backfill: enqueue the pass; dry run without it',
109
- subcommands: ['backfill'],
110
- },
111
- {
112
- name: 'force',
113
- type: 'boolean',
114
- summary: 'backfill: sweep a name the ledger records as completed, as a NEW ledger row',
115
- subcommands: ['backfill'],
116
- },
117
- {
118
- name: 'status',
119
- type: 'string',
120
- summary: 'backfill: filter by running, completed or failed',
121
- subcommands: ['backfill'],
122
- },
123
- {
124
- name: 'limit',
125
- type: 'string',
126
- summary: 'backfill: max ledger rows to return',
127
- subcommands: ['backfill'],
128
- },
129
- // Declared because `X_MIGRATION_IRREVERSIBLE`'s own fix line names it. A `fix:` is copied
130
- // and run verbatim, so a flag the parser refuses would make the error unfollowable.
131
- {
132
- name: 'allow-destructive',
133
- type: 'boolean',
134
- summary: 'let x db gen emit a drop whose down cannot restore the rows',
135
- },
136
- ],
137
- },
48
+ spec: dbSpec,
138
49
  async run(ctx: CommandContext): Promise<CommandResult> {
139
50
  const root = requireAppRoot('db', ctx.cwd).dir;
140
51
  // No default, and no `?? 'migrate'` here either: `gen` writes a migration file and `reset`
@@ -299,6 +210,16 @@ async function runReset(ctx: CommandContext, root: string): Promise<CommandResul
299
210
  fix: 'drop and recreate the database yourself, then run: x db migrate',
300
211
  });
301
212
  }
213
+ // A running `x dev` has this directory open: deleting it under a live embedded Postgres is data
214
+ // loss mid-write, not a reset. The dev lock is the one fact that says someone holds it.
215
+ const holder = liveDevLock(services.stateDir);
216
+ if (holder !== undefined) {
217
+ throw new DevAlreadyRunningError({
218
+ lock: holder,
219
+ stateDir: services.stateDir,
220
+ embeddedDb: true,
221
+ });
222
+ }
302
223
  await rm(join(services.stateDir, 'pgdata'), { recursive: true, force: true });
303
224
  return runMigrate(ctx, root, msg('cli.db.reset.done'));
304
225
  }
@@ -0,0 +1,136 @@
1
+ // The helm half of `x deploy`: which release, in which namespace, waited on for how long, and what
2
+ // the rollout reported. Split from cmd-deploy.ts, which owns the plan and the compose method.
3
+ //
4
+ // `helm upgrade --install` with no `--wait` exits 0 the moment the API server ACCEPTS the objects,
5
+ // so a deploy whose pods never became ready reported success; and helm's default 5m timeout failed
6
+ // an upgrade whose migrate hook was still applying a long migration, leaving the Job running.
7
+
8
+ // why: Bun exposes no path-join primitive; the chart path is handed to helm as one joined string.
9
+ import { join } from 'node:path';
10
+ import { UltimateError } from '@ultimat3/core';
11
+ import { APP_CONFIG_EXPORT } from './app-auth';
12
+ import { APP_CONFIG_FILE } from './app-root';
13
+ import { BadFlagError } from './errors';
14
+
15
+ /** Long enough for a migration a pre-upgrade hook must finish; helm's own 5m default is not. */
16
+ export const HELM_DEFAULT_TIMEOUT = '15m';
17
+
18
+ /** What a helm deploy is aimed at, resolved once from the flags and `app.config.ts`. */
19
+ export interface HelmTarget {
20
+ readonly release: string;
21
+ readonly namespace: string | undefined;
22
+ readonly timeout: string;
23
+ }
24
+
25
+ /** Helm's own `time.ParseDuration` grammar, positive units only: `15m`, `900s`, `1h30m`. */
26
+ const DURATION = /^(?:\d+(?:h|m|s|ms))+$/;
27
+ /** A namespace and a release are both DNS-1123 labels; helm caps a release at 53 characters. */
28
+ const LABEL = /^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/;
29
+ const RELEASE_MAX = 53;
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
32
+ typeof value === 'object' && value !== null;
33
+
34
+ /** `--timeout`, screened: a value helm would reject is refused before anything is spawned. */
35
+ export function readHelmTimeout(raw: string | undefined): string {
36
+ if (raw === undefined) return HELM_DEFAULT_TIMEOUT;
37
+ if (DURATION.test(raw) && /[1-9]/.test(raw)) return raw;
38
+ throw new BadFlagError({
39
+ flag: 'timeout',
40
+ command: 'deploy',
41
+ reason: 'it is not a positive helm duration such as 15m, 900s or 1h30m',
42
+ fix: 'x deploy --method helm --timeout 15m --json',
43
+ });
44
+ }
45
+
46
+ /** `--namespace` and `--release`, each a DNS-1123 label or a refusal naming the flag. */
47
+ export function readLabel(flag: 'namespace' | 'release', raw: string): string {
48
+ const max = flag === 'release' ? RELEASE_MAX : 63;
49
+ if (raw.length <= max && LABEL.test(raw)) return raw;
50
+ throw new BadFlagError({
51
+ flag,
52
+ command: 'deploy',
53
+ reason: `it must be a lowercase DNS-1123 label of at most ${max} characters`,
54
+ fix:
55
+ flag === 'release'
56
+ ? 'x deploy --method helm --release my-app --json'
57
+ : 'x deploy --method helm --namespace my-namespace --json',
58
+ });
59
+ }
60
+
61
+ /**
62
+ * The release name: `--release`, else `app.config.ts`'s `name`. It was the literal `app` for every
63
+ * app, so two apps deployed to one namespace were one release and the second upgrade replaced the
64
+ * first. `--release` exists for a release that already carries another name — `app`, for every
65
+ * cluster an earlier version deployed to — because a release name is cluster state the framework
66
+ * does not own and cannot rename.
67
+ */
68
+ export async function readReleaseName(root: string, flag: string | undefined): Promise<string> {
69
+ if (flag !== undefined) return readLabel('release', flag);
70
+ const module = (await import(join(root, APP_CONFIG_FILE))) as Record<string, unknown>;
71
+ const config = module[APP_CONFIG_EXPORT];
72
+ const name = isRecord(config) ? config['name'] : undefined;
73
+ if (typeof name === 'string' && name.length <= RELEASE_MAX && LABEL.test(name)) return name;
74
+ throw new UltimateError({
75
+ code: 'X_CONFIG_INVALID',
76
+ cause:
77
+ typeof name === 'string'
78
+ ? `app.config.ts names the app "${name.slice(0, 80)}", which is not a helm release name (a DNS-1123 label of at most ${RELEASE_MAX} characters)`
79
+ : 'app.config.ts exports no config.name, and x deploy --method helm names the release after it',
80
+ fix: 'name the release explicitly: x deploy --method helm --release my-app --json',
81
+ });
82
+ }
83
+
84
+ /** The one `helm upgrade`, waited on: `--wait` for the rollout, `--output json` for the verdict. */
85
+ export function helmUpgradeArgs(
86
+ root: string,
87
+ target: HelmTarget,
88
+ imageOverrides: readonly string[],
89
+ ): readonly string[] {
90
+ return [
91
+ 'helm',
92
+ 'upgrade',
93
+ '--install',
94
+ target.release,
95
+ join(root, 'docker', 'helm'),
96
+ ...(target.namespace === undefined ? [] : ['--namespace', target.namespace]),
97
+ '--wait',
98
+ '--timeout',
99
+ target.timeout,
100
+ '--output',
101
+ 'json',
102
+ ...imageOverrides,
103
+ ];
104
+ }
105
+
106
+ /** What `--json` reports about the rollout helm waited on. */
107
+ export interface Rollout {
108
+ readonly release: string;
109
+ readonly namespace: string | null;
110
+ readonly revision: number | null;
111
+ readonly status: string;
112
+ }
113
+
114
+ /**
115
+ * helm's `--output json` release record, read structurally. `status` is helm's own word —
116
+ * `deployed`, `failed`, `pending-upgrade` — and `unknown` only when helm printed no record at all,
117
+ * never a guess dressed as one.
118
+ */
119
+ export function readRollout(target: HelmTarget, stdout: string): Rollout {
120
+ let parsed: unknown;
121
+ try {
122
+ parsed = JSON.parse(stdout);
123
+ } catch {
124
+ parsed = undefined;
125
+ }
126
+ const info = isRecord(parsed) ? parsed['info'] : undefined;
127
+ const status = isRecord(info) ? info['status'] : undefined;
128
+ const revision = isRecord(parsed) ? parsed['version'] : undefined;
129
+ const namespace = isRecord(parsed) ? parsed['namespace'] : undefined;
130
+ return {
131
+ release: target.release,
132
+ namespace: typeof namespace === 'string' ? namespace : (target.namespace ?? null),
133
+ revision: typeof revision === 'number' ? revision : null,
134
+ status: typeof status === 'string' ? status : 'unknown',
135
+ };
136
+ }
@@ -0,0 +1,40 @@
1
+ // `x deploy`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-deploy.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const deploySpec: CommandSpec = {
7
+ name: 'deploy',
8
+ summary: 'run the container deploy plan: migrate first, then the serving roles',
9
+ usage:
10
+ 'x deploy --image repo/app:tag [--method compose|helm] [--release name] [--namespace ns] [--timeout 15m] [--dry-run] [--json]',
11
+ requiresApp: true,
12
+ flags: [
13
+ { name: 'image', type: 'string', summary: 'image reference to deploy' },
14
+ { name: 'method', type: 'string', summary: 'compose | helm', default: 'compose' },
15
+ { name: 'dry-run', type: 'boolean', summary: 'print the plan, run nothing' },
16
+ {
17
+ name: 'release',
18
+ type: 'string',
19
+ summary: 'helm release name (default: app.config.ts name)',
20
+ },
21
+ { name: 'namespace', type: 'string', summary: 'helm namespace (default: the kube context)' },
22
+ {
23
+ name: 'timeout',
24
+ type: 'string',
25
+ // No `default:` — the parser would then fill it on every compose deploy too, and the refusal
26
+ // of a helm-only flag there could not tell an operator's value from its own.
27
+ summary: 'how long helm waits for the migrate hook and the rollout (default 15m)',
28
+ },
29
+ // `--critical` was here and is gone. It parsed, it was echoed into the plan JSON as
30
+ // `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
31
+ // nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
32
+ // `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
33
+ // into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
34
+ // `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
35
+ // as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
36
+ // force-navigates a client. A deploy also has no channel to one — the plan is
37
+ // `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
38
+ // and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
39
+ ],
40
+ };