@ultimat3/cli 8.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/CLAUDE.md +9 -3
- package/package.json +26 -25
- package/src/affected.ts +0 -3
- package/src/app-boundaries.ts +4 -5
- package/src/app-env.ts +7 -2
- package/src/app-load.ts +7 -0
- 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 +20 -6
- 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-assets.ts +4 -7
- package/src/dev-cache.ts +140 -36
- package/src/dev-lock.ts +8 -7
- package/src/dev-purge.ts +120 -0
- package/src/dev-queue.ts +31 -6
- package/src/dev-render.ts +11 -14
- package/src/dev-runtime.ts +62 -15
- package/src/dev-storage.ts +8 -2
- package/src/dev-sync.ts +37 -2
- package/src/document-styles.ts +4 -2
- 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 +34 -10
- package/src/island-routes.ts +7 -1
- package/src/island-styles.ts +1 -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 +34 -20
- package/src/runtime-overrides.ts +1 -1
- package/src/serve.ts +1 -1
- package/src/solid-loader.ts +1 -1
- package/src/static-report.ts +41 -3
- package/src/style-csp.ts +2 -1
- package/src/templates/scaffold-container.ts +12 -0
- package/src/templates/scaffold-docs.ts +11 -4
- package/src/templates/scaffold-domain-package.ts +3 -1
- package/src/templates/scaffold-repo.ts +19 -9
- 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-runtime.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// boot installs, only backed by embedded drivers.
|
|
5
5
|
|
|
6
6
|
import { mkdirSync } from 'node:fs';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { configureAuthLimiters, postgresAuthLimiter, resetAuthLimiters } from '@ultimat3/auth';
|
|
7
9
|
import type { PurgeDriver } from '@ultimat3/cache';
|
|
8
10
|
import { isNoopPurgeDriver, selectPurgeDriver } from '@ultimat3/cache';
|
|
9
11
|
import {
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
registerReadinessCheck,
|
|
12
14
|
renderThrowable,
|
|
13
15
|
resolveEnvironment,
|
|
16
|
+
systemClock,
|
|
14
17
|
} from '@ultimat3/core';
|
|
15
18
|
import type { RateLimitStore } from '@ultimat3/http';
|
|
16
19
|
import { postgresRateLimitStore } from '@ultimat3/http';
|
|
@@ -27,7 +30,8 @@ import type { Transport, TransportSelection } from '@ultimat3/realtime/server';
|
|
|
27
30
|
import { selectTransport } from '@ultimat3/realtime/server';
|
|
28
31
|
import type { Storage } from '@ultimat3/storage';
|
|
29
32
|
import { defineStorage, localDriver, s3Driver, usesDevStorageSecret } from '@ultimat3/storage';
|
|
30
|
-
import { startCacheTiers } from './dev-cache';
|
|
33
|
+
import { loadCacheTiers, startCacheTiers } from './dev-cache';
|
|
34
|
+
import { installRetentionSweep } from './dev-purge';
|
|
31
35
|
import type { DevDbClient } from './dev-queue';
|
|
32
36
|
import { pgExecutorFor, startQueue } from './dev-queue';
|
|
33
37
|
import type { DevServices, Env } from './dev-services';
|
|
@@ -182,8 +186,14 @@ export function startStorage(services: DevServices, env: Env, override?: Storage
|
|
|
182
186
|
// missing" from a helper the operator never configured. Not an outright ban on the local disk in
|
|
183
187
|
// production: a single-node Compose deploy on a mounted volume WITH a real secret is a rung on
|
|
184
188
|
// the scale ladder, and refusing it would be a deploy-shape decision, not a security fix.
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
// `{ env }` on ALL THREE, never the ambient `process.env`: this function is HANDED the boot's
|
|
190
|
+
// environment and reads `S3_BUCKET` off it one branch above, so a guard asking a second source
|
|
191
|
+
// could answer `development` for a process booting as `production` — or, with
|
|
192
|
+
// `usesDevStorageSecret({ env })` left bare, refuse a boot whose own env carries a real
|
|
193
|
+
// `STORAGE_SIGNING_SECRET` because the PROCESS does not. Which environment, whether a secret
|
|
194
|
+
// exists, and the name the message prints are one question about one table.
|
|
195
|
+
if (!isLocal({ env }) && usesDevStorageSecret({ env })) {
|
|
196
|
+
throw new LocalDiskUnsafeError({ environment: resolveEnvironment({ env }), root });
|
|
187
197
|
}
|
|
188
198
|
try {
|
|
189
199
|
mkdirSync(root, { recursive: true });
|
|
@@ -194,7 +204,9 @@ export function startStorage(services: DevServices, env: Env, override?: Storage
|
|
|
194
204
|
`mount a writable volume at ${root}, or set S3_ENDPOINT and S3_BUCKET to use object storage instead`,
|
|
195
205
|
);
|
|
196
206
|
}
|
|
197
|
-
|
|
207
|
+
// The guard three lines up reads `env`; so must the disk it guards. Otherwise the boot's
|
|
208
|
+
// environment decides whether signing is allowed and the process's decides what key is used.
|
|
209
|
+
return defineStorage({ disks: { local: localDriver({ root, env }) }, default: 'local' });
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
/**
|
|
@@ -288,16 +300,45 @@ export async function startServices(
|
|
|
288
300
|
const bus: TransportSelection = selectTransport(env);
|
|
289
301
|
const queue = await startQueue(services, overrides);
|
|
290
302
|
const { db, jobs, outbox, events } = queue;
|
|
303
|
+
// The same executor the jobs driver, the outbox, the event bus and the idempotency store run
|
|
304
|
+
// on — one pool, one `Bun.sql` that does NOT satisfy `PgExecutor` (`Bun.sql.query` is
|
|
305
|
+
// `undefined`), one wrapper.
|
|
306
|
+
const executor = pgExecutorFor(db);
|
|
307
|
+
const rateLimitStore = postgresRateLimitStore({ executor });
|
|
291
308
|
// Boot is a sequence of external resources, and every step after the first can reject — the
|
|
292
309
|
// queue is already up, so from here an unwind must release it exactly like everything after it.
|
|
310
|
+
// Which is why the `try` opens on the NEXT line and not eight steps further down: it did, and
|
|
311
|
+
// the steps above it registered process-wide state (`configureAuthLimiters`, the `purge()` job
|
|
312
|
+
// and its `task`) that throws on a name a previous boot in this process left in the registry —
|
|
313
|
+
// `X_JOB_NAME_TAKEN`, outside the unwind, so `x dev` exited holding the PGlite lock, the pool
|
|
314
|
+
// and the ambient accessors. Nothing may be pushed onto `started` from outside this block.
|
|
293
315
|
const started: (() => void | Promise<void>)[] = [() => queue.stop()];
|
|
294
|
-
// One readiness check per resource this boot OWNS, released with it. Nothing in the tree
|
|
295
|
-
// registered one, so `/readyz` was `markReady()` alone — "this process bound a socket" — and
|
|
296
|
-
// `packages/http/src/server.ts` calls that BEFORE `Bun.serve`, while `dev-roles.ts` calls it
|
|
297
|
-
// before `sync`, `worker` and `scheduler` start. The chart's `readinessProbe` and the container
|
|
298
|
-
// healthcheck both route on it, so a replica whose pool was gone kept taking traffic.
|
|
299
|
-
started.push(probedReadinessCheck('database', () => db.ping()));
|
|
300
316
|
try {
|
|
317
|
+
// Where failed sign-ins are counted, installed BEFORE `loadApp` for the reason the idempotency
|
|
318
|
+
// store is: `defineAuth` is the app's call and it runs when the app's modules import, so a seam
|
|
319
|
+
// filled afterwards is one every app would have to fill itself. A FACTORY and not a limiter —
|
|
320
|
+
// the app declares `maxAttempts`/`windowMs`/`lockoutMs` and this boot has not read them yet, so
|
|
321
|
+
// a limiter built here would be refused by `assertAuthLimiterPolicy` on any app that tuned one.
|
|
322
|
+
//
|
|
323
|
+
// Until this line every deployment the framework produces counted lockouts per POD, while
|
|
324
|
+
// `x new` scaffolds `replicas: 2` and `docker/helm` runs three — `maxAttempts × N` guesses per
|
|
325
|
+
// account, and a lockout one replica established invisible to the rest.
|
|
326
|
+
configureAuthLimiters((policy) =>
|
|
327
|
+
postgresAuthLimiter({ executor, clock: systemClock, policy }),
|
|
328
|
+
);
|
|
329
|
+
started.push(() => resetAuthLimiters());
|
|
330
|
+
// The hourly sweep over the three framework tables this boot is responsible for. Every one of
|
|
331
|
+
// them ships a `purgeExpired()` that nothing called, so every row written was a row kept —
|
|
332
|
+
// `x_rate_limit` takes one upsert per request the web role serves, assets included.
|
|
333
|
+
started.push(
|
|
334
|
+
installRetentionSweep({ idempotency: queue.idempotency, rateLimit: rateLimitStore }),
|
|
335
|
+
);
|
|
336
|
+
// One readiness check per resource this boot OWNS, released with it. Nothing in the tree
|
|
337
|
+
// registered one, so `/readyz` was `markReady()` alone — "this process bound a socket" — and
|
|
338
|
+
// `packages/http/src/server.ts` calls that BEFORE `Bun.serve`, while `dev-roles.ts` calls it
|
|
339
|
+
// before `sync`, `worker` and `scheduler` start. The chart's `readinessProbe` and the container
|
|
340
|
+
// healthcheck both route on it, so a replica whose pool was gone kept taking traffic.
|
|
341
|
+
started.push(probedReadinessCheck('database', () => db.ping()));
|
|
301
342
|
// Dialled here rather than at selection: an unreachable bus must fail at `x dev`, not on the
|
|
302
343
|
// first change nobody receives, and the socket is a resource the unwind below has to release.
|
|
303
344
|
// A supplied transport is already connected and is NOT closed here: whoever built it owns its
|
|
@@ -331,7 +372,13 @@ export async function startServices(
|
|
|
331
372
|
// recomputed every cached read. Released with `resetTiers()`, which drops the whole registry:
|
|
332
373
|
// this boot is the only thing that registers one, and a tier left behind would purge for a
|
|
333
374
|
// process that has stopped.
|
|
334
|
-
|
|
375
|
+
// The ladder is the app's declaration, never the environment's. `cache.tiers` was declared,
|
|
376
|
+
// validated and documented while NOTHING read it — so an app asking for one rung got four,
|
|
377
|
+
// and one asking for a shared tier got whatever `REDIS_URL` happened to say. `.x` is
|
|
378
|
+
// `resolveServices`' own join, so its parent is the app root: the one fact this function needs
|
|
379
|
+
// and does not already carry.
|
|
380
|
+
const tiers = await loadCacheTiers(dirname(services.stateDir));
|
|
381
|
+
started.push(startCacheTiers({ env, purge, transport, tiers }));
|
|
335
382
|
|
|
336
383
|
return {
|
|
337
384
|
services,
|
|
@@ -343,12 +390,12 @@ export async function startServices(
|
|
|
343
390
|
storage,
|
|
344
391
|
mail,
|
|
345
392
|
mailDetail: selection.detail,
|
|
393
|
+
// Built above, beside the auth limiter factory and the retention sweep that purges its
|
|
394
|
+
// table: three readers of one executor, resolved once. `startWeb` is what an override
|
|
395
|
+
// replaces it at.
|
|
396
|
+
rateLimitStore,
|
|
346
397
|
// The env key that selected the bus — or the honest answer that no env key did, because a
|
|
347
398
|
// boot line reading `NATS_URL` over a transport the host handed in is a lie a script parses.
|
|
348
|
-
// The same executor the jobs driver, the outbox, the event bus and the idempotency store
|
|
349
|
-
// run on — one pool, one `Bun.sql` that does NOT satisfy `PgExecutor` (`Bun.sql.query` is
|
|
350
|
-
// `undefined`), one wrapper. `startWeb` is what an override replaces it at.
|
|
351
|
-
rateLimitStore: postgresRateLimitStore({ executor: pgExecutorFor(db) }),
|
|
352
399
|
transportDetail: overrides?.transport === undefined ? bus.detail : 'runtime override',
|
|
353
400
|
presenceTtlMs: bus.presenceTtlMs,
|
|
354
401
|
purge,
|
package/src/dev-storage.ts
CHANGED
|
@@ -151,8 +151,14 @@ export function parseByteRange(
|
|
|
151
151
|
if (from === '' && to === '') return undefined;
|
|
152
152
|
if (from === '') {
|
|
153
153
|
const wanted = Number(to);
|
|
154
|
-
// A suffix longer than the object is the whole object, not a refusal
|
|
155
|
-
|
|
154
|
+
// A suffix longer than the object is the whole object, not a refusal — but there is no whole
|
|
155
|
+
// object to fall back to at `size === 0`, and the arithmetic below answers `{ start: 0, end:
|
|
156
|
+
// -1 }`, which the route renders as `content-range: bytes 0--1/0` with status 206. RFC 9110
|
|
157
|
+
// requires 416. The non-suffix branch already gets this right through `start >= size`; this
|
|
158
|
+
// one had no equivalent test.
|
|
159
|
+
return size === 0 || wanted === 0
|
|
160
|
+
? UNSATISFIABLE
|
|
161
|
+
: { start: Math.max(size - wanted, 0), end: size - 1 };
|
|
156
162
|
}
|
|
157
163
|
const start = Number(from);
|
|
158
164
|
const end = to === '' ? size - 1 : Math.min(Number(to), size - 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,8 +4,10 @@
|
|
|
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
|
-
import { routeEntries
|
|
9
|
+
import { routeEntries } from '@ultimat3/render';
|
|
10
|
+
import { stylesFor } from '@ultimat3/render/server';
|
|
9
11
|
import type { Finding } from './output';
|
|
10
12
|
|
|
11
13
|
/** The stylesheet an app is expected to own, named in the fix so it is one edit, not a hunt. */
|
|
@@ -48,7 +50,7 @@ export function checkDocumentStyles(documents: readonly SurfaceDocument[]): read
|
|
|
48
50
|
code: 'X_STYLES_GLOBAL_MISSING',
|
|
49
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`,
|
|
50
52
|
fix: `add apps/web/${APP_GLOBAL_STYLESHEET} containing \`@use '@ultimat3/ui/global.scss';\` and apps/web/${APP_GLOBAL_MODULE} containing \`import './global.scss';\``,
|
|
51
|
-
docs:
|
|
53
|
+
docs: ERROR_DOCS_URL,
|
|
52
54
|
at: `apps/web/${APP_GLOBAL_STYLESHEET}`,
|
|
53
55
|
}));
|
|
54
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,
|