@ultimat3/cli 1.2.0 → 3.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 (141) hide show
  1. package/CLAUDE.md +761 -0
  2. package/README.md +42 -9
  3. package/package.json +25 -23
  4. package/src/api-routes.ts +16 -0
  5. package/src/app-auth.ts +32 -0
  6. package/src/app-entities.ts +18 -0
  7. package/src/app-env.ts +103 -0
  8. package/src/app-load.ts +20 -3
  9. package/src/bin.ts +4 -3
  10. package/src/budgets.ts +134 -9
  11. package/src/cmd-build.ts +69 -21
  12. package/src/cmd-db-branch.ts +219 -0
  13. package/src/cmd-db.ts +458 -153
  14. package/src/cmd-deploy.ts +59 -6
  15. package/src/cmd-dev.ts +92 -18
  16. package/src/cmd-docs.ts +167 -0
  17. package/src/cmd-doctor.ts +74 -10
  18. package/src/cmd-env.ts +95 -0
  19. package/src/cmd-errors.ts +33 -13
  20. package/src/cmd-fix.ts +5 -1
  21. package/src/cmd-generate.ts +146 -111
  22. package/src/cmd-help.ts +16 -5
  23. package/src/cmd-i18n.ts +2 -0
  24. package/src/cmd-jobs.ts +47 -33
  25. package/src/cmd-mcp.ts +11 -2
  26. package/src/cmd-new.ts +14 -8
  27. package/src/cmd-planned.ts +55 -10
  28. package/src/cmd-policy.ts +1 -0
  29. package/src/cmd-registries.ts +3 -0
  30. package/src/cmd-secrets.ts +368 -0
  31. package/src/cmd-tasks.ts +1 -0
  32. package/src/cmd-test.ts +29 -24
  33. package/src/cmd-verify.ts +197 -25
  34. package/src/db-backfill.ts +401 -0
  35. package/src/db-branch.ts +269 -0
  36. package/src/db-destructive.ts +29 -0
  37. package/src/db-finding.ts +28 -0
  38. package/src/db-generate.ts +144 -0
  39. package/src/db-seed.ts +294 -0
  40. package/src/db-snapshot.ts +24 -0
  41. package/src/dev-assets.ts +108 -23
  42. package/src/dev-cache.ts +122 -0
  43. package/src/dev-dashboard.ts +19 -4
  44. package/src/dev-hooks.ts +27 -2
  45. package/src/dev-n-plus-one.ts +191 -0
  46. package/src/dev-queue.ts +105 -19
  47. package/src/dev-render.ts +158 -26
  48. package/src/dev-roles-fixture.ts +67 -0
  49. package/src/dev-roles.ts +167 -78
  50. package/src/dev-runtime.ts +117 -40
  51. package/src/dev-services.ts +15 -0
  52. package/src/dev-storage.ts +247 -0
  53. package/src/dev-sync.ts +107 -0
  54. package/src/dev-traces.ts +37 -7
  55. package/src/dispatch.ts +4 -2
  56. package/src/document-styles.ts +54 -0
  57. package/src/drift.ts +78 -10
  58. package/src/error-catalog.ts +8 -18
  59. package/src/error-codes.ts +192 -0
  60. package/src/error-contract.ts +29 -7
  61. package/src/error-fixes.ts +114 -0
  62. package/src/errors.ts +201 -138
  63. package/src/exec.ts +42 -8
  64. package/src/fix-command.ts +268 -0
  65. package/src/flag-number.ts +67 -0
  66. package/src/framework-scope.ts +49 -0
  67. package/src/generate-kinds.ts +97 -0
  68. package/src/guards.ts +186 -0
  69. package/src/index.ts +92 -15
  70. package/src/island-bundle.ts +166 -0
  71. package/src/island-routes.ts +50 -0
  72. package/src/jobs-driver.ts +33 -0
  73. package/src/jobs-json.ts +24 -0
  74. package/src/jobs-report.ts +17 -4
  75. package/src/mcp-db-target.ts +52 -27
  76. package/src/mcp-errors.ts +128 -19
  77. package/src/mcp-host.ts +44 -25
  78. package/src/messages.ts +93 -2
  79. package/src/metrics-endpoint.ts +64 -16
  80. package/src/migrations.ts +37 -4
  81. package/src/otlp-export.ts +64 -0
  82. package/src/output.ts +46 -16
  83. package/src/parse.ts +41 -3
  84. package/src/policy-facts.ts +38 -6
  85. package/src/policy-fixture.ts +14 -7
  86. package/src/prerender.ts +111 -2
  87. package/src/registry.ts +21 -3
  88. package/src/runtime-overrides.ts +66 -0
  89. package/src/safe-url-label.ts +24 -0
  90. package/src/scaffold-fixture.ts +10 -0
  91. package/src/scaffold-typecheck.ts +16 -38
  92. package/src/serve.ts +185 -13
  93. package/src/shell-quote.ts +15 -0
  94. package/src/source-files.ts +4 -0
  95. package/src/statement-loop.ts +74 -0
  96. package/src/style-csp.ts +18 -0
  97. package/src/sync-authenticator.ts +59 -0
  98. package/src/templates/action.ts +15 -30
  99. package/src/templates/admin-page.ts +103 -0
  100. package/src/templates/admin.ts +11 -7
  101. package/src/templates/backfill.ts +212 -0
  102. package/src/templates/entity.ts +72 -31
  103. package/src/templates/guard.ts +143 -0
  104. package/src/templates/index.ts +12 -1
  105. package/src/templates/island.ts +67 -0
  106. package/src/templates/job.ts +53 -13
  107. package/src/templates/naming.ts +17 -1
  108. package/src/templates/policy.ts +35 -28
  109. package/src/templates/query.ts +24 -5
  110. package/src/templates/resource.ts +19 -11
  111. package/src/templates/route.ts +90 -15
  112. package/src/templates/scaffold-app.ts +142 -45
  113. package/src/templates/scaffold-claude-agents.ts +149 -0
  114. package/src/templates/scaffold-claude-commands.ts +221 -0
  115. package/src/templates/scaffold-claude.ts +134 -0
  116. package/src/templates/scaffold-container.ts +46 -2
  117. package/src/templates/scaffold-db-package.ts +91 -0
  118. package/src/templates/scaffold-docs.ts +24 -5
  119. package/src/templates/scaffold-domain-package.ts +90 -0
  120. package/src/templates/scaffold-env.ts +87 -0
  121. package/src/templates/scaffold-i18n.ts +4 -1
  122. package/src/templates/scaffold-mcp-package.ts +49 -0
  123. package/src/templates/scaffold-package-shape.ts +25 -4
  124. package/src/templates/scaffold-repo.ts +116 -257
  125. package/src/templates/scaffold-roles.ts +68 -0
  126. package/src/templates/scaffold-ui-package.ts +56 -0
  127. package/src/templates/slice-foundation.ts +88 -0
  128. package/src/templates/wrap.ts +95 -0
  129. package/src/test-counts.ts +35 -0
  130. package/src/test-select.ts +30 -15
  131. package/src/test-shards.ts +20 -11
  132. package/src/test-workers.ts +50 -0
  133. package/src/ts-scan.ts +284 -15
  134. package/src/tsconfig-references.ts +103 -0
  135. package/src/verify-floor.ts +133 -0
  136. package/src/verify-step.ts +19 -0
  137. package/src/verify-test-run.ts +72 -0
  138. package/src/verify-tests.ts +160 -71
  139. package/src/version-loader.ts +20 -3
  140. package/src/workspace-checks.ts +87 -16
  141. package/src/write-line.ts +34 -0
package/src/cmd-build.ts CHANGED
@@ -3,13 +3,15 @@
3
3
 
4
4
  import { existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
+ import { frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
6
7
  import { requireAppRoot } from './app-root';
7
8
  import { runVerify } from './cmd-verify';
8
9
  import type { CliCommand, CommandContext } from './command';
9
10
  import { BuildEntryMissingError, UnknownCommandError } from './errors';
11
+ import type { ExecResult } from './exec';
10
12
  import { execOutput } from './exec';
11
13
  import { msg } from './messages';
12
- import type { CommandResult, Finding } from './output';
14
+ import type { CommandResult } from './output';
13
15
  import { flagString } from './parse';
14
16
 
15
17
  export const BUILD_TARGETS = ['docker', 'binary', 'static'] as const;
@@ -51,12 +53,20 @@ export function dockerArgs(root: string, tag: string): readonly string[] {
51
53
  return ['docker', 'build', '-f', join(root, BUILD_ENTRY.docker), '-t', tag, root];
52
54
  }
53
55
 
56
+ /**
57
+ * The define is not optional. A single-file executable carries no `package.json`, so
58
+ * `frameworkVersion()` has nothing to read and throws — which is exactly how this target came to
59
+ * compile an artifact that could never boot. The value is this CLI's own `@ultimat3/core`, which is
60
+ * the app's too: the packages release in lockstep and `x new` pins them together.
61
+ */
54
62
  export function binaryArgs(root: string, out: string): readonly string[] {
55
63
  return [
56
64
  'bun',
57
65
  'build',
58
66
  '--compile',
59
67
  '--minify',
68
+ '--define',
69
+ `${VERSION_DEFINE}=${JSON.stringify(frameworkVersion())}`,
60
70
  join(root, BUILD_ENTRY.binary),
61
71
  '--outfile',
62
72
  out,
@@ -76,6 +86,57 @@ export function argsFor(
76
86
  return staticArgs(paths.root, paths.out);
77
87
  }
78
88
 
89
+ /**
90
+ * The static gate refused, so nothing was built. Reported under `build`, not `verify`: `command`
91
+ * is the field an agent keys `--json` off, and answering `"verify"` sent it to re-run a gate it
92
+ * never asked for while hiding that the build had not started. The steps and the summary are the
93
+ * gate's own — they are what says which check to fix.
94
+ */
95
+ export function preflightResult(verify: CommandResult): CommandResult {
96
+ return { ...verify, command: 'build' };
97
+ }
98
+
99
+ /**
100
+ * The build's result from the builder's, kept pure so the two things a reader acts on — the
101
+ * summary line and the `--json` payload — are testable without spawning `docker`.
102
+ *
103
+ * `summary` used to be `msg('cli.build.done')` whatever the exit code, so a failed build printed
104
+ * `✗ built docker`; and the builder's own logs went only into `lines`, which is declared human-only
105
+ * and which `renderJson` drops — so CI, which runs `--json`, got the exit code and nothing to act
106
+ * on. The output now rides in `data` and `lines` renders that same string.
107
+ */
108
+ export function buildResult(input: {
109
+ readonly target: BuildTarget;
110
+ readonly artifact: string;
111
+ readonly command: readonly string[];
112
+ readonly result: ExecResult;
113
+ }): CommandResult {
114
+ const { result, target } = input;
115
+ const output = result.ok ? '' : execOutput(result);
116
+ return {
117
+ ok: result.ok,
118
+ command: 'build',
119
+ summary: msg(result.ok ? 'cli.build.done' : 'cli.build.failed', { target }),
120
+ findings: result.ok
121
+ ? []
122
+ : [
123
+ {
124
+ code: 'X_BUILD_FAILED',
125
+ cause: `${input.command.join(' ')} exited ${result.code}`,
126
+ fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
127
+ docs: 'https://ultimate.dev/errors/X_BUILD_FAILED',
128
+ },
129
+ ],
130
+ data: {
131
+ target,
132
+ artifact: input.artifact,
133
+ durationMs: result.durationMs,
134
+ ...(result.ok ? {} : { output }),
135
+ },
136
+ lines: result.ok ? [] : output.split('\n'),
137
+ };
138
+ }
139
+
79
140
  export const buildCommand: CliCommand = {
80
141
  spec: {
81
142
  name: 'build',
@@ -103,31 +164,18 @@ export const buildCommand: CliCommand = {
103
164
  );
104
165
  const verifyResult = await runVerify(verifySteps, { root, runner: ctx.runner });
105
166
  if (!verifyResult.ok) {
106
- return verifyResult;
167
+ return preflightResult(verifyResult);
107
168
  }
108
169
 
109
170
  const out =
110
171
  flagString(ctx.args, 'out') ?? join(root, '.x', target === 'static' ? 'static' : 'app');
111
172
  const tag = flagString(ctx.args, 'tag') ?? 'ultimate-app:dev';
112
173
  const command = argsFor(target, { root, tag, out });
113
- const result = await ctx.runner(command, { cwd: root });
114
- const findings: readonly Finding[] = result.ok
115
- ? []
116
- : [
117
- {
118
- code: 'X_BUILD_FAILED',
119
- cause: `${command.join(' ')} exited ${result.code}`,
120
- fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
121
- docs: 'https://ultimate.dev/errors/X_BUILD_FAILED',
122
- },
123
- ];
124
- return {
125
- ok: result.ok,
126
- command: 'build',
127
- summary: msg('cli.build.done', { target }),
128
- findings,
129
- data: { target, artifact: target === 'docker' ? tag : out, durationMs: result.durationMs },
130
- lines: result.ok ? [] : execOutput(result).split('\n'),
131
- };
174
+ return buildResult({
175
+ target,
176
+ artifact: target === 'docker' ? tag : out,
177
+ command,
178
+ result: await ctx.runner(command, { cwd: root }),
179
+ });
132
180
  },
133
181
  };
@@ -0,0 +1,219 @@
1
+ // `x db branch ls|create|drop` — the wiring alone: which verb, which database, which refusal.
2
+ // A VERB is required and comes from a closed set, so a branch name can never be read as one:
3
+ // `x db branch ls` used to clone a database called `ls`, because the argument was the name.
4
+ // The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
5
+
6
+ import { createPostgresClient, type DbClient } from '@ultimat3/db';
7
+ import type { CommandContext } from './command';
8
+ import type { BranchRow } from './db-branch';
9
+ import {
10
+ BRANCH_SUBCOMMANDS,
11
+ branchDatabaseName,
12
+ createExternalBranch,
13
+ createPgliteBranch,
14
+ databaseNameOf,
15
+ dropExternalBranch,
16
+ dropPgliteBranch,
17
+ isBranchName,
18
+ isBranchSubcommand,
19
+ listExternalBranches,
20
+ listPgliteBranches,
21
+ pgliteBranchLocation,
22
+ previewUrl,
23
+ } from './db-branch';
24
+ import { stepFinding } from './db-finding';
25
+ import type { DevServices } from './dev-services';
26
+ import { resolveServices } from './dev-services';
27
+ import { MissingPositionalError, UnknownCommandError } from './errors';
28
+ import { msg } from './messages';
29
+ import type { CommandResult, Finding } from './output';
30
+ import { flagString, nearest } from './parse';
31
+ import { portFromEnv } from './serve';
32
+ import { renderTable } from './table';
33
+
34
+ /**
35
+ * Always runnable, always the next thing a caller needs: what branches there are. Spelled twice
36
+ * because `UnknownCommandError` prefixes its `suggestion` with `x ` and `MissingPositionalError`
37
+ * takes a whole invocation — one of them handed back `x x db branch ls --json`.
38
+ */
39
+ const LIST_ARGV = 'db branch ls --json';
40
+ const LIST_FIX = `x ${LIST_ARGV}`;
41
+
42
+ /**
43
+ * A near miss on a three-verb set is a typo; anything else is the bare-name form this replaced —
44
+ * `x db branch feat-new-billing` used to CREATE, so the refusal hands the caller's own name back
45
+ * inside the command that still does it. Every answer here is a complete, runnable invocation,
46
+ * `x` excluded — the error class adds it.
47
+ */
48
+ function branchRetry(word: string, name: string | undefined): string {
49
+ const near = nearest(word, [...BRANCH_SUBCOMMANDS]);
50
+ if (near !== undefined) return name === undefined ? LIST_ARGV : `db branch ${near} ${name}`;
51
+ return isBranchName(word) ? `db branch create ${word}` : LIST_ARGV;
52
+ }
53
+
54
+ /**
55
+ * `x db branch` against an external Postgres, on ONE connection.
56
+ *
57
+ * `role: 'migrate'` is the profile, not a decoration: it is `max: 1` with no statement timeout, and
58
+ * both halves are load-bearing. `CREATE DATABASE ... TEMPLATE` is refused while any OTHER session
59
+ * is connected to the template, so a pool that spread three statements over three connections
60
+ * would leave two idle sessions holding the source open against itself; and cloning a real
61
+ * database routinely outlives the 10s a `web` profile allows.
62
+ */
63
+ async function withBranchClient<T>(url: string, fn: (client: DbClient) => Promise<T>): Promise<T> {
64
+ const client = createPostgresClient({ url, role: 'migrate', applicationName: 'x-db-branch' });
65
+ try {
66
+ return await fn(client);
67
+ } finally {
68
+ // Or the CLI exits holding a connection, and the next command waits for it to time out.
69
+ await client.close();
70
+ }
71
+ }
72
+
73
+ const branchesOf = (services: DevServices): Promise<readonly BranchRow[]> =>
74
+ services.db.mode === 'embedded'
75
+ ? listPgliteBranches(services.db.url)
76
+ : withBranchClient(services.db.url, listExternalBranches);
77
+
78
+ const failure = (summary: string, finding: Finding): CommandResult => ({
79
+ ok: false,
80
+ command: 'db',
81
+ summary,
82
+ findings: [finding],
83
+ });
84
+
85
+ export async function runBranchCommand(ctx: CommandContext, root: string): Promise<CommandResult> {
86
+ const verb = ctx.args.positionals[0];
87
+ if (verb === undefined) {
88
+ throw new MissingPositionalError({
89
+ command: 'db branch',
90
+ positional: BRANCH_SUBCOMMANDS.join('|'),
91
+ example: LIST_FIX,
92
+ });
93
+ }
94
+ const name = ctx.args.positionals[1] ?? flagString(ctx.args, 'name');
95
+ if (!isBranchSubcommand(verb)) {
96
+ throw new UnknownCommandError({
97
+ path: `db branch ${verb}`,
98
+ known: BRANCH_SUBCOMMANDS,
99
+ suggestion: branchRetry(verb, name),
100
+ });
101
+ }
102
+ const services = resolveServices(root, ctx.env);
103
+ if (verb === 'ls') return runList(services);
104
+ if (name === undefined) {
105
+ throw new MissingPositionalError({
106
+ command: `db branch ${verb}`,
107
+ positional: 'name',
108
+ example: `x db branch ${verb} feature-x`,
109
+ });
110
+ }
111
+ return verb === 'create' ? runCreate(ctx, services, name) : runDrop(services, name);
112
+ }
113
+
114
+ const row = (branch: BranchRow): readonly string[] => [
115
+ branch.name,
116
+ branch.location,
117
+ branch.createdAt ?? msg('cli.db.branch.unknown'),
118
+ branch.sizeBytes === null ? msg('cli.db.branch.unknown') : String(branch.sizeBytes),
119
+ ];
120
+
121
+ async function runList(services: DevServices): Promise<CommandResult> {
122
+ let branches: readonly BranchRow[];
123
+ try {
124
+ branches = await branchesOf(services);
125
+ } catch (error) {
126
+ return failure(msg('cli.db.branch.failed'), stepFinding(error, 'X_DB_BRANCH_FAILED'));
127
+ }
128
+ return {
129
+ ok: true,
130
+ command: 'db',
131
+ summary:
132
+ branches.length === 0
133
+ ? msg('cli.db.branch.none')
134
+ : msg('cli.db.branch.listed', { count: branches.length }),
135
+ lines:
136
+ branches.length === 0
137
+ ? []
138
+ : renderTable(['name', 'location', 'created-at', 'size-bytes'], branches.map(row)).map(
139
+ (line) => ` ${line}`,
140
+ ),
141
+ data: branches.map((branch) => ({ ...branch })),
142
+ };
143
+ }
144
+
145
+ async function runCreate(
146
+ ctx: CommandContext,
147
+ services: DevServices,
148
+ name: string,
149
+ ): Promise<CommandResult> {
150
+ // `portFromEnv`, never a bare `Number.parseInt`: the latter reads `PORT=abc` as `NaN` and put
151
+ // `http://feat.localhost:NaN` in `data.preview` — a machine-readable field naming no port.
152
+ const port = portFromEnv(ctx.env);
153
+ let branch: BranchRow;
154
+ try {
155
+ branch =
156
+ services.db.mode === 'embedded'
157
+ ? await createPgliteBranch(services.db.url, name)
158
+ : await withBranchClient(services.db.url, (client) => createExternalBranch(client, name));
159
+ } catch (error) {
160
+ return failure(msg('cli.db.branch.failed'), stepFinding(error, 'X_DB_BRANCH_FAILED'));
161
+ }
162
+ return {
163
+ ok: true,
164
+ command: 'db',
165
+ summary: msg('cli.db.branch.ready', { name: branch.name }),
166
+ data: {
167
+ branch: branch.name,
168
+ database: branch.location,
169
+ preview: previewUrl(branch.name, port),
170
+ mode: services.db.mode,
171
+ },
172
+ };
173
+ }
174
+
175
+ /**
176
+ * You may only drop what `ls` shows, and that is the whole guard — stronger than a confirmation
177
+ * flag, because it is the typo that is impossible rather than the keystroke that is tedious. An
178
+ * external branch is a database carrying `createBranch`'s marker comment AND this database's own
179
+ * prefix, so neither the shared database this session is connected to nor another app's clone on
180
+ * the same server is in the set; an embedded one is a `pgdata-<name>` directory, so `pgdata` itself
181
+ * is not either. `@ultimat3/db`'s own `X_BRANCH_EXISTS` fix line is `x db branch drop <name>` with
182
+ * no flag on it, so a flag here would break a shipped instruction.
183
+ *
184
+ * The check is not made here, and that is the point: `false` from either drop means "there was no
185
+ * such branch", decided by the same call that deletes, on the same connection, one statement
186
+ * earlier. A listing taken here and acted on below is two connections and a window wide enough to
187
+ * hold a whole `create` — and the wiring layer is exactly where a guard must not live.
188
+ */
189
+ async function runDrop(services: DevServices, name: string): Promise<CommandResult> {
190
+ try {
191
+ const dropped =
192
+ services.db.mode === 'embedded'
193
+ ? await dropPgliteBranch(services.db.url, name)
194
+ : await withBranchClient(services.db.url, (client) => dropExternalBranch(client, name));
195
+ if (!dropped) return notABranch(services, name);
196
+ return {
197
+ ok: true,
198
+ command: 'db',
199
+ summary: msg('cli.db.branch.dropped', { name }),
200
+ data: { branch: name, mode: services.db.mode },
201
+ };
202
+ } catch (error) {
203
+ return failure(msg('cli.db.branch.failed'), stepFinding(error, 'X_DB_BRANCH_FAILED'));
204
+ }
205
+ }
206
+
207
+ /** Names what the drop WOULD have touched, so the refusal is checkable rather than assertable. */
208
+ function notABranch(services: DevServices, name: string): CommandResult {
209
+ const target =
210
+ services.db.mode === 'embedded'
211
+ ? pgliteBranchLocation(services.db.url, name)
212
+ : branchDatabaseName(databaseNameOf(services.db.url), name);
213
+ return failure(msg('cli.db.branch.failed'), {
214
+ code: 'X_DB_BRANCH_FAILED',
215
+ cause: `"${name}" is not a branch of this database, so nothing was dropped (it would be ${target})`,
216
+ fix: LIST_FIX,
217
+ docs: 'https://ultimate.dev/errors/X_DB_BRANCH_FAILED',
218
+ });
219
+ }