@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.
- package/CLAUDE.md +15 -1
- package/README.md +8 -3
- package/package.json +25 -25
- package/src/app-boundaries.ts +55 -5
- package/src/bin.ts +6 -3
- package/src/ci-log.ts +0 -0
- package/src/cmd-db-backfill.ts +240 -0
- package/src/cmd-db-branch.ts +3 -2
- package/src/cmd-db.ts +35 -156
- package/src/cmd-deploy.ts +37 -3
- package/src/cmd-dev.ts +7 -1
- package/src/cmd-errors.ts +2 -3
- package/src/cmd-fix.ts +3 -3
- package/src/cmd-i18n.ts +67 -5
- package/src/cmd-jobs.ts +27 -4
- package/src/cmd-mcp.ts +18 -9
- package/src/cmd-new.ts +91 -4
- package/src/cmd-policy.ts +3 -2
- package/src/cmd-pr.ts +55 -4
- package/src/cmd-registries.ts +3 -2
- package/src/cmd-shot.ts +68 -6
- package/src/cmd-tasks.ts +9 -4
- package/src/cmd-verify.ts +47 -6
- package/src/dev-cache.ts +1 -1
- package/src/dev-lock.ts +124 -12
- package/src/dev-queue.ts +12 -7
- package/src/dev-replicator.ts +3 -7
- package/src/dev-roles-fixture.ts +1 -1
- package/src/dev-roles.ts +40 -8
- package/src/dev-runtime.ts +96 -4
- package/src/dev-sync.ts +9 -4
- package/src/dispatch.ts +35 -5
- package/src/drift.ts +52 -7
- package/src/error-codes.ts +5 -0
- package/src/framework-scope.ts +57 -5
- package/src/generate-kinds.ts +19 -1
- package/src/i18n-registration.ts +67 -4
- package/src/index.ts +1 -1
- package/src/jobs-report.ts +10 -13
- package/src/mcp-errors.ts +3 -0
- package/src/messages.ts +12 -0
- package/src/output.ts +22 -2
- package/src/parse.ts +81 -37
- package/src/realtime-browser-probe-fixture.ts +9 -0
- package/src/runtime-overrides.ts +11 -3
- package/src/shot-settle.ts +57 -0
- package/src/shot-verdict.ts +27 -4
- package/src/sync-authenticator.ts +86 -14
- package/src/templates/guard-bare-error.ts +122 -0
- package/src/templates/guard-raw-colour.ts +138 -0
- package/src/templates/guard-untranslated-string.ts +138 -0
- package/src/templates/guard-unzoned-date.ts +142 -0
- package/src/templates/index.ts +3 -0
- package/src/templates/island.ts +2 -1
- package/src/templates/route.ts +1 -1
- package/src/templates/scaffold-app.ts +3 -82
- package/src/templates/scaffold-container.ts +30 -4
- package/src/templates/scaffold-db-package.ts +14 -6
- package/src/templates/scaffold-docs.ts +24 -13
- package/src/templates/scaffold-entries.ts +131 -0
- package/src/templates/scaffold-guards.ts +26 -0
- package/src/templates/scaffold-repo.ts +37 -6
- package/src/test-select.ts +4 -3
- package/src/verify-run.ts +25 -3
- package/src/verify-step.ts +11 -2
- package/src/verify-tests.ts +11 -3
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
package/src/cmd-registries.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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 =
|
|
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
|
-
{
|
|
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
|
|
4
|
-
//
|
|
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
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
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
|
/**
|