@ultimat3/cli 7.0.0 → 8.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 (67) hide show
  1. package/CLAUDE.md +15 -1
  2. package/README.md +8 -3
  3. package/package.json +25 -25
  4. package/src/app-boundaries.ts +55 -5
  5. package/src/bin.ts +6 -3
  6. package/src/ci-log.ts +0 -0
  7. package/src/cmd-db-backfill.ts +240 -0
  8. package/src/cmd-db-branch.ts +3 -2
  9. package/src/cmd-db.ts +35 -156
  10. package/src/cmd-deploy.ts +37 -3
  11. package/src/cmd-dev.ts +7 -1
  12. package/src/cmd-errors.ts +2 -3
  13. package/src/cmd-fix.ts +3 -3
  14. package/src/cmd-i18n.ts +67 -5
  15. package/src/cmd-jobs.ts +27 -4
  16. package/src/cmd-mcp.ts +18 -9
  17. package/src/cmd-new.ts +91 -4
  18. package/src/cmd-policy.ts +3 -2
  19. package/src/cmd-pr.ts +55 -4
  20. package/src/cmd-registries.ts +3 -2
  21. package/src/cmd-shot.ts +68 -6
  22. package/src/cmd-tasks.ts +9 -4
  23. package/src/cmd-verify.ts +47 -6
  24. package/src/dev-cache.ts +1 -1
  25. package/src/dev-lock.ts +124 -12
  26. package/src/dev-queue.ts +12 -7
  27. package/src/dev-replicator.ts +3 -7
  28. package/src/dev-roles-fixture.ts +1 -1
  29. package/src/dev-roles.ts +40 -8
  30. package/src/dev-runtime.ts +96 -4
  31. package/src/dev-sync.ts +9 -4
  32. package/src/dispatch.ts +35 -5
  33. package/src/drift.ts +52 -7
  34. package/src/error-codes.ts +5 -0
  35. package/src/framework-scope.ts +57 -5
  36. package/src/generate-kinds.ts +19 -1
  37. package/src/i18n-registration.ts +67 -4
  38. package/src/index.ts +1 -1
  39. package/src/jobs-report.ts +10 -13
  40. package/src/mcp-errors.ts +3 -0
  41. package/src/messages.ts +12 -0
  42. package/src/output.ts +22 -2
  43. package/src/parse.ts +81 -37
  44. package/src/realtime-browser-probe-fixture.ts +9 -0
  45. package/src/runtime-overrides.ts +11 -3
  46. package/src/shot-settle.ts +57 -0
  47. package/src/shot-verdict.ts +27 -4
  48. package/src/sync-authenticator.ts +86 -14
  49. package/src/templates/guard-bare-error.ts +122 -0
  50. package/src/templates/guard-raw-colour.ts +138 -0
  51. package/src/templates/guard-untranslated-string.ts +138 -0
  52. package/src/templates/guard-unzoned-date.ts +142 -0
  53. package/src/templates/index.ts +3 -0
  54. package/src/templates/island.ts +2 -1
  55. package/src/templates/route.ts +1 -1
  56. package/src/templates/scaffold-app.ts +3 -82
  57. package/src/templates/scaffold-container.ts +30 -4
  58. package/src/templates/scaffold-db-package.ts +14 -6
  59. package/src/templates/scaffold-docs.ts +24 -13
  60. package/src/templates/scaffold-entries.ts +131 -0
  61. package/src/templates/scaffold-guards.ts +26 -0
  62. package/src/templates/scaffold-repo.ts +37 -6
  63. package/src/test-select.ts +4 -3
  64. package/src/verify-run.ts +25 -3
  65. package/src/verify-step.ts +11 -2
  66. package/src/verify-tests.ts +11 -3
  67. package/src/write-line.ts +23 -5
package/src/cmd-new.ts CHANGED
@@ -5,9 +5,11 @@
5
5
  import { existsSync } from 'node:fs';
6
6
  import { chmod } from 'node:fs/promises';
7
7
  import { isAbsolute, join, resolve } from 'node:path';
8
+ import { renderThrowable } from '@ultimat3/core';
8
9
  import { dedupe } from './cmd-generate';
9
10
  import type { CliCommand, CommandContext } from './command';
10
11
  import { MissingPositionalError } from './errors';
12
+ import type { Runner } from './exec';
11
13
  import { msg } from './messages';
12
14
  import type { CommandResult } from './output';
13
15
  import { flagBool, flagString } from './parse';
@@ -21,6 +23,70 @@ export interface NewAppOptions {
21
23
  readonly example: boolean;
22
24
  }
23
25
 
26
+ /** `--no-git`'s `problem`, matched rather than re-spelled where the report decides on a line. */
27
+ export const SKIPPED = 'skipped by --no-git';
28
+
29
+ /**
30
+ * What `git init && git add -A && git commit` did, reported on `data.git` either way.
31
+ *
32
+ * A type alias and not an `interface`, because it IS `CommandResult.data`: an interface has no
33
+ * implicit index signature, so it is not assignable to `JsonValue` and the `--json` contract would
34
+ * not compile (TS2322).
35
+ */
36
+ export type RepositoryInit = {
37
+ readonly initialized: boolean;
38
+ readonly committed: boolean;
39
+ /**
40
+ * Why not — `null` when both halves ran. Required rather than optional, so `data.git` has one
41
+ * shape for a machine reading `--json`: a key that appears only on failure is a key every
42
+ * consumer has to guess at. Never a raw thrown value; `renderThrowable` writes it.
43
+ */
44
+ readonly problem: string | null;
45
+ };
46
+
47
+ /**
48
+ * Plain `git init`, with no `--initial-branch`: that flag is git 2.28+, and a scaffold that failed
49
+ * on an older git would trade a working tree for a branch name. The repository takes whatever
50
+ * `init.defaultBranch` this machine already agreed on.
51
+ */
52
+ const GIT_STEPS: readonly (readonly string[])[] = [
53
+ ['git', 'init'],
54
+ ['git', 'add', '-A'],
55
+ ['git', 'commit', '-m', 'x new'],
56
+ ];
57
+
58
+ /** What `--no-git` records, so `data.git` has the same shape whichever way the flag went. */
59
+ const NO_GIT: RepositoryInit = { initialized: false, committed: false, problem: SKIPPED };
60
+
61
+ /**
62
+ * A scaffold is a REPOSITORY, because three surfaces of this CLI already assume one and answered
63
+ * `not a git repository` in a fresh app: `x affected`, `x ci` and `x pr`. It was four —
64
+ * `X_ROUTE_FILE_INVALID`'s `fix:` was a `git mv` that exits 128 with no `.git` to run in, and that
65
+ * one is now a plain `mv -n` (`packages/render/src/registry.ts`) rather than a reason to init.
66
+ *
67
+ * It never fails `x new`. The command's job is the tree, that tree is on disk by the time this
68
+ * runs, and a box with no `git` or no configured `user.email` would otherwise get an app it cannot
69
+ * see — so the outcome is DATA, and a failure is one line naming the commands to run by hand.
70
+ */
71
+ export async function initRepository(runner: Runner, dir: string): Promise<RepositoryInit> {
72
+ let initialized = false;
73
+ for (const command of GIT_STEPS) {
74
+ try {
75
+ const result = await runner(command, { cwd: dir });
76
+ if (!result.ok) {
77
+ const detail = (result.stderr.trim() || result.stdout.trim()).split('\n')[0] ?? '';
78
+ return { initialized, committed: false, problem: `${command.join(' ')}: ${detail}` };
79
+ }
80
+ } catch (error) {
81
+ // The thrown value is genuinely unknown — `exec` refuses a missing program by throwing —
82
+ // and core's renderer is the one spelling that cannot itself throw on a hostile `toString`.
83
+ return { initialized, committed: false, problem: renderThrowable(error) };
84
+ }
85
+ initialized = true;
86
+ }
87
+ return { initialized: true, committed: true, problem: null };
88
+ }
89
+
24
90
  /** Pure: the complete file list for a new app, so `--dry-run` and the test see the same thing. */
25
91
  export function planNewApp(options: NewAppOptions): readonly GeneratedFile[] {
26
92
  const app = names(options.name);
@@ -68,13 +134,25 @@ export const newCommand: CliCommand = {
68
134
  spec: {
69
135
  name: 'new',
70
136
  summary: 'scaffold a new Ultimate monorepo that already runs',
71
- usage: 'x new <name> [--dir path] [--no-example] [--dry-run] [--force] [--json]',
137
+ // Every flag the table below declares, in the spelling that turns it off where the default is
138
+ // on: the usage line offered `--no-example` while the table listed `--example`, and a reader
139
+ // had to reconcile the two to answer "which one do I get if I type neither".
140
+ usage: 'x new <name> [--dir path] [--no-example] [--no-git] [--dry-run] [--force] [--json]',
72
141
  flags: [
73
142
  { name: 'dir', type: 'string', summary: 'parent directory (default: cwd)' },
74
143
  {
144
+ // The summary carries the default and the negation because the page has to answer "which
145
+ // one do I get if I type neither": the usage line offered `--no-example`, this table said
146
+ // `--example`, and `default: true` is a field only `--json` renders. 134 files against 107.
75
147
  name: 'example',
76
148
  type: 'boolean',
77
- summary: 'include the example feature slice',
149
+ summary: 'include the example feature slice (default: on; --no-example for an empty app/)',
150
+ default: true,
151
+ },
152
+ {
153
+ name: 'git',
154
+ type: 'boolean',
155
+ summary: 'git init and commit the scaffold (default: on; --no-git for a bare directory)',
78
156
  default: true,
79
157
  },
80
158
  { name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
@@ -124,12 +202,21 @@ export const newCommand: CliCommand = {
124
202
  };
125
203
  }
126
204
  const written = await writeNewApp(target, options);
205
+ const git =
206
+ ctx.args.flags.get('git') === false ? NO_GIT : await initRepository(ctx.runner, target);
207
+ const lines = [msg('cli.new.wrote', { count: written.files.length, dir: target })];
208
+ if (git.problem !== null && git.problem !== SKIPPED) {
209
+ lines.push(msg('cli.new.noRepository', { problem: git.problem }));
210
+ // Raw, unlike the two prose lines around it: this one is an instruction to run verbatim, and
211
+ // a translated command is a broken one (`packages/cli/CLAUDE.md`).
212
+ lines.push(` run: cd ${target} && git init && git add -A && git commit -m 'x new'`);
213
+ }
127
214
  return {
128
215
  ok: true,
129
216
  command: 'new',
130
217
  summary: msg('cli.new.done', { name: app.kebab }),
131
- data: { dir: written.dir, files: written.files },
132
- lines: [` ${written.files.length} files in ${target}`],
218
+ data: { dir: written.dir, files: written.files, git },
219
+ lines,
133
220
  };
134
221
  },
135
222
  };
package/src/cmd-policy.ts CHANGED
@@ -2,13 +2,14 @@
2
2
  // only: the fact-gathering (registries, matrix rows) lives in `policy-facts.ts`, so the matrix
3
3
  // logic is testable without an app — same split as `cmd-jobs.ts` / `jobs-report.ts`.
4
4
 
5
+ import { nearestName } from '@ultimat3/core';
5
6
  import { loadApp } from './app-load';
6
7
  import { requireAppRoot } from './app-root';
7
8
  import type { CliCommand, CommandContext } from './command';
8
9
  import { DeclarationUnknownError, MissingPositionalError } from './errors';
9
10
  import { msg } from './messages';
10
11
  import type { CommandResult, Finding, JsonValue } from './output';
11
- import { nearest } from './parse';
12
+
12
13
  import type { DeclarationExplanation } from './policy-facts';
13
14
  import { explainPolicy, knownPolicySubjects, listPolicy } from './policy-facts';
14
15
  import { renderTable } from './table';
@@ -91,7 +92,7 @@ function runExplain(ctx: CommandContext, findings: readonly Finding[]): CommandR
91
92
  const explanation = explainPolicy(name);
92
93
  if (explanation === undefined) {
93
94
  const known = knownPolicySubjects();
94
- const suggestion = nearest(name, known);
95
+ const suggestion = nearestName(name, known);
95
96
  throw new DeclarationUnknownError(
96
97
  suggestion === undefined
97
98
  ? { kind: 'policy', singular: 'policy subject', name, known, verb: 'explain' }
package/src/cmd-pr.ts CHANGED
@@ -60,9 +60,27 @@ export const prCommand: CliCommand = {
60
60
  flags: [
61
61
  { name: 'repo', type: 'string', summary: 'owner/name; the checkout own remote by default' },
62
62
  { name: 'pr', type: 'string', summary: 'pull request number; this branch own by default' },
63
- { name: 'all', type: 'boolean', summary: 'review: resolved threads too, not just open ones' },
64
- { name: 'full', type: 'boolean', summary: 'review: whole comment bodies, never truncated' },
65
- { name: 'body', type: 'string', summary: 'reply: the comment text to post in the thread' },
63
+ // Scoped to the subcommand each summary already names: `resolve` and `reply` WRITE to
64
+ // somebody else's pull request, and a flag they silently ignore is a flag whose caller
65
+ // believed it did something to a request that cannot be re-run.
66
+ {
67
+ name: 'all',
68
+ type: 'boolean',
69
+ summary: 'review: resolved threads too, not just open ones',
70
+ subcommands: ['review'],
71
+ },
72
+ {
73
+ name: 'full',
74
+ type: 'boolean',
75
+ summary: 'review: whole comment bodies, never truncated',
76
+ subcommands: ['review'],
77
+ },
78
+ {
79
+ name: 'body',
80
+ type: 'string',
81
+ summary: 'reply: the comment text to post in the thread',
82
+ subcommands: ['reply'],
83
+ },
66
84
  ],
67
85
  },
68
86
  async run(ctx: CommandContext): Promise<CommandResult> {
@@ -175,7 +193,7 @@ function threadLines(thread: PrThread, bodyLines: number): readonly string[] {
175
193
  msg('cli.pr.thread.comment', { author: comment.author, createdAt: comment.createdAt }),
176
194
  );
177
195
  const clamped = clampBody(comment.body, bodyLines);
178
- for (const line of clamped.lines) out.push(` | ${line}`);
196
+ out.push(...commentBlock(thread.id, clamped.lines));
179
197
  if (clamped.hidden > 0) out.push(msg('cli.pr.body.truncated', { hidden: clamped.hidden }));
180
198
  }
181
199
  const hidden = thread.commentCount - thread.comments.length;
@@ -183,6 +201,39 @@ function threadLines(thread: PrThread, bodyLines: number): readonly string[] {
183
201
  return out;
184
202
  }
185
203
 
204
+ const BLOCK_OPEN = '<comment id=';
205
+ const BLOCK_CLOSE = '</comment>';
206
+
207
+ /**
208
+ * The fence, as a READER would parse it rather than as this file spells it.
209
+ *
210
+ * Two literal `replaceAll`s were the whole neutralisation, and markup is not spelled one way:
211
+ * `</comment >`, `</COMMENT>` and `< comment id=` all end or open a block for anything reading
212
+ * tags, and none of the three matched. One pattern over `<`, an optional `/`, and whitespace
213
+ * around a case-insensitive `comment` covers every spelling of the delimiter; the escape goes on
214
+ * the `<`, so what the reviewer wrote after it survives byte for byte.
215
+ */
216
+ const BLOCK_DELIMITER = /<(\s*\/?\s*comment\b)/gi;
217
+
218
+ /**
219
+ * One comment body, fenced and labelled with the thread id it came from — `@ultimat3/ai`'s
220
+ * `documentBlock` (`rag.ts`), applied to the other place foreign text enters an agent's context.
221
+ * `x pr review` exists because an agent cannot read the GitHub web UI, and a review body is
222
+ * written by anyone who can comment on the pull request: rendered as bare indented text it arrived
223
+ * in that agent's context indistinguishable from the command's own output, which is prompt
224
+ * injection with a shell attached.
225
+ *
226
+ * The fence is neutralised INSIDE the payload rather than deleted, so every word the reviewer
227
+ * wrote still reads, and the label is stripped of the three characters that would end the
228
+ * attribute. Influence only, and deliberately not sold as more: a fence tells a reader this text
229
+ * is data, and it can never stop one that decides otherwise.
230
+ */
231
+ export function commentBlock(id: string, lines: readonly string[]): readonly string[] {
232
+ const label = id.replaceAll('"', "'").replaceAll('>', ')').replaceAll('<', '(');
233
+ const body = lines.map((line) => line.replace(BLOCK_DELIMITER, '<\\$1'));
234
+ return [`${BLOCK_OPEN}"${label}">`, ...body, BLOCK_CLOSE];
235
+ }
236
+
186
237
  /**
187
238
  * A review body is prose written for a browser: the ones in this repo run to six thousand
188
239
  * characters with a shell script embedded in each. Clamped in ONE place, so `--json` carries the
@@ -6,6 +6,7 @@
6
6
 
7
7
  import type { ActionDescriptor, AnyAction } from '@ultimat3/action';
8
8
  import { describeActions, getAction, jsonSchemaOf } from '@ultimat3/action';
9
+ import { nearestName } from '@ultimat3/core';
9
10
  import type { EntityDescription, RegistryEntry } from '@ultimat3/entity';
10
11
  import { describeEntities, getEntity } from '@ultimat3/entity';
11
12
  import type { AnyQuery, QueryDescriptor } from '@ultimat3/query';
@@ -17,7 +18,7 @@ import { DeclarationUnknownError, MissingPositionalError } from './errors';
17
18
  import { msg } from './messages';
18
19
  import type { CommandResult, Finding, JsonValue } from './output';
19
20
  import type { CommandSpec } from './parse';
20
- import { nearest } from './parse';
21
+
21
22
  import { renderTable } from './table';
22
23
 
23
24
  /**
@@ -154,7 +155,7 @@ function describeResult<D extends { readonly name: string }, Raw extends { descr
154
155
  const raw = kind.find(name);
155
156
  if (raw === undefined) {
156
157
  const known = kind.list().map((item) => item.name);
157
- const suggestion = nearest(name, known);
158
+ const suggestion = nearestName(name, known);
158
159
  throw new DeclarationUnknownError(
159
160
  suggestion === undefined
160
161
  ? { kind: kind.kind, singular: kind.singular, name, known }
package/src/cmd-shot.ts CHANGED
@@ -22,7 +22,8 @@ import { intFlagOr, PORT_RANGE } from './flag-number';
22
22
  import type { CommandResult } from './output';
23
23
  import type { ParsedArgs } from './parse';
24
24
  import { flagBool, flagString } from './parse';
25
- import type { ShotArtifacts } from './shot-verdict';
25
+ import { SETTLE_POLL_MS, settleIslands } from './shot-settle';
26
+ import type { IslandCount, ShotArtifacts } from './shot-verdict';
26
27
  import {
27
28
  buildVerdict,
28
29
  ISLAND_PROBE,
@@ -62,10 +63,50 @@ const pathOf = (url: string): string => {
62
63
  }
63
64
  };
64
65
 
66
+ /**
67
+ * A reserved name (RFC 2606) that resolves nowhere, so the origin check below can never be
68
+ * satisfied by an accident of what the app's own host happens to be.
69
+ */
70
+ const ROUTE_BASE = 'http://route.invalid';
71
+
72
+ /**
73
+ * Where a browser would actually go. The refusal above reads `scheme:` and nothing else, and this
74
+ * is the question it was standing in for: a path is a path only if resolving it lands back on the
75
+ * origin it was resolved against.
76
+ */
77
+ const resolvedOrigin = (path: string): string => {
78
+ try {
79
+ return new URL(path, ROUTE_BASE).origin;
80
+ } catch {
81
+ // A path `new URL` will not parse is one no browser will fetch either, and reporting it as the
82
+ // origin it is not is the honest answer here.
83
+ return '';
84
+ }
85
+ };
86
+
87
+ const refuseRoute = (reason: string): never => {
88
+ throw new BadFlagError({
89
+ flag: 'route',
90
+ command: 'shot',
91
+ reason,
92
+ // A placeholder, because there is nothing safe to substitute: unlike an absolute URL, an
93
+ // origin-escaping route carries no path the caller can be assumed to have meant.
94
+ fix: 'x shot /<path> --json',
95
+ });
96
+ };
97
+
65
98
  /**
66
99
  * A path on the app, never a URL. `x shot https://example.com` would photograph somebody else's
67
100
  * site through a headless browser inside your network, which is the SSRF shape `allowHosts` exists
68
101
  * to refuse — so it is refused here, at the argument, where the reader can still see why.
102
+ *
103
+ * `scheme:` was the ONLY spelling refused until 2026-08-22, and it is one of four: `//evil/x` is a
104
+ * protocol-relative URL, `\evil\x` is the same thing to every URL parser (a backslash IS a slash
105
+ * for a special scheme), and a TAB inside the path is deleted by the parser before the host is
106
+ * read, so `/⇥/evil/x` becomes `//evil/x`. Each one reached `new URL(route, server.url)` and came
107
+ * back pointed at another host. `allowHostsFrom` one layer down could not catch any of them: it
108
+ * allows a HOSTNAME, and the hostname it is given is the one the page has already left — which is
109
+ * how `x shot //localhost:9200/_cat/indices` photographed whatever else was on the dev box.
69
110
  */
70
111
  export function readRoute(raw: string | undefined): string {
71
112
  if (raw === undefined || raw.trim() === '') {
@@ -82,7 +123,20 @@ export function readRoute(raw: string | undefined): string {
82
123
  fix: `x shot ${pathOf(route)} --json`,
83
124
  });
84
125
  }
85
- return route.startsWith('/') ? route : `/${route}`;
126
+ const path = route.startsWith('/') ? route : `/${route}`;
127
+ // Its own refusal rather than folded into the origin check: `/a\b` stays on this origin and is
128
+ // still not the route that was typed — the verdict would record `/a\b` beside a picture of
129
+ // `/a/b`, which is the artifact lying about its own subject.
130
+ if (path.includes('\\')) {
131
+ return refuseRoute(`"${route}" contains a backslash, which a URL parser reads as "/"`);
132
+ }
133
+ const origin = resolvedOrigin(path);
134
+ if (origin !== ROUTE_BASE) {
135
+ return refuseRoute(
136
+ `"${route}" is not a path on the app: a browser resolves it to ${origin === '' ? 'no URL at all' : origin}`,
137
+ );
138
+ }
139
+ return path;
86
140
  }
87
141
 
88
142
  /**
@@ -164,10 +218,18 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
164
218
  // The probe may legitimately answer nothing — a page that refuses evaluation, a driver with no
165
219
  // JS engine. `null` says so; a `0` would read as "the route renders no islands", which is a
166
220
  // different and much more alarming claim.
167
- const islands = await page
168
- .evaluate(ISLAND_PROBE)
169
- .then(parseIslandProbe)
170
- .catch(() => null);
221
+ const probe = (): Promise<IslandCount | null> =>
222
+ page
223
+ .evaluate(ISLAND_PROBE)
224
+ .then(parseIslandProbe)
225
+ .catch(() => null);
226
+ // The same budget again, and deliberately no new flag: `settleMs` is the deadline at which the
227
+ // runtime CALLS `import()`, so a mount gets exactly as long to settle as the runtime got to
228
+ // start it — and `--settle 0`, which asks for no wait, still gets none.
229
+ const islands = await settleIslands(probe, {
230
+ windowMs: options.settleMs,
231
+ pollMs: SETTLE_POLL_MS,
232
+ });
171
233
  const bytes = await page.screenshot({ fullPage: options.fullPage });
172
234
  // Read AFTER the capture, so an error logged while the page settled is in the verdict that
173
235
  // ships with the picture it explains.
package/src/cmd-tasks.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // an agent reading `0 3 * * *` and guessing. CLI wiring only; the pure computation lives in
4
4
  // `tasks-facts.ts` — the same split `cmd-jobs.ts` makes against `jobs-report.ts`.
5
5
 
6
- import { systemClock } from '@ultimat3/core';
6
+ import { nearestName, systemClock } from '@ultimat3/core';
7
7
  import type { TaskHandle } from '@ultimat3/jobs';
8
8
  import type { CronPhrases } from '@ultimat3/time';
9
9
  import { loadApp } from './app-load';
@@ -12,7 +12,7 @@ import type { CliCommand, CommandContext } from './command';
12
12
  import { BadFlagError, DeclarationUnknownError } from './errors';
13
13
  import { msg } from './messages';
14
14
  import type { CommandResult, Finding, JsonValue } from './output';
15
- import { flagString, nearest } from './parse';
15
+ import { flagString } from './parse';
16
16
  import { renderTable } from './table';
17
17
  import {
18
18
  findTaskHandle,
@@ -92,7 +92,7 @@ function requireHandle(ctx: CommandContext): TaskHandle {
92
92
  const handle = findTaskHandle(name);
93
93
  if (handle !== undefined) return handle;
94
94
  const known = knownTaskNames();
95
- const suggestion = nearest(name, known);
95
+ const suggestion = nearestName(name, known);
96
96
  throw new DeclarationUnknownError(
97
97
  suggestion === undefined
98
98
  ? { kind: 'tasks', singular: 'task', name, known, verb: 'show' }
@@ -138,7 +138,12 @@ export const tasksCommand: CliCommand = {
138
138
  subcommands: ['list', 'show'],
139
139
  defaultSubcommand: 'list',
140
140
  flags: [
141
- { name: 'count', type: 'string', summary: 'show: how many upcoming occurrences to list' },
141
+ {
142
+ name: 'count',
143
+ type: 'string',
144
+ summary: 'show: how many upcoming occurrences to list',
145
+ subcommands: ['show'],
146
+ },
142
147
  ],
143
148
  },
144
149
  async run(ctx: CommandContext): Promise<CommandResult> {
package/src/cmd-verify.ts CHANGED
@@ -1,17 +1,25 @@
1
1
  // `x verify` — the contract. Every check is a named step with its own pass/fail and duration, the
2
2
  // same list in the terminal and in --json, and a non-zero exit if any step fails. Green means
3
- // shippable (axiom 5): one step list, no second checklist, no CI-only step, and no way to narrow
4
- // the run — `--only` and `--skip` would make "green" mean whatever the caller chose.
3
+ // shippable (axiom 5): one step list, no second checklist, no CI-only step.
4
+ //
5
+ // `--only <step>` is the ONE narrowing, decided as D6, and it does not weaken that: the GATE is
6
+ // the no-flag run, and a narrowed run says `NOT A GATE RUN` in the summary and in `--json` so no
7
+ // reader of either can take it for one. `--skip` stays refused — it would let a caller drop the
8
+ // step that was going to fail and still read the output as a whole-tree verdict.
5
9
 
10
+ import { nearestName } from '@ultimat3/core';
6
11
  import { requireAppRoot } from './app-root';
7
12
  import type { CliCommand, CommandContext } from './command';
13
+ import { BadFlagError } from './errors';
8
14
  import { readIntFlag } from './flag-number';
9
15
  import type { CommandResult } from './output';
10
16
  import type { ParsedArgs } from './parse';
17
+ import { flagString } from './parse';
11
18
  import { WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
12
19
  import { VERIFY_STEPS } from './verify-checks';
13
20
  import { runVerify } from './verify-run';
14
21
  import type { VerifyStepName } from './verify-step';
22
+ import { VERIFY_STEP_NAMES } from './verify-step';
15
23
 
16
24
  // One import path for the gate, unchanged by the split: `index.ts`, `x build` and the MCP host all
17
25
  // reach the list and the runner through this module, and a second path to either would be the
@@ -23,30 +31,63 @@ export const verifyCommand: CliCommand = {
23
31
  spec: {
24
32
  name: 'verify',
25
33
  summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
26
- usage: 'x verify [--workers N] [--json]',
34
+ usage: 'x verify [--only <step>] [--workers N] [--json]',
27
35
  requiresApp: true,
28
- // The only flag, and it is not `--only`/`--skip` in disguise: it changes how wide the test
29
- // steps spread, never which steps run. Every step still runs, so "green" still means the
30
- // same thing at `--workers 1` as at `--workers 8`.
36
+ // Two flags, and only one of them narrows. `--workers` changes how wide the test steps
37
+ // spread, never which steps run. `--only` runs one step and says so in both renderers —
38
+ // never silently, which is the whole of what makes it safe to have.
31
39
  flags: [
32
40
  {
33
41
  name: 'workers',
34
42
  type: 'string',
35
43
  summary: `test processes per parallel step (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
36
44
  },
45
+ {
46
+ name: 'only',
47
+ type: 'string',
48
+ summary:
49
+ 'run ONE step by name — an iteration loop, NOT A GATE RUN; the gate is this command with no flag',
50
+ },
37
51
  ],
38
52
  },
39
53
  async run(ctx: CommandContext): Promise<CommandResult> {
40
54
  const root = requireAppRoot('verify', ctx.cwd).dir;
55
+ // Both readers before the run: an unrunnable flag must be refused in milliseconds, not after
56
+ // `tsc -b` has spent fourteen seconds on a run the caller cannot use.
41
57
  const workers = readWorkers(ctx.args);
58
+ const only = readOnlyStep(ctx.args);
42
59
  return runVerify(VERIFY_STEPS, {
43
60
  root,
44
61
  runner: ctx.runner,
45
62
  ...(workers === undefined ? {} : { workers }),
63
+ ...(only === undefined ? {} : { only }),
46
64
  });
47
65
  },
48
66
  };
49
67
 
68
+ /**
69
+ * The step `--only` names, or nothing. Refused against `VERIFY_STEP_NAMES` — the same constant the
70
+ * runner's list is built from — so a typo can never be read as "narrow to no steps at all", which
71
+ * is a run that passes by checking nothing.
72
+ *
73
+ * A near miss leads with the step it is near; a word near NOTHING gets the gate itself rather than
74
+ * an invented lead, which is the rule `parse.ts` already follows for a command that resembles
75
+ * none. Both arms are commands that run.
76
+ */
77
+ export const readOnlyStep = (args: ParsedArgs): VerifyStepName | undefined => {
78
+ const raw = flagString(args, 'only');
79
+ if (raw === undefined) return undefined;
80
+ const found = VERIFY_STEP_NAMES.find((name) => name === raw);
81
+ if (found !== undefined) return found;
82
+ const suggestion = nearestName(raw, VERIFY_STEP_NAMES);
83
+ throw new BadFlagError({
84
+ flag: 'only',
85
+ command: 'verify',
86
+ reason: `"${raw}" is not a gate step (${VERIFY_STEP_NAMES.join(', ')})`,
87
+ fix: suggestion === undefined ? 'x verify --json' : `x verify --only ${suggestion} --json`,
88
+ });
89
+ };
90
+
50
91
  /**
51
92
  * Both bounds are the constants the flag summary already names, so `x help verify` and the reader
52
93
  * cannot disagree. Exported for the test that pins them: the command's `run` reaches this only
package/src/dev-cache.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  resetTiers,
18
18
  } from '@ultimat3/cache';
19
19
  import { logger } from '@ultimat3/core';
20
- import type { Transport, TransportSubscription } from '@ultimat3/realtime';
20
+ import type { Transport, TransportSubscription } from '@ultimat3/realtime/server';
21
21
  import type { Env } from './dev-services';
22
22
 
23
23
  /**