@ultimat3/cli 10.0.0 → 11.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 +98 -0
- package/package.json +28 -26
- package/src/budgets.ts +6 -0
- package/src/cmd-db-backfill.ts +14 -1
- package/src/cmd-dev.ts +3 -0
- package/src/cmd-new.ts +34 -4
- package/src/command.ts +12 -0
- package/src/dev-assets.ts +6 -0
- package/src/dev-hooks.ts +8 -0
- package/src/dev-purge.ts +8 -2
- package/src/dev-render.ts +5 -2
- package/src/dev-roles.ts +26 -2
- package/src/dispatch.ts +8 -0
- package/src/error-catalog.ts +11 -1
- package/src/error-codes.ts +15 -0
- package/src/error-contract.ts +61 -4
- package/src/error-pages.ts +79 -0
- package/src/errors.ts +25 -0
- package/src/favicon.ts +113 -0
- package/src/fix-path.ts +104 -0
- package/src/hold.ts +73 -7
- package/src/index.ts +24 -1
- package/src/live-routes.ts +181 -0
- package/src/mcp-errors.ts +7 -0
- package/src/messages.ts +6 -4
- package/src/prerender.ts +32 -3
- package/src/script-csp.ts +17 -0
- package/src/serve.ts +12 -1
- package/src/templates/admin-page.ts +11 -7
- package/src/templates/imports.ts +26 -0
- package/src/templates/route.ts +2 -2
- package/src/templates/scaffold-app.ts +18 -6
- package/src/templates/scaffold-auth.ts +151 -0
- package/src/templates/scaffold-mcp-package.ts +6 -3
- package/src/templates/scaffold-repo.ts +1 -0
- package/src/ts-scan.ts +117 -6
- package/src/verify-checks.ts +17 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// action registry rather than re-listed, plus the test that pins every projected tool describing
|
|
3
3
|
// itself.
|
|
4
4
|
|
|
5
|
+
import { sortedImports } from './imports';
|
|
5
6
|
import type { GeneratedFile, NameSet } from './naming';
|
|
6
7
|
import { packageShapeFiles } from './scaffold-package-shape';
|
|
7
8
|
|
|
@@ -44,9 +45,11 @@ const mcpIndex = (
|
|
|
44
45
|
app: NameSet,
|
|
45
46
|
): string => `// The app's own MCP tools. Every action with mcp.expose is already a tool; add app-specific
|
|
46
47
|
// read-only helpers here. Authorization is the action's policy, unchanged.
|
|
47
|
-
|
|
48
|
-
import
|
|
49
|
-
import {
|
|
48
|
+
${sortedImports([
|
|
49
|
+
`import * as api from '@${app.kebab}/web/api/health';`,
|
|
50
|
+
`import { registerActions } from '@ultimat3/action';`,
|
|
51
|
+
`import { defineAppMcp } from '@ultimat3/mcp';`,
|
|
52
|
+
])}
|
|
50
53
|
|
|
51
54
|
// Names come from export names, so the registry agrees with the module the app already wrote.
|
|
52
55
|
registerActions(api);
|
|
@@ -63,6 +63,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
63
63
|
"@ultimat3/core": "^${version}",
|
|
64
64
|
"@ultimat3/db": "^${version}",
|
|
65
65
|
"@ultimat3/entity": "^${version}",
|
|
66
|
+
"@ultimat3/http": "^${version}",
|
|
66
67
|
"@ultimat3/i18n": "^${version}",
|
|
67
68
|
"@ultimat3/jobs": "^${version}",
|
|
68
69
|
"@ultimat3/mcp": "^${version}",
|
package/src/ts-scan.ts
CHANGED
|
@@ -19,6 +19,17 @@ export interface CodeSite extends SourceSite {
|
|
|
19
19
|
readonly code: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface UnresolvedCodeSite extends SourceSite {
|
|
23
|
+
/** The identifier exactly as written at the `code:` position. */
|
|
24
|
+
readonly name: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One file's codes, and the names at a `code:` position this scan could not turn into one. */
|
|
28
|
+
export interface CodeScan {
|
|
29
|
+
readonly sites: readonly CodeSite[];
|
|
30
|
+
readonly unresolved: readonly UnresolvedCodeSite[];
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
// `ReadonlySet`, so a consumer cannot mutate what every scan in this package reads.
|
|
23
34
|
export const QUOTES: ReadonlySet<string> = new Set(["'", '"', '`']);
|
|
24
35
|
export const OPENERS: ReadonlySet<string> = new Set(['(', '[', '{']);
|
|
@@ -202,27 +213,111 @@ const CODE_AT_KEY = /\bcode\s*[:=]\s*(['"`])(X_[A-Z0-9_]+)\1/g;
|
|
|
202
213
|
const CODE_LITERAL = /(['"`])(X_[A-Z0-9_]+)\1/g;
|
|
203
214
|
const CODE_KEY = /^[\t ]*(X_[A-Z0-9_]+)\s*:/gm;
|
|
204
215
|
|
|
216
|
+
/**
|
|
217
|
+
* A `code` KEY, and never a member assignment: `found.code = SOMETHING` projects somebody else's
|
|
218
|
+
* code and declares none. The literal form above keeps its looser `\b` deliberately — a scanner
|
|
219
|
+
* that stopped collecting a code it has collected for four majors would shrink the manifest.
|
|
220
|
+
*/
|
|
221
|
+
const CODE_KEY_POSITION = /(?<![.\w$])code\s*[:=]\s*/g;
|
|
222
|
+
|
|
223
|
+
/** Cheap enough to run on every file, so the masking pass below is paid only where it can pay. */
|
|
224
|
+
const HAS_CODE_IDENTIFIER = /(?<![.\w$])code\s*[:=]\s*[A-Za-z_$]/;
|
|
225
|
+
|
|
226
|
+
/** Sticky: the value expression is read at an exact offset, never out of a slice that may cut. */
|
|
227
|
+
const VALUE_IDENTIFIER = /([A-Za-z_$][\w$]*)\s*([.([]?)/y;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A module-scope `const NAME = 'X_…'`, and the names of every other module-scope const. Anchored
|
|
231
|
+
* at column 0, which is what makes it module scope without a parser: a `const` inside a function
|
|
232
|
+
* can be shadowed by another in a sibling scope, and a resolver that picked one of them would be
|
|
233
|
+
* guessing. The second set is the answer "that name IS declared here, and it is not a code" —
|
|
234
|
+
* `const STATUS_NOT_FOUND = 404` in `@ultimat3/realtime`'s NATS fake is the live instance, and a
|
|
235
|
+
* rule that reported it would be a rule the reader has to argue with.
|
|
236
|
+
*/
|
|
237
|
+
const CODE_CONST =
|
|
238
|
+
/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]*)?=\s*(['"`])(X_[A-Z0-9_]+)\2/gm;
|
|
239
|
+
const MODULE_CONST = /^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*[:=]/gm;
|
|
240
|
+
|
|
241
|
+
/** House shape for a constant. A lowercase name at a `code:` is a type annotation or a re-raise. */
|
|
242
|
+
const CODE_CONSTANT_NAME = /^[A-Z][A-Z0-9_]+$/;
|
|
243
|
+
|
|
244
|
+
interface ModuleConstants {
|
|
245
|
+
/** Name → the code it holds. */
|
|
246
|
+
readonly codes: ReadonlyMap<string, string>;
|
|
247
|
+
/** Every module-scope const name, code-valued or not. */
|
|
248
|
+
readonly names: ReadonlySet<string>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function moduleConstants(text: string): ModuleConstants {
|
|
252
|
+
const codes = new Map<string, string>();
|
|
253
|
+
const names = new Set<string>();
|
|
254
|
+
for (const match of text.matchAll(MODULE_CONST)) names.add(match[1] as string);
|
|
255
|
+
for (const match of text.matchAll(CODE_CONST)) codes.set(match[1] as string, match[3] as string);
|
|
256
|
+
return { codes, names };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The bare identifier a key's value is, or `undefined` when the value is anything else. A member
|
|
261
|
+
* read, an index and a call are all refused: `SEO_ERROR_CODES.metaMissing` is how two packages
|
|
262
|
+
* raise every code they own, the registry branch below already collects those literals, and
|
|
263
|
+
* judging the read would report eighteen working sites as broken.
|
|
264
|
+
*/
|
|
265
|
+
function valueIdentifier(masked: string, from: number): string | undefined {
|
|
266
|
+
VALUE_IDENTIFIER.lastIndex = from;
|
|
267
|
+
const match = VALUE_IDENTIFIER.exec(masked);
|
|
268
|
+
return match === null || match[2] !== '' ? undefined : match[1];
|
|
269
|
+
}
|
|
270
|
+
|
|
205
271
|
/**
|
|
206
272
|
* Codes this file declares: every `code:` / `code =` throw site, plus — in a package's own code
|
|
207
273
|
* registry — every entry of its code list or title table, whichever shape it uses. A registry is
|
|
208
274
|
* the only place a bare `X_*` literal is a declaration; anywhere else it is a reference (an env
|
|
209
275
|
* var named `X_BUILD_ID`, an HTTP status map keyed by code) and collecting it would invent a code.
|
|
276
|
+
*
|
|
277
|
+
* A `code:` written as an IDENTIFIER is resolved against the module-scope consts of the same file,
|
|
278
|
+
* and reported as `unresolved` when nothing there gives it a value (#277). Both halves matter and
|
|
279
|
+
* neither is optional: `const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` is what a DRY author writes, and
|
|
280
|
+
* a scan that skipped it silently left the code out of the manifest, out of `wiki/Error-Codes.md`'s
|
|
281
|
+
* demanded rows, out of `bun run gate-codes` and out of `x errors explain` — permissive, and quiet.
|
|
282
|
+
* The identifier half reads the MASKED text: `packages/cli/src/templates/` emits app source by the
|
|
283
|
+
* dozen inside template literals, and a `code: STALE` in one of those is text, not a declaration.
|
|
210
284
|
*/
|
|
211
|
-
export function
|
|
285
|
+
export function scanCodeDeclarations(source: string, at: string): CodeScan {
|
|
212
286
|
const text = stripComments(source);
|
|
213
287
|
const lineAt = lineIndex(text);
|
|
214
288
|
const sites = new Map<string, CodeSite>();
|
|
289
|
+
const unresolved: UnresolvedCodeSite[] = [];
|
|
215
290
|
const add = (code: string, index: number): void => {
|
|
216
291
|
if (!sites.has(code)) sites.set(code, { at, line: lineAt(index), code });
|
|
217
292
|
};
|
|
218
293
|
for (const match of text.matchAll(CODE_AT_KEY)) add(match[2] as string, match.index);
|
|
294
|
+
if (HAS_CODE_IDENTIFIER.test(text)) {
|
|
295
|
+
const masked = maskLiterals(source);
|
|
296
|
+
const constants = moduleConstants(text);
|
|
297
|
+
for (const key of masked.matchAll(CODE_KEY_POSITION)) {
|
|
298
|
+
const name = valueIdentifier(masked, key.index + key[0].length);
|
|
299
|
+
if (name === undefined) continue;
|
|
300
|
+
const code = constants.codes.get(name);
|
|
301
|
+
if (code !== undefined) add(code, key.index);
|
|
302
|
+
else if (!constants.names.has(name) && CODE_CONSTANT_NAME.test(name)) {
|
|
303
|
+
unresolved.push({ at, line: lineAt(key.index), name });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
219
307
|
if (isCodeRegistry(text)) {
|
|
220
308
|
for (const match of text.matchAll(CODE_LITERAL)) add(match[2] as string, match.index);
|
|
221
309
|
for (const match of text.matchAll(CODE_KEY)) add(match[1] as string, match.index);
|
|
222
310
|
}
|
|
223
|
-
return [...sites.values()];
|
|
311
|
+
return { sites: [...sites.values()], unresolved };
|
|
224
312
|
}
|
|
225
313
|
|
|
314
|
+
/**
|
|
315
|
+
* The codes alone, for every caller that has no report to attach a finding to. One scanner, one
|
|
316
|
+
* answer: the manifest, the docs check, `bun run gate-codes` and `x errors explain` all read this.
|
|
317
|
+
*/
|
|
318
|
+
export const scanCodes = (source: string, at: string): readonly CodeSite[] =>
|
|
319
|
+
scanCodeDeclarations(source, at).sites;
|
|
320
|
+
|
|
226
321
|
export interface CodeFixSite extends CodeSite {
|
|
227
322
|
/**
|
|
228
323
|
* The fix literal exactly as written, `${…}` included. Absent when the throw site builds its
|
|
@@ -250,6 +345,8 @@ function soleLiteral(
|
|
|
250
345
|
return found.length === 1 ? found[0] : undefined;
|
|
251
346
|
}
|
|
252
347
|
|
|
348
|
+
const CODE_NAME = /^X_[A-Z0-9_]+$/;
|
|
349
|
+
|
|
253
350
|
/**
|
|
254
351
|
* Every `X_*` code paired with the `fix:` written beside it — in the SAME object literal, which is
|
|
255
352
|
* the whole rule. `new UltimateError({ code, cause, fix })` is the one shape this framework raises
|
|
@@ -263,6 +360,15 @@ function soleLiteral(
|
|
|
263
360
|
export function scanCodeFixSites(source: string, at: string): readonly CodeFixSite[] {
|
|
264
361
|
const masked = maskLiterals(source);
|
|
265
362
|
const lineAt = lineIndex(masked);
|
|
363
|
+
// Lazily, because most files hold no `code:` at all and stripping is a whole extra pass over
|
|
364
|
+
// the text. Same resolver `scanCodeDeclarations` reads, so `x errors explain` can never see a
|
|
365
|
+
// smaller set of throw sites than the manifest does.
|
|
366
|
+
let constants: ModuleConstants | undefined;
|
|
367
|
+
const constantCode = (name: string | undefined): string | undefined => {
|
|
368
|
+
if (name === undefined) return undefined;
|
|
369
|
+
constants ??= moduleConstants(stripComments(source));
|
|
370
|
+
return constants.codes.get(name);
|
|
371
|
+
};
|
|
266
372
|
const keys = new Map<number, { readonly kind: 'code' | 'fix'; readonly from: number }>();
|
|
267
373
|
for (const key of masked.matchAll(CODE_OR_FIX_KEY)) {
|
|
268
374
|
keys.set(key.index, {
|
|
@@ -293,11 +399,16 @@ export function scanCodeFixSites(source: string, at: string): readonly CodeFixSi
|
|
|
293
399
|
const scope = stack.at(-1);
|
|
294
400
|
if (key === undefined || scope === undefined) continue;
|
|
295
401
|
const literal = soleLiteral(masked, source, key.from, lineAt);
|
|
296
|
-
if (literal === undefined) continue;
|
|
297
402
|
if (key.kind === 'fix') {
|
|
298
|
-
if (!fixes.has(scope)) fixes.set(scope, literal.fix);
|
|
299
|
-
|
|
300
|
-
|
|
403
|
+
if (literal !== undefined && !fixes.has(scope)) fixes.set(scope, literal.fix);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// A fix has no second reading, so it stays literal-only; a code has exactly one, which is the
|
|
407
|
+
// module-scope const its own file declares it in.
|
|
408
|
+
const code =
|
|
409
|
+
literal === undefined ? constantCode(valueIdentifier(masked, key.from)) : literal.fix;
|
|
410
|
+
if (code !== undefined && CODE_NAME.test(code) && !codes.has(scope)) {
|
|
411
|
+
codes.set(scope, { at, line: literal?.line ?? lineAt(key.from), code });
|
|
301
412
|
}
|
|
302
413
|
}
|
|
303
414
|
return [...codes].map(([scope, site]) => {
|
package/src/verify-checks.ts
CHANGED
|
@@ -24,9 +24,10 @@ import { checkBudgets, readBuildStats } from './budgets';
|
|
|
24
24
|
import { checkDestructiveMigrations } from './db-destructive';
|
|
25
25
|
import { checkDocumentStyles, documentSurfaces } from './document-styles';
|
|
26
26
|
import { checkSourceDrift } from './drift';
|
|
27
|
-
import { checkErrorFixReport } from './error-contract';
|
|
27
|
+
import { checkErrorCodeResolution, checkErrorFixReport } from './error-contract';
|
|
28
28
|
import { guardFindings } from './guards';
|
|
29
29
|
import { catalogFindings } from './i18n-registration';
|
|
30
|
+
import { liveRouteFindings } from './live-routes';
|
|
30
31
|
import { msg } from './messages';
|
|
31
32
|
import type { Finding } from './output';
|
|
32
33
|
import { findingFrom } from './output';
|
|
@@ -124,7 +125,15 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
124
125
|
// it does not have. "checked 412, could not read 27" is what a reader can act on.
|
|
125
126
|
async run(ctx) {
|
|
126
127
|
const report = await checkErrorFixReport(ctx.root);
|
|
127
|
-
|
|
128
|
+
// The third rule on this step, and the one that is about the code rather than the fix: a
|
|
129
|
+
// `code:` reached through a name nothing in its own file declares is a code no reader of the
|
|
130
|
+
// set can see — not the manifest, not the reference page's coverage rule, not
|
|
131
|
+
// `x errors explain`. It runs here because it needs source and nothing else (#277).
|
|
132
|
+
const findings = [
|
|
133
|
+
...report.findings,
|
|
134
|
+
...(await checkErrorCodeResolution(ctx.root)),
|
|
135
|
+
...(await hostFindings(ctx, 'errors')),
|
|
136
|
+
];
|
|
128
137
|
return {
|
|
129
138
|
...fromFindings(findings),
|
|
130
139
|
output: msg('cli.verify.fixCoverage', {
|
|
@@ -171,7 +180,8 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
171
180
|
},
|
|
172
181
|
{
|
|
173
182
|
name: 'budgets',
|
|
174
|
-
summary:
|
|
183
|
+
summary:
|
|
184
|
+
'per-route JS bytes and LCP, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
|
|
175
185
|
// The global-style assertion rides here rather than becoming an eighteenth step, because this
|
|
176
186
|
// step already asks the one question it asks: what does the document this build emits actually
|
|
177
187
|
// contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
|
|
@@ -202,6 +212,10 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
202
212
|
return fromFindings([
|
|
203
213
|
...findings,
|
|
204
214
|
...checkDocumentStyles(documentSurfaces()),
|
|
215
|
+
// The third rider, and the same question this step already asks one level down: what
|
|
216
|
+
// JavaScript does this route's document boot? A live read with no island is a route
|
|
217
|
+
// whose answer is "none", which no suite can fail on — the page renders, at 200.
|
|
218
|
+
...(await liveRouteFindings(ctx.root)),
|
|
205
219
|
...checkBudgets(manifest, stats),
|
|
206
220
|
]);
|
|
207
221
|
},
|