@ultimat3/cli 2.0.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.
package/CLAUDE.md CHANGED
@@ -7,12 +7,13 @@ Tier 5. May import tiers 0–4. Nothing imports this except `create-ultimate`.
7
7
  | Entry | `src/bin.ts` (`#!/usr/bin/env bun`) — argv, stdout, exit code only |
8
8
  | stdout | `write-line.ts`'s `writeLine` — synchronous fd 1, never `process.stdout.write`, which truncates at the 64KB pipe buffer when `process.exit` follows. Exported, because `create-ultimate`'s entry point needs the same one |
9
9
  | Numeric flags | `flag-number.ts` — one reader for `--port` / `--workers` / `--shard`. A bare `Number.parseInt` accepts `4abc` and answers `NaN`, which turned three checks into ones that cannot fail |
10
+ | Shell quoting | `shell-quote.ts`'s `quoteArg` — every value the CLI pastes into a `fix:` or a reproduce line, `exec.ts`'s missing-program refusal and `test-shards.ts`'s reproduce command both. A name holding a space or a `;` interpolated bare is an instruction that runs something else |
10
11
  | Missing positionals | `MissingPositionalError`, never `BadFlagError` (names a flag that does not exist) and never `UnknownCommandError` (says a known command form is not one). Its `example` is a REAL invocation — `x g route <name>` in a shell is a redirect |
11
12
  | Bare subcommands | `CommandSpec.defaultSubcommand`, **declared**. The parser answered `subcommands[0]` until 1.2.0, so `x db` ran `gen` — the migration GENERATOR — because it sorted first, and `x mcp` started a server. A command with no defensible default declares none and `MissingSubcommandError` refuses the bare form; `parse.test.ts` pins the set at exactly `db` and `mcp`. Its fix is `x help <command>`, never `x <command> --help`: the subcommand is resolved *after* the flag loop, so the latter throws the same error again — a fix line that reproduced its own failure |
12
13
  | I/O | only `dispatch.ts` renders or exits; commands return `CommandResult` |
13
14
  | Staying up | a command still listening when `run` resolves returns `hold` (`hold.ts`), or `bin.ts` exits out from under it |
14
15
  | `--json` | every command, no exceptions — same data as the human render |
15
- | Errors | codes + titles in `src/error-codes.ts`, classes in `src/errors.ts`, subclass `UltimateError`, never a bare `Error` |
16
+ | Errors | codes + titles in `src/error-codes.ts`, classes in `src/errors.ts`, subclass `UltimateError`, never a bare `Error`. A class may sit beside its one thrower when `errors.ts` has no room under the 500-line ceiling — `db-seed.ts` and `metrics-endpoint.ts` do |
16
17
  | Subprocesses | only through `exec.ts`, so a test can inject a fake `Runner` |
17
18
  | Templates | `templates/*.ts` return strings; no fixture files on disk |
18
19
  | Strings | rendered output through `messages.ts`, missing key renders `⟦key⟧` — see below for what is *not* rendered output |
@@ -285,6 +286,7 @@ regex and `+` is a quantifier — `n1` is what actually selects these tests.
285
286
  | `drift.ts` | `checkSourceDrift`: the `.hash` sidecar `x verify`'s `drift` step compares, no database needed |
286
287
  | `db-destructive.ts` | `checkDestructiveMigrations`: the same step's second half — every committed `up` that drops, truncates or retypes must carry `-- destructive: true` |
287
288
  | `db-backfill.ts` | `x db backfill --list`: the flag parsing, the ledger read and the table |
289
+ | `db-seed.ts` | `x db seed`, everything except the argv: `SEED_GLOBS` (where a seed is declared), `discoverSeeds`, `parseSeedTierFlag`, `selectSeeds` (which seeds this invocation runs, and its two refusals — `X_DECLARATION_UNKNOWN` and `X_SEED_ENVIRONMENT`), `runSeeds` (one transaction **per seed**, never one around the run) and the two renderers. The `db-backfill.ts` split repeated: a driver plus plain strings in, plain rows out, so every rule is testable with no `ParsedArgs` and no boot. Which tiers an environment takes is `@ultimat3/entity`'s `seedTiersFor` — two copies of "may this seed run" would be two answers |
288
290
 
289
291
  `jobs-driver.ts` is the ONE place a CLI command gets hold of the app's queue — `withJobDriver`,
290
292
  which `x jobs` and `x db backfill` both call. It reuses an ambient `jobDriver()` when a process
@@ -399,6 +401,29 @@ with nothing running, and against a database three migrations behind. An app who
399
401
  load generates **nothing**: a short registry is indistinguishable from deleted entities, and the
400
402
  diff would be a DROP nobody asked for.
401
403
 
404
+ **An empty diff re-records the `.hash` sidecar, and that is what makes `X_DB_DRIFT` followable.**
405
+ The hash `checkSourceDrift` compares covers every non-test file under `packages/db/src` — a seed, a
406
+ helper, a decorator — not only the ones that imply DDL, and narrowing that glob would trade a loud
407
+ error for a silent gap in the one check that catches "entities changed and no migration was
408
+ generated". So detection stays broad and the REMEDY carries the weight: `x db gen "describe the
409
+ change"` — the exact `fix:` the error hands out — records the current hash against the newest
410
+ migration when the diff is empty, instead of writing nothing and leaving the gate red forever with
411
+ hand-editing a generated file as the only way out. `GeneratedFiles.outcome` is the four things a run
412
+ can be — `generated`, `hash-recorded`, `unchanged`, `blocked` — and `runGen` projects it onto
413
+ `--json` on **every** branch: reporting `hash-recorded` as `generated` would name a migration nobody
414
+ can apply, and reporting it as `unchanged` would hide a file this command wrote. That second one
415
+ shipped: the no-migration branch hardcoded `data: { migration: null, files: [] }`, so the run that
416
+ wrote the sidecar reported writing nothing to the machine reading the output.
417
+
418
+ Nothing is masked, and the branch proves it rather than promising it: `loadApp` reported no findings
419
+ (the registry is whole, never short), `declaredSchema` returned a real snapshot (`X_MIGRATION_SNAPSHOT_MISSING`
420
+ otherwise), and the emptiness is `generateMigration`'s own verdict — the same call the written path
421
+ takes. A migration with no migration id to record against writes nothing, which is the
422
+ `x new --no-example` case: an entity against zero migrations is `create table` for all of it and
423
+ never an empty diff. `reconcileSchemaHash` also declines to write when an OLDER migration already
424
+ recorded the hash, because `checkSourceDrift` already answers clean there and restamping the newest
425
+ sidecar would claim it produced a schema it did not — one predicate, `isRecorded`, read by both.
426
+
402
427
  One migration is one file, split by a lone `-- down` line. `<id>.down.sql` is a pre-1.2.0
403
428
  hand-written layout and `readMigrations` skips it — read as a migration it sorts next to its own
404
429
  `up` and drops every table the pair exists to reverse.
@@ -488,8 +513,10 @@ session-scoped and the grant dies when the connection returns to the pool, so ev
488
513
  itself as leader and a rolling update double-fires every task.
489
514
 
490
515
  The relay runs on `worker` and only `worker` — the role that exists wherever jobs run at all.
491
- Duplicating it is safe (publish-then-mark is at-least-once and the idempotency key collapses the
492
- repeat) but pointless.
516
+ Duplicating it is safe — the claim is a **lease** taken in the statement that locks the row
517
+ (`@ultimat3/jobs`' `outbox-pg.ts`, fenced on `claimed_by`), so two relays never hold one batch —
518
+ but pointless. The idempotency key is not the reason and never was: its conflict target is a
519
+ partial index over live states, so it collapses a repeat only while the first job is still live.
493
520
 
494
521
  `SQL_IDEMPOTENCY_TABLE` is applied beside `SQL_JOBS_TABLE`, and the store is installed by the boot
495
522
  rather than by the app, even though `@ultimat3/action` documents
@@ -581,6 +608,16 @@ route: a tenant-scoped key takes `AUTHORIZED_OBJECT_CACHE` (`private, max-age=0`
581
608
  `authorization`/`cookie`), and only a key no tenant owns keeps `immutable`. A genuinely public image
582
609
  belongs under `apps/web/site/`, which is a static asset and never touches that disk.
583
610
 
611
+ **A variant is CACHED only at a width the framework can mint.** The cache key is built entirely
612
+ from caller-supplied query values, so `?w=1`, `?w=2`, … each wrote a new object to the app's only
613
+ disk, on a route every signed-in tenant may reach for their own keys. `@ultimat3/seo`'s
614
+ `MAX_IMAGE_WIDTH` (8192) bounds that and does not close it. `isMintableWidth` is the bound:
615
+ `DEFAULT_WIDTHS` **plus the source's own intrinsic width**, which is exactly the set `usableWidths`
616
+ puts in a `srcset` — the constant alone would refuse the widest entry of any image whose intrinsic
617
+ width is not one of the eight. Anything outside it is still served; only the `put` is refused, so
618
+ no caller gains a new 4xx. `?q=` is deliberately still unbounded here — the closed set for quality
619
+ is `@ultimat3/seo`'s to declare, not this file's.
620
+
584
621
  `ICON_SOURCE` lives here, not in `cmd-doctor.ts`, because this is the module that reads it: the
585
622
  diagnostic checks what `x dev` serves, so one constant cannot pass the check and serve nothing.
586
623
  It is a **PNG** — core decodes PNG and JPEG only, and the SVG this used to name could never
package/README.md CHANGED
@@ -82,6 +82,7 @@ is held to the same error contract shipped source is (`X_GUARD_INVALID`, `X_GUAR
82
82
  | `dispatch.ts` | parse → run → render → exit; the only I/O boundary |
83
83
  | `parse.ts` | flags, subcommands, `--json`, `--help`, suggestions |
84
84
  | `flag-number.ts` | the one integer-flag reader — `--port`, `--workers`, `--shard` |
85
+ | `shell-quote.ts` | the one POSIX quoter for a value pasted into a `fix:` or a reproduce line |
85
86
  | `output.ts` | one data shape, two renderers, the 3-line error format |
86
87
  | `registry.ts` | the one command list |
87
88
  | `generate-kinds.ts` | which generators exist, and how a command line names one |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,28 +35,28 @@
35
35
  "dev": "bun run src/bin.ts dev"
36
36
  },
37
37
  "dependencies": {
38
- "@ultimat3/action": "2.0.0",
39
- "@ultimat3/admin": "2.0.0",
40
- "@ultimat3/ai": "2.0.0",
41
- "@ultimat3/cache": "2.0.0",
42
- "@ultimat3/core": "2.0.0",
43
- "@ultimat3/db": "2.0.0",
44
- "@ultimat3/entity": "2.0.0",
45
- "@ultimat3/http": "2.0.0",
46
- "@ultimat3/i18n": "2.0.0",
47
- "@ultimat3/jobs": "2.0.0",
48
- "@ultimat3/mail": "2.0.0",
49
- "@ultimat3/manifest": "2.0.0",
50
- "@ultimat3/mcp": "2.0.0",
51
- "@ultimat3/policy": "2.0.0",
52
- "@ultimat3/pwa": "2.0.0",
53
- "@ultimat3/query": "2.0.0",
54
- "@ultimat3/realtime": "2.0.0",
55
- "@ultimat3/render": "2.0.0",
56
- "@ultimat3/schema": "2.0.0",
57
- "@ultimat3/seo": "2.0.0",
58
- "@ultimat3/storage": "2.0.0",
59
- "@ultimat3/testing": "2.0.0",
60
- "@ultimat3/time": "2.0.0"
38
+ "@ultimat3/action": "3.0.0",
39
+ "@ultimat3/admin": "3.0.0",
40
+ "@ultimat3/ai": "3.0.0",
41
+ "@ultimat3/cache": "3.0.0",
42
+ "@ultimat3/core": "3.0.0",
43
+ "@ultimat3/db": "3.0.0",
44
+ "@ultimat3/entity": "3.0.0",
45
+ "@ultimat3/http": "3.0.0",
46
+ "@ultimat3/i18n": "3.0.0",
47
+ "@ultimat3/jobs": "3.0.0",
48
+ "@ultimat3/mail": "3.0.0",
49
+ "@ultimat3/manifest": "3.0.0",
50
+ "@ultimat3/mcp": "3.0.0",
51
+ "@ultimat3/policy": "3.0.0",
52
+ "@ultimat3/pwa": "3.0.0",
53
+ "@ultimat3/query": "3.0.0",
54
+ "@ultimat3/realtime": "3.0.0",
55
+ "@ultimat3/render": "3.0.0",
56
+ "@ultimat3/schema": "3.0.0",
57
+ "@ultimat3/seo": "3.0.0",
58
+ "@ultimat3/storage": "3.0.0",
59
+ "@ultimat3/testing": "3.0.0",
60
+ "@ultimat3/time": "3.0.0"
61
61
  }
62
62
  }
package/src/budgets.ts CHANGED
@@ -123,6 +123,23 @@ export async function readBuildStats(root: string): Promise<BuildStats | undefin
123
123
 
124
124
  const SCRIPT_TAG = /<script(?<attrs>[^>]*)>(?<body>[\s\S]*?)<\/script>/g;
125
125
  const SRC_ATTR = /\ssrc="(?<src>[^"]*)"/;
126
+ const TYPE_ATTR = /\stype="(?<type>[^"]*)"/;
127
+
128
+ /**
129
+ * `application/ld+json`, `application/json`, any `…+json`: the body is data, not code — the rule
130
+ * `@ultimat3/render`'s `head.ts` already states, restated because its `carriesJson` reads a
131
+ * `HeadTag` and is not exported, and this side has an attribute string off the emitted document.
132
+ * Without it a page shipping only `meta.ld` structured data and island props measured 8kb of JS
133
+ * and failed a 2kb budget with a `fix:` naming an import chain that does not exist.
134
+ */
135
+ const carriesJson = (attrs: string): boolean => {
136
+ // Everything from the first `;` is a MIME PARAMETER and not the type: a real document writes
137
+ // `type="application/ld+json; charset=utf-8"`, which does not END with `json`, so the suffix
138
+ // test alone charged an SEO structured-data block as executable JavaScript all over again.
139
+ const [type = ''] = (TYPE_ATTR.exec(attrs)?.groups?.['type'] ?? '').split(';');
140
+ return type.trim().toLowerCase().endsWith('json');
141
+ };
142
+
126
143
  /**
127
144
  * An island's chunk is reached by `import()` from inside the hydration runtime, so it never appears
128
145
  * as a `<script src>` — and a document weighed by script tags alone was charged for the runtime and
@@ -144,8 +161,9 @@ export interface MeasuredJs {
144
161
  }
145
162
 
146
163
  /**
147
- * What a rendered document actually makes the browser execute: the bytes of every inline script,
148
- * the size of every file a `src` points at, and the size of every island chunk it boots. Measured
164
+ * What a rendered document actually makes the browser execute: the bytes of every inline script
165
+ * the parser will run, the size of every file a `src` points at, and the size of every island
166
+ * chunk it boots. A JSON-typed script is skipped — it is data the parser never runs. Measured
149
167
  * from the emitted HTML rather than from the declared graph, because the graph is what a route
150
168
  * *says* it ships and this gate exists to catch the case where those two disagree.
151
169
  */
@@ -162,7 +180,9 @@ export async function measureDocumentJs(html: string, out: string): Promise<Meas
162
180
  };
163
181
 
164
182
  for (const match of html.matchAll(SCRIPT_TAG)) {
165
- const src = SRC_ATTR.exec(match.groups?.['attrs'] ?? '')?.groups?.['src'];
183
+ const attrs = match.groups?.['attrs'] ?? '';
184
+ if (carriesJson(attrs)) continue;
185
+ const src = SRC_ATTR.exec(attrs)?.groups?.['src'];
166
186
  if (src === undefined) {
167
187
  jsBytes += Buffer.byteLength(match.groups?.['body'] ?? '', 'utf8');
168
188
  continue;
@@ -11,6 +11,7 @@ import {
11
11
  branchDatabaseName,
12
12
  createExternalBranch,
13
13
  createPgliteBranch,
14
+ databaseNameOf,
14
15
  dropExternalBranch,
15
16
  dropPgliteBranch,
16
17
  isBranchName,
@@ -27,6 +28,7 @@ import { MissingPositionalError, UnknownCommandError } from './errors';
27
28
  import { msg } from './messages';
28
29
  import type { CommandResult, Finding } from './output';
29
30
  import { flagString, nearest } from './parse';
31
+ import { portFromEnv } from './serve';
30
32
  import { renderTable } from './table';
31
33
 
32
34
  /**
@@ -145,7 +147,9 @@ async function runCreate(
145
147
  services: DevServices,
146
148
  name: string,
147
149
  ): Promise<CommandResult> {
148
- const port = Number.parseInt(ctx.env['PORT'] ?? '3000', 10);
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);
149
153
  let branch: BranchRow;
150
154
  try {
151
155
  branch =
@@ -205,7 +209,7 @@ function notABranch(services: DevServices, name: string): CommandResult {
205
209
  const target =
206
210
  services.db.mode === 'embedded'
207
211
  ? pgliteBranchLocation(services.db.url, name)
208
- : branchDatabaseName(services.db.url.split('/').at(-1) ?? 'postgres', name);
212
+ : branchDatabaseName(databaseNameOf(services.db.url), name);
209
213
  return failure(msg('cli.db.branch.failed'), {
210
214
  code: 'X_DB_BRANCH_FAILED',
211
215
  cause: `"${name}" is not a branch of this database, so nothing was dropped (it would be ${target})`,
package/src/cmd-db.ts CHANGED
@@ -1,4 +1,4 @@
1
- // `x db gen|migrate|reset|studio|branch|backfill` — everything that touches the database. One
1
+ // `x db gen|migrate|reset|seed|studio|branch|backfill` — everything that touches the database. One
2
2
  // subcommand per line and no fall-through: a word this file does not know is refused, never
3
3
  // re-read as an argument to the last branch. `branch` itself is `cmd-db-branch.ts`.
4
4
  //
@@ -12,7 +12,8 @@
12
12
  import { rm } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
14
  import { resolveEnvironment } from '@ultimat3/core';
15
- import { type DriftReport, driftError } from '@ultimat3/db';
15
+ import { type DriftReport, driftError, withTransaction } from '@ultimat3/db';
16
+ import { postgresDriver } from '@ultimat3/entity';
16
17
  import { BackfillPendingError } from '@ultimat3/jobs';
17
18
  import { loadApp } from './app-load';
18
19
  import { requireAppRoot } from './app-root';
@@ -34,6 +35,16 @@ import {
34
35
  import { BRANCH_SUBCOMMANDS } from './db-branch';
35
36
  import { stepFinding } from './db-finding';
36
37
  import { generateAppMigration } from './db-generate';
38
+ import type { SeedPassRow } from './db-seed';
39
+ import {
40
+ discoverSeeds,
41
+ parseSeedTierFlag,
42
+ renderSeedTable,
43
+ runSeeds,
44
+ seedPassToJson,
45
+ seedTotals,
46
+ selectSeeds,
47
+ } from './db-seed';
37
48
  import { resolveServices } from './dev-services';
38
49
  import {
39
50
  BadFlagError,
@@ -49,14 +60,22 @@ import { findingFrom } from './output';
49
60
  import { flagBool, flagString } from './parse';
50
61
  import { runMigrations } from './serve';
51
62
 
52
- export const DB_SUBCOMMANDS = ['gen', 'migrate', 'reset', 'studio', 'branch', 'backfill'] as const;
63
+ export const DB_SUBCOMMANDS = [
64
+ 'gen',
65
+ 'migrate',
66
+ 'reset',
67
+ 'seed',
68
+ 'studio',
69
+ 'branch',
70
+ 'backfill',
71
+ ] as const;
53
72
 
54
73
  export const dbCommand: CliCommand = {
55
74
  spec: {
56
75
  name: 'db',
57
- summary: 'gen, migrate, reset, studio, branch, backfill',
76
+ summary: 'gen, migrate, reset, seed, studio, branch, backfill',
58
77
  usage:
59
- 'x db gen "add publish_at" | migrate | reset | studio | branch ls | branch create <name> | branch drop <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
78
+ '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
79
  requiresApp: true,
61
80
  subcommands: DB_SUBCOMMANDS,
62
81
  // Declared from the constant `runBranchCommand` validates against, never a second literal: it
@@ -64,7 +83,21 @@ export const dbCommand: CliCommand = {
64
83
  // hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
65
84
  subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
66
85
  flags: [
67
- { name: 'name', type: 'string', summary: 'migration or branch name, or backfill to filter' },
86
+ {
87
+ name: 'name',
88
+ type: 'string',
89
+ summary: 'migration, branch or seed name, or backfill to filter',
90
+ },
91
+ {
92
+ name: 'tier',
93
+ type: 'string',
94
+ summary: 'seed: which tier to run — reference or dev; also ULTIMATE_SEED_TIER',
95
+ },
96
+ {
97
+ name: 'dry-run',
98
+ type: 'boolean',
99
+ summary: 'seed: report what each seed would write, and write nothing',
100
+ },
68
101
  { name: 'list', type: 'boolean', summary: 'backfill: print the x_backfills ledger' },
69
102
  {
70
103
  name: 'pending',
@@ -106,6 +139,7 @@ export const dbCommand: CliCommand = {
106
139
  if (sub === 'gen') return runGen(ctx, root, argument ?? 'change');
107
140
  if (sub === 'migrate') return runMigrate(ctx, root, msg('cli.db.migrate.applied'));
108
141
  if (sub === 'reset') return runReset(ctx, root);
142
+ if (sub === 'seed') return runSeed(ctx, root);
109
143
  if (sub === 'studio') throw plannedSubcommand('db', 'studio');
110
144
  if (sub === 'backfill') return runBackfill(ctx, root);
111
145
  if (sub === 'branch') return runBranchCommand(ctx, root);
@@ -125,8 +159,14 @@ export const dbCommand: CliCommand = {
125
159
 
126
160
  /**
127
161
  * Source in, files out — no database is opened, so this answers the same in CI and on a laptop
128
- * with nothing running. A diff that finds nothing writes nothing and still exits 0: "no change" is
129
- * an answer, and an empty migration would take a ledger row and a checksum forever.
162
+ * with nothing running. A diff that finds nothing writes no MIGRATION and still exits 0: "no
163
+ * change" is an answer, and an empty migration would take a ledger row and a checksum forever. It
164
+ * may still write the `.hash` sidecar `x verify`'s `drift` step reads, which is what makes
165
+ * `X_DB_DRIFT`'s `fix:` — this command — a real instruction rather than a no-op.
166
+ *
167
+ * So there are THREE answers, not two, and `--json` carries `outcome` on every one: collapsing
168
+ * `hash-recorded` into either neighbour tells the machine reading this output that a migration
169
+ * exists when none does, or that nothing was written when the sidecar was.
130
170
  */
131
171
  async function runGen(ctx: CommandContext, root: string, name: string): Promise<CommandResult> {
132
172
  let generated: Awaited<ReturnType<typeof generateAppMigration>>;
@@ -148,9 +188,21 @@ async function runGen(ctx: CommandContext, root: string, name: string): Promise<
148
188
  return {
149
189
  ok: generated.findings.length === 0,
150
190
  command: 'db',
151
- summary: msg('cli.db.gen.unchanged'),
191
+ // The sidecar path, never a bare id: `hash-recorded` writes exactly one, and it is the file
192
+ // the `drift` step reads back.
193
+ summary:
194
+ generated.outcome === 'hash-recorded'
195
+ ? msg('cli.db.gen.recorded', { file: generated.files[0] ?? '' })
196
+ : msg('cli.db.gen.unchanged'),
152
197
  findings: generated.findings,
153
- data: { migration: null, files: [] },
198
+ // `files` is what this command WROTE, so the empty array here was a false claim.
199
+ lines: generated.files.map((file) => ` ${file}`),
200
+ data: {
201
+ outcome: generated.outcome,
202
+ migration: null,
203
+ files: [...generated.files],
204
+ schemaHash: generated.schemaHash ?? null,
205
+ },
154
206
  };
155
207
  }
156
208
  return {
@@ -159,6 +211,7 @@ async function runGen(ctx: CommandContext, root: string, name: string): Promise<
159
211
  summary: msg('cli.db.gen.written', { id: migration.id }),
160
212
  lines: generated.files.map((file) => ` ${file}`),
161
213
  data: {
214
+ outcome: generated.outcome,
162
215
  migration: migration.id,
163
216
  name: migration.name,
164
217
  files: [...generated.files],
@@ -233,6 +286,81 @@ async function runReset(ctx: CommandContext, root: string): Promise<CommandResul
233
286
  return runMigrate(ctx, root, msg('cli.db.reset.done'));
234
287
  }
235
288
 
289
+ /**
290
+ * `x db seed [<name>]` — the fixture graph, applied and replayable.
291
+ *
292
+ * The environment is resolved BEFORE anything is imported or connected: a run this environment does
293
+ * not take must refuse without having opened a connection to the database it was refusing to write
294
+ * to. `selectSeeds` asks the same question a second time, on the seeds themselves, because seeding
295
+ * is the one irreversible thing this command does (`db-seed.ts`).
296
+ *
297
+ * `withJobDriver` is the boot, though nothing here claims a job: it is the CLI's one answer to
298
+ * "which database is this command talking to", and it also puts a real queue behind any
299
+ * `handle.enqueue()` a seeded write triggers. A second boot path would be a second answer.
300
+ */
301
+ async function runSeed(ctx: CommandContext, root: string): Promise<CommandResult> {
302
+ const environment = resolveEnvironment({ env: ctx.env });
303
+ const requested = parseSeedTierFlag(
304
+ flagString(ctx.args, 'tier') ?? ctx.env['ULTIMATE_SEED_TIER'],
305
+ );
306
+ const name = ctx.args.positionals[0] ?? flagString(ctx.args, 'name');
307
+ const dryRun = flagBool(ctx.args, 'dry-run');
308
+ const discovery = await discoverSeeds(root);
309
+ const chosen = selectSeeds({
310
+ discovered: discovery.seeds,
311
+ ...(name === undefined ? {} : { name }),
312
+ environment,
313
+ requested,
314
+ });
315
+ if (chosen.length === 0) {
316
+ return {
317
+ ok: true,
318
+ command: 'db',
319
+ summary: msg('cli.db.seed.none'),
320
+ findings: discovery.findings,
321
+ data: seedPassToJson([]),
322
+ };
323
+ }
324
+ return withJobDriver(root, ctx, async () => {
325
+ const rows = await runSeeds({
326
+ seeds: chosen,
327
+ driver: postgresDriver(),
328
+ dryRun,
329
+ env: ctx.env,
330
+ // One transaction per seed, so a seed that throws takes only its own rows with it.
331
+ transaction: (work) => withTransaction(() => work()),
332
+ });
333
+ return seedPassResult(rows, dryRun, discovery.findings);
334
+ });
335
+ }
336
+
337
+ function seedPassResult(
338
+ rows: readonly SeedPassRow[],
339
+ dryRun: boolean,
340
+ findings: readonly Finding[],
341
+ ): CommandResult {
342
+ const totals = seedTotals(rows);
343
+ const failures = rows.flatMap((row) => (row.finding === null ? [] : [row.finding]));
344
+ return {
345
+ ok: failures.length === 0 && findings.length === 0,
346
+ command: 'db',
347
+ summary:
348
+ totals.failed > 0
349
+ ? msg('cli.db.seed.failed', { failed: totals.failed, count: rows.length })
350
+ : dryRun
351
+ ? msg('cli.db.seed.dryRun', { count: rows.length })
352
+ : msg('cli.db.seed.done', {
353
+ count: rows.length,
354
+ inserted: totals.inserted,
355
+ updated: totals.updated,
356
+ skipped: totals.skipped,
357
+ }),
358
+ findings: [...failures, ...findings],
359
+ lines: renderSeedTable(rows).map((line) => ` ${line}`),
360
+ data: seedPassToJson(rows),
361
+ };
362
+ }
363
+
236
364
  /**
237
365
  * Four shapes, one subcommand: `--list` reads the `x_backfills` ledger, `--pending` diffs it
238
366
  * against what the app DECLARED, and `<name>` / `--all` gate a pass and put it on the queue. A
package/src/cmd-dev.ts CHANGED
@@ -42,6 +42,7 @@ import { msg } from './messages';
42
42
  import type { CommandResult, Finding } from './output';
43
43
  import { findingFrom } from './output';
44
44
  import { flagString } from './parse';
45
+ import { metricsPortFor } from './serve';
45
46
  import { loopFacts, loopFinding, loopNotice } from './statement-loop';
46
47
 
47
48
  const DEFAULT_PORT = 3000;
@@ -185,6 +186,10 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
185
186
  const running = await startRoles({
186
187
  roles: options.roles ?? DEV_ROLES,
187
188
  port: options.port,
189
+ // `serve.ts`'s expression, called rather than restated: `METRICS_PORT` was read in the
190
+ // container and ignored here, so the scrape port an operator moved was the one port `x dev`
191
+ // could not move — and the second `x dev` on a box died binding the hardcoded 9090.
192
+ metricsPort: metricsPortFor(options.env, options.port),
188
193
  buildId,
189
194
  runtime,
190
195
  routes,
@@ -266,14 +271,16 @@ export const devCommand: CliCommand = {
266
271
  spec: {
267
272
  name: 'dev',
268
273
  summary: 'all roles in one process: embedded services, sub-second reload, /_x mounted',
269
- usage: 'x dev [--port 3000] [--role web,worker] [--json]',
274
+ usage: 'x dev [--port 3000] [--role web,worker] [--once] [--json]',
270
275
  requiresApp: true,
271
276
  flags: [
272
277
  { name: 'port', type: 'string', summary: 'HTTP port', default: String(DEFAULT_PORT) },
273
278
  {
274
279
  name: 'role',
275
280
  type: 'string',
276
- summary: `roles to run (default: all of ${DEV_ROLES.join(',')})`,
281
+ // `replicator` is named because it is selectable and NOT default — it takes a replication
282
+ // slot on a shared database, which is not something every `x dev` should do by starting.
283
+ summary: `roles to run (default: all of ${DEV_ROLES.join(',')}; replicator is opt-in)`,
277
284
  },
278
285
  { name: 'once', type: 'boolean', summary: 'boot, report, exit — for smoke tests and CI' },
279
286
  ],
package/src/cmd-doctor.ts CHANGED
@@ -11,9 +11,10 @@ import type { CliCommand, CommandContext } from './command';
11
11
  import { checkMigrationSnapshots } from './db-snapshot';
12
12
  import { ICON_SOURCE } from './dev-assets';
13
13
  import { checkSourceDrift } from './drift';
14
- import { intFlagOr, PORT_RANGE } from './flag-number';
14
+ import { intFlagOr, neighbouringPort, PORT_RANGE } from './flag-number';
15
15
  import { msg } from './messages';
16
16
  import type { CommandResult, Finding } from './output';
17
+ import type { ParsedArgs } from './parse';
17
18
 
18
19
  /**
19
20
  * The injection seam `runDoctor` reads instead of the environment. Not a semver surface —
@@ -126,7 +127,7 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
126
127
  finding(
127
128
  'X_PORT_IN_USE',
128
129
  `port ${probe.port} is already listening`,
129
- `x dev --port ${probe.port + 1}`,
130
+ `x dev --port ${neighbouringPort(probe.port)}`,
130
131
  ),
131
132
  );
132
133
  }
@@ -165,6 +166,18 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
165
166
  return findings;
166
167
  }
167
168
 
169
+ /**
170
+ * The port to TEST, and the one place `x doctor` reads it. `PORT_RANGE.min` is 0 because `x dev
171
+ * --port 0` means "let the kernel pick"; here 0 means nothing, and `Bun.serve({ port: 0 })` always
172
+ * succeeds — so the port check could not fail, which is worse than not running it.
173
+ */
174
+ export const doctorPort = (args: ParsedArgs): number =>
175
+ intFlagOr(
176
+ args,
177
+ { name: 'port', command: 'doctor', ...PORT_RANGE, min: 1, example: 'x doctor --port 3000' },
178
+ DEFAULT_DOCTOR_PORT,
179
+ );
180
+
168
181
  const portFree = async (port: number): Promise<boolean> => {
169
182
  try {
170
183
  const server = Bun.serve({ port, fetch: () => new Response('') });
@@ -213,11 +226,7 @@ export const doctorCommand: CliCommand = {
213
226
  ],
214
227
  },
215
228
  async run(ctx: CommandContext): Promise<CommandResult> {
216
- const port = intFlagOr(
217
- ctx.args,
218
- { name: 'port', command: 'doctor', ...PORT_RANGE, example: 'x doctor --port 3000' },
219
- DEFAULT_DOCTOR_PORT,
220
- );
229
+ const port = doctorPort(ctx.args);
221
230
  const findings = await runDoctor(probeFor(ctx.cwd, ctx.bunVersion, port));
222
231
  return {
223
232
  ok: findings.length === 0,
package/src/cmd-new.ts CHANGED
@@ -67,7 +67,7 @@ export const newCommand: CliCommand = {
67
67
  spec: {
68
68
  name: 'new',
69
69
  summary: 'scaffold a new Ultimate monorepo that already runs',
70
- usage: 'x new <name> [--dir path] [--no-example] [--dry-run] [--json]',
70
+ usage: 'x new <name> [--dir path] [--no-example] [--dry-run] [--force] [--json]',
71
71
  flags: [
72
72
  { name: 'dir', type: 'string', summary: 'parent directory (default: cwd)' },
73
73
  {
package/src/cmd-test.ts CHANGED
@@ -9,9 +9,10 @@ import { readIntFlag } from './flag-number';
9
9
  import type { CommandResult } from './output';
10
10
  import type { ParsedArgs } from './parse';
11
11
  import { flagString } from './parse';
12
+ import { quoteArg } from './shell-quote';
12
13
  import { discoverTests, missingSelection, readSample, readType, sampleFiles } from './test-select';
13
- import { quoteArg, runShards } from './test-shards';
14
- import { defaultWorkers } from './test-workers';
14
+ import { runShards } from './test-shards';
15
+ import { defaultWorkers, WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
15
16
  import type { TestType } from './verify-tests';
16
17
  import { TEST_TYPES } from './verify-tests';
17
18
 
@@ -25,6 +26,12 @@ const readIndex = (args: ParsedArgs, name: string, min: number): number | undefi
25
26
  name,
26
27
  command: 'test',
27
28
  min,
29
+ // The ceiling the summary already claimed and the reader never enforced: `--workers 5000` was
30
+ // accepted, `planShards` clamps only to the file count, and `runParallel` `Promise.all`s them —
31
+ // one Bun process per test FILE, each with the framework module graph and a cloned database.
32
+ // `--worker` is an index into that split, so the same bound holds it (the exact upper index is
33
+ // `workers - 1`, refused a line below by the check that knows the real width).
34
+ max: WORKER_CEILING,
28
35
  example: `x test --${name} ${Math.max(min, 1)}`,
29
36
  });
30
37
 
@@ -54,7 +61,11 @@ export const testCommand: CliCommand = {
54
61
  usage: `x test [${TEST_TYPES.join('|')}] [--filter text] [--sample N] [--workers N] [--worker I] [--json]`,
55
62
  positionalChoices: TEST_TYPES,
56
63
  flags: [
57
- { name: 'workers', type: 'string', summary: 'process count (default: CPUs - 1, max 8)' },
64
+ {
65
+ name: 'workers',
66
+ type: 'string',
67
+ summary: `process count (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
68
+ },
58
69
  {
59
70
  name: 'worker',
60
71
  type: 'string',