@ultimat3/cli 3.0.0 → 4.1.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 +69 -10
- package/package.json +24 -24
- package/src/budgets.ts +8 -5
- package/src/cmd-deploy.ts +42 -14
- package/src/cmd-docs.ts +7 -3
- package/src/cmd-errors.ts +5 -4
- package/src/cmd-fix.ts +15 -3
- package/src/cmd-generate.ts +29 -4
- package/src/cmd-help.ts +25 -4
- package/src/cmd-i18n.ts +8 -5
- package/src/cmd-jobs.ts +6 -5
- package/src/cmd-mcp.ts +16 -12
- package/src/cmd-new.ts +9 -13
- package/src/cmd-planned.ts +13 -0
- package/src/cmd-policy.ts +8 -6
- package/src/cmd-registries.ts +7 -6
- package/src/cmd-routes.ts +27 -4
- package/src/cmd-secrets.ts +6 -6
- package/src/cmd-verify.ts +55 -3
- package/src/command.ts +10 -2
- package/src/dev-cache.ts +9 -9
- package/src/dev-render.ts +6 -1
- package/src/dev-runtime.ts +2 -2
- package/src/dispatch.ts +33 -4
- package/src/error-codes.ts +5 -0
- package/src/error-contract.ts +31 -4
- package/src/fix-command.ts +9 -2
- package/src/fix-imports.ts +118 -0
- package/src/fix-scan.ts +251 -0
- package/src/flag-reads.ts +114 -0
- package/src/i18n-audit.ts +2 -1
- package/src/index.ts +8 -1
- package/src/jobs-drain.ts +6 -1
- package/src/mcp-errors.ts +5 -0
- package/src/mcp-host.ts +4 -2
- package/src/messages.ts +3 -0
- package/src/otlp-export.ts +14 -0
- package/src/output.ts +8 -5
- package/src/parse.ts +6 -1
- package/src/seo-meta.ts +105 -0
- package/src/templates/action.ts +39 -7
- package/src/templates/backfill.ts +3 -1
- package/src/templates/index.ts +10 -1
- package/src/templates/job.ts +6 -2
- package/src/templates/query.ts +6 -1
- package/src/templates/route.ts +18 -9
- package/src/templates/scaffold-api.ts +100 -0
- package/src/templates/scaffold-app.ts +8 -48
- package/src/templates/scaffold-container.ts +44 -9
- package/src/templates/scaffold-helm-templates.ts +327 -0
- package/src/templates/scaffold-helm.ts +144 -0
- package/src/templates/scaffold-repo.ts +25 -8
- package/src/ts-scan.ts +12 -174
- package/src/verify-step.ts +5 -0
package/src/cmd-new.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { chmod } from 'node:fs/promises';
|
|
|
7
7
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
8
8
|
import { dedupe } from './cmd-generate';
|
|
9
9
|
import type { CliCommand, CommandContext } from './command';
|
|
10
|
+
import { MissingPositionalError } from './errors';
|
|
10
11
|
import { msg } from './messages';
|
|
11
12
|
import type { CommandResult } from './output';
|
|
12
13
|
import { flagBool, flagString } from './parse';
|
|
@@ -25,7 +26,7 @@ export function planNewApp(options: NewAppOptions): readonly GeneratedFile[] {
|
|
|
25
26
|
const app = names(options.name);
|
|
26
27
|
const files: GeneratedFile[] = [
|
|
27
28
|
...repoFiles(app, loadVersion(), options.example),
|
|
28
|
-
...appFiles(app),
|
|
29
|
+
...appFiles(app, options.example),
|
|
29
30
|
];
|
|
30
31
|
if (options.example) {
|
|
31
32
|
files.push(...resourceFiles('post', { surfaceDir: 'apps/web/app', feature: 'post' }));
|
|
@@ -82,20 +83,15 @@ export const newCommand: CliCommand = {
|
|
|
82
83
|
},
|
|
83
84
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
84
85
|
const raw = ctx.args.positionals[0];
|
|
86
|
+
// The class, not a hand-built finding with the same code: `MissingPositionalError` is what
|
|
87
|
+
// names the missing POSITIONAL, and a finding assembled here is a second, unenforced copy of
|
|
88
|
+
// a cause the class already writes — one that said "x new needs a name" and not what a name is.
|
|
85
89
|
if (raw === undefined) {
|
|
86
|
-
|
|
87
|
-
ok: false,
|
|
90
|
+
throw new MissingPositionalError({
|
|
88
91
|
command: 'new',
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
code: 'X_CLI_BAD_FLAG',
|
|
93
|
-
cause: 'x new needs a name',
|
|
94
|
-
fix: 'x new myapp',
|
|
95
|
-
docs: 'https://ultimate.dev/errors/X_CLI_BAD_FLAG',
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
};
|
|
92
|
+
positional: 'name',
|
|
93
|
+
example: 'x new myapp',
|
|
94
|
+
});
|
|
99
95
|
}
|
|
100
96
|
const app = names(raw);
|
|
101
97
|
const target = resolve(parentDir(ctx.cwd, flagString(ctx.args, 'dir')), app.kebab);
|
package/src/cmd-planned.ts
CHANGED
|
@@ -91,6 +91,19 @@ export const PLANNED_COMMANDS: readonly PlannedCommand[] = [
|
|
|
91
91
|
},
|
|
92
92
|
];
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The planned command a resolved command NAME belongs to, if any. Exported for `dispatch.ts`'s
|
|
96
|
+
* parse-failure branch: the parser refuses an undeclared flag before any `run` is reached, and a
|
|
97
|
+
* planned command declares only the four global flags — so `x logs tail --follow` answered
|
|
98
|
+
* X_CLI_BAD_FLAG listing "known: json, help, cwd, verbose", a flag set belonging to a command that
|
|
99
|
+
* does not exist yet, while `x logs tail` answered the honest X_NOT_IMPLEMENTED one invocation away.
|
|
100
|
+
*
|
|
101
|
+
* Takes the name `commandFor` resolved, never the raw word: aliases are that function's business
|
|
102
|
+
* and a second matcher here would be a second answer to "which command did they type".
|
|
103
|
+
*/
|
|
104
|
+
export const plannedCommandFor = (name: string | undefined): PlannedCommand | undefined =>
|
|
105
|
+
PLANNED_COMMANDS.find((planned) => planned.name === name);
|
|
106
|
+
|
|
94
107
|
export interface PlannedSubcommand {
|
|
95
108
|
readonly command: string;
|
|
96
109
|
readonly subcommand: string;
|
package/src/cmd-policy.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { loadApp } from './app-load';
|
|
6
6
|
import { requireAppRoot } from './app-root';
|
|
7
7
|
import type { CliCommand, CommandContext } from './command';
|
|
8
|
-
import {
|
|
8
|
+
import { DeclarationUnknownError, MissingPositionalError } from './errors';
|
|
9
9
|
import { msg } from './messages';
|
|
10
10
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
11
11
|
import { nearest } from './parse';
|
|
@@ -74,11 +74,13 @@ function declarationLines(declaration: DeclarationExplanation): readonly string[
|
|
|
74
74
|
function requireSubject(ctx: CommandContext): string {
|
|
75
75
|
const name = ctx.args.positionals[0];
|
|
76
76
|
if (name === undefined) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// Never `BadFlagError`: its cause read `--subject on "x policy"`, and the next thing an agent
|
|
78
|
+
// typed was `x policy explain --subject posts:read`, which is a second X_CLI_BAD_FLAG for a
|
|
79
|
+
// flag this command does not declare. The positional is what is missing, so it is what is named.
|
|
80
|
+
throw new MissingPositionalError({
|
|
81
|
+
command: 'policy explain',
|
|
82
|
+
positional: 'subject',
|
|
83
|
+
example: 'x policy list --json',
|
|
82
84
|
});
|
|
83
85
|
}
|
|
84
86
|
return name;
|
package/src/cmd-registries.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { describeQueries, getQuery } from '@ultimat3/query';
|
|
|
13
13
|
import { loadApp } from './app-load';
|
|
14
14
|
import { requireAppRoot } from './app-root';
|
|
15
15
|
import type { CliCommand, CommandContext } from './command';
|
|
16
|
-
import {
|
|
16
|
+
import { DeclarationUnknownError, MissingPositionalError } from './errors';
|
|
17
17
|
import { msg } from './messages';
|
|
18
18
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
19
19
|
import type { CommandSpec } from './parse';
|
|
@@ -143,11 +143,12 @@ function describeResult<D extends { readonly name: string }, Raw extends { descr
|
|
|
143
143
|
): CommandResult {
|
|
144
144
|
const name = ctx.args.positionals[0];
|
|
145
145
|
if (name === undefined) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
// A positional, so `MissingPositionalError` — `--name on "x actions"` named a flag no registry
|
|
147
|
+
// command declares, and reading it as one is a second refusal for the first one's advice.
|
|
148
|
+
throw new MissingPositionalError({
|
|
149
|
+
command: `${kind.kind} describe`,
|
|
150
|
+
positional: 'name',
|
|
151
|
+
example: `x ${kind.kind} list --json`,
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
const raw = kind.find(name);
|
package/src/cmd-routes.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// The rows are `@ultimat3/render`'s own `describeRoutes()`: the CLI prints the route table, it
|
|
5
5
|
// does not keep a second one.
|
|
6
6
|
|
|
7
|
-
import type { RouteDescriptor } from '@ultimat3/render';
|
|
8
|
-
import { describeRoutes } from '@ultimat3/render';
|
|
7
|
+
import type { RouteDescriptor, Surface } from '@ultimat3/render';
|
|
8
|
+
import { describeRoutes, SURFACES } from '@ultimat3/render';
|
|
9
9
|
import { loadApp } from './app-load';
|
|
10
10
|
import { requireAppRoot } from './app-root';
|
|
11
11
|
import type { CliCommand, CommandContext } from './command';
|
|
12
|
+
import { BadFlagError } from './errors';
|
|
12
13
|
import { msg } from './messages';
|
|
13
14
|
import type { CommandResult, JsonValue } from './output';
|
|
14
15
|
import { flagString } from './parse';
|
|
@@ -43,18 +44,40 @@ const routeJson = (routes: readonly RouteDescriptor[]): JsonValue =>
|
|
|
43
44
|
budget: { js: route.budgetJs, lcp: route.budgetLcp },
|
|
44
45
|
}));
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A closed set, because the filter was a bare `===`: `x routes --surface App` and `--surface pages`
|
|
49
|
+
* matched no row and reported `0 routes` with exit 0, which is the same output an app with no
|
|
50
|
+
* routes gives — so a typo and an empty route table are indistinguishable, and only one of them is
|
|
51
|
+
* a bug the caller can see. `SURFACES` is `@ultimat3/render`'s own declaration of what a surface
|
|
52
|
+
* is; a list restated here would be a second answer to it (`x g --surface` is `generate-kinds.ts`'s
|
|
53
|
+
* narrower question — which surface to SCAFFOLD onto — and takes site|app alone).
|
|
54
|
+
*/
|
|
55
|
+
export function readSurfaceFilter(raw: string | undefined): Surface | undefined {
|
|
56
|
+
const surfaces: readonly string[] = SURFACES;
|
|
57
|
+
if (raw === undefined) return undefined;
|
|
58
|
+
if (surfaces.includes(raw)) return raw as Surface;
|
|
59
|
+
throw new BadFlagError({
|
|
60
|
+
flag: 'surface',
|
|
61
|
+
command: 'routes',
|
|
62
|
+
reason: `"${raw}" is not a surface (known: ${SURFACES.join(', ')})`,
|
|
63
|
+
fix: 'x routes --surface app --json',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
export const routesCommand: CliCommand = {
|
|
47
68
|
spec: {
|
|
48
69
|
name: 'routes',
|
|
49
70
|
summary: 'the route table: path, surface, render mode, hydrate, offline',
|
|
50
|
-
usage: 'x routes [--surface site|app] [--json]',
|
|
71
|
+
usage: 'x routes [--surface site|app|api|shared] [--json]',
|
|
51
72
|
requiresApp: true,
|
|
52
73
|
flags: [{ name: 'surface', type: 'string', summary: 'filter by surface' }],
|
|
53
74
|
},
|
|
54
75
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
55
76
|
const root = requireAppRoot('routes', ctx.cwd).dir;
|
|
77
|
+
// Read before the app is loaded: a typo must not cost a boot to report, the rule `x mcp`'s
|
|
78
|
+
// `--transport` already follows.
|
|
79
|
+
const surface = readSurfaceFilter(flagString(ctx.args, 'surface'));
|
|
56
80
|
const { findings } = await loadApp(root);
|
|
57
|
-
const surface = flagString(ctx.args, 'surface');
|
|
58
81
|
const routes = describeRoutes().filter(
|
|
59
82
|
(route) => surface === undefined || route.surface === surface,
|
|
60
83
|
);
|
package/src/cmd-secrets.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { ENV_SCHEMA_EXPORT, loadEnvSchema } from './app-env';
|
|
|
32
32
|
import { requireAppRoot } from './app-root';
|
|
33
33
|
import type { CliCommand, CommandContext } from './command';
|
|
34
34
|
import {
|
|
35
|
-
|
|
35
|
+
MissingPositionalError,
|
|
36
36
|
SecretsEditFailedError,
|
|
37
37
|
SecretsEditorMissingError,
|
|
38
38
|
SecretsExistsError,
|
|
@@ -210,11 +210,11 @@ async function edit(ctx: CommandContext, io: SecretsIo): Promise<CommandResult>
|
|
|
210
210
|
async function set(ctx: CommandContext, io: SecretsIo): Promise<CommandResult> {
|
|
211
211
|
const name = ctx.args.positionals[0];
|
|
212
212
|
if (name === undefined) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
command: 'secrets',
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
// The name is the positional this command seals under, never a `--name` flag it does not have.
|
|
214
|
+
throw new MissingPositionalError({
|
|
215
|
+
command: 'secrets set',
|
|
216
|
+
positional: 'NAME',
|
|
217
|
+
example: 'printf %s "$TOKEN" | x secrets set STRIPE_KEY --json',
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
220
|
const { root, key } = open(ctx, 'set');
|
package/src/cmd-verify.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
MANIFEST_FILENAME,
|
|
14
14
|
verifyContract,
|
|
15
15
|
} from '@ultimat3/manifest';
|
|
16
|
+
import type { MetaIssue } from '@ultimat3/seo';
|
|
17
|
+
import { validateMeta } from '@ultimat3/seo';
|
|
16
18
|
import { checkAgentsMd } from './app-agents-md';
|
|
17
19
|
import { checkAppBoundaries } from './app-boundaries';
|
|
18
20
|
import { envExampleFindings } from './app-env';
|
|
@@ -24,13 +26,14 @@ import type { CliCommand, CommandContext } from './command';
|
|
|
24
26
|
import { checkDestructiveMigrations } from './db-destructive';
|
|
25
27
|
import { checkDocumentStyles, documentSurfaces } from './document-styles';
|
|
26
28
|
import { checkSourceDrift } from './drift';
|
|
27
|
-
import {
|
|
29
|
+
import { checkErrorFixReport } from './error-contract';
|
|
28
30
|
import { readIntFlag } from './flag-number';
|
|
29
31
|
import { guardFindings } from './guards';
|
|
30
32
|
import { msg } from './messages';
|
|
31
33
|
import type { CommandResult, Finding, StepResult } from './output';
|
|
32
34
|
import { findingFrom } from './output';
|
|
33
35
|
import type { ParsedArgs } from './parse';
|
|
36
|
+
import { scanSiteMeta } from './seo-meta';
|
|
34
37
|
import { WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
|
|
35
38
|
import {
|
|
36
39
|
floorProblemFindings,
|
|
@@ -115,8 +118,21 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
115
118
|
summary: 'every X_* code has a runnable fix and a docs page',
|
|
116
119
|
// The fix-line half runs anywhere source does. The docs half needs a reference page to check
|
|
117
120
|
// against, and which file that is belongs to the host repo — hence `hostFindings`.
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
//
|
|
122
|
+
// The coverage line rides in `output`, which `--json` carries verbatim: a scan without a
|
|
123
|
+
// parser cannot read every fix, and a step that reports only findings claims a completeness
|
|
124
|
+
// it does not have. "checked 412, could not read 27" is what a reader can act on.
|
|
125
|
+
async run(ctx) {
|
|
126
|
+
const report = await checkErrorFixReport(ctx.root);
|
|
127
|
+
const findings = [...report.findings, ...(await hostFindings(ctx, 'errors'))];
|
|
128
|
+
return {
|
|
129
|
+
...fromFindings(findings),
|
|
130
|
+
output: msg('cli.verify.fixCoverage', {
|
|
131
|
+
checked: report.checked,
|
|
132
|
+
unreadable: report.unreadable,
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
},
|
|
120
136
|
},
|
|
121
137
|
...TEST_STEPS,
|
|
122
138
|
{
|
|
@@ -190,6 +206,30 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
190
206
|
]);
|
|
191
207
|
},
|
|
192
208
|
},
|
|
209
|
+
{
|
|
210
|
+
name: 'seo',
|
|
211
|
+
summary: 'every indexable site/ route has a title and a description a search result can render',
|
|
212
|
+
// The SEO checkers shipped in `@ultimat3/seo` with no caller anywhere — `validateMeta` and its
|
|
213
|
+
// asserts were reachable only by an app that called them itself, which is what
|
|
214
|
+
// `packages/seo/src/errors.ts`'s own header said. This is the caller.
|
|
215
|
+
//
|
|
216
|
+
// Its own step rather than a rider on `budgets`: that step asks what a document WEIGHS and
|
|
217
|
+
// this one asks what it SAYS, and a missing `<title>` reported under `budgets` would hand the
|
|
218
|
+
// reader a fix for the wrong question (axiom 4). It costs no second app load — `loadApp`
|
|
219
|
+
// imports each module once per process, so this runs on the registries `budgets` just filled.
|
|
220
|
+
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
221
|
+
async run(ctx) {
|
|
222
|
+
const scan = await scanSiteMeta(ctx.root);
|
|
223
|
+
// No `baseUrl`: an app declares no base URL anywhere (`packages/core/src/config.ts` has no
|
|
224
|
+
// such key), so canonical checks are skipped rather than run against an origin this file
|
|
225
|
+
// invented. `seo-meta.ts` spells out why that is the honest half.
|
|
226
|
+
const report = validateMeta(scan.records);
|
|
227
|
+
return {
|
|
228
|
+
ok: scan.findings.length === 0 && report.ok,
|
|
229
|
+
findings: [...scan.findings, ...report.issues.map(seoFinding)],
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
},
|
|
193
233
|
{
|
|
194
234
|
name: 'manifest',
|
|
195
235
|
summary: 'the files an agent reads: generated facts, hand-written conventions, the env example',
|
|
@@ -384,6 +424,18 @@ function findingOf(error: unknown, step: string): Finding {
|
|
|
384
424
|
};
|
|
385
425
|
}
|
|
386
426
|
|
|
427
|
+
/**
|
|
428
|
+
* One `MetaIssue` as the gate reports it. `at` is the route FILE and never the URL: every seo error
|
|
429
|
+
* already names the file in its cause, and `at` is what an agent opens.
|
|
430
|
+
*/
|
|
431
|
+
const seoFinding = (issue: MetaIssue): Finding => ({
|
|
432
|
+
code: issue.code,
|
|
433
|
+
cause: issue.cause,
|
|
434
|
+
fix: issue.fix,
|
|
435
|
+
docs: `https://ultimate.dev/errors/${issue.code}`,
|
|
436
|
+
at: issue.file,
|
|
437
|
+
});
|
|
438
|
+
|
|
387
439
|
export const verifyCommand: CliCommand = {
|
|
388
440
|
spec: {
|
|
389
441
|
name: 'verify',
|
package/src/command.ts
CHANGED
|
@@ -20,14 +20,22 @@ export interface CliCommand {
|
|
|
20
20
|
run(ctx: CommandContext): Promise<CommandResult>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* `ok` is written AFTER the spread in both helpers, and that order is the whole contract: the
|
|
25
|
+
* function's NAME is the verdict, and `extra` may carry every other field. Spread last, a caller
|
|
26
|
+
* passing `{ ok: true }` to `failed()` got a result `exitCodeFor` exits 0 on while its own summary
|
|
27
|
+
* says it failed — a green CI over a red command. `command` and `summary` stay before the spread
|
|
28
|
+
* on purpose: those are arguments a caller may legitimately refine, and only the verdict is the
|
|
29
|
+
* helper's to keep.
|
|
30
|
+
*/
|
|
23
31
|
export const ok = (
|
|
24
32
|
command: string,
|
|
25
33
|
summary: string,
|
|
26
34
|
extra: Partial<CommandResult> = {},
|
|
27
|
-
): CommandResult => ({
|
|
35
|
+
): CommandResult => ({ command, summary, ...extra, ok: true });
|
|
28
36
|
|
|
29
37
|
export const failed = (
|
|
30
38
|
command: string,
|
|
31
39
|
summary: string,
|
|
32
40
|
extra: Partial<CommandResult> = {},
|
|
33
|
-
): CommandResult => ({
|
|
41
|
+
): CommandResult => ({ command, summary, ...extra, ok: false });
|
package/src/dev-cache.ts
CHANGED
|
@@ -79,25 +79,25 @@ export function startCacheTiers(options: CacheTiersOptions): () => Promise<void>
|
|
|
79
79
|
registerInvalidationBroadcast(async (wireTags) => {
|
|
80
80
|
await options.transport.publish(CACHE_INVALIDATE_SUBJECT, JSON.stringify(wireTags));
|
|
81
81
|
});
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
82
|
+
// Not awaited HERE: the boot must not block on a subscribe, and a bus that refuses one is a
|
|
83
|
+
// process that misses peer invalidations, never a process that fails to start. The PROMISE is
|
|
84
|
+
// held rather than a handle assigned inside a `.then`, because the release ran first whenever
|
|
85
|
+
// `stop()` beat the round trip — a NATS bus plus a boot that throws in `bootRoles`, or a test
|
|
86
|
+
// that boots and stops immediately — and the subscription that landed afterwards was live with
|
|
87
|
+
// nobody left holding it. `mcp-host.ts`'s lazy `started` is the same shape.
|
|
88
|
+
const subscribing: Promise<TransportSubscription | undefined> = options.transport
|
|
86
89
|
.subscribe(CACHE_INVALIDATE_SUBJECT, (payload: string) => {
|
|
87
90
|
void applyBroadcast(payload);
|
|
88
91
|
})
|
|
89
|
-
.then((handle) => {
|
|
90
|
-
subscription = handle;
|
|
91
|
-
})
|
|
92
92
|
.catch((error: unknown) => {
|
|
93
93
|
logger.warn('cache.broadcast.subscribe-failed', { error: messageOf(error) });
|
|
94
|
+
return undefined;
|
|
94
95
|
});
|
|
95
96
|
|
|
96
97
|
// `resetTiers()` drops the registry AND the broadcast in one call: this boot is the only thing
|
|
97
98
|
// that registers either, and a tier left behind would purge for a process that has stopped.
|
|
98
99
|
return async () => {
|
|
99
|
-
|
|
100
|
-
subscription = undefined;
|
|
100
|
+
(await subscribing)?.unsubscribe();
|
|
101
101
|
resetTiers();
|
|
102
102
|
};
|
|
103
103
|
}
|
package/src/dev-render.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
createIsrController,
|
|
25
25
|
headFromMeta,
|
|
26
26
|
hydrateRuntime,
|
|
27
|
+
isrKey,
|
|
27
28
|
metaContextFor,
|
|
28
29
|
renderComponent,
|
|
29
30
|
renderHead,
|
|
@@ -185,7 +186,11 @@ async function resultFor(
|
|
|
185
186
|
return { status: 200, headers: staticHeaders(contentHash(body), options.buildId), body };
|
|
186
187
|
}
|
|
187
188
|
case 'isr': {
|
|
188
|
-
|
|
189
|
+
// `isrKey(url)`, never `url.pathname`: the query is part of what was rendered — this
|
|
190
|
+
// route's own `meta` reads `data.url` — so two URLs differing only in their query are two
|
|
191
|
+
// documents. Keyed on the pathname alone, the first render answered every later query
|
|
192
|
+
// string (#171). Render owns the derivation so no second caller can invent another.
|
|
193
|
+
const served = await isr.serve(isrKey(url), () =>
|
|
189
194
|
documentFrom(entry, request, data, options),
|
|
190
195
|
);
|
|
191
196
|
return served.result;
|
package/src/dev-runtime.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { mkdirSync } from 'node:fs';
|
|
7
7
|
import type { PurgeDriver } from '@ultimat3/cache';
|
|
8
8
|
import { isNoopPurgeDriver, selectPurgeDriver } from '@ultimat3/cache';
|
|
9
|
-
import { isLocal, resolveEnvironment } from '@ultimat3/core';
|
|
9
|
+
import { isLocal, renderThrowable, resolveEnvironment } from '@ultimat3/core';
|
|
10
10
|
import type { EventBus, JobDriver, OutboxStore } from '@ultimat3/jobs';
|
|
11
11
|
import type { MailDriver } from '@ultimat3/mail';
|
|
12
12
|
import {
|
|
@@ -167,7 +167,7 @@ export function startStorage(services: DevServices, env: Env, override?: Storage
|
|
|
167
167
|
try {
|
|
168
168
|
mkdirSync(root, { recursive: true });
|
|
169
169
|
} catch (cause) {
|
|
170
|
-
const detail =
|
|
170
|
+
const detail = renderThrowable(cause);
|
|
171
171
|
throw new StorageUnwritableError(
|
|
172
172
|
`the embedded storage disk needs ${root} and it could not be created: ${detail}`,
|
|
173
173
|
`mount a writable volume at ${root}, or set S3_ENDPOINT and S3_BUCKET to use object storage instead`,
|
package/src/dispatch.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// path as results, so a failure is machine-readable exactly like a success.
|
|
4
4
|
|
|
5
5
|
import { isAbsolute, resolve } from 'node:path';
|
|
6
|
-
import { requireBunVersion } from './app-root';
|
|
6
|
+
import { requireAppRoot, requireBunVersion } from './app-root';
|
|
7
7
|
import { createHelpCommand } from './cmd-help';
|
|
8
|
+
import { plannedCommandFor } from './cmd-planned';
|
|
8
9
|
import type { CommandContext } from './command';
|
|
9
|
-
import { UnknownCommandError } from './errors';
|
|
10
|
+
import { CliNotImplementedError, UnknownCommandError } from './errors';
|
|
10
11
|
import type { Runner } from './exec';
|
|
11
12
|
import { exec } from './exec';
|
|
12
13
|
import type { CommandResult } from './output';
|
|
@@ -42,14 +43,33 @@ const errorResult = (command: string, error: unknown): CommandResult => ({
|
|
|
42
43
|
* end without terminating the test runner.
|
|
43
44
|
*/
|
|
44
45
|
export async function dispatch(options: DispatchOptions): Promise<number> {
|
|
45
|
-
|
|
46
|
+
// Its own branch, ahead of the parse: an unsupported Bun is a fact about the environment and
|
|
47
|
+
// outranks anything argv says, including the planned pre-empt below.
|
|
46
48
|
try {
|
|
47
49
|
requireBunVersion(options.bunVersion);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
options.write(render(errorResult('x', error), wantsJson(options.argv)));
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let args: ParsedArgs;
|
|
56
|
+
try {
|
|
48
57
|
args = parseArgs(options.argv, SPECS);
|
|
49
58
|
} catch (error) {
|
|
59
|
+
// A PLANNED command is not built, so every invocation of one must say that and nothing else.
|
|
60
|
+
// The parser refuses an undeclared flag before any `run` is reached and a planned command
|
|
61
|
+
// declares only the four globals, so `x logs tail --follow` reported X_CLI_BAD_FLAG — a flag
|
|
62
|
+
// list for a command that does not exist yet — while `x logs tail` reported the honest
|
|
63
|
+
// X_NOT_IMPLEMENTED with a runnable fix. Substituted here rather than in `parse.ts`, which is
|
|
64
|
+
// pure and knows nothing about what a command means; the precedent is the help swap below.
|
|
65
|
+
const planned = plannedCommandFor(commandFor(options.argv[0] ?? '')?.spec.name);
|
|
66
|
+
const failure =
|
|
67
|
+
planned === undefined
|
|
68
|
+
? error
|
|
69
|
+
: new CliNotImplementedError({ feature: `x ${planned.name}`, fix: planned.fix });
|
|
50
70
|
// `wantsJson`, not `includes('--json')`: a typo'd flag or a typo'd command is exactly the case
|
|
51
71
|
// an agent hits while always passing `-j`, and the short form rendered prose it then parsed.
|
|
52
|
-
const result = errorResult('x',
|
|
72
|
+
const result = errorResult(planned?.name ?? 'x', failure);
|
|
53
73
|
options.write(render(result, wantsJson(options.argv)));
|
|
54
74
|
return 1;
|
|
55
75
|
}
|
|
@@ -85,6 +105,15 @@ export async function dispatch(options: DispatchOptions): Promise<number> {
|
|
|
85
105
|
};
|
|
86
106
|
|
|
87
107
|
try {
|
|
108
|
+
// The reader `CommandSpec.requiresApp` never had. Its doc said "the dispatcher enforces it" and
|
|
109
|
+
// `dispatch` did not read the field at all: the guarantee held only because all 17 declaring
|
|
110
|
+
// commands happen to call `requireAppRoot` themselves, so a new command that declares it and
|
|
111
|
+
// forgets the call ran outside an app with no refusal. Each of those 17 calls stays — they are
|
|
112
|
+
// what hands a command the root it works in, and several name a subcommand this cannot see
|
|
113
|
+
// (`env init`, `secrets set`) — but the DECLARATION is now what decides, ahead of any check a
|
|
114
|
+
// command makes about its own arguments. `--help` is exempt because `target` is then the help
|
|
115
|
+
// command, which declares nothing: usage for a command must be readable from anywhere.
|
|
116
|
+
if (target.spec.requiresApp === true) requireAppRoot(target.spec.name, ctx.cwd);
|
|
88
117
|
const result = await target.run(ctx);
|
|
89
118
|
options.write(render(result, args.json, args.flags.get('verbose') === true));
|
|
90
119
|
// `x dev` and `x mcp serve --transport http` are still listening here: report first, so the
|
package/src/error-codes.ts
CHANGED
|
@@ -86,6 +86,10 @@ export const CLI_OWNED_ERROR_CODES = [
|
|
|
86
86
|
'X_GUARD_INVALID',
|
|
87
87
|
'X_GUARD_FAILED',
|
|
88
88
|
'X_GUARD_FINDING_INVALID',
|
|
89
|
+
// The CLI's own declarations, held to each other. A flag the parser accepts and no code reads
|
|
90
|
+
// is a promise in `x help` with nothing behind it — `x deploy --critical` said "forces clients
|
|
91
|
+
// to reload" and reached no reader outside the plan JSON it was written into.
|
|
92
|
+
'X_CLI_FLAG_UNREAD',
|
|
89
93
|
// The two halves of `x secrets edit` that belong to the terminal rather than to the envelope.
|
|
90
94
|
// `@ultimat3/core` owns every X_SECRETS_* code about the file and the key; an editor is the
|
|
91
95
|
// CLI's problem alone, and core would have no `fix:` to offer for one.
|
|
@@ -179,6 +183,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
|
179
183
|
X_GUARD_INVALID: 'a file in guards/ exports no usable guard',
|
|
180
184
|
X_GUARD_FAILED: 'an app guard threw instead of returning findings',
|
|
181
185
|
X_GUARD_FINDING_INVALID: "an app guard's finding breaks the error contract",
|
|
186
|
+
X_CLI_FLAG_UNREAD: 'a command declares a flag no code reads',
|
|
182
187
|
X_SECRETS_EDITOR_MISSING: 'no $EDITOR to open the decrypted secrets in',
|
|
183
188
|
X_SECRETS_EDIT_FAILED: 'the editor exited non-zero, so nothing was resealed',
|
|
184
189
|
};
|
package/src/error-contract.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { docsFor } from './error-codes';
|
|
10
10
|
import { citedCommandProblem, loadCommandCatalog } from './fix-command';
|
|
11
|
+
import { createHelperResolver } from './fix-imports';
|
|
12
|
+
import { scanFixSites } from './fix-scan';
|
|
11
13
|
import type { Finding } from './output';
|
|
12
14
|
import { eachSourceFile, isGenerated, isTest } from './source-files';
|
|
13
15
|
import type { CodeSite, FixSite } from './ts-scan';
|
|
14
|
-
import { isCodeRegistry, scanBorrowedCodes, scanCodes
|
|
16
|
+
import { isCodeRegistry, scanBorrowedCodes, scanCodes } from './ts-scan';
|
|
15
17
|
|
|
16
18
|
/** Advice, not instruction. The list is the one in `docs/architecture/04-error-contract.md`. */
|
|
17
19
|
export const BANNED_PHRASES: readonly RegExp[] = [
|
|
@@ -77,12 +79,33 @@ const fixFinding = (site: FixSite, problem: string): Finding => ({
|
|
|
77
79
|
* The catalog is loaded ONCE per run rather than per fix line: it is a dynamic import (see
|
|
78
80
|
* `fix-command.ts` for the cycle it breaks) and this walks every shipped source file.
|
|
79
81
|
*/
|
|
80
|
-
export
|
|
82
|
+
export interface ErrorFixReport {
|
|
83
|
+
readonly findings: readonly Finding[];
|
|
84
|
+
/** Fix literals actually read, and held to both rules. */
|
|
85
|
+
readonly checked: number;
|
|
86
|
+
/**
|
|
87
|
+
* Fix arguments at a known builder that hold no single literal — a parameter passed through, a
|
|
88
|
+
* concatenation, a table lookup. The step prints it, because a gate that says "checked 412,
|
|
89
|
+
* could not read 27" is honest and one that says nothing is the false green this check exists to
|
|
90
|
+
* close. It does NOT cover a builder imported from another PACKAGE: `candidatePaths` resolves
|
|
91
|
+
* relative specifiers only, and that gap is 3 call sites across this repo, measured 2026-08.
|
|
92
|
+
*/
|
|
93
|
+
readonly unreadable: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function checkErrorFixReport(root: string): Promise<ErrorFixReport> {
|
|
81
97
|
const findings: Finding[] = [];
|
|
82
98
|
const catalog = await loadCommandCatalog();
|
|
99
|
+
const imports = createHelperResolver(root);
|
|
100
|
+
let checked = 0;
|
|
101
|
+
let unreadable = 0;
|
|
83
102
|
for await (const path of eachSourceFile(root)) {
|
|
84
103
|
if (isTest(path) || isGenerated(path)) continue;
|
|
85
|
-
|
|
104
|
+
const source = await Bun.file(join(root, path)).text();
|
|
105
|
+
const scan = scanFixSites(source, path, await imports(path, source));
|
|
106
|
+
checked += scan.sites.length;
|
|
107
|
+
unreadable += scan.unreadable;
|
|
108
|
+
for (const site of scan.sites) {
|
|
86
109
|
// The interpolation-blanked form for both rules: `x ${name}` names no command this can
|
|
87
110
|
// resolve, and reading `<value>` as one would be a finding nobody can act on.
|
|
88
111
|
const fix = staticFix(site.fix);
|
|
@@ -90,9 +113,13 @@ export async function checkErrorFixes(root: string): Promise<readonly Finding[]>
|
|
|
90
113
|
if (problem !== undefined) findings.push(fixFinding(site, problem));
|
|
91
114
|
}
|
|
92
115
|
}
|
|
93
|
-
return findings;
|
|
116
|
+
return { findings, checked, unreadable };
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
/** The findings alone, for every caller that reports no coverage line. */
|
|
120
|
+
export const checkErrorFixes = async (root: string): Promise<readonly Finding[]> =>
|
|
121
|
+
(await checkErrorFixReport(root)).findings;
|
|
122
|
+
|
|
96
123
|
/**
|
|
97
124
|
* A code is documented when the reference page names it. Deliberately not "owns a table row": the
|
|
98
125
|
* page legitimately groups near-identical codes onto one row, and a rule that forbade that would
|
package/src/fix-command.ts
CHANGED
|
@@ -28,8 +28,15 @@ import { GLOBAL_FLAGS } from './parse';
|
|
|
28
28
|
// in `@ultimat3/mcp` — is `X_CLI_UNKNOWN_COMMAND` when run and resolved clean while a placeholder
|
|
29
29
|
// was invisible to the reader. Second and fourth slots are open positionals (`x new my-app`,
|
|
30
30
|
// `x db branch drop <name>`), where a placeholder is exactly right.
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// A `:` is part of a word only when a letter follows it, which is what separates the shipped
|
|
32
|
+
// positional `admin:page` from prose that ends a citation with a colon (`x verify: the gate`).
|
|
33
|
+
// Read without it, `x g admin:page` cites `x g admin` — a positional the CLI does not ship —
|
|
34
|
+
// and the one documented invocation of the admin-page generator was a standing false finding.
|
|
35
|
+
const WORD = String.raw`[a-z][a-z\d-]*(?::[a-z][a-z\d-]*)?`;
|
|
36
|
+
const CITATION = new RegExp(
|
|
37
|
+
String.raw`(?:^|[\s;|&("'\x60])x\s+(${WORD})(?:\s+(${WORD}))?(?:\s+(${WORD}|<[^>]*>))?`,
|
|
38
|
+
'g',
|
|
39
|
+
);
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* A long flag, `--` stripped. `--no-<name>` is the parser's negation of a boolean, so it resolves
|