@ultimat3/cli 25.1.0 → 26.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 (44) hide show
  1. package/README.md +3 -3
  2. package/package.json +28 -28
  3. package/src/cmd-db-branch.ts +15 -11
  4. package/src/cmd-db-spec.ts +2 -2
  5. package/src/cmd-deploy-drain.ts +6 -1
  6. package/src/cmd-docs.ts +1 -1
  7. package/src/cmd-jobs-spec.ts +11 -7
  8. package/src/cmd-jobs.ts +10 -10
  9. package/src/cmd-planned.ts +3 -3
  10. package/src/cmd-registries-spec.ts +6 -6
  11. package/src/cmd-registries.ts +6 -6
  12. package/src/cmd-routes-spec.ts +1 -1
  13. package/src/cmd-routes.ts +9 -52
  14. package/src/cmd-tasks.ts +2 -2
  15. package/src/db-branch.ts +6 -6
  16. package/src/db-finding.ts +1 -1
  17. package/src/dev-restart-retire.ts +3 -1
  18. package/src/doctor-guards.ts +74 -19
  19. package/src/document-styles.ts +2 -4
  20. package/src/error-contract.ts +1 -1
  21. package/src/errors.ts +4 -4
  22. package/src/fix-command.ts +1 -1
  23. package/src/index.ts +6 -6
  24. package/src/jobs-report.ts +3 -3
  25. package/src/jobs-table.ts +1 -1
  26. package/src/live-routes.ts +0 -1
  27. package/src/mcp-db-target.ts +1 -1
  28. package/src/mcp-errors.ts +2 -2
  29. package/src/mcp-host.ts +4 -2
  30. package/src/messages.ts +4 -2
  31. package/src/parse.ts +23 -2
  32. package/src/role-replicator.ts +1 -1
  33. package/src/route-table.ts +113 -0
  34. package/src/runtime-jobs.ts +1 -1
  35. package/src/serve-boot.ts +8 -2
  36. package/src/serve-drain.ts +5 -1
  37. package/src/serve-retire.ts +7 -3
  38. package/src/site-seo.ts +1 -4
  39. package/src/templates/scaffold-claude-agents.ts +4 -4
  40. package/src/templates/scaffold-claude-commands.ts +2 -2
  41. package/src/templates/scaffold-claude.ts +1 -1
  42. package/src/templates/scaffold-guards.ts +12 -0
  43. package/src/templates/scaffold-helm-templates.ts +6 -2
  44. package/src/templates/scaffold-helm.ts +4 -1
package/README.md CHANGED
@@ -19,9 +19,9 @@ Commands and the `x verify` step count, `As of 2026-08`:
19
19
  | `x doctor` | environment, ports, drift, PWA prerequisites, `APP_URL` | every finding carries a fix command; probes the port `x dev` binds ([which one](../../wiki/CLI-Reference.md#x-doctor)); the framework's deploy secrets only where the machine names `staging`/`production` |
20
20
  | `x deploy` | container deploy plan | compose or helm; zero platform primitives |
21
21
  | `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
22
- | `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `describe <name>`, straight off the registries |
22
+ | `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `show <name>`, straight off the registries |
23
23
  | `x tasks list\|show` | cron tasks | timezone and next run, off `registeredTasks()` |
24
- | `x jobs ls\|show\|retry\|cancel\|rm\|promote\|pause\|resume` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`; `ls --json` pages as `rows`/`nextCursor`/`hasMore`; `drain` is planned (`X_NOT_IMPLEMENTED`) |
24
+ | `x jobs list\|show\|retry\|cancel\|delete\|promote\|pause\|resume` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`; `list --json` pages as `rows`/`nextCursor`/`hasMore`; `drain` is planned (`X_NOT_IMPLEMENTED`) |
25
25
  | `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
26
26
  | `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it, plus the framework's deploy-required secrets (`ULTIMATE_CURSOR_SECRET`, `STORAGE_SIGNING_SECRET` unless `S3_ENDPOINT` is set), which `check` asks for wherever the boot refuses them — outside `development`/`test`, or with no environment named |
27
27
  | `x auth seal-mfa` | one-shot auth maintenance | seals every `x_users.mfa_secret` still in the clear; idempotent, `{ sealed, alreadySealed, skipped }` |
@@ -171,7 +171,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
171
171
  | `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
172
172
  | `app-transport.ts` / `browser-transport.ts` | one browser transport, on `boundaries` in every app: a raw `fetch(`, `new WebSocket(` or `new XMLHttpRequest(` in an island's closure is `X_BROWSER_TRANSPORT_BYPASS`; a server barrel there is `X_BROWSER_SERVER_BARREL` |
173
173
  | `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
174
- | `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each. `ROLE=worker` also retires on SIGUSR2 (`serve-retire.ts`, 25.0.0): stop claiming, finish every held job, abort nothing, drain, exit 0 — what the chart's opt-in `roles.worker.retireSeconds` `preStop` sends; a no-op on Windows ([Deployment](../../wiki/Deployment.md#retiring-a-worker-before-sigterm)) |
174
+ | `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each. `ROLE=worker` also retires on SIGUSR2 (`serve-retire.ts`, 25.0.0): stop claiming, finish every held job, abort nothing, drain, exit 0 — what the chart's opt-in `roles.worker.retireSeconds` `preStop` sends; core's `isRetiring()` is true from the signal on, and a SIGTERM mid-retire drains on `drain.workerDeadlineMs` when declared (`lifecycleForRole`); a no-op on Windows ([Deployment](../../wiki/Deployment.md#retiring-a-worker-before-sigterm)) |
175
175
  | `prerender.ts` | `x build --target static`: which `site/` routes qualify, and where the bytes land |
176
176
  | `metrics-endpoint.ts` | the `METRICS_PATH` scrape listener every role opens, on `METRICS_PORT`; opening it starts the `process_*` series, labelled by `startMetricsEndpoint({ role })` |
177
177
  | `role-load.ts` / `document-graph.ts` / `serve-web.ts` | what each container role imports: `web`, `sync` and `replicator` every app module; `worker` and `scheduler` the API index plus every module that reaches no component or stylesheet |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "25.1.0",
3
+ "version": "26.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",
@@ -39,33 +39,33 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@babel/core": "^8.0.1",
42
- "@ultimat3/action": "25.1.0",
43
- "@ultimat3/admin": "25.1.0",
44
- "@ultimat3/ai": "25.1.0",
45
- "@ultimat3/auth": "25.1.0",
46
- "@ultimat3/cache": "25.1.0",
47
- "@ultimat3/core": "25.1.0",
48
- "@ultimat3/db": "25.1.0",
49
- "@ultimat3/entity": "25.1.0",
50
- "@ultimat3/flags": "25.1.0",
51
- "@ultimat3/http": "25.1.0",
52
- "@ultimat3/i18n": "25.1.0",
53
- "@ultimat3/jobs": "25.1.0",
54
- "@ultimat3/mail": "25.1.0",
55
- "@ultimat3/manifest": "25.1.0",
56
- "@ultimat3/mcp": "25.1.0",
57
- "@ultimat3/money": "25.1.0",
58
- "@ultimat3/notify": "25.1.0",
59
- "@ultimat3/policy": "25.1.0",
60
- "@ultimat3/pwa": "25.1.0",
61
- "@ultimat3/query": "25.1.0",
62
- "@ultimat3/realtime": "25.1.0",
63
- "@ultimat3/render": "25.1.0",
64
- "@ultimat3/schema": "25.1.0",
65
- "@ultimat3/seo": "25.1.0",
66
- "@ultimat3/storage": "25.1.0",
67
- "@ultimat3/testing": "25.1.0",
68
- "@ultimat3/time": "25.1.0",
42
+ "@ultimat3/action": "26.0.0",
43
+ "@ultimat3/admin": "26.0.0",
44
+ "@ultimat3/ai": "26.0.0",
45
+ "@ultimat3/auth": "26.0.0",
46
+ "@ultimat3/cache": "26.0.0",
47
+ "@ultimat3/core": "26.0.0",
48
+ "@ultimat3/db": "26.0.0",
49
+ "@ultimat3/entity": "26.0.0",
50
+ "@ultimat3/flags": "26.0.0",
51
+ "@ultimat3/http": "26.0.0",
52
+ "@ultimat3/i18n": "26.0.0",
53
+ "@ultimat3/jobs": "26.0.0",
54
+ "@ultimat3/mail": "26.0.0",
55
+ "@ultimat3/manifest": "26.0.0",
56
+ "@ultimat3/mcp": "26.0.0",
57
+ "@ultimat3/money": "26.0.0",
58
+ "@ultimat3/notify": "26.0.0",
59
+ "@ultimat3/policy": "26.0.0",
60
+ "@ultimat3/pwa": "26.0.0",
61
+ "@ultimat3/query": "26.0.0",
62
+ "@ultimat3/realtime": "26.0.0",
63
+ "@ultimat3/render": "26.0.0",
64
+ "@ultimat3/schema": "26.0.0",
65
+ "@ultimat3/seo": "26.0.0",
66
+ "@ultimat3/storage": "26.0.0",
67
+ "@ultimat3/testing": "26.0.0",
68
+ "@ultimat3/time": "26.0.0",
69
69
  "babel-preset-solid": "^1.9.15"
70
70
  }
71
71
  }
@@ -1,6 +1,7 @@
1
- // `x db branch ls|create|drop` — the wiring alone: which verb, which database, which refusal.
1
+ // `x db branch list|create|delete` — the wiring alone: which verb, which database, which refusal.
2
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.
3
+ // `x db branch ls` used to clone a database called `ls`, because the argument was the name. One
4
+ // verb each across every `x` registry (#709): `list` and `delete`, no `ls`/`drop` alias.
4
5
  // The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
5
6
 
6
7
  import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
@@ -26,7 +27,7 @@ import { stepFinding } from './db-finding';
26
27
  import { MissingPositionalError, UnknownCommandError } from './errors';
27
28
  import { msg } from './messages';
28
29
  import type { CommandResult, Finding } from './output';
29
- import { flagString } from './parse';
30
+ import { flagString, retiredVerbReplacement } from './parse';
30
31
  import type { DevServices } from './runtime-bindings';
31
32
  import { resolveServices } from './runtime-bindings';
32
33
  import { portFromEnv } from './serve';
@@ -35,9 +36,9 @@ import { renderTable } from './table';
35
36
  /**
36
37
  * Always runnable, always the next thing a caller needs: what branches there are. Spelled twice
37
38
  * because `UnknownCommandError` prefixes its `suggestion` with `x ` and `MissingPositionalError`
38
- * takes a whole invocation — one of them handed back `x x db branch ls --json`.
39
+ * takes a whole invocation — one of them handed back `x x db branch list --json`.
39
40
  */
40
- const LIST_ARGV = 'db branch ls --json';
41
+ const LIST_ARGV = 'db branch list --json';
41
42
  const LIST_FIX = `x ${LIST_ARGV}`;
42
43
 
43
44
  /**
@@ -47,7 +48,10 @@ const LIST_FIX = `x ${LIST_ARGV}`;
47
48
  * `x` excluded — the error class adds it.
48
49
  */
49
50
  function branchRetry(word: string, name: string | undefined): string {
50
- const near = nearestName(word, [...BRANCH_SUBCOMMANDS]);
51
+ // A retired verb (`ls`, `drop`) is a legal branch NAME too, so without this `drop feat` would be
52
+ // answered with `create drop` — the exact confusion the closed verb set exists to end.
53
+ const near =
54
+ retiredVerbReplacement(word, BRANCH_SUBCOMMANDS) ?? nearestName(word, [...BRANCH_SUBCOMMANDS]);
51
55
  if (near !== undefined) return name === undefined ? LIST_ARGV : `db branch ${near} ${name}`;
52
56
  return isBranchName(word) ? `db branch create ${word}` : LIST_ARGV;
53
57
  }
@@ -101,7 +105,7 @@ export async function runBranchCommand(ctx: CommandContext, root: string): Promi
101
105
  });
102
106
  }
103
107
  const services = resolveServices(root, ctx.env);
104
- if (verb === 'ls') return runList(services);
108
+ if (verb === 'list') return runList(services);
105
109
  if (name === undefined) {
106
110
  throw new MissingPositionalError({
107
111
  command: `db branch ${verb}`,
@@ -109,7 +113,7 @@ export async function runBranchCommand(ctx: CommandContext, root: string): Promi
109
113
  example: `x db branch ${verb} feature-x`,
110
114
  });
111
115
  }
112
- return verb === 'create' ? runCreate(ctx, services, name) : runDrop(services, name);
116
+ return verb === 'create' ? runCreate(ctx, services, name) : runDelete(services, name);
113
117
  }
114
118
 
115
119
  const row = (branch: BranchRow): readonly string[] => [
@@ -174,12 +178,12 @@ async function runCreate(
174
178
  }
175
179
 
176
180
  /**
177
- * You may only drop what `ls` shows, and that is the whole guard — stronger than a confirmation
181
+ * You may only delete what `list` shows, and that is the whole guard — stronger than a confirmation
178
182
  * flag, because it is the typo that is impossible rather than the keystroke that is tedious. An
179
183
  * external branch is a database carrying `createBranch`'s marker comment AND this database's own
180
184
  * prefix, so neither the shared database this session is connected to nor another app's clone on
181
185
  * the same server is in the set; an embedded one is a `pgdata-<name>` directory, so `pgdata` itself
182
- * is not either. `@ultimat3/db`'s own `X_BRANCH_EXISTS` fix line is `x db branch drop <name>` with
186
+ * is not either. `@ultimat3/db`'s own `X_BRANCH_EXISTS` fix line is `x db branch delete <name>` with
183
187
  * no flag on it, so a flag here would break a shipped instruction.
184
188
  *
185
189
  * The check is not made here, and that is the point: `false` from either drop means "there was no
@@ -187,7 +191,7 @@ async function runCreate(
187
191
  * earlier. A listing taken here and acted on below is two connections and a window wide enough to
188
192
  * hold a whole `create` — and the wiring layer is exactly where a guard must not live.
189
193
  */
190
- async function runDrop(services: DevServices, name: string): Promise<CommandResult> {
194
+ async function runDelete(services: DevServices, name: string): Promise<CommandResult> {
191
195
  try {
192
196
  const dropped =
193
197
  services.db.mode === 'embedded'
@@ -18,11 +18,11 @@ export const dbSpec: CommandSpec = {
18
18
  name: 'db',
19
19
  summary: 'gen, migrate, reset, seed, studio, branch, backfill',
20
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]',
21
+ 'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch list | branch create <name> | branch delete <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
22
22
  requiresApp: true,
23
23
  subcommands: DB_SUBCOMMANDS,
24
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
25
+ // is what lets the `errors` step resolve `x db branch list` — a fix line three shipped errors
26
26
  // hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
27
27
  subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
28
28
  // Each flag whose summary begins `<subcommand>:` declares that scope, and the parser refuses
@@ -12,7 +12,8 @@ const seconds = (ms: number): number => Math.ceil(ms / 1000);
12
12
  /**
13
13
  * The `--set` pairs, or none when the app declares no budget (the chart's defaults are core's).
14
14
  * The chart's `drain.deadlineSeconds` is ONE budget for every role, and so is `drain.deadlineMs`:
15
- * the web role drains on it too. The margins stay the chart's.
15
+ * the web role drains on it too — the worker on `drain.workerDeadlineSeconds` instead, when the app
16
+ * declares `drain.workerDeadlineMs`. The margins stay the chart's.
16
17
  *
17
18
  * The grace is budgeted at no less than core's production default: the value read here is resolved
18
19
  * in THIS process's environment, and an `x deploy` run under `NODE_ENV=development` resolves the
@@ -24,6 +25,10 @@ export function helmDrainOverrides(drain: Partial<DrainConfig> | undefined): rea
24
25
  if (drain?.deadlineMs !== undefined) {
25
26
  out.push('--set', `drain.deadlineSeconds=${seconds(drain.deadlineMs)}`);
26
27
  }
28
+ // The worker role's own budget: the chart sizes only the worker's grace period from it.
29
+ if (drain?.workerDeadlineMs !== undefined) {
30
+ out.push('--set', `drain.workerDeadlineSeconds=${seconds(drain.workerDeadlineMs)}`);
31
+ }
27
32
  if (drain === undefined) return out;
28
33
  const grace = Math.max(drain.readinessGraceMs ?? 0, READINESS_GRACE_DEFAULT_MS);
29
34
  out.push('--set', `drain.readinessGraceSeconds=${seconds(grace)}`);
package/src/cmd-docs.ts CHANGED
@@ -144,7 +144,7 @@ export const docsCommand: CliCommand = {
144
144
  }
145
145
 
146
146
  const entries = await scanInstalledDocs(scope);
147
- // `x jobs ls --limit`'s reader, and its `command` parameter exists for exactly this second
147
+ // `x jobs list --limit`'s reader, and its `command` parameter exists for exactly this second
148
148
  // caller. A local `Number.parseInt` accepted `--limit 1e9` as 1 and answered with one match,
149
149
  // and fell silently through to the default for `abc`, `0` and `-3` — a bound other than the one
150
150
  // typed, from the same binary that refuses all four one command over.
@@ -3,12 +3,16 @@
3
3
 
4
4
  import type { CommandSpec } from './parse';
5
5
 
6
+ /**
7
+ * One verb each, the same across every `x` registry (#709): `list`, `show <one>`, `delete`. No
8
+ * `ls`/`rm` aliases — two spellings of one verb is the ambiguity axiom 1 forbids.
9
+ */
6
10
  export const JOBS_SUBCOMMANDS = [
7
- 'ls',
11
+ 'list',
8
12
  'show',
9
13
  'retry',
10
14
  'cancel',
11
- 'rm',
15
+ 'delete',
12
16
  'promote',
13
17
  'pause',
14
18
  'resume',
@@ -20,13 +24,13 @@ export const jobsSpec: CommandSpec = {
20
24
  // Never ENDING in "(planned)": that suffix is how `x help` and the planned-table tests tell a
21
25
  // planned COMMAND from a shipped one, and `x jobs` ships — only its `drain` does not.
22
26
  summary:
23
- 'list, show, retry, cancel, remove and promote jobs; pause and resume a queue (drain is planned)',
27
+ 'list, show, retry, cancel, delete and promote jobs; pause and resume a queue (drain is planned)',
24
28
  usage:
25
- 'x jobs [ls|show <id>|retry <id>|cancel <id>|rm <id>|promote <id>|pause <queue>|resume <queue>|drain] [--queue q] [--state s] [--name n] [--limit n] [--after cursor] [--from-step name] [--reason text] [--json]',
29
+ 'x jobs [list|show <id>|retry <id>|cancel <id>|delete <id>|promote <id>|pause <queue>|resume <queue>|drain] [--queue q] [--state s] [--name n] [--limit n] [--after cursor] [--from-step name] [--reason text] [--json]',
26
30
  requiresApp: true,
27
31
  subcommands: JOBS_SUBCOMMANDS,
28
32
  // The bare `x jobs` lists; it never retries, cancels or drains anything.
29
- defaultSubcommand: 'ls',
33
+ defaultSubcommand: 'list',
30
34
  flags: [
31
35
  { name: 'queue', type: 'string', summary: 'filter by queue name' },
32
36
  { name: 'state', type: 'string', summary: 'filter by job state' },
@@ -35,8 +39,8 @@ export const jobsSpec: CommandSpec = {
35
39
  {
36
40
  name: 'after',
37
41
  type: 'string',
38
- summary: 'ls: the next page — the cursor the previous page printed',
39
- subcommands: ['ls'],
42
+ summary: 'list: the next page — the cursor the previous page printed',
43
+ subcommands: ['list'],
40
44
  },
41
45
  // Each of these is read by ONE subcommand — `retryJob`, `cancelJob` — and says so in its own
42
46
  // summary. The scope is what makes the parser refuse it anywhere else instead
package/src/cmd-jobs.ts CHANGED
@@ -1,4 +1,4 @@
1
- // `x jobs ls|show|retry|cancel|rm|promote|pause|resume` — introspect and recover the job queue,
1
+ // `x jobs list|show|retry|cancel|delete|promote|pause|resume` — introspect and recover the job queue,
2
2
  // bound to `@ultimat3/jobs`'s own introspection so the CLI, `/_x` and MCP report identically;
3
3
  // `drain` is planned (`cmd-planned.ts`): Postgres is the one durable driver. CLI wiring only:
4
4
  // the driver-injected logic is `jobs-report.ts`, the `--json` shapes `jobs-json.ts`, the table
@@ -56,7 +56,7 @@ function requireIdPositional(ctx: CommandContext, sub: string, driver: JobDriver
56
56
  throw new MissingPositionalError({
57
57
  command: `jobs ${sub}`,
58
58
  positional: 'id',
59
- example: 'x jobs ls --json',
59
+ example: 'x jobs list --json',
60
60
  });
61
61
  }
62
62
  if (!JOB_ID.test(id)) throw new JobUnknownError({ id, driver: driver.name });
@@ -86,11 +86,11 @@ function refuseOversizedPage(limit: string | undefined): void {
86
86
  command: 'jobs',
87
87
  reason: `one page holds at most ${MAX_JOB_PAGE} jobs`,
88
88
  // The bound is the queue's own constant, screened like any value spliced into a command.
89
- fix: `x jobs ls --limit ${renderFixShellArg(String(MAX_JOB_PAGE), '200')} --json # then pass its data.nextCursor as --after while data.hasMore is true`,
89
+ fix: `x jobs list --limit ${renderFixShellArg(String(MAX_JOB_PAGE), '200')} --json # then pass its data.nextCursor as --after while data.hasMore is true`,
90
90
  });
91
91
  }
92
92
 
93
- async function runLs(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
93
+ async function runList(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
94
94
  const limit = flagString(ctx.args, 'limit');
95
95
  refuseOversizedPage(limit);
96
96
  const result = await listJobs(driver, {
@@ -208,8 +208,8 @@ async function runCancel(driver: JobDriver, ctx: CommandContext): Promise<Comman
208
208
  }
209
209
 
210
210
  /** Exit 0 means the row is gone. An id nobody queued is `X_JOB_UNKNOWN`; a running one refuses. */
211
- async function runRm(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
212
- const id = requireIdPositional(ctx, 'rm', driver);
211
+ async function runDelete(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
212
+ const id = requireIdPositional(ctx, 'delete', driver);
213
213
  const removed = await removeJob(driver, id);
214
214
  if (removed === undefined) throw new JobUnknownError({ id, driver: driver.name });
215
215
  return {
@@ -249,14 +249,14 @@ async function runPause(
249
249
  };
250
250
  }
251
251
 
252
- /** The subcommands that answer a `JobTrace`. `ls`, `rm`, `promote` and the rest read rows only. */
252
+ /** The subcommands that answer a `JobTrace`. `list`, `delete`, `promote` and the rest read rows only. */
253
253
  const TRACE_SUBCOMMANDS: ReadonlySet<string> = new Set(['show', 'retry', 'cancel']);
254
254
 
255
255
  export const jobsCommand: CliCommand = {
256
256
  spec: jobsSpec,
257
257
  async run(ctx: CommandContext): Promise<CommandResult> {
258
258
  const root = requireAppRoot('jobs', ctx.cwd).dir;
259
- const sub = ctx.args.subcommand ?? 'ls';
259
+ const sub = ctx.args.subcommand ?? 'list';
260
260
  // BEFORE `withJobDriver`, which boots the source queue: the answer needs no server, so a box
261
261
  // whose database is down gets it rather than the boot failure of a queue never to be used —
262
262
  // and nothing is leased. 24.x's `--to`/`--dry-run` are deleted, so the parser refuses them
@@ -272,10 +272,10 @@ export const jobsCommand: CliCommand = {
272
272
  if (sub === 'show') return runShow(driver, ctx);
273
273
  if (sub === 'retry') return runRetry(driver, ctx);
274
274
  if (sub === 'cancel') return runCancel(driver, ctx);
275
- if (sub === 'rm') return runRm(driver, ctx);
275
+ if (sub === 'delete') return runDelete(driver, ctx);
276
276
  if (sub === 'promote') return runPromote(driver, ctx);
277
277
  if (sub === 'pause' || sub === 'resume') return runPause(driver, ctx, sub);
278
- return runLs(driver, ctx);
278
+ return runList(driver, ctx);
279
279
  });
280
280
  },
281
281
  };
@@ -33,8 +33,8 @@ export const PLANNED_COMMANDS: readonly PlannedCommand[] = [
33
33
  {
34
34
  name: 'branch',
35
35
  summary: 'copy-on-write branch environments with a preview URL',
36
- usage: 'x branch [<name>|rm <name>] [--json]',
37
- fix: 'x db branch ls --json # the database half: ls, create <name>, drop <name>',
36
+ usage: 'x branch [<name>|delete <name>] [--json]',
37
+ fix: 'x db branch list --json # the database half: list, create <name>, delete <name>',
38
38
  },
39
39
  {
40
40
  name: 'status',
@@ -134,7 +134,7 @@ export const PLANNED_SUBCOMMANDS: readonly PlannedSubcommand[] = [
134
134
  {
135
135
  command: 'jobs',
136
136
  subcommand: 'drain',
137
- fix: 'x jobs ls --json # the queue as it stands; Postgres is the only durable driver, so there is nowhere to drain to',
137
+ fix: 'x jobs list --json # the queue as it stands; Postgres is the only durable driver, so there is nowhere to drain to',
138
138
  },
139
139
  ];
140
140
 
@@ -7,8 +7,8 @@ import type { CommandSpec } from './parse';
7
7
  export const actionsSpec: CommandSpec = {
8
8
  name: 'actions',
9
9
  summary: 'the action registry: input/output schema, policy, tags, MCP exposure',
10
- usage: 'x actions [list|describe <name>] [--json]',
11
- subcommands: ['list', 'describe'],
10
+ usage: 'x actions [list|show <name>] [--json]',
11
+ subcommands: ['list', 'show'],
12
12
  defaultSubcommand: 'list',
13
13
  requiresApp: true,
14
14
  };
@@ -16,8 +16,8 @@ export const actionsSpec: CommandSpec = {
16
16
  export const queriesSpec: CommandSpec = {
17
17
  name: 'queries',
18
18
  summary: 'the query registry: schema, policy, live, cache tags',
19
- usage: 'x queries [list|describe <name>] [--json]',
20
- subcommands: ['list', 'describe'],
19
+ usage: 'x queries [list|show <name>] [--json]',
20
+ subcommands: ['list', 'show'],
21
21
  defaultSubcommand: 'list',
22
22
  requiresApp: true,
23
23
  };
@@ -25,8 +25,8 @@ export const queriesSpec: CommandSpec = {
25
25
  export const entitiesSpec: CommandSpec = {
26
26
  name: 'entities',
27
27
  summary: 'the entity registry: columns, invariants, indexes, tenancy',
28
- usage: 'x entities [list|describe <name>] [--json]',
29
- subcommands: ['list', 'describe'],
28
+ usage: 'x entities [list|show <name>] [--json]',
29
+ subcommands: ['list', 'show'],
30
30
  defaultSubcommand: 'list',
31
31
  requiresApp: true,
32
32
  };
@@ -2,7 +2,7 @@
2
2
  // Replaces grepping a source tree, which is what an agent does when the framework has no answer
3
3
  // to "what actions/queries/entities exist". One table, one generic command body: the three
4
4
  // commands differ only in which registry they read, a row's columns, and (for a query only) the
5
- // one extra field `describe` surfaces that its descriptor does not already carry.
5
+ // one extra field `show` surfaces that its descriptor does not already carry.
6
6
 
7
7
  import type { ActionDescriptor, AnyAction } from '@ultimat3/action';
8
8
  import { describeActions, getAction, jsonSchemaOf } from '@ultimat3/action';
@@ -35,7 +35,7 @@ const detailLines = (payload: Readonly<Record<string, JsonValue>>): readonly str
35
35
  Object.entries(payload).map(([key, value]) => ` ${key}: ${formatValue(value)}`);
36
36
 
37
37
  /**
38
- * One config per registry: how `list` renders a row, and what `describe` adds beyond the
38
+ * One config per registry: how `list` renders a row, and what `show` adds beyond the
39
39
  * descriptor itself — only a query's input schema does (`QUERIES.extra`); `ActionDescriptor`
40
40
  * already carries `input`/`output`, and an entity has no invocation shape to surface.
41
41
  */
@@ -116,7 +116,7 @@ function listResult<D extends { readonly name: string }, Raw extends { describe(
116
116
  };
117
117
  }
118
118
 
119
- function describeResult<D extends { readonly name: string }, Raw extends { describe(): D }>(
119
+ function showResult<D extends { readonly name: string }, Raw extends { describe(): D }>(
120
120
  kind: RegistryKind<D, Raw>,
121
121
  ctx: CommandContext,
122
122
  findings: readonly Finding[],
@@ -126,7 +126,7 @@ function describeResult<D extends { readonly name: string }, Raw extends { descr
126
126
  // A positional, so `MissingPositionalError` — `--name on "x actions"` named a flag no registry
127
127
  // command declares, and reading it as one is a second refusal for the first one's advice.
128
128
  throw new MissingPositionalError({
129
- command: `${kind.kind} describe`,
129
+ command: `${kind.kind} show`,
130
130
  positional: 'name',
131
131
  example: `x ${kind.kind} list --json`,
132
132
  });
@@ -158,8 +158,8 @@ async function runRegistryCommand<
158
158
  >(kind: RegistryKind<D, Raw>, ctx: CommandContext): Promise<CommandResult> {
159
159
  const root = requireAppRoot(kind.kind, ctx.cwd).dir;
160
160
  const { findings } = await loadApp(root);
161
- return ctx.args.subcommand === 'describe'
162
- ? describeResult(kind, ctx, findings)
161
+ return ctx.args.subcommand === 'show'
162
+ ? showResult(kind, ctx, findings)
163
163
  : listResult(kind, findings);
164
164
  }
165
165
 
@@ -5,7 +5,7 @@ import type { CommandSpec } from './parse';
5
5
 
6
6
  export const routesSpec: CommandSpec = {
7
7
  name: 'routes',
8
- summary: 'the route table: path, surface, render mode, hydrate, offline',
8
+ summary: 'every served route: pages, then actions, queries and runtime.ts routes',
9
9
  usage: 'x routes [--surface site|app|api|shared] [--json]',
10
10
  requiresApp: true,
11
11
  flags: [{ name: 'surface', type: 'string', summary: 'filter by surface' }],
package/src/cmd-routes.ts CHANGED
@@ -1,63 +1,20 @@
1
1
  // `x routes` — the route table as a table, or as JSON. Replaces grepping a router directory, which
2
2
  // is what an agent does when the framework has no answer to "what URLs exist".
3
3
  //
4
- // The rows are `@ultimat3/render`'s own `describePages()`: the CLI prints the route table, it
5
- // does not keep a second one.
4
+ // The rows are `route-table.ts`'s — the one builder the MCP dev tool `routes.list` reads too — so
5
+ // the CLI keeps no second table.
6
6
 
7
- import type { RouteDescriptor, Surface } from '@ultimat3/render';
8
- import { describePages, SURFACES } from '@ultimat3/render';
7
+ import type { Surface } from '@ultimat3/render';
8
+ import { SURFACES } from '@ultimat3/render';
9
9
  import { loadApp } from './app-load';
10
10
  import { requireAppRoot } from './app-root';
11
11
  import { routesSpec } from './cmd-routes-spec';
12
12
  import type { CliCommand, CommandContext } from './command';
13
13
  import { BadFlagError, UnknownCommandError } from './errors';
14
14
  import { msg } from './messages';
15
- import type { CommandResult, JsonValue } from './output';
15
+ import type { CommandResult } from './output';
16
16
  import { flagString } from './parse';
17
-
18
- /**
19
- * Where a route comes from. A file route names its file; a MOUNTED one names the call that
20
- * mounted it and every permission that gates it — there is no `page.tsx` to name, and printing the
21
- * package would send a reader looking for a route file inside `node_modules`.
22
- */
23
- const declaredBy = (route: RouteDescriptor): string =>
24
- route.mount === undefined
25
- ? route.file
26
- : `${route.mount.by}() · ${route.mount.permissions.join(' + ')}`;
27
-
28
- /** Fixed-width columns so the output diffs cleanly between runs and between machines. */
29
- export function renderRouteTable(routes: readonly RouteDescriptor[]): readonly string[] {
30
- const rows = routes.map((route) => [
31
- route.path,
32
- route.surface,
33
- route.mode,
34
- route.hydrate,
35
- route.offline,
36
- declaredBy(route),
37
- ]);
38
- const header = ['path', 'surface', 'render', 'hydrate', 'offline', 'file'];
39
- const widths = header.map((title, index) =>
40
- Math.max(title.length, ...rows.map((row) => (row[index] ?? '').length)),
41
- );
42
- const line = (cells: readonly string[]): string =>
43
- cells.map((value, index) => value.padEnd(widths[index] ?? 0)).join(' ');
44
- return [line(header), ...rows.map(line)];
45
- }
46
-
47
- const routeJson = (routes: readonly RouteDescriptor[]): JsonValue =>
48
- routes.map((route) => ({
49
- path: route.path,
50
- surface: route.surface,
51
- file: route.file,
52
- render: route.mode,
53
- hydrate: route.hydrate,
54
- offline: route.offline,
55
- budget: { js: route.budgetJs },
56
- // Absent on a file route, never `null`: only a mounted route has the fact.
57
- ...(route.mount === undefined
58
- ? {}
59
- : { mount: { by: route.mount.by, permissions: [...route.mount.permissions] } }),
60
- }));
17
+ import { plainAppRoutes, renderRouteRows, routeRows } from './route-table';
61
18
 
62
19
  /**
63
20
  * A closed set, because the filter was a bare `===`: `x routes --surface App` and `--surface pages`
@@ -97,7 +54,7 @@ export const routesCommand: CliCommand = {
97
54
  // `--transport` already follows.
98
55
  const surface = readSurfaceFilter(flagString(ctx.args, 'surface'));
99
56
  const { findings } = await loadApp(root);
100
- const routes = describePages().filter(
57
+ const routes = routeRows(await plainAppRoutes(root)).filter(
101
58
  (route) => surface === undefined || route.surface === surface,
102
59
  );
103
60
  return {
@@ -107,9 +64,9 @@ export const routesCommand: CliCommand = {
107
64
  routes.length === 0
108
65
  ? msg('cli.routes.empty')
109
66
  : msg('cli.routes.count', { count: routes.length }),
110
- lines: routes.length === 0 ? [] : renderRouteTable(routes).map((line) => ` ${line}`),
67
+ lines: routes.length === 0 ? [] : renderRouteRows(routes).map((line) => ` ${line}`),
111
68
  findings,
112
- data: { routes: routeJson(routes) },
69
+ data: { routes: routes.map((route) => route.json) },
113
70
  };
114
71
  },
115
72
  };
package/src/cmd-tasks.ts CHANGED
@@ -141,8 +141,8 @@ function requireHandle(ctx: CommandContext): TaskHandle {
141
141
  const suggestion = nearestName(name, known);
142
142
  throw new DeclarationUnknownError(
143
143
  suggestion === undefined
144
- ? { kind: 'tasks', singular: 'task', name, known, verb: 'show' }
145
- : { kind: 'tasks', singular: 'task', name, known, suggestion, verb: 'show' },
144
+ ? { kind: 'tasks', singular: 'task', name, known }
145
+ : { kind: 'tasks', singular: 'task', name, known, suggestion },
146
146
  );
147
147
  }
148
148
 
package/src/db-branch.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  * `reap` is deliberately absent: a nightly sweep is a `task` (`reapBranches` from `@ultimat3/db`),
28
28
  * and a CLI verb for it would be a second path to one job with a max-age nobody can default.
29
29
  */
30
- export const BRANCH_SUBCOMMANDS = ['ls', 'create', 'drop'] as const;
30
+ export const BRANCH_SUBCOMMANDS = ['list', 'create', 'delete'] as const;
31
31
 
32
32
  export type BranchSubcommand = (typeof BRANCH_SUBCOMMANDS)[number];
33
33
 
@@ -91,7 +91,7 @@ export function branchDatabaseName(source: string, branch: string): string {
91
91
  * `db.migrate` is aimed at a private database from a URL alone, with no connection to ask
92
92
  * `current_database()` — so "a branch of somebody" is the only question it can pose, and the
93
93
  * answer it wants for `analytics_branch_feat` is still "not the shared database". Anything holding
94
- * a client asks `branchNameIn` instead, which is the question `ls` and `drop` need.
94
+ * a client asks `branchNameIn` instead, which is the question `list` and `delete` need.
95
95
  */
96
96
  export const branchNameOf = (database: string): string | null =>
97
97
  /_branch_(.+)$/.exec(database)?.[1] ?? null;
@@ -112,7 +112,7 @@ export function branchNameIn(source: string, database: string): string | null {
112
112
  /**
113
113
  * The embedded peer: `branchPglite` copies `<dir>` to `<dir>-<name>`, so the branch name is the
114
114
  * suffix. `pgliteBranchDir` is the forward rule and this is its inverse — written once, because
115
- * `x db branch ls` and the MCP host's branch check must agree about what a branch directory is.
115
+ * `x db branch list` and the MCP host's branch check must agree about what a branch directory is.
116
116
  */
117
117
  export function pgliteBranchName(dir: string, source: string): string | null {
118
118
  return dir.startsWith(`${source}-`) ? dir.slice(source.length + 1) : null;
@@ -213,7 +213,7 @@ async function branchesOf(client: DbClient, source: string): Promise<readonly Br
213
213
  /**
214
214
  * Only databases carrying `createBranch`'s own marker comment, and only branches of the database
215
215
  * this session is connected to. A database this listing does not name is one `drop` may not touch,
216
- * which is what makes "you may only drop what `ls` shows" a guard rather than a courtesy — and a
216
+ * which is what makes "you may only delete what `list` shows" a guard rather than a courtesy — and a
217
217
  * row belonging to another app on the same server made that guard answer for a database it had
218
218
  * never seen.
219
219
  */
@@ -224,7 +224,7 @@ export async function listExternalBranches(client: DbClient): Promise<readonly B
224
224
  /**
225
225
  * Through `createBranch`, never a hand-written `CREATE DATABASE`: it validates the name, refuses a
226
226
  * database that already exists with `X_BRANCH_EXISTS`, and writes the marker comment that makes
227
- * the clone visible to `ls`. The CLI shelled out to `psql` until now and wrote no marker at all,
227
+ * the clone visible to `list`. The CLI shelled out to `psql` until now and wrote no marker at all,
228
228
  * so every branch it made was invisible to the only lister the framework has.
229
229
  */
230
230
  export async function createExternalBranch(client: DbClient, branch: string): Promise<BranchRow> {
@@ -232,7 +232,7 @@ export async function createExternalBranch(client: DbClient, branch: string): Pr
232
232
  const database = branchDatabaseName(source, branch);
233
233
  const info = await createBranch(database, { client, base: source });
234
234
  return {
235
- // The name `ls` will show for it, derived the way `ls` derives one — a create that reported a
235
+ // The name `list` will show for it, derived the way `list` derives one — a create that reported a
236
236
  // name the listing then spells differently is a `drop` the caller has to guess at.
237
237
  name: branchNameIn(source, database) ?? database,
238
238
  location: database,
package/src/db-finding.ts CHANGED
@@ -10,7 +10,7 @@ import { findingFrom, isUltimateErrorShape } from './output';
10
10
  /**
11
11
  * The engine names its own failures — `X_MIGRATION_CONFLICT` carries the ledger row that disagrees,
12
12
  * `X_MIGRATION_IRREVERSIBLE` carries the exact `--allow-destructive` line to rerun, and
13
- * `X_BRANCH_EXISTS` carries the `x db branch drop` that clears it — so those reach the caller
13
+ * `X_BRANCH_EXISTS` carries the `x db branch delete` that clears it — so those reach the caller
14
14
  * verbatim. `X_DB_GEN_FAILED` / `X_DB_MIGRATE_FAILED` / `X_DB_BRANCH_FAILED` are what is left: the
15
15
  * step failed for a reason no framework error claimed, and the raw message is all there is.
16
16
  */