@ultimat3/cli 9.0.0 → 10.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/package.json +26 -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 +4 -3
- package/src/cmd-build.ts +2 -2
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-deploy.ts +14 -3
- 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 +2 -2
- package/src/cmd-shot.ts +22 -2
- package/src/db-finding.ts +2 -2
- package/src/db-seed.ts +0 -3
- package/src/dev-cache.ts +12 -5
- package/src/dev-lock.ts +8 -7
- package/src/dev-runtime.ts +11 -3
- package/src/dev-storage.ts +8 -2
- package/src/dev-sync.ts +37 -2
- package/src/document-styles.ts +2 -1
- package/src/drift.ts +3 -2
- package/src/error-codes.ts +9 -2
- package/src/error-contract.ts +5 -5
- package/src/errors.ts +1 -29
- package/src/flag-reads.ts +2 -2
- package/src/generate-write.ts +3 -2
- package/src/guards.ts +4 -4
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +32 -4
- package/src/island-routes.ts +7 -1
- package/src/mcp-errors.ts +2 -2
- package/src/metrics-endpoint.ts +0 -2
- package/src/output.ts +2 -2
- package/src/prerender.ts +32 -19
- package/src/static-report.ts +41 -3
- 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-repo.ts +16 -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 +3 -2
- 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
package/src/dev-sync.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Split from `dev-roles.ts` because it is the one role with an authenticator, a presence registry
|
|
3
3
|
// and a listener of its own — and because that file is the boot's index, not its detail.
|
|
4
4
|
|
|
5
|
-
import { createContext, logger } from '@ultimat3/core';
|
|
5
|
+
import { createContext, logger, UltimateError } from '@ultimat3/core';
|
|
6
6
|
import { listQueries } from '@ultimat3/query';
|
|
7
7
|
import {
|
|
8
8
|
ChannelHub,
|
|
@@ -15,8 +15,43 @@ import {
|
|
|
15
15
|
SocketRegistry,
|
|
16
16
|
} from '@ultimat3/realtime/server';
|
|
17
17
|
import type { StartRolesOptions } from './dev-roles';
|
|
18
|
+
import { neighbouringPort, PORT_RANGE } from './flag-number';
|
|
18
19
|
import { syncAuthenticator } from './sync-authenticator';
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Beside its one thrower rather than in `errors.ts`, which is at 461 of the 500-line ceiling —
|
|
23
|
+
* the arrangement `db-seed.ts` and `metrics-endpoint.ts` already take. The code is
|
|
24
|
+
* `X_PORT_INVALID`, this package's own: "the port asked for is not one" is what it already means,
|
|
25
|
+
* and a second code for the same fact is the synonym the registry exists to prevent.
|
|
26
|
+
*/
|
|
27
|
+
class SyncPortUnavailableError extends UltimateError {
|
|
28
|
+
constructor(input: { port: number }) {
|
|
29
|
+
super({
|
|
30
|
+
code: 'X_PORT_INVALID',
|
|
31
|
+
cause: `the sync role binds PORT + 1, and PORT=${input.port} is the top of the range — it would ask for ${input.port + 1}, which is not a TCP port`,
|
|
32
|
+
fix: `x dev --port ${neighbouringPort(input.port)} # leaves ${PORT_RANGE.max} free for the sync node`,
|
|
33
|
+
meta: { port: input.port },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The port the sync node listens on. `PORT + 1`, and `0` stays `0` — the kernel picks, and adding
|
|
40
|
+
* one to it would pick a specific port instead.
|
|
41
|
+
*
|
|
42
|
+
* REFUSED at the top of the range, never clamped. `PORT_RANGE.max` is 65535 and `portValue`
|
|
43
|
+
* accepts it, so `x dev --port 65535` handed `Bun.serve` 65536 and the bare `RangeError` reached
|
|
44
|
+
* the terminal as `X_CLI_UNEXPECTED` with `fix: x doctor --json`. Clamping to 65534 would be worse
|
|
45
|
+
* than refusing: `PORT + 1` is the rule `docker/docker-compose.prod.yml` publishes `3001:3001`
|
|
46
|
+
* from and `docker/helm` derives `PORT = .port - 1` from, so a node quietly on `PORT - 1` is a
|
|
47
|
+
* socket nothing else in the deployment computes.
|
|
48
|
+
*/
|
|
49
|
+
export function syncPortFor(port: number): number {
|
|
50
|
+
if (port === 0) return 0;
|
|
51
|
+
if (port >= PORT_RANGE.max) throw new SyncPortUnavailableError({ port });
|
|
52
|
+
return port + 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
/** What `startRoles` holds on to: where the node listens, and how to take it down. */
|
|
21
56
|
export interface RunningSync {
|
|
22
57
|
readonly url: string;
|
|
@@ -97,7 +132,7 @@ export async function startSync(options: StartRolesOptions): Promise<RunningSync
|
|
|
97
132
|
});
|
|
98
133
|
await node.start();
|
|
99
134
|
try {
|
|
100
|
-
const listener = listenSyncNode(node, { port: options.port
|
|
135
|
+
const listener = listenSyncNode(node, { port: syncPortFor(options.port) });
|
|
101
136
|
return {
|
|
102
137
|
url: listener.url,
|
|
103
138
|
stop: async () => {
|
package/src/document-styles.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// browser drops every one of those declarations from, byte-for-byte identical to a working page
|
|
5
5
|
// apart from the styling nobody sees missing. A silent failure is exactly what axiom 3 exists for.
|
|
6
6
|
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import type { Surface } from '@ultimat3/render';
|
|
8
9
|
import { routeEntries } from '@ultimat3/render';
|
|
9
10
|
import { stylesFor } from '@ultimat3/render/server';
|
|
@@ -49,7 +50,7 @@ export function checkDocumentStyles(documents: readonly SurfaceDocument[]): read
|
|
|
49
50
|
code: 'X_STYLES_GLOBAL_MISSING',
|
|
50
51
|
cause: `a ${document.surface}/ document carries ${document.css.length} characters of CSS and defines no :root custom properties, so every var(--color-*) and var(--space-*) in it resolves to nothing`,
|
|
51
52
|
fix: `add apps/web/${APP_GLOBAL_STYLESHEET} containing \`@use '@ultimat3/ui/global.scss';\` and apps/web/${APP_GLOBAL_MODULE} containing \`import './global.scss';\``,
|
|
52
|
-
docs:
|
|
53
|
+
docs: ERROR_DOCS_URL,
|
|
53
54
|
at: `apps/web/${APP_GLOBAL_STYLESHEET}`,
|
|
54
55
|
}));
|
|
55
56
|
}
|
package/src/drift.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
14
15
|
import { describeEntities } from '@ultimat3/entity';
|
|
15
16
|
import { countDeclaredEntities } from './app-entities';
|
|
16
17
|
import { loadApp } from './app-load';
|
|
@@ -181,7 +182,7 @@ export async function checkSourceDrift(
|
|
|
181
182
|
code: 'X_DB_DRIFT',
|
|
182
183
|
cause: 'packages/db has a schema but no migration recorded it',
|
|
183
184
|
fix: 'x db gen "initial"',
|
|
184
|
-
docs:
|
|
185
|
+
docs: ERROR_DOCS_URL,
|
|
185
186
|
at: MIGRATIONS_DIR,
|
|
186
187
|
},
|
|
187
188
|
];
|
|
@@ -192,7 +193,7 @@ export async function checkSourceDrift(
|
|
|
192
193
|
code: 'X_DB_DRIFT',
|
|
193
194
|
cause: `schema hashes to ${current}, newest migration ${latest.file} recorded ${latest.hash}`,
|
|
194
195
|
fix: 'x db gen "describe the change"',
|
|
195
|
-
docs:
|
|
196
|
+
docs: ERROR_DOCS_URL,
|
|
196
197
|
at: `${DB_PACKAGE}/src`,
|
|
197
198
|
},
|
|
198
199
|
];
|
package/src/error-codes.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The X_* codes owned by @ultimat3/cli, and nothing else: the two lists, their titles, the one
|
|
2
|
-
// registration call
|
|
2
|
+
// registration call. Every code names the exact command that resolves it, because
|
|
3
3
|
// the CLI is the surface an agent reads first — a failure here has to be actionable without a doc
|
|
4
4
|
// lookup or a second round-trip. The classes that throw these codes live in `./errors`.
|
|
5
5
|
import { registerErrorCodes } from '@ultimat3/core';
|
|
@@ -217,4 +217,11 @@ registerErrorCodes(
|
|
|
217
217
|
Object.fromEntries(Object.entries(CLI_ERROR_TITLES).map(([code, title]) => [code, { title }])),
|
|
218
218
|
);
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
// This file exports NO `docsFor(code)`, and adding one back is the defect. A CLI error passes no
|
|
221
|
+
// `docs:` at all — `UltimateError` fills it from `describeErrorCode(code).docs`, which is
|
|
222
|
+
// `@ultimat3/core`'s `ERROR_DOCS_URL`: one page for every code, never one per code, because `wiki/`
|
|
223
|
+
// is the framework's only public documentation surface and a code lives there in a TABLE ROW, which
|
|
224
|
+
// has no anchor. A `Finding` is a plain object with no constructor to fill it, so it carries
|
|
225
|
+
// `ERROR_DOCS_URL` imported from core — the same constant, not a second copy of it. The
|
|
226
|
+
// `https://ultimate.dev/errors/<code>` links `docsFor` built until 9.x answered 404, host included,
|
|
227
|
+
// on every error the CLI has ever thrown.
|
package/src/error-contract.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
// `join` is `node:`-only by necessity: Bun exposes no path-join primitive.
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
import {
|
|
9
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
10
10
|
import { citedCommandProblem, loadCommandCatalog } from './fix-command';
|
|
11
11
|
import { createHelperResolver } from './fix-imports';
|
|
12
12
|
import { scanFixSites } from './fix-scan';
|
|
@@ -63,7 +63,7 @@ const fixFinding = (site: FixSite, problem: string): Finding => ({
|
|
|
63
63
|
code: 'X_ERROR_FIX_INVALID',
|
|
64
64
|
cause: problem,
|
|
65
65
|
fix: `rewrite the fix at ${site.at}:${site.line} as a command to run, a call to paste, or an edit naming a file`,
|
|
66
|
-
docs:
|
|
66
|
+
docs: ERROR_DOCS_URL,
|
|
67
67
|
at: `${site.at}:${site.line}`,
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -132,7 +132,7 @@ const undocumentedFinding = (code: string, at: string, line: number, page: strin
|
|
|
132
132
|
code: 'X_ERROR_CODE_UNDOCUMENTED',
|
|
133
133
|
cause: `${code} is declared at ${at}:${line} and ${page} has no entry for it`,
|
|
134
134
|
fix: `add a row for ${code} to ${page}, with its cause and the command that fixes it`,
|
|
135
|
-
docs:
|
|
135
|
+
docs: ERROR_DOCS_URL,
|
|
136
136
|
at: page,
|
|
137
137
|
});
|
|
138
138
|
|
|
@@ -163,7 +163,7 @@ const unregisteredFinding = (code: string, page: string): Finding => ({
|
|
|
163
163
|
code: 'X_ERROR_CODE_UNREGISTERED',
|
|
164
164
|
cause: `${page} documents ${code} as a live code and nothing registers it, so "x errors explain ${code}" refuses a code this page promises`,
|
|
165
165
|
fix: `register ${code} through registerErrorCodes() in its package's src/errors.ts, or move its row under "${RESERVED_HEADING}" in ${page}`,
|
|
166
|
-
docs:
|
|
166
|
+
docs: ERROR_DOCS_URL,
|
|
167
167
|
at: page,
|
|
168
168
|
});
|
|
169
169
|
|
|
@@ -249,7 +249,7 @@ export async function checkErrorCodeDocs(root: string, page: string): Promise<re
|
|
|
249
249
|
code: 'X_ERROR_CODE_UNDOCUMENTED',
|
|
250
250
|
cause: `the error reference ${page} does not exist, so no code can be documented`,
|
|
251
251
|
fix: `create ${page} with a row per X_* code, or stop naming it as the error reference`,
|
|
252
|
-
docs:
|
|
252
|
+
docs: ERROR_DOCS_URL,
|
|
253
253
|
at: page,
|
|
254
254
|
},
|
|
255
255
|
];
|
package/src/errors.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// that resolves it — the codes themselves, their titles and their registration are `./error-codes`,
|
|
3
3
|
// so a package importing a class does not pull the table and vice versa.
|
|
4
4
|
import { UltimateError } from '@ultimat3/core';
|
|
5
|
-
import { docsFor } from './error-codes';
|
|
6
5
|
|
|
7
6
|
/** An unknown command or subcommand. Carries a suggestion so the retry is one keystroke away. */
|
|
8
7
|
export class UnknownCommandError extends UltimateError {
|
|
@@ -11,7 +10,6 @@ export class UnknownCommandError extends UltimateError {
|
|
|
11
10
|
code: 'X_CLI_UNKNOWN_COMMAND',
|
|
12
11
|
cause: `"x ${input.path}" is not a command (known: ${input.known.join(', ')})`,
|
|
13
12
|
fix: input.suggestion === undefined ? 'x help' : `x ${input.suggestion}`,
|
|
14
|
-
docs: docsFor('X_CLI_UNKNOWN_COMMAND'),
|
|
15
13
|
});
|
|
16
14
|
}
|
|
17
15
|
}
|
|
@@ -27,7 +25,6 @@ export class BadFlagError extends UltimateError {
|
|
|
27
25
|
code: 'X_CLI_BAD_FLAG',
|
|
28
26
|
cause: `--${input.flag} on "x ${input.command}": ${input.reason}`,
|
|
29
27
|
fix: input.fix ?? `x ${input.command} --help`,
|
|
30
|
-
docs: docsFor('X_CLI_BAD_FLAG'),
|
|
31
28
|
});
|
|
32
29
|
}
|
|
33
30
|
}
|
|
@@ -46,7 +43,6 @@ export class MissingPositionalError extends UltimateError {
|
|
|
46
43
|
code: 'X_CLI_BAD_FLAG',
|
|
47
44
|
cause: `"x ${input.command}" needs a <${input.positional}> positional and got none`,
|
|
48
45
|
fix: input.example,
|
|
49
|
-
docs: docsFor('X_CLI_BAD_FLAG'),
|
|
50
46
|
});
|
|
51
47
|
}
|
|
52
48
|
}
|
|
@@ -70,7 +66,6 @@ export class MissingSubcommandError extends UltimateError {
|
|
|
70
66
|
code: 'X_CLI_BAD_FLAG',
|
|
71
67
|
cause: `"x ${input.command}" takes a subcommand and got none (one of: ${input.known.join(', ')})`,
|
|
72
68
|
fix: `x help ${input.command}`,
|
|
73
|
-
docs: docsFor('X_CLI_BAD_FLAG'),
|
|
74
69
|
});
|
|
75
70
|
}
|
|
76
71
|
}
|
|
@@ -82,7 +77,6 @@ export class VerifyFailedError extends UltimateError {
|
|
|
82
77
|
code: 'X_VERIFY_FAILED',
|
|
83
78
|
cause: `${input.failed.length} verify step(s) failed: ${input.failed.join(', ')}`,
|
|
84
79
|
fix: 'x verify --json',
|
|
85
|
-
docs: docsFor('X_VERIFY_FAILED'),
|
|
86
80
|
});
|
|
87
81
|
}
|
|
88
82
|
}
|
|
@@ -94,7 +88,6 @@ export class NotInAppError extends UltimateError {
|
|
|
94
88
|
code: 'X_NOT_IN_APP',
|
|
95
89
|
cause: `"x ${input.command}" must run inside an Ultimate app; no app.config.ts at or above ${input.from}`,
|
|
96
90
|
fix: 'x new myapp && cd myapp',
|
|
97
|
-
docs: docsFor('X_NOT_IN_APP'),
|
|
98
91
|
});
|
|
99
92
|
}
|
|
100
93
|
}
|
|
@@ -106,7 +99,6 @@ export class BunVersionError extends UltimateError {
|
|
|
106
99
|
code: 'X_BUN_VERSION',
|
|
107
100
|
cause: `Bun ${input.found} is older than the required ${input.required}`,
|
|
108
101
|
fix: 'bun upgrade',
|
|
109
|
-
docs: docsFor('X_BUN_VERSION'),
|
|
110
102
|
});
|
|
111
103
|
}
|
|
112
104
|
}
|
|
@@ -127,7 +119,6 @@ export class NoTestFilesError extends UltimateError {
|
|
|
127
119
|
code: 'X_TEST_NO_FILES',
|
|
128
120
|
cause: `no *.test.ts files${where} under ${input.root}`,
|
|
129
121
|
fix: parts.length === 0 ? 'x test --json # run it from the repo root' : 'x test',
|
|
130
|
-
docs: docsFor('X_TEST_NO_FILES'),
|
|
131
122
|
});
|
|
132
123
|
}
|
|
133
124
|
}
|
|
@@ -146,7 +137,6 @@ export class ScaffoldPathEscapeError extends UltimateError {
|
|
|
146
137
|
fix:
|
|
147
138
|
input.fix ??
|
|
148
139
|
`make the path relative to the app root with no ".." segment, then re-run: bun test packages/cli/src/scaffold-typecheck.contract.test.ts`,
|
|
149
|
-
docs: docsFor('X_SCAFFOLD_PATH_ESCAPE'),
|
|
150
140
|
});
|
|
151
141
|
}
|
|
152
142
|
}
|
|
@@ -163,7 +153,6 @@ export class GenerateJsonInvalidError extends UltimateError {
|
|
|
163
153
|
code: 'X_GENERATE_JSON_INVALID',
|
|
164
154
|
cause: `${input.path} is declared merge: 'json' but the generator's own contents for it do not parse as a JSON object`,
|
|
165
155
|
fix: `fix the template that emits ${input.path}, then re-run: bun test packages/cli/src/cmd-generate.test.ts`,
|
|
166
|
-
docs: docsFor('X_GENERATE_JSON_INVALID'),
|
|
167
156
|
});
|
|
168
157
|
}
|
|
169
158
|
}
|
|
@@ -182,7 +171,6 @@ export class CatalogExistsError extends UltimateError {
|
|
|
182
171
|
code: 'X_GENERATE_CONFLICT',
|
|
183
172
|
cause: `${input.path} already exists`,
|
|
184
173
|
fix: `x i18n sync ${input.locale}`,
|
|
185
|
-
docs: docsFor('X_GENERATE_CONFLICT'),
|
|
186
174
|
});
|
|
187
175
|
}
|
|
188
176
|
}
|
|
@@ -198,7 +186,6 @@ export class AppPackageInvalidError extends UltimateError {
|
|
|
198
186
|
code: 'X_APP_PACKAGE_INVALID',
|
|
199
187
|
cause: `${input.path} ${input.problem}, so the manifest has no app name or version to gate on`,
|
|
200
188
|
fix: 'bun pm pkg set name=my-app version=0.1.0',
|
|
201
|
-
docs: docsFor('X_APP_PACKAGE_INVALID'),
|
|
202
189
|
});
|
|
203
190
|
}
|
|
204
191
|
}
|
|
@@ -216,7 +203,6 @@ export class ErrorCodeUnknownError extends UltimateError {
|
|
|
216
203
|
input.suggestion === undefined
|
|
217
204
|
? 'x errors list --json'
|
|
218
205
|
: `x errors explain ${input.suggestion}`,
|
|
219
|
-
docs: docsFor('X_ERROR_CODE_UNKNOWN'),
|
|
220
206
|
});
|
|
221
207
|
}
|
|
222
208
|
}
|
|
@@ -243,7 +229,6 @@ export class DeclarationUnknownError extends UltimateError {
|
|
|
243
229
|
input.suggestion === undefined
|
|
244
230
|
? `x ${input.kind} list --json`
|
|
245
231
|
: `x ${input.kind} ${input.verb ?? 'describe'} ${input.suggestion}`,
|
|
246
|
-
docs: docsFor('X_DECLARATION_UNKNOWN'),
|
|
247
232
|
});
|
|
248
233
|
}
|
|
249
234
|
}
|
|
@@ -255,7 +240,6 @@ export class JobUnknownError extends UltimateError {
|
|
|
255
240
|
code: 'X_JOB_UNKNOWN',
|
|
256
241
|
cause: `the "${input.driver}" queue holds no job with id "${input.id}"`,
|
|
257
242
|
fix: 'x jobs ls --json',
|
|
258
|
-
docs: docsFor('X_JOB_UNKNOWN'),
|
|
259
243
|
});
|
|
260
244
|
}
|
|
261
245
|
}
|
|
@@ -274,7 +258,6 @@ export class FixTargetUnknownError extends UltimateError {
|
|
|
274
258
|
input.suggestion === undefined
|
|
275
259
|
? 'x routes --json # every registered route file, app-root-relative'
|
|
276
260
|
: `x fix boundary ${input.suggestion}`,
|
|
277
|
-
docs: docsFor('X_FIX_TARGET_UNKNOWN'),
|
|
278
261
|
});
|
|
279
262
|
}
|
|
280
263
|
}
|
|
@@ -290,7 +273,6 @@ export class BuildEntryMissingError extends UltimateError {
|
|
|
290
273
|
code: 'X_BUILD_ENTRY_MISSING',
|
|
291
274
|
cause: `x build --target ${input.target} builds from ${input.entry}, and the app does not have it`,
|
|
292
275
|
fix: `x new scratch-app --dry-run --json # its file list carries ${input.entry}; copy that file into this app`,
|
|
293
|
-
docs: docsFor('X_BUILD_ENTRY_MISSING'),
|
|
294
276
|
});
|
|
295
277
|
}
|
|
296
278
|
}
|
|
@@ -306,7 +288,6 @@ export class IslandBuildFailedError extends UltimateError {
|
|
|
306
288
|
code: 'X_BUILD_FAILED',
|
|
307
289
|
cause: `${input.file} is an island entry point and would not bundle: ${input.logs}`,
|
|
308
290
|
fix: `bun build --target browser ${input.file}`,
|
|
309
|
-
docs: docsFor('X_BUILD_FAILED'),
|
|
310
291
|
});
|
|
311
292
|
}
|
|
312
293
|
}
|
|
@@ -321,7 +302,6 @@ export class RoleUnknownError extends UltimateError {
|
|
|
321
302
|
code: 'X_ROLE_UNKNOWN',
|
|
322
303
|
cause: `ROLE="${input.role}" is not a role (known: ${input.known.join(', ')})`,
|
|
323
304
|
fix: `docker run -e ROLE=web my-app:latest # one of: ${input.known.join(', ')}`,
|
|
324
|
-
docs: docsFor('X_ROLE_UNKNOWN'),
|
|
325
305
|
});
|
|
326
306
|
}
|
|
327
307
|
}
|
|
@@ -344,7 +324,6 @@ export class RuntimeDriverSplitError extends UltimateError {
|
|
|
344
324
|
// queues, and "they match" is exactly the reading that makes this bug invisible.
|
|
345
325
|
cause: `an app module installed a ${input.driver} driver (ambient: "${input.ambient}") that is not the object this boot captured ("${input.captured}"), so enqueues and claims would use different queues`,
|
|
346
326
|
fix: `pass the driver to the boot instead of installing it from an app module: runRole({ root, env, runtime: { ${input.driver}: yourDriver } })`,
|
|
347
|
-
docs: docsFor('X_RUNTIME_DRIVER_SPLIT'),
|
|
348
327
|
});
|
|
349
328
|
}
|
|
350
329
|
}
|
|
@@ -362,7 +341,6 @@ export class PortInvalidError extends UltimateError {
|
|
|
362
341
|
code: 'X_PORT_INVALID',
|
|
363
342
|
cause: `${name}="${input.value}" is not a TCP port number between 0 and 65535`,
|
|
364
343
|
fix: `docker run -e ${name}=${name === 'PORT' ? 3000 : 9090} my-app:latest`,
|
|
365
|
-
docs: docsFor('X_PORT_INVALID'),
|
|
366
344
|
});
|
|
367
345
|
}
|
|
368
346
|
}
|
|
@@ -382,7 +360,6 @@ export class EnvSchemaMissingError extends UltimateError {
|
|
|
382
360
|
code: 'X_CONFIG_INVALID',
|
|
383
361
|
cause: `x env ${input.subcommand} needs the env declaration, and app.config.ts exports no "envSchema"`,
|
|
384
362
|
fix: "add to app.config.ts: export const envSchema = { DATABASE_URL: { type: 'url', description: 'Postgres connection URL' } } satisfies EnvSchema; export const env = defineEnv(envSchema);",
|
|
385
|
-
docs: docsFor('X_CONFIG_INVALID'),
|
|
386
363
|
});
|
|
387
364
|
}
|
|
388
365
|
}
|
|
@@ -394,7 +371,6 @@ export class CliNotImplementedError extends UltimateError {
|
|
|
394
371
|
code: 'X_NOT_IMPLEMENTED',
|
|
395
372
|
cause: `${input.feature} is not implemented in this build`,
|
|
396
373
|
fix: input.fix,
|
|
397
|
-
docs: docsFor('X_NOT_IMPLEMENTED'),
|
|
398
374
|
});
|
|
399
375
|
}
|
|
400
376
|
}
|
|
@@ -410,7 +386,7 @@ export class CliNotImplementedError extends UltimateError {
|
|
|
410
386
|
*/
|
|
411
387
|
export class StorageUnwritableError extends UltimateError {
|
|
412
388
|
constructor(cause: string, fix: string) {
|
|
413
|
-
super({ code: 'X_STORAGE_UNWRITABLE', cause, fix
|
|
389
|
+
super({ code: 'X_STORAGE_UNWRITABLE', cause, fix });
|
|
414
390
|
}
|
|
415
391
|
}
|
|
416
392
|
|
|
@@ -435,7 +411,6 @@ export class LocalDiskUnsafeError extends UltimateError {
|
|
|
435
411
|
`disk at ${input.root} — and with no STORAGE_SIGNING_SECRET it would sign upload grants ` +
|
|
436
412
|
'with the development key published in @ultimat3/storage',
|
|
437
413
|
fix: 'export S3_ENDPOINT=https://s3.example.com S3_BUCKET=my-app-uploads # or keep the disk on a mounted volume: export STORAGE_SIGNING_SECRET="$(openssl rand -hex 32)"',
|
|
438
|
-
docs: docsFor('X_ENV_MISSING'),
|
|
439
414
|
});
|
|
440
415
|
}
|
|
441
416
|
}
|
|
@@ -450,7 +425,6 @@ export class SecretsEditorMissingError extends UltimateError {
|
|
|
450
425
|
code: 'X_SECRETS_EDITOR_MISSING',
|
|
451
426
|
cause: `x secrets edit opens the decrypted secrets in an editor and none of ${input.vars.join(', ')} is set`,
|
|
452
427
|
fix: 'EDITOR=nano x secrets edit',
|
|
453
|
-
docs: docsFor('X_SECRETS_EDITOR_MISSING'),
|
|
454
428
|
});
|
|
455
429
|
}
|
|
456
430
|
}
|
|
@@ -466,7 +440,6 @@ export class SecretsEditFailedError extends UltimateError {
|
|
|
466
440
|
code: 'X_SECRETS_EDIT_FAILED',
|
|
467
441
|
cause: `"${input.editor}" exited ${input.code}, so the decrypted buffer was discarded and the committed secrets file was not rewritten`,
|
|
468
442
|
fix: 'x secrets edit',
|
|
469
|
-
docs: docsFor('X_SECRETS_EDIT_FAILED'),
|
|
470
443
|
});
|
|
471
444
|
}
|
|
472
445
|
}
|
|
@@ -483,7 +456,6 @@ export class SecretsExistsError extends UltimateError {
|
|
|
483
456
|
code: 'X_GENERATE_CONFLICT',
|
|
484
457
|
cause: `${input.path} already exists, and x secrets init would replace it`,
|
|
485
458
|
fix: input.fix,
|
|
486
|
-
docs: docsFor('X_GENERATE_CONFLICT'),
|
|
487
459
|
});
|
|
488
460
|
}
|
|
489
461
|
}
|
package/src/flag-reads.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
// `join`/`relative` are `node:`-only by necessity: Bun exposes no path-join primitive.
|
|
11
11
|
import { join, relative } from 'node:path';
|
|
12
|
-
import {
|
|
12
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
13
13
|
import type { Finding } from './output';
|
|
14
14
|
import type { CommandSpec, FlagSpec } from './parse';
|
|
15
15
|
import { GLOBAL_FLAGS } from './parse';
|
|
@@ -64,7 +64,7 @@ const unreadFinding = (declared: DeclaredFlag, at: string): Finding => ({
|
|
|
64
64
|
code: 'X_CLI_FLAG_UNREAD',
|
|
65
65
|
cause: `x ${declared.command} declares --${declared.flag.name} ("${declared.flag.summary}") and no file in the CLI's source reads it, so the flag parses and changes nothing`,
|
|
66
66
|
fix: `read it in ${at} with flag${declared.flag.type === 'boolean' ? 'Bool' : 'String'}(ctx.args, '${declared.flag.name}'), or delete it from the spec's flags`,
|
|
67
|
-
docs:
|
|
67
|
+
docs: ERROR_DOCS_URL,
|
|
68
68
|
at,
|
|
69
69
|
});
|
|
70
70
|
|
package/src/generate-write.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// app root, and `node:path` is the only API that resolves one. `node:fs` for the exists check.
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { resolve, sep } from 'node:path';
|
|
10
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
10
11
|
import { GenerateJsonInvalidError, ScaffoldPathEscapeError } from './errors';
|
|
11
12
|
import { mergeJsonDeep } from './json-merge';
|
|
12
13
|
import type { Finding } from './output';
|
|
@@ -125,7 +126,7 @@ async function planJsonMerge(
|
|
|
125
126
|
code: 'X_GENERATE_CONFLICT',
|
|
126
127
|
cause: `${file.path} exists but is not a JSON object, so its keys cannot be merged`,
|
|
127
128
|
fix: `edit ${file.path} by hand until it parses as a JSON object, or delete it and re-run x g`,
|
|
128
|
-
docs:
|
|
129
|
+
docs: ERROR_DOCS_URL,
|
|
129
130
|
at: file.path,
|
|
130
131
|
},
|
|
131
132
|
};
|
|
@@ -178,7 +179,7 @@ function planFile(
|
|
|
178
179
|
// when run, and a `fix:` is copied and pasted verbatim. Same construction as
|
|
179
180
|
// `generate-kinds.ts`'s `assertSurfaceSupported`.
|
|
180
181
|
fix: `${invocation} --force # overwrites ${file.path}, or pass a different name`,
|
|
181
|
-
docs:
|
|
182
|
+
docs: ERROR_DOCS_URL,
|
|
182
183
|
at: file.path,
|
|
183
184
|
},
|
|
184
185
|
};
|
package/src/guards.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
|
-
import { renderCauseValue, renderThrowable } from '@ultimat3/core';
|
|
12
|
+
import { ERROR_DOCS_URL, renderCauseValue, renderThrowable } from '@ultimat3/core';
|
|
13
13
|
import { fixProblem } from './error-contract';
|
|
14
14
|
import type { Finding } from './output';
|
|
15
15
|
import type { HostCheck } from './verify-step';
|
|
@@ -105,7 +105,7 @@ const failed = (path: string, cause: string): Finding => ({
|
|
|
105
105
|
code: 'X_GUARD_FAILED',
|
|
106
106
|
cause,
|
|
107
107
|
fix: `return a finding from ${path} instead of throwing, then: x verify`,
|
|
108
|
-
docs:
|
|
108
|
+
docs: ERROR_DOCS_URL,
|
|
109
109
|
at: path,
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -113,7 +113,7 @@ const invalid = (path: string, cause: string): Finding => ({
|
|
|
113
113
|
code: 'X_GUARD_INVALID',
|
|
114
114
|
cause,
|
|
115
115
|
fix: `export a \`guard\` object — { summary, check } — from ${path}, then: x verify`,
|
|
116
|
-
docs:
|
|
116
|
+
docs: ERROR_DOCS_URL,
|
|
117
117
|
at: path,
|
|
118
118
|
});
|
|
119
119
|
|
|
@@ -121,7 +121,7 @@ const findingInvalid = (path: string, cause: string): Finding => ({
|
|
|
121
121
|
code: 'X_GUARD_FINDING_INVALID',
|
|
122
122
|
cause: `${path} returned a finding that is not one: ${cause}`,
|
|
123
123
|
fix: `rewrite what ${path} returns as a code, a cause and a fix naming a command or a file, then: x verify`,
|
|
124
|
-
docs:
|
|
124
|
+
docs: ERROR_DOCS_URL,
|
|
125
125
|
at: path,
|
|
126
126
|
});
|
|
127
127
|
|
package/src/index.ts
CHANGED
|
@@ -221,12 +221,7 @@ export {
|
|
|
221
221
|
} from './output';
|
|
222
222
|
export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
|
|
223
223
|
export { flagBool, flagList, flagString, GLOBAL_FLAGS, nearest, parseArgs } from './parse';
|
|
224
|
-
export type {
|
|
225
|
-
PrerenderedPage,
|
|
226
|
-
PrerenderOptions,
|
|
227
|
-
PrerenderReport,
|
|
228
|
-
UnmeasuredRoute,
|
|
229
|
-
} from './prerender';
|
|
224
|
+
export type { PrerenderedPage, PrerenderOptions, PrerenderReport } from './prerender';
|
|
230
225
|
export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
|
|
231
226
|
export { COMMANDS, cliVersion, commandFor, SPECS } from './registry';
|
|
232
227
|
export type { MigratedApp, ServedApp, ServeOptions, StartedApp } from './serve';
|
|
@@ -254,6 +249,7 @@ export type {
|
|
|
254
249
|
SkippedRoute,
|
|
255
250
|
SkipReason,
|
|
256
251
|
StaticReport,
|
|
252
|
+
UnmeasuredRoute,
|
|
257
253
|
} from './static-report';
|
|
258
254
|
export {
|
|
259
255
|
parseStaticReport,
|
package/src/island-bundle.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
|
|
7
7
|
// POSIX by construction), `join`/`basename` the filesystem side.
|
|
8
8
|
import { basename, join, posix, relative, sep } from 'node:path';
|
|
9
|
+
import { renderThrowable } from '@ultimat3/core';
|
|
9
10
|
import { ISLAND_EXTENSION, IslandInvalidError, islandModuleId } from '@ultimat3/render';
|
|
10
11
|
import { contentHash } from '@ultimat3/render/server';
|
|
11
12
|
import { IslandBuildFailedError } from './errors';
|
|
@@ -118,11 +119,38 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
|
|
|
118
119
|
* import or syntax error, and flattening them is what puts the line number in the cause instead of
|
|
119
120
|
* the word "Bundle failed".
|
|
120
121
|
*/
|
|
121
|
-
function describeBuildError(error: unknown): string {
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
export function describeBuildError(error: unknown): string {
|
|
123
|
+
// `renderThrowable`, never `instanceof` + `.message` + `String()`. All three run on a value this
|
|
124
|
+
// process did not build — a `Proxy` traps `getPrototypeOf`, a `message` getter can raise, and
|
|
125
|
+
// `String()` throws outright on a Symbol — and what comes back is carried in
|
|
126
|
+
// `IslandBuildFailedError.logs`, which `errors.ts` interpolates straight into a `cause:`. That
|
|
127
|
+
// is a cross-file hop neither `scripts/catch-render.ts` nor `scripts/error-render.ts` can
|
|
128
|
+
// follow: a throw here loses the whole refusal and replaces it with a TypeError about reporting.
|
|
129
|
+
//
|
|
130
|
+
// The AggregateError branch stays, and it is the reason this function exists: `Bun.build` packs
|
|
131
|
+
// one entry per unresolved import or syntax error into `errors`, and flattening them is what
|
|
132
|
+
// puts a line number in the cause instead of the words "Bundle failed". `stringField` decides
|
|
133
|
+
// whether the value really is that shape, because `instanceof` is a question a Proxy answers.
|
|
134
|
+
const aggregate = aggregatedErrors(error);
|
|
135
|
+
if (aggregate !== undefined && aggregate.length > 0) {
|
|
136
|
+
return aggregate.map((one: unknown) => renderThrowable(one)).join('; ');
|
|
137
|
+
}
|
|
138
|
+
return renderThrowable(error);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `value.errors`, read the way `@ultimat3/core`'s `stringField` reads a string field: narrowed
|
|
143
|
+
* first, dereferenced inside a `try`, `undefined` for anything else. `instanceof AggregateError`
|
|
144
|
+
* is a question a `Proxy` answers with its own `getPrototypeOf` trap, so it is not a check.
|
|
145
|
+
*/
|
|
146
|
+
function aggregatedErrors(value: unknown): readonly unknown[] | undefined {
|
|
147
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
148
|
+
try {
|
|
149
|
+
const held: unknown = (value as Record<string, unknown>)['errors'];
|
|
150
|
+
return Array.isArray(held) ? held : undefined;
|
|
151
|
+
} catch {
|
|
152
|
+
return undefined;
|
|
124
153
|
}
|
|
125
|
-
return error instanceof Error ? error.message : String(error);
|
|
126
154
|
}
|
|
127
155
|
|
|
128
156
|
export interface BuildIslandsOptions {
|
package/src/island-routes.ts
CHANGED
|
@@ -34,7 +34,13 @@ export function islandRoutes(source: IslandSource): readonly Route[] {
|
|
|
34
34
|
error: {
|
|
35
35
|
code: 'X_ROUTE_NOT_FOUND',
|
|
36
36
|
cause: `no island chunk is built at ${request.pathname} — the document that asked for it was rendered against an older build`,
|
|
37
|
-
|
|
37
|
+
// No `x` citation, deliberately — `dev-lock.ts`'s shape. This route is mounted
|
|
38
|
+
// in exactly two places (`cmd-dev.ts`, `serve.ts`) and NEITHER reads `.x/static`:
|
|
39
|
+
// in `x dev` the chunks are rebuilt on the watcher tick, and in the container they
|
|
40
|
+
// were built at boot. `x build --target static` — which is what this said — writes
|
|
41
|
+
// an export directory neither process serves from, so running it changed nothing
|
|
42
|
+
// for the only two readers this line has ever had.
|
|
43
|
+
fix: 'reload the page — this process serves only the chunks it built, and the document holding this URL came from an earlier build',
|
|
38
44
|
},
|
|
39
45
|
},
|
|
40
46
|
{ status: 404 },
|
package/src/mcp-errors.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describeErrorCode, hasErrorCode, listErrorCodes } from '@ultimat3/core';
|
|
6
6
|
import type { ErrorExplanation } from '@ultimat3/mcp';
|
|
7
7
|
import type { CliErrorCode } from './error-codes';
|
|
8
|
-
import { CLI_ERROR_CODES
|
|
8
|
+
import { CLI_ERROR_CODES } from './error-codes';
|
|
9
9
|
import { codeFixes, codeFixScan } from './error-fixes';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -214,7 +214,7 @@ export function explainErrorCode(code: string): ErrorExplanation | undefined {
|
|
|
214
214
|
code,
|
|
215
215
|
cause: described.title,
|
|
216
216
|
fix: cli ? CLI_FIXES[code] : projectedFix(code),
|
|
217
|
-
docs:
|
|
217
|
+
docs: described.docs,
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
|
package/src/metrics-endpoint.ts
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
stringField,
|
|
12
12
|
UltimateError,
|
|
13
13
|
} from '@ultimat3/core';
|
|
14
|
-
import { docsFor } from './error-codes';
|
|
15
14
|
import { neighbouringPort } from './flag-number';
|
|
16
15
|
|
|
17
16
|
/**
|
|
@@ -44,7 +43,6 @@ export class MetricsPortInUseError extends UltimateError {
|
|
|
44
43
|
code: 'X_PORT_IN_USE',
|
|
45
44
|
cause: `the metrics port ${input.port} is already bound, so no role could open its scrape listener`,
|
|
46
45
|
fix: `METRICS_PORT=${neighbouringPort(input.port)} x dev --json`,
|
|
47
|
-
docs: docsFor('X_PORT_IN_USE'),
|
|
48
46
|
});
|
|
49
47
|
}
|
|
50
48
|
}
|
package/src/output.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and the JSON renderer are projections of it, so `--json` can never drift from the terminal
|
|
3
3
|
// output (axiom 4). The human renderer owns the canonical 3-line error format.
|
|
4
4
|
|
|
5
|
-
import { renderThrowable, singleLine, stringField } from '@ultimat3/core';
|
|
5
|
+
import { ERROR_DOCS_URL, renderThrowable, singleLine, stringField } from '@ultimat3/core';
|
|
6
6
|
import { msg } from './messages';
|
|
7
7
|
|
|
8
8
|
export interface Finding {
|
|
@@ -107,7 +107,7 @@ export function findingFrom(value: unknown): Finding {
|
|
|
107
107
|
code: 'X_CLI_UNEXPECTED',
|
|
108
108
|
cause: renderThrowable(value),
|
|
109
109
|
fix: 'x doctor --json',
|
|
110
|
-
docs:
|
|
110
|
+
docs: ERROR_DOCS_URL,
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|