@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.
Files changed (53) hide show
  1. package/package.json +26 -26
  2. package/src/affected.ts +0 -3
  3. package/src/app-boundaries.ts +4 -5
  4. package/src/app-env.ts +7 -2
  5. package/src/browser-launcher.ts +0 -2
  6. package/src/budgets.ts +4 -3
  7. package/src/cmd-build.ts +2 -2
  8. package/src/cmd-db-branch.ts +2 -2
  9. package/src/cmd-deploy.ts +14 -3
  10. package/src/cmd-docs.ts +2 -1
  11. package/src/cmd-doctor.ts +3 -5
  12. package/src/cmd-env.ts +2 -2
  13. package/src/cmd-fix.ts +2 -4
  14. package/src/cmd-new.ts +2 -2
  15. package/src/cmd-shot.ts +22 -2
  16. package/src/db-finding.ts +2 -2
  17. package/src/db-seed.ts +0 -3
  18. package/src/dev-cache.ts +12 -5
  19. package/src/dev-lock.ts +8 -7
  20. package/src/dev-runtime.ts +11 -3
  21. package/src/dev-storage.ts +8 -2
  22. package/src/dev-sync.ts +37 -2
  23. package/src/document-styles.ts +2 -1
  24. package/src/drift.ts +3 -2
  25. package/src/error-codes.ts +9 -2
  26. package/src/error-contract.ts +5 -5
  27. package/src/errors.ts +1 -29
  28. package/src/flag-reads.ts +2 -2
  29. package/src/generate-write.ts +3 -2
  30. package/src/guards.ts +4 -4
  31. package/src/index.ts +2 -6
  32. package/src/island-bundle.ts +32 -4
  33. package/src/island-routes.ts +7 -1
  34. package/src/mcp-errors.ts +2 -2
  35. package/src/metrics-endpoint.ts +0 -2
  36. package/src/output.ts +2 -2
  37. package/src/prerender.ts +32 -19
  38. package/src/static-report.ts +41 -3
  39. package/src/templates/scaffold-container.ts +12 -0
  40. package/src/templates/scaffold-docs.ts +1 -1
  41. package/src/templates/scaffold-domain-package.ts +3 -1
  42. package/src/templates/scaffold-repo.ts +16 -8
  43. package/src/templates/slice-foundation.ts +3 -5
  44. package/src/test-shards.ts +2 -2
  45. package/src/tsconfig-references.ts +2 -2
  46. package/src/verify-checks.ts +3 -2
  47. package/src/verify-floor.ts +8 -6
  48. package/src/verify-run.ts +2 -2
  49. package/src/verify-step.ts +2 -1
  50. package/src/verify-test-run.ts +2 -2
  51. package/src/workspace-checks.ts +10 -12
  52. package/src/workspace-graph.ts +3 -2
  53. 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 === 0 ? 0 : options.port + 1 });
135
+ const listener = listenSyncNode(node, { port: syncPortFor(options.port) });
101
136
  return {
102
137
  url: listener.url,
103
138
  stop: async () => {
@@ -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: 'https://ultimate.dev/errors/X_STYLES_GLOBAL_MISSING',
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: 'https://ultimate.dev/errors/X_DB_DRIFT',
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: 'https://ultimate.dev/errors/X_DB_DRIFT',
196
+ docs: ERROR_DOCS_URL,
196
197
  at: `${DB_PACKAGE}/src`,
197
198
  },
198
199
  ];
@@ -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 and `docsFor`. Every code names the exact command that resolves it, because
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
- export const docsFor = (code: CliErrorCode): string => `https://ultimate.dev/errors/${code}`;
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.
@@ -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 { docsFor } from './error-codes';
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: docsFor('X_ERROR_FIX_INVALID'),
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: docsFor('X_ERROR_CODE_UNDOCUMENTED'),
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: docsFor('X_ERROR_CODE_UNREGISTERED'),
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: docsFor('X_ERROR_CODE_UNDOCUMENTED'),
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, docs: docsFor('X_STORAGE_UNWRITABLE') });
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 { docsFor } from './error-codes';
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: docsFor('X_CLI_FLAG_UNREAD'),
67
+ docs: ERROR_DOCS_URL,
68
68
  at,
69
69
  });
70
70
 
@@ -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: 'https://ultimate.dev/errors/X_GENERATE_CONFLICT',
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: 'https://ultimate.dev/errors/X_GENERATE_CONFLICT',
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: 'https://ultimate.dev/errors/X_GUARD_FAILED',
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: 'https://ultimate.dev/errors/X_GUARD_INVALID',
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: 'https://ultimate.dev/errors/X_GUARD_FINDING_INVALID',
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,
@@ -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
- if (error instanceof AggregateError) {
123
- return error.errors.map((one: unknown) => String(one)).join('; ');
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 {
@@ -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
- fix: 'x build --target static --json # then reload, so the page carries this build’s chunk URLs',
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, docsFor } from './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: cli ? docsFor(code) : described.docs,
217
+ docs: described.docs,
218
218
  };
219
219
  }
220
220
 
@@ -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: 'https://ultimate.dev/errors/X_CLI_UNEXPECTED',
110
+ docs: ERROR_DOCS_URL,
111
111
  };
112
112
  }
113
113