@ultimat3/cli 9.0.0 → 11.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 +71 -0
- package/package.json +28 -26
- package/src/affected.ts +0 -3
- package/src/app-boundaries.ts +4 -5
- package/src/app-env.ts +7 -2
- package/src/browser-launcher.ts +0 -2
- package/src/budgets.ts +10 -3
- package/src/cmd-build.ts +2 -2
- package/src/cmd-db-backfill.ts +14 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-deploy.ts +14 -3
- package/src/cmd-dev.ts +3 -0
- package/src/cmd-docs.ts +2 -1
- package/src/cmd-doctor.ts +3 -5
- package/src/cmd-env.ts +2 -2
- package/src/cmd-fix.ts +2 -4
- package/src/cmd-new.ts +36 -6
- package/src/cmd-shot.ts +22 -2
- package/src/command.ts +12 -0
- package/src/db-finding.ts +2 -2
- package/src/db-seed.ts +0 -3
- package/src/dev-assets.ts +6 -0
- package/src/dev-cache.ts +12 -5
- package/src/dev-hooks.ts +8 -0
- package/src/dev-lock.ts +8 -7
- package/src/dev-purge.ts +8 -2
- package/src/dev-render.ts +5 -2
- package/src/dev-roles.ts +26 -2
- package/src/dev-runtime.ts +11 -3
- package/src/dev-storage.ts +8 -2
- package/src/dev-sync.ts +37 -2
- package/src/dispatch.ts +8 -0
- package/src/document-styles.ts +2 -1
- package/src/drift.ts +3 -2
- package/src/error-catalog.ts +11 -1
- package/src/error-codes.ts +19 -2
- package/src/error-contract.ts +30 -7
- package/src/error-pages.ts +79 -0
- package/src/errors.ts +26 -29
- package/src/favicon.ts +113 -0
- package/src/fix-path.ts +104 -0
- package/src/flag-reads.ts +2 -2
- package/src/generate-write.ts +3 -2
- package/src/guards.ts +4 -4
- package/src/hold.ts +73 -7
- package/src/index.ts +16 -6
- package/src/island-bundle.ts +32 -4
- package/src/island-routes.ts +7 -1
- package/src/live-routes.ts +181 -0
- package/src/mcp-errors.ts +7 -2
- package/src/messages.ts +6 -4
- package/src/metrics-endpoint.ts +0 -2
- package/src/output.ts +2 -2
- package/src/prerender.ts +64 -22
- package/src/script-csp.ts +17 -0
- package/src/serve.ts +12 -1
- package/src/static-report.ts +41 -3
- 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-container.ts +12 -0
- package/src/templates/scaffold-docs.ts +1 -1
- package/src/templates/scaffold-domain-package.ts +3 -1
- package/src/templates/scaffold-mcp-package.ts +6 -3
- package/src/templates/scaffold-repo.ts +17 -8
- package/src/templates/slice-foundation.ts +3 -5
- package/src/test-shards.ts +2 -2
- package/src/tsconfig-references.ts +2 -2
- package/src/verify-checks.ts +10 -3
- package/src/verify-floor.ts +8 -6
- package/src/verify-run.ts +2 -2
- package/src/verify-step.ts +2 -1
- package/src/verify-test-run.ts +2 -2
- package/src/workspace-checks.ts +10 -12
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +7 -1
|
@@ -31,6 +31,9 @@ export type Role = (typeof ROLES)[number];
|
|
|
31
31
|
* Never a bare Error: an agent reading the failure needs the code, the cause and the exact command
|
|
32
32
|
* that resolves it. Adding across currencies is a bug in the caller's data, not a runtime hiccup,
|
|
33
33
|
* so the fix names the conversion that has to happen first.
|
|
34
|
+
*
|
|
35
|
+
* No \`docs:\`: \`UltimateError\` resolves the link from the code's registered descriptor, so it has
|
|
36
|
+
* one home. A per-code URL written here is a page that does not exist.
|
|
34
37
|
*/
|
|
35
38
|
export class ${app.pascal}CurrencyMismatchError extends UltimateError {
|
|
36
39
|
constructor(input: { readonly left: string; readonly right: string }) {
|
|
@@ -38,7 +41,6 @@ export class ${app.pascal}CurrencyMismatchError extends UltimateError {
|
|
|
38
41
|
code: '${currencyCode(app)}',
|
|
39
42
|
cause: \`cannot add \${input.left} to \${input.right}: Money is only additive inside one currency\`,
|
|
40
43
|
fix: \`bun test packages/domain/src/index.test.ts # add() is defined only inside one currency — convert \${input.right} to \${input.left} before calling it\`,
|
|
41
|
-
docs: 'https://ultimate.dev/errors/${currencyCode(app)}',
|
|
42
44
|
});
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -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}",
|
|
@@ -123,13 +124,21 @@ const envDeclaration = (): string => `${envSchemaSource()}
|
|
|
123
124
|
export const env = defineEnv(envSchema);`;
|
|
124
125
|
|
|
125
126
|
/**
|
|
126
|
-
* No `installPrompt`, no `afterSignInPath`, no `modelEnv`. All
|
|
127
|
-
* `defineConfig`, defaulted by it, and read by NO file; all
|
|
128
|
-
* `packages/core/src/config.ts` as of 2026-08-22,
|
|
129
|
-
* the marker this comment used to cite. So scaffolding
|
|
130
|
-
* is `TS2353` in the generated app's first `x verify
|
|
131
|
-
*
|
|
132
|
-
*
|
|
127
|
+
* No `installPrompt`, no `afterSignInPath`, no `modelEnv`, and no `realtime.tier`. All four were
|
|
128
|
+
* declared by `defineConfig`, defaulted by it, and read by NO file; all four are DELETED from
|
|
129
|
+
* `packages/core/src/config.ts` — the first three as of 2026-08-22, `tier` on 2026-08-23 — whose
|
|
130
|
+
* header now records the removals rather than the marker this comment used to cite. So scaffolding
|
|
131
|
+
* one is no longer a switch with no wire: it is `TS2353` in the generated app's first `x verify`,
|
|
132
|
+
* which is CI's `scaffold-smoke` job. The note belongs HERE rather than in the emitted file: an app
|
|
133
|
+
* author has no use for a comment about keys their config does not name.
|
|
134
|
+
*
|
|
135
|
+
* `tier` is the one that got through, and it says what the list was worth. It accepted
|
|
136
|
+
* `'channels' | 'live-queries' | 'local-first'`, so `'local-first'` read as a durable client store
|
|
137
|
+
* that does not exist (`createOpfsLocalStore` throws `X_NOT_IMPLEMENTED`) — silent, and shaped like
|
|
138
|
+
* a capability. Which realtime tier an app is on is decided by what it DECLARES: a `channel()`
|
|
139
|
+
* topic, a `live: true` query, a local store. `scaffold-config.test.ts` no longer holds a list of
|
|
140
|
+
* dead names to remember; it resolves every key path this literal writes against what `defineConfig`
|
|
141
|
+
* really returns, so the fourteenth deletion fails there with no edit here.
|
|
133
142
|
*/
|
|
134
143
|
const appConfig = (
|
|
135
144
|
app: NameSet,
|
|
@@ -154,7 +163,7 @@ export const config = defineConfig({
|
|
|
154
163
|
cache: { tiers: ['request-memo', 'lru'] },
|
|
155
164
|
jobs: { queues: ['${app.kebab}-default'], concurrency: 4 },
|
|
156
165
|
// In-process transport by default; set urlEnv and transport: 'nats' to scale past one node.
|
|
157
|
-
realtime: { enabled: true,
|
|
166
|
+
realtime: { enabled: true, transport: 'memory' },
|
|
158
167
|
pwa: { enabled: true, offline: 'runtime' },
|
|
159
168
|
ai: { mcp: { expose: true, path: '/mcp' } },
|
|
160
169
|
});
|
|
@@ -19,10 +19,7 @@ import { policyFiles } from './policy';
|
|
|
19
19
|
*/
|
|
20
20
|
export type SliceModule = 'entity' | 'policy' | 'errors';
|
|
21
21
|
|
|
22
|
-
/** The feature's own code, derived once.
|
|
23
|
-
* `.../X_NOT_FOUND` beside a `code:` of `X_INVOICE_NOT_FOUND`, so following the link from a real
|
|
24
|
-
* failure landed on a different code's page — the same interpolation `error-codes.ts`'s `docsFor`
|
|
25
|
-
* already does for every framework code. */
|
|
22
|
+
/** The feature's own code, derived once. */
|
|
26
23
|
const notFoundCode = (feature: NameSet): string =>
|
|
27
24
|
`X_${feature.kebab.toUpperCase().split('-').join('_')}_NOT_FOUND`;
|
|
28
25
|
|
|
@@ -41,13 +38,14 @@ const errorsSource = (feature: NameSet): string => {
|
|
|
41
38
|
|
|
42
39
|
import { UltimateError } from '@ultimat3/core';
|
|
43
40
|
|
|
41
|
+
// No \`docs:\`. \`UltimateError\` resolves it from the code's registered descriptor, so the link has
|
|
42
|
+
// one home; a per-code URL written here is a page that does not exist.
|
|
44
43
|
export class ${feature.pascal}NotFoundError extends UltimateError {
|
|
45
44
|
constructor(input: { id: string }) {
|
|
46
45
|
super({
|
|
47
46
|
code: '${errorCode}',
|
|
48
47
|
cause: \`no ${feature.kebab} with id \${input.id}\`,
|
|
49
48
|
fix: 'x queries list --json, then pass an id the ${feature.kebab} read returns',
|
|
50
|
-
docs: 'https://ultimate.dev/errors/${errorCode}',
|
|
51
49
|
});
|
|
52
50
|
}
|
|
53
51
|
}
|
package/src/test-shards.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// cmd-test.ts because a printed reproduction is only true if it carries every input to the split —
|
|
4
4
|
// that rule is this file's, and argv parsing is that one's.
|
|
5
5
|
|
|
6
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
7
|
import type { AffectedSelection } from './affected';
|
|
7
|
-
import { docsFor } from './error-codes';
|
|
8
8
|
import type { Runner } from './exec';
|
|
9
9
|
import { execOutput } from './exec';
|
|
10
10
|
import { msg } from './messages';
|
|
@@ -137,7 +137,7 @@ const failureOf = (shard: Shard, code: number, plan: ReproduceOptions): Finding
|
|
|
137
137
|
code: 'X_TEST_SHARD_FAILED',
|
|
138
138
|
cause: `shard ${shard.index} of ${plan.workers} exited ${code} (${shard.files.length} file(s))`,
|
|
139
139
|
fix: reproduceFor(shard, plan),
|
|
140
|
-
docs:
|
|
140
|
+
docs: ERROR_DOCS_URL,
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
export async function runShards(options: RunShardsOptions): Promise<CommandResult> {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// root that has no `tsconfig.json` at all, so an `existsSync` ahead of it was a second question
|
|
8
8
|
// with one answer.
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
11
11
|
import type { Finding } from './output';
|
|
12
12
|
import { maskLiterals, stripComments } from './ts-scan';
|
|
13
13
|
|
|
@@ -82,7 +82,7 @@ export const unreferencedFinding = (dir: string): Finding => ({
|
|
|
82
82
|
code: 'X_PACKAGE_UNREFERENCED',
|
|
83
83
|
cause: `packages/${dir} is a published workspace and ${ROOT_TSCONFIG} has no reference to it, so tsc -b never builds it`,
|
|
84
84
|
fix: `add { "path": "./packages/${dir}" } to "references" in ${ROOT_TSCONFIG}, then run bunx tsc -b --pretty false`,
|
|
85
|
-
docs:
|
|
85
|
+
docs: ERROR_DOCS_URL,
|
|
86
86
|
at: ROOT_TSCONFIG,
|
|
87
87
|
});
|
|
88
88
|
|
package/src/verify-checks.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import type { Manifest } from '@ultimat3/manifest';
|
|
8
9
|
import {
|
|
9
10
|
AGENTS_MD_FILENAME,
|
|
@@ -26,6 +27,7 @@ import { checkSourceDrift } from './drift';
|
|
|
26
27
|
import { checkErrorFixReport } from './error-contract';
|
|
27
28
|
import { guardFindings } from './guards';
|
|
28
29
|
import { catalogFindings } from './i18n-registration';
|
|
30
|
+
import { liveRouteFindings } from './live-routes';
|
|
29
31
|
import { msg } from './messages';
|
|
30
32
|
import type { Finding } from './output';
|
|
31
33
|
import { findingFrom } from './output';
|
|
@@ -170,7 +172,8 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
170
172
|
},
|
|
171
173
|
{
|
|
172
174
|
name: 'budgets',
|
|
173
|
-
summary:
|
|
175
|
+
summary:
|
|
176
|
+
'per-route JS bytes and LCP, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
|
|
174
177
|
// The global-style assertion rides here rather than becoming an eighteenth step, because this
|
|
175
178
|
// step already asks the one question it asks: what does the document this build emits actually
|
|
176
179
|
// contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
|
|
@@ -201,6 +204,10 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
201
204
|
return fromFindings([
|
|
202
205
|
...findings,
|
|
203
206
|
...checkDocumentStyles(documentSurfaces()),
|
|
207
|
+
// The third rider, and the same question this step already asks one level down: what
|
|
208
|
+
// JavaScript does this route's document boot? A live read with no island is a route
|
|
209
|
+
// whose answer is "none", which no suite can fail on — the page renders, at 200.
|
|
210
|
+
...(await liveRouteFindings(ctx.root)),
|
|
204
211
|
...checkBudgets(manifest, stats),
|
|
205
212
|
]);
|
|
206
213
|
},
|
|
@@ -330,7 +337,7 @@ async function specFindings(root: string, manifest: Manifest): Promise<readonly
|
|
|
330
337
|
code: 'X_MANIFEST_STALE',
|
|
331
338
|
cause: `${OPENAPI_FILE} does not match the actions the code registers`,
|
|
332
339
|
fix: 'x manifest',
|
|
333
|
-
docs:
|
|
340
|
+
docs: ERROR_DOCS_URL,
|
|
334
341
|
at: OPENAPI_FILE,
|
|
335
342
|
},
|
|
336
343
|
];
|
|
@@ -344,6 +351,6 @@ const seoFinding = (issue: MetaIssue): Finding => ({
|
|
|
344
351
|
code: issue.code,
|
|
345
352
|
cause: issue.cause,
|
|
346
353
|
fix: issue.fix,
|
|
347
|
-
docs:
|
|
354
|
+
docs: ERROR_DOCS_URL,
|
|
348
355
|
at: issue.file,
|
|
349
356
|
});
|
package/src/verify-floor.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// all, and `join` builds the host-separator path to it.
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
|
|
11
11
|
import type { Finding } from './output';
|
|
12
12
|
import { VERIFY_STEP_NAMES } from './verify-step';
|
|
13
13
|
|
|
@@ -43,8 +43,10 @@ export function parseVerifyFloor(
|
|
|
43
43
|
try {
|
|
44
44
|
payload = JSON.parse(text);
|
|
45
45
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// `renderThrowable`, never `error instanceof Error ? error.message : String(error)`: both halves
|
|
47
|
+
// run on a value this process did not build, and either can throw one line before the guard that
|
|
48
|
+
// was meant to make the path safe (`metrics-endpoint.ts` states the same rule over `stringField`).
|
|
49
|
+
return { steps: [], problems: [`it does not parse as JSON (${renderThrowable(error)})`] };
|
|
48
50
|
}
|
|
49
51
|
const steps = asRecord(payload)?.['steps'];
|
|
50
52
|
if (!Array.isArray(steps)) {
|
|
@@ -92,7 +94,7 @@ export const vanishedSuiteFinding = (step: string): Finding => ({
|
|
|
92
94
|
code: 'X_VERIFY_SUITE_VANISHED',
|
|
93
95
|
cause: `${VERIFY_FLOOR_FILE} requires the ${step} step and this run found nothing for it to check`,
|
|
94
96
|
fix: `x verify --json # restore the ${step} suite, or drop "${step}" from ${VERIFY_FLOOR_FILE} in the commit that says why`,
|
|
95
|
-
docs:
|
|
97
|
+
docs: ERROR_DOCS_URL,
|
|
96
98
|
at: VERIFY_FLOOR_FILE,
|
|
97
99
|
});
|
|
98
100
|
|
|
@@ -111,7 +113,7 @@ export const skippedSuiteFinding = (step: string, skipped: number): Finding => (
|
|
|
111
113
|
code: 'X_VERIFY_SUITE_VANISHED',
|
|
112
114
|
cause: `${VERIFY_FLOOR_FILE} requires the ${step} step and all ${skipped} test(s) it found skipped themselves, so nothing ran`,
|
|
113
115
|
fix: `x test ${step} --json # then set what the suite skips without, or drop "${step}" from ${VERIFY_FLOOR_FILE} in the commit that says why`,
|
|
114
|
-
docs:
|
|
116
|
+
docs: ERROR_DOCS_URL,
|
|
115
117
|
at: VERIFY_FLOOR_FILE,
|
|
116
118
|
});
|
|
117
119
|
|
|
@@ -128,6 +130,6 @@ export const floorProblemFindings = (floor: VerifyFloor | undefined): readonly F
|
|
|
128
130
|
code: 'X_CONFIG_INVALID',
|
|
129
131
|
cause: `${VERIFY_FLOOR_FILE} is not a suite floor: ${problem}`,
|
|
130
132
|
fix: `x verify --json # then write ${VERIFY_FLOOR_FILE} as {"steps":["unit","contract"]}, naming only steps it ran`,
|
|
131
|
-
docs:
|
|
133
|
+
docs: ERROR_DOCS_URL,
|
|
132
134
|
at: VERIFY_FLOOR_FILE,
|
|
133
135
|
}));
|
package/src/verify-run.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `cmd-verify.ts` because `x build` and the MCP host run the gate without going through the
|
|
3
3
|
// command: what a run means — never bail early, count what actually ran — belongs to neither.
|
|
4
4
|
|
|
5
|
-
import { renderThrowable } from '@ultimat3/core';
|
|
5
|
+
import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
|
|
6
6
|
import { msg } from './messages';
|
|
7
7
|
import type { CommandResult, Finding, StepResult } from './output';
|
|
8
8
|
import {
|
|
@@ -139,6 +139,6 @@ function findingOf(error: unknown, step: string): Finding {
|
|
|
139
139
|
code: 'X_VERIFY_FAILED',
|
|
140
140
|
cause: `step "${step}" threw: ${cause}`,
|
|
141
141
|
fix: 'x verify --json',
|
|
142
|
-
docs:
|
|
142
|
+
docs: ERROR_DOCS_URL,
|
|
143
143
|
};
|
|
144
144
|
}
|
package/src/verify-step.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// repo feeds it findings it could not produce on its own. Split from the step list so a step
|
|
3
3
|
// implementation can live beside the code it checks without importing the list.
|
|
4
4
|
|
|
5
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
5
6
|
import type { ExecResult, Runner } from './exec';
|
|
6
7
|
import { execOutput } from './exec';
|
|
7
8
|
import type { Finding } from './output';
|
|
@@ -106,7 +107,7 @@ export function fromExec(result: ExecResult, finding: Omit<Finding, 'docs'>): St
|
|
|
106
107
|
if (result.ok) return { ok: true, findings: [], output: execOutput(result) };
|
|
107
108
|
return {
|
|
108
109
|
ok: false,
|
|
109
|
-
findings: [{ ...finding, docs:
|
|
110
|
+
findings: [{ ...finding, docs: ERROR_DOCS_URL }],
|
|
110
111
|
output: execOutput(result),
|
|
111
112
|
};
|
|
112
113
|
}
|
package/src/verify-test-run.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// happens to them once selected — a wrong file list is never a race, and a race is never a
|
|
4
4
|
// selection bug.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
7
|
import type { Runner } from './exec';
|
|
8
8
|
import { execOutput } from './exec';
|
|
9
9
|
import type { Finding } from './output';
|
|
@@ -51,7 +51,7 @@ export async function runParallel(options: ParallelRunOptions): Promise<StepOutc
|
|
|
51
51
|
// The reproduction has to name every input to the split, or it reruns a different file set:
|
|
52
52
|
// `reproduceFor` is the one place that rule lives, shared with `x test`.
|
|
53
53
|
fix: reproduceFor(shard, { workers: shards.length, type: options.type }),
|
|
54
|
-
docs:
|
|
54
|
+
docs: ERROR_DOCS_URL,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
// Only the failing shards' output: a green 8-way split would otherwise print eight summaries,
|
package/src/workspace-checks.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
-
import { renderCauseValue } from '@ultimat3/core';
|
|
8
|
+
import { ERROR_DOCS_URL, renderCauseValue } from '@ultimat3/core';
|
|
9
9
|
import type { Finding } from './output';
|
|
10
10
|
import { eachSourceFile, isGenerated } from './source-files';
|
|
11
11
|
import { checkRootReferences } from './tsconfig-references';
|
|
@@ -14,13 +14,11 @@ export const LINE_CEILING = 500;
|
|
|
14
14
|
|
|
15
15
|
export const PACKAGE_FILES = ['README.md', 'CLAUDE.md', 'tsconfig.json', 'src/index.ts'] as const;
|
|
16
16
|
|
|
17
|
-
const docs = (code: string): string => `https://ultimate.dev/errors/${code}`;
|
|
18
|
-
|
|
19
17
|
export const tooLongFinding = (path: string, lines: number): Finding => ({
|
|
20
18
|
code: 'X_FILE_TOO_LONG',
|
|
21
19
|
cause: `${path} is ${lines} lines, over the ${LINE_CEILING} line ceiling`,
|
|
22
20
|
fix: `split ${path}: one file, one responsibility`,
|
|
23
|
-
docs:
|
|
21
|
+
docs: ERROR_DOCS_URL,
|
|
24
22
|
at: path,
|
|
25
23
|
});
|
|
26
24
|
|
|
@@ -49,7 +47,7 @@ export const missingFileFinding = (dir: string, file: string, scaffolder: boolea
|
|
|
49
47
|
fix: scaffolder
|
|
50
48
|
? `bun run scripts/new-package.ts ${dir} --only ${file}`
|
|
51
49
|
: `add packages/${dir}/${file}, shaped like the one in a sibling package`,
|
|
52
|
-
docs:
|
|
50
|
+
docs: ERROR_DOCS_URL,
|
|
53
51
|
at: `packages/${dir}/${file}`,
|
|
54
52
|
});
|
|
55
53
|
|
|
@@ -68,7 +66,7 @@ export const badVersionFinding = (dir: string, found: unknown): Finding => ({
|
|
|
68
66
|
// this finding with a TypeError from inside the gate.
|
|
69
67
|
cause: `packages/${dir}/package.json has no semver "version" (found ${renderCauseValue(found)})`,
|
|
70
68
|
fix: `set a semver "version" in packages/${dir}/package.json, then: bun run verify`,
|
|
71
|
-
docs:
|
|
69
|
+
docs: ERROR_DOCS_URL,
|
|
72
70
|
at: `packages/${dir}/package.json`,
|
|
73
71
|
});
|
|
74
72
|
|
|
@@ -103,7 +101,7 @@ export const versionSkewFinding = (dir: string, found: string, expected: string)
|
|
|
103
101
|
code: 'X_RELEASE_VERSION_SKEW',
|
|
104
102
|
cause: `packages/${dir} is at ${found}, not the lockstep version ${expected}`,
|
|
105
103
|
fix: `bun run scripts/release.ts --version ${expected}`,
|
|
106
|
-
docs:
|
|
104
|
+
docs: ERROR_DOCS_URL,
|
|
107
105
|
at: `packages/${dir}/package.json`,
|
|
108
106
|
});
|
|
109
107
|
|
|
@@ -116,7 +114,7 @@ export const pinSkewFinding = (
|
|
|
116
114
|
code: 'X_RELEASE_VERSION_SKEW',
|
|
117
115
|
cause: `packages/${dir} pins ${dep} at ${range}, not the lockstep version ${expected}`,
|
|
118
116
|
fix: `bun run scripts/release.ts --version ${expected}`,
|
|
119
|
-
docs:
|
|
117
|
+
docs: ERROR_DOCS_URL,
|
|
120
118
|
at: `packages/${dir}/package.json`,
|
|
121
119
|
});
|
|
122
120
|
|
|
@@ -167,7 +165,7 @@ export const missingPublishedFileFinding = (dir: string, entry: string): Finding
|
|
|
167
165
|
code: 'X_PACKAGE_SHAPE',
|
|
168
166
|
cause: `packages/${dir}/package.json ships "${entry}" in "files", but packages/${dir}/${entry} does not exist`,
|
|
169
167
|
fix: `add packages/${dir}/${entry}, or drop "${entry}" from "files" in packages/${dir}/package.json`,
|
|
170
|
-
docs:
|
|
168
|
+
docs: ERROR_DOCS_URL,
|
|
171
169
|
at: `packages/${dir}/package.json`,
|
|
172
170
|
});
|
|
173
171
|
|
|
@@ -175,7 +173,7 @@ export const publishesTestsFinding = (dir: string): Finding => ({
|
|
|
175
173
|
code: 'X_PACKAGE_SHAPE',
|
|
176
174
|
cause: `packages/${dir}/package.json does not exclude ${TEST_EXCLUSION} from "files"`,
|
|
177
175
|
fix: `add "${TEST_EXCLUSION}" to "files" in packages/${dir}/package.json, after "src"`,
|
|
178
|
-
docs:
|
|
176
|
+
docs: ERROR_DOCS_URL,
|
|
179
177
|
at: `packages/${dir}/package.json`,
|
|
180
178
|
});
|
|
181
179
|
|
|
@@ -196,7 +194,7 @@ export const buildArtifactsFinding = (dir: string, count: number): Finding => ({
|
|
|
196
194
|
code: 'X_PACKAGE_SHAPE',
|
|
197
195
|
cause: `packages/${dir}/src/ contains ${count} build artifacts (.d.ts, .js, .map files)`,
|
|
198
196
|
fix: `find packages/${dir}/src ${SWEEP_PREDICATE} -delete`,
|
|
199
|
-
docs:
|
|
197
|
+
docs: ERROR_DOCS_URL,
|
|
200
198
|
at: `packages/${dir}/src/`,
|
|
201
199
|
});
|
|
202
200
|
|
|
@@ -209,7 +207,7 @@ export const noFilesAllowlistFinding = (dir: string): Finding => ({
|
|
|
209
207
|
code: 'X_PACKAGE_SHAPE',
|
|
210
208
|
cause: `packages/${dir}/package.json publishes with no "files" allowlist, so the tarball carries whatever is in the directory`,
|
|
211
209
|
fix: `add "files": ["src", "${TEST_EXCLUSION}", "README.md", "LICENSE"] to packages/${dir}/package.json`,
|
|
212
|
-
docs:
|
|
210
|
+
docs: ERROR_DOCS_URL,
|
|
213
211
|
at: `packages/${dir}/package.json`,
|
|
214
212
|
});
|
|
215
213
|
|
package/src/workspace-graph.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// for `tsc` and is invisible to bun, to `--filter` ordering and to any change-detection tool.
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import type { Finding } from './output';
|
|
8
9
|
import { eachSourceFile, isGenerated, isTest, isVendored } from './source-files';
|
|
9
10
|
import { maskLiterals } from './ts-scan';
|
|
@@ -184,7 +185,7 @@ export const unreadableWorkspaceFinding = (path: string): Finding => ({
|
|
|
184
185
|
code: 'X_APP_PACKAGE_INVALID',
|
|
185
186
|
cause: `${path} is claimed by the root "workspaces" globs and supplies no readable "name"`,
|
|
186
187
|
fix: `repair the JSON and the "name" in ${path}, or drop its directory from "workspaces" in package.json`,
|
|
187
|
-
docs:
|
|
188
|
+
docs: ERROR_DOCS_URL,
|
|
188
189
|
at: path,
|
|
189
190
|
});
|
|
190
191
|
|
|
@@ -198,7 +199,7 @@ export const undeclaredWorkspaceDepFinding = (
|
|
|
198
199
|
// The exact line to paste, at the version the target really carries: a `workspace:*` range would
|
|
199
200
|
// resolve and then fail `checkLockstep`, which compares a sibling pin against the version.
|
|
200
201
|
fix: `add "${to.name}": "${to.version ?? '0.0.0'}" to "dependencies" in ${from.dir}/package.json`,
|
|
201
|
-
docs:
|
|
202
|
+
docs: ERROR_DOCS_URL,
|
|
202
203
|
at: `${from.dir}/package.json`,
|
|
203
204
|
});
|
|
204
205
|
|
package/src/write-line.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
// `node:fs`, and unavoidable: Bun has no synchronous stdout write of its own.
|
|
7
7
|
import { writeSync } from 'node:fs';
|
|
8
|
+
// The one import beyond `node:fs`, and it costs nothing here: `create-ultimate` reaches this
|
|
9
|
+
// module through `@ultimat3/cli`'s barrel, which has already evaluated core.
|
|
10
|
+
import { stringField } from '@ultimat3/core';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Write to stdout and be certain it arrived, even if the next statement exits the process.
|
|
@@ -29,7 +32,10 @@ function writeTo(fd: 1 | 2, line: string): void {
|
|
|
29
32
|
try {
|
|
30
33
|
written += writeSync(fd, buffer, written, buffer.length - written);
|
|
31
34
|
} catch (cause) {
|
|
32
|
-
|
|
35
|
+
// `stringField`, never a cast plus a property read — the rule `metrics-endpoint.ts` states
|
|
36
|
+
// and `caught-value-reads.test.ts` enforces. Here it is also the difference between
|
|
37
|
+
// rethrowing and an infinite loop: a `code` that cannot be read must not read as `EAGAIN`.
|
|
38
|
+
if (stringField(cause, 'code') !== 'EAGAIN') throw cause;
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
}
|