@ultimat3/cli 9.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +71 -0
- package/package.json +28 -26
- package/src/affected.ts +0 -3
- package/src/app-boundaries.ts +4 -5
- package/src/app-env.ts +7 -2
- package/src/browser-launcher.ts +0 -2
- package/src/budgets.ts +10 -3
- package/src/cmd-build.ts +2 -2
- package/src/cmd-db-backfill.ts +14 -1
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-deploy.ts +14 -3
- package/src/cmd-dev.ts +3 -0
- package/src/cmd-docs.ts +2 -1
- package/src/cmd-doctor.ts +3 -5
- package/src/cmd-env.ts +2 -2
- package/src/cmd-fix.ts +2 -4
- package/src/cmd-new.ts +36 -6
- package/src/cmd-shot.ts +22 -2
- package/src/command.ts +12 -0
- package/src/db-finding.ts +2 -2
- package/src/db-seed.ts +0 -3
- package/src/dev-assets.ts +6 -0
- package/src/dev-cache.ts +12 -5
- package/src/dev-hooks.ts +8 -0
- package/src/dev-lock.ts +8 -7
- package/src/dev-purge.ts +8 -2
- package/src/dev-render.ts +5 -2
- package/src/dev-roles.ts +26 -2
- package/src/dev-runtime.ts +11 -3
- package/src/dev-storage.ts +8 -2
- package/src/dev-sync.ts +37 -2
- package/src/dispatch.ts +8 -0
- package/src/document-styles.ts +2 -1
- package/src/drift.ts +3 -2
- package/src/error-catalog.ts +11 -1
- package/src/error-codes.ts +19 -2
- package/src/error-contract.ts +30 -7
- package/src/error-pages.ts +79 -0
- package/src/errors.ts +26 -29
- package/src/favicon.ts +113 -0
- package/src/fix-path.ts +104 -0
- package/src/flag-reads.ts +2 -2
- package/src/generate-write.ts +3 -2
- package/src/guards.ts +4 -4
- package/src/hold.ts +73 -7
- package/src/index.ts +16 -6
- package/src/island-bundle.ts +32 -4
- package/src/island-routes.ts +7 -1
- package/src/live-routes.ts +181 -0
- package/src/mcp-errors.ts +7 -2
- package/src/messages.ts +6 -4
- package/src/metrics-endpoint.ts +0 -2
- package/src/output.ts +2 -2
- package/src/prerender.ts +64 -22
- package/src/script-csp.ts +17 -0
- package/src/serve.ts +12 -1
- package/src/static-report.ts +41 -3
- package/src/templates/admin-page.ts +11 -7
- package/src/templates/imports.ts +26 -0
- package/src/templates/route.ts +2 -2
- package/src/templates/scaffold-app.ts +18 -6
- package/src/templates/scaffold-auth.ts +151 -0
- package/src/templates/scaffold-container.ts +12 -0
- package/src/templates/scaffold-docs.ts +1 -1
- package/src/templates/scaffold-domain-package.ts +3 -1
- package/src/templates/scaffold-mcp-package.ts +6 -3
- package/src/templates/scaffold-repo.ts +17 -8
- package/src/templates/slice-foundation.ts +3 -5
- package/src/test-shards.ts +2 -2
- package/src/tsconfig-references.ts +2 -2
- package/src/verify-checks.ts +10 -3
- package/src/verify-floor.ts +8 -6
- package/src/verify-run.ts +2 -2
- package/src/verify-step.ts +2 -1
- package/src/verify-test-run.ts +2 -2
- package/src/workspace-checks.ts +10 -12
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +7 -1
package/CLAUDE.md
CHANGED
|
@@ -193,6 +193,7 @@ cast: a `null` where an id was expected would otherwise become a mutation agains
|
|
|
193
193
|
| `fix-imports.ts` | which of those factories a file can call that it did not declare — one relative specifier, one file read |
|
|
194
194
|
| `error-contract.ts` | the rules, the two checks that turn them into findings, and `collectDeclaredCodes` |
|
|
195
195
|
| `fix-command.ts` | resolving an `x <command>` a `fix:` cites against the registry |
|
|
196
|
+
| `fix-path.ts` | resolving a PATH or a glob a `fix:` cites against the root the gate is running in |
|
|
196
197
|
| `source-files.ts` | which files are shipped source — shared with `filesize`, never a second list |
|
|
197
198
|
|
|
198
199
|
**A `fix:` may not cite a command this build does not ship.** Six shipped fix lines named
|
|
@@ -201,6 +202,26 @@ one passed — the text rule checks that a fix NAMES a command, never that the r
|
|
|
201
202
|
`fix-command.ts` resolves the citation, and a PLANNED command fails too: `x logs` parses, `x help`
|
|
202
203
|
lists it, and running it hands the reader `X_NOT_IMPLEMENTED` instead of the fix.
|
|
203
204
|
|
|
205
|
+
**A `fix:` may not cite a file this repo does not have, either.** That was the other half, and
|
|
206
|
+
nothing resolved it: `X_UI_RUNTIME_MISSING` told its reader to paste a line no generator ever wrote,
|
|
207
|
+
through every gate since it shipped (#274, #246). A file token is one of the four things that make a
|
|
208
|
+
fix an instruction at all (`COMMAND_TOKENS`), so `fix-path.ts` is built from the SAME extension list
|
|
209
|
+
— a token that satisfies the instruction rule is exactly the token this one has to resolve, and two
|
|
210
|
+
lists would be a citation the second rule cannot see. `X_ERROR_FIX_PATH_MISSING` is its own code:
|
|
211
|
+
`X_ERROR_FIX_INVALID` means the fix is not an instruction, this one means it is one and points at
|
|
212
|
+
nothing, and the repairs differ.
|
|
213
|
+
|
|
214
|
+
It is narrow so a finding never has to be argued with — three shapes are not judged at all, because
|
|
215
|
+
each resolves against something other than the root the gate is running in: a scoped specifier
|
|
216
|
+
(`@ultimat3/ui/global.scss`, which resolves through `node_modules`), a dot-relative path
|
|
217
|
+
(`./global.scss`, which resolves against the reader's own file) and any path whose **parent
|
|
218
|
+
directory** this root does not have (`src/errors.ts`, `apps/web/server.ts`,
|
|
219
|
+
`packages/i18n/catalogs/en.json` — all three name a directory a generated app has and this repo does
|
|
220
|
+
not). What is left is the citation a root really can answer: a directory that exists, named as
|
|
221
|
+
holding a file it does not hold. A glob must match at least one file. Measured over all three roots
|
|
222
|
+
the gate runs in — the framework, `examples/dummy`, `dummy/social-media-clone` — **117 path citations
|
|
223
|
+
read, 0 findings**, so it enforces outright with no pin table.
|
|
224
|
+
|
|
204
225
|
The rule is **conditional, and that is load-bearing**: *if* a fix cites `x <command>`, it must
|
|
205
226
|
resolve. It does not require every fix to name one — `set OTEL_EXPORTER_OTLP_ENDPOINT=…` and
|
|
206
227
|
`counter('orders_total', { maxSeries: 4000 })` are executable and correctly cite nothing, and a
|
|
@@ -542,7 +563,9 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
|
|
|
542
563
|
| `otlp-export.ts` | the exporters `OTEL_EXPORTER_OTLP_ENDPOINT` switches on, and their drain hooks |
|
|
543
564
|
| `dev-render.ts` | one HTTP route per registered `route`, through render's own mode function |
|
|
544
565
|
| `style-csp.ts` | the `style-src` sha256 of every inline `<style>` the web role serves |
|
|
566
|
+
| `script-csp.ts` | the `script-src` sha256 of every inline `<script>` it serves — the hydration runtime, from `@ultimat3/render`'s own `HYDRATE_RUNTIME_BODIES` |
|
|
545
567
|
| `dev-assets.ts` | the image pipeline's only HTTP surface: `/icons/*` and `/media/*` |
|
|
568
|
+
| `favicon.ts` | `/favicon.ico`: the app's own file, and the bytes the framework answers with when there is none |
|
|
546
569
|
| `dev-hooks.ts` | the pipeline's `authorize` seam, decided from the app's own `Policy` objects |
|
|
547
570
|
| `dev-roles.ts` | `--role` selection plus start/stop for `web`, `sync`, `worker`, `scheduler` |
|
|
548
571
|
| `dev-dashboard.ts` | the `DevSources` hooks only this process can answer, and the two CLI panels |
|
|
@@ -654,6 +677,23 @@ missing.
|
|
|
654
677
|
| `prerender.ts` | build first, write the chunks into the export, then measure |
|
|
655
678
|
| `budgets.ts` | `measureDocumentJs` weighs `data-x-entry` as well as `<script src>` |
|
|
656
679
|
|
|
680
|
+
**A stats row is keyed by the route's DECLARED path, and holds its heaviest page**
|
|
681
|
+
(`As of 2026-08-23`). `checkBudgets` looks a route up by `route.url` off the manifest, which is the
|
|
682
|
+
pattern (`/blog/:slug`), and `prerenderSite` pushed `artifact.path` — the filled one
|
|
683
|
+
(`/blog/hello`). So no dynamic static route had ever been weighed: each was `X_BUDGET_UNMEASURED`
|
|
684
|
+
and `X_BUDGET_EXCEEDED` could not fire for the whole class. The heaviest page and not the first,
|
|
685
|
+
because a budget is a ceiling and the page that breaks it is the one the route answers for; the
|
|
686
|
+
report's `emitted` list still names every filled path.
|
|
687
|
+
|
|
688
|
+
**Both halves of the CSP are computed at boot, `As of 2026-08-23`.** `style-csp.ts` was alone, and
|
|
689
|
+
the hydration runtime is emitted INLINE in every document carrying an island — so with
|
|
690
|
+
`script-src 'self' 'wasm-unsafe-eval'` no island booted in any container. `x dev` sends the policy
|
|
691
|
+
report-only (`dev: true`), which is exactly why nobody saw it: the page hydrated on a laptop and
|
|
692
|
+
never in the image. `startWeb` extends both directives; the property is pinned end to end by
|
|
693
|
+
`dev-roles-script-csp.test.ts`, which parses a served document, hashes every executable inline
|
|
694
|
+
script in it and asserts the response's own `script-src` names each one — with `dev: false`, the
|
|
695
|
+
only mode in which the policy can block anything.
|
|
696
|
+
|
|
657
697
|
**One `Bun.build` per island, never one call with N entry points**, and `splitting: false`. The
|
|
658
698
|
island's `src` is a string, so no import edge reaches it and the page's graph stays the page's
|
|
659
699
|
(axiom 6) — a shared chunk would put that number back behind a graph walk, and the budget compares
|
|
@@ -719,6 +759,21 @@ width is not one of the eight. Anything outside it is still served; only the `pu
|
|
|
719
759
|
no caller gains a new 4xx. `?q=` is deliberately still unbounded here — the closed set for quality
|
|
720
760
|
is `@ultimat3/seo`'s to declare, not this file's.
|
|
721
761
|
|
|
762
|
+
**`/favicon.ico` is a mechanism, not a scaffolded file.** Every browser requests it unprompted, the
|
|
763
|
+
scaffold wrote none and neither served surface mounted a route, so a permanent 404 sat in the console
|
|
764
|
+
of every app the framework produces — noise that trains the reader to ignore console errors, which is
|
|
765
|
+
the opposite of what `--json` and an executable `fix:` are for (#272). Two rungs and one path: the
|
|
766
|
+
app's own `apps/web/site/favicon.ico` wins, and `favicon.ts` answers a 32x32 PNG encoded through
|
|
767
|
+
`@ultimat3/core`'s own pipeline when there is none — the same encoder `x new`'s icon goes through, so
|
|
768
|
+
there is no second image format in the tree and no base64 blob nobody can verify. It is deliberately
|
|
769
|
+
NOT derived from `ICON_SOURCE`: resizing the install icon needs `@ultimat3/pwa`'s pipeline and would
|
|
770
|
+
make the answer depend on a file that may be absent, which is a third rung under a mechanism that has
|
|
771
|
+
exactly two. The file is read per REQUEST, so dropping one into a running `x dev` takes effect
|
|
772
|
+
without a restart. It mounts through `assetRoutes`, which is the one route set `serve.ts` and
|
|
773
|
+
`cmd-dev.ts` both compose — a favicon added to one of them alone is a 404 that comes back in
|
|
774
|
+
production only — and `prerenderSite` writes the same bytes into the static export, because an
|
|
775
|
+
artifact served with no process behind it has to carry every byte the browser will ask for.
|
|
776
|
+
|
|
722
777
|
`ICON_SOURCE` lives here, not in `cmd-doctor.ts`, because this is the module that reads it: the
|
|
723
778
|
diagnostic checks what `x dev` serves, so one constant cannot pass the check and serve nothing.
|
|
724
779
|
It is a **PNG** — core decodes PNG and JPEG only, and the SVG this used to name could never
|
|
@@ -745,6 +800,22 @@ watcher — *after* the drain, so an in-flight request still has the database it
|
|
|
745
800
|
Ctrl-C is therefore the same three phases production runs, not a kill that leaves `.x/pgdata`
|
|
746
801
|
locked by a process that no longer exists.
|
|
747
802
|
|
|
803
|
+
**The release runs INSIDE the drain's own deadline, `As of 2026-08-23`, and it is the same
|
|
804
|
+
deadline.** `drain()` ABANDONS a hook that overruns `ShutdownReason.deadlineAt` — the process is
|
|
805
|
+
meant to exit without it — and `release` re-enters the very same teardown one call later:
|
|
806
|
+
`app.stop()` → `startRoles().stop()` → `worker.stop()`, memoised in the package that owns it, so
|
|
807
|
+
awaiting it is awaiting the promise the drain just walked away from. Unbounded, that hung past
|
|
808
|
+
`terminationGracePeriodSeconds` and the kubelet SIGKILLed a process that had already drained
|
|
809
|
+
cleanly. The budget is the hook's own `reason.deadlineAt`, not a stopwatch of ours, so there is one
|
|
810
|
+
number and not two; an overrun is logged as `X_SHUTDOWN_TIMEOUT` and a REJECTION still rejects,
|
|
811
|
+
because `dispatch` awaits the hold inside its own `try`.
|
|
812
|
+
|
|
813
|
+
**`options.exit` has exactly one caller: `runRole` in `serve.ts`.** `bin.ts` ends in
|
|
814
|
+
`process.exit(code)`, so `x dev` and `x mcp` need nothing; `apps/web/server.ts` — which is what a
|
|
815
|
+
container runs — has no such line, and one non-unref'd interval anywhere in the app then holds an
|
|
816
|
+
event loop with nothing left to do. A function rather than a boolean because `process.exit` inside
|
|
817
|
+
a library is untestable, and the caller is the one that knows.
|
|
818
|
+
|
|
748
819
|
Commands: `bun test`, `bunx tsc --noEmit -p tsconfig.json`.
|
|
749
820
|
|
|
750
821
|
## A declared flag with no reader is a promise `x help` makes and nothing keeps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,31 +37,33 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "
|
|
41
|
-
"@ultimat3/admin": "
|
|
42
|
-
"@ultimat3/ai": "
|
|
43
|
-
"@ultimat3/auth": "
|
|
44
|
-
"@ultimat3/cache": "
|
|
45
|
-
"@ultimat3/core": "
|
|
46
|
-
"@ultimat3/db": "
|
|
47
|
-
"@ultimat3/entity": "
|
|
48
|
-
"@ultimat3/
|
|
49
|
-
"@ultimat3/
|
|
50
|
-
"@ultimat3/
|
|
51
|
-
"@ultimat3/
|
|
52
|
-
"@ultimat3/
|
|
53
|
-
"@ultimat3/
|
|
54
|
-
"@ultimat3/
|
|
55
|
-
"@ultimat3/
|
|
56
|
-
"@ultimat3/
|
|
57
|
-
"@ultimat3/
|
|
58
|
-
"@ultimat3/
|
|
59
|
-
"@ultimat3/
|
|
60
|
-
"@ultimat3/
|
|
61
|
-
"@ultimat3/
|
|
62
|
-
"@ultimat3/
|
|
63
|
-
"@ultimat3/
|
|
64
|
-
"@ultimat3/
|
|
40
|
+
"@ultimat3/action": "11.0.0",
|
|
41
|
+
"@ultimat3/admin": "11.0.0",
|
|
42
|
+
"@ultimat3/ai": "11.0.0",
|
|
43
|
+
"@ultimat3/auth": "11.0.0",
|
|
44
|
+
"@ultimat3/cache": "11.0.0",
|
|
45
|
+
"@ultimat3/core": "11.0.0",
|
|
46
|
+
"@ultimat3/db": "11.0.0",
|
|
47
|
+
"@ultimat3/entity": "11.0.0",
|
|
48
|
+
"@ultimat3/flags": "11.0.0",
|
|
49
|
+
"@ultimat3/http": "11.0.0",
|
|
50
|
+
"@ultimat3/i18n": "11.0.0",
|
|
51
|
+
"@ultimat3/jobs": "11.0.0",
|
|
52
|
+
"@ultimat3/mail": "11.0.0",
|
|
53
|
+
"@ultimat3/manifest": "11.0.0",
|
|
54
|
+
"@ultimat3/mcp": "11.0.0",
|
|
55
|
+
"@ultimat3/money": "11.0.0",
|
|
56
|
+
"@ultimat3/policy": "11.0.0",
|
|
57
|
+
"@ultimat3/pwa": "11.0.0",
|
|
58
|
+
"@ultimat3/query": "11.0.0",
|
|
59
|
+
"@ultimat3/realtime": "11.0.0",
|
|
60
|
+
"@ultimat3/render": "11.0.0",
|
|
61
|
+
"@ultimat3/schema": "11.0.0",
|
|
62
|
+
"@ultimat3/scraping": "11.0.0",
|
|
63
|
+
"@ultimat3/seo": "11.0.0",
|
|
64
|
+
"@ultimat3/storage": "11.0.0",
|
|
65
|
+
"@ultimat3/testing": "11.0.0",
|
|
66
|
+
"@ultimat3/time": "11.0.0",
|
|
65
67
|
"babel-preset-solid": "^1.9.15"
|
|
66
68
|
}
|
|
67
69
|
}
|
package/src/affected.ts
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// directory is checkout-relative while a caller's scan yields paths relative to its own root.
|
|
14
14
|
import { join, relative } from 'node:path';
|
|
15
15
|
import { singleLine, UltimateError } from '@ultimat3/core';
|
|
16
|
-
import { docsFor } from './error-codes';
|
|
17
16
|
import { BadFlagError } from './errors';
|
|
18
17
|
import type { ExecResult, Runner } from './exec';
|
|
19
18
|
import { execOutput } from './exec';
|
|
@@ -193,7 +192,6 @@ export async function gitRoot(runner: Runner, cwd: string, command: string): Pro
|
|
|
193
192
|
code: 'X_CLI_UNEXPECTED',
|
|
194
193
|
cause: `x ${command} reads its diff from git and "git rev-parse --show-toplevel" exited ${result.code} in ${cwd}: ${singleLine(execOutput(result))}`,
|
|
195
194
|
fix: `run x ${command} from inside a git checkout — confirm with: git rev-parse --show-toplevel`,
|
|
196
|
-
docs: docsFor('X_CLI_UNEXPECTED'),
|
|
197
195
|
});
|
|
198
196
|
}
|
|
199
197
|
|
|
@@ -245,7 +243,6 @@ export async function changedFiles(
|
|
|
245
243
|
code: 'X_CLI_UNEXPECTED',
|
|
246
244
|
cause: `"${failed.command.join(' ')}" exited ${failed.code} in ${options.cwd}: ${singleLine(execOutput(failed))}`,
|
|
247
245
|
fix: `run it yourself to see why: ${failed.command.join(' ')}`,
|
|
248
|
-
docs: docsFor('X_CLI_UNEXPECTED'),
|
|
249
246
|
});
|
|
250
247
|
}
|
|
251
248
|
return [...new Set(runs.flatMap((run) => paths(run.stdout)))].sort();
|
package/src/app-boundaries.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join as joinPath } from 'node:path';
|
|
15
15
|
// The POSIX variants resolve specifiers against import-graph keys, which are POSIX on every host.
|
|
16
16
|
import { dirname, join, normalize, relative } from 'node:path/posix';
|
|
17
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
17
18
|
import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
|
|
18
19
|
import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
|
|
19
20
|
import type { Finding } from './output';
|
|
@@ -48,8 +49,6 @@ const CODE_OF: Readonly<Record<BoundaryRule, BoundaryCode>> = {
|
|
|
48
49
|
*/
|
|
49
50
|
export const boundaryCodeOf = (rule: BoundaryRule): BoundaryCode => CODE_OF[rule];
|
|
50
51
|
|
|
51
|
-
const docs = (code: BoundaryCode): string => `https://ultimate.dev/errors/${code}`;
|
|
52
|
-
|
|
53
52
|
const isRoute = (path: string): boolean => /\/(page|layout|route)\.[cm]?tsx?$/.test(path);
|
|
54
53
|
const isService = (path: string): boolean => /\/service\.[cm]?ts$/.test(path);
|
|
55
54
|
const isDbSpecifier = (specifier: string): boolean =>
|
|
@@ -128,7 +127,7 @@ const surfaceFindings = (graph: ImportGraph): readonly Finding[] =>
|
|
|
128
127
|
code,
|
|
129
128
|
cause: violation.cause,
|
|
130
129
|
fix: violation.fix,
|
|
131
|
-
docs:
|
|
130
|
+
docs: ERROR_DOCS_URL,
|
|
132
131
|
at: violation.importer,
|
|
133
132
|
};
|
|
134
133
|
});
|
|
@@ -198,7 +197,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
|
|
|
198
197
|
code: 'X_BOUNDARY_ROUTE_TO_DB',
|
|
199
198
|
cause: `route imports the database ("${specifier}") — routes call actions and queries`,
|
|
200
199
|
fix: generate('query', file.path, 'then call it from'),
|
|
201
|
-
docs:
|
|
200
|
+
docs: ERROR_DOCS_URL,
|
|
202
201
|
at: file.path,
|
|
203
202
|
});
|
|
204
203
|
}
|
|
@@ -207,7 +206,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
|
|
|
207
206
|
code: 'X_BOUNDARY_SERVICE_TO_HTTP',
|
|
208
207
|
cause: `service imports HTTP ("${specifier}") — a service that knows about requests cannot be reused by a job`,
|
|
209
208
|
fix: generate('action', file.path, 'read the request there and pass plain values to'),
|
|
210
|
-
docs:
|
|
209
|
+
docs: ERROR_DOCS_URL,
|
|
211
210
|
at: file.path,
|
|
212
211
|
});
|
|
213
212
|
}
|
package/src/app-env.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
checkEnvExample,
|
|
12
|
+
ENV_EXAMPLE_PATH,
|
|
13
|
+
ERROR_DOCS_URL,
|
|
14
|
+
renderEnvExample,
|
|
15
|
+
} from '@ultimat3/core';
|
|
11
16
|
import { APP_CONFIG_FILE } from './app-root';
|
|
12
17
|
import type { Finding } from './output';
|
|
13
18
|
import { findingFrom } from './output';
|
|
@@ -63,7 +68,7 @@ const driftFinding = (cause: string): Finding => ({
|
|
|
63
68
|
// The generator, not the assertion: `assertEnvExample`'s own fix is a `Bun.write(…)` call for
|
|
64
69
|
// an app that has a schema object in scope, and a gate reader has a shell.
|
|
65
70
|
fix: 'x env example',
|
|
66
|
-
docs:
|
|
71
|
+
docs: ERROR_DOCS_URL,
|
|
67
72
|
at: ENV_EXAMPLE_PATH,
|
|
68
73
|
});
|
|
69
74
|
|
package/src/browser-launcher.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { UltimateError } from '@ultimat3/core';
|
|
8
8
|
import type { CdpLauncherLike, ScrapeDriver } from '@ultimat3/scraping';
|
|
9
9
|
import { localBrowser } from '@ultimat3/scraping';
|
|
10
|
-
import { docsFor } from './error-codes';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* The one library this works against. Playwright is not an alternative and is not a flag:
|
|
@@ -33,7 +32,6 @@ export class ShotBrowserMissingError extends UltimateError {
|
|
|
33
32
|
// one literal, and a fix line the gate cannot read is a fix line nothing holds to the
|
|
34
33
|
// contract. `browser-launcher.test.ts` pins it against the constant instead.
|
|
35
34
|
fix: 'bun add -d puppeteer-core',
|
|
36
|
-
docs: docsFor('X_SHOT_BROWSER_MISSING'),
|
|
37
35
|
meta: { root: input.root, package: BROWSER_PACKAGE },
|
|
38
36
|
});
|
|
39
37
|
}
|
package/src/budgets.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
11
12
|
import type { Manifest, RouteFact } from '@ultimat3/manifest';
|
|
12
13
|
import { formatBytes, parseByteBudget } from '@ultimat3/render';
|
|
13
14
|
import type { Finding } from './output';
|
|
@@ -15,6 +16,12 @@ import type { Finding } from './output';
|
|
|
15
16
|
export const BUILD_STATS_FILE = join('.x', 'build-stats.json');
|
|
16
17
|
|
|
17
18
|
export interface RouteStats {
|
|
19
|
+
/**
|
|
20
|
+
* The route's DECLARED path — `/blog/:slug`, never `/blog/hello`. `checkBudgets` looks a row up
|
|
21
|
+
* by `route.url` off the manifest, which is the pattern, so a row keyed by a filled path is a
|
|
22
|
+
* row nothing can find: every dynamic static route read as `X_BUDGET_UNMEASURED`. A route that
|
|
23
|
+
* prerenders many pages contributes ONE row, holding its heaviest.
|
|
24
|
+
*/
|
|
18
25
|
readonly path: string;
|
|
19
26
|
readonly jsBytes: number;
|
|
20
27
|
/**
|
|
@@ -75,7 +82,7 @@ function unmeasuredFinding(url: string, declared: string, built: boolean): Findi
|
|
|
75
82
|
fix: built
|
|
76
83
|
? `x build --target static --json # its "unmeasured" list says why ${url} could not be weighed`
|
|
77
84
|
: 'x build --target static --json && x verify --json',
|
|
78
|
-
docs:
|
|
85
|
+
docs: ERROR_DOCS_URL,
|
|
79
86
|
at: url,
|
|
80
87
|
};
|
|
81
88
|
}
|
|
@@ -106,7 +113,7 @@ export function checkBudgets(
|
|
|
106
113
|
code: 'X_BUDGET_EXCEEDED',
|
|
107
114
|
cause: `${route.url} ships ${formatBytes(measured.jsBytes)} of JS over a ${formatBytes(js)} budget via ${chainOf(measured)}`,
|
|
108
115
|
fix: `x routes --json to see the chain, then move the heavy import behind hydrate: 'interaction'`,
|
|
109
|
-
docs:
|
|
116
|
+
docs: ERROR_DOCS_URL,
|
|
110
117
|
at: route.url,
|
|
111
118
|
});
|
|
112
119
|
}
|
|
@@ -115,7 +122,7 @@ export function checkBudgets(
|
|
|
115
122
|
code: 'X_BUDGET_EXCEEDED',
|
|
116
123
|
cause: `${route.url} LCP ${measured.lcpMs}ms over the ${lcp}ms budget`,
|
|
117
124
|
fix: `raise the budget in defineRoute, or switch render to 'isr' to serve it prebuilt`,
|
|
118
|
-
docs:
|
|
125
|
+
docs: ERROR_DOCS_URL,
|
|
119
126
|
at: route.url,
|
|
120
127
|
});
|
|
121
128
|
}
|
package/src/cmd-build.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
7
7
|
import { requireAppRoot } from './app-root';
|
|
8
8
|
import { runVerify } from './cmd-verify';
|
|
9
9
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -143,7 +143,7 @@ export function buildResult(input: {
|
|
|
143
143
|
code: 'X_BUILD_FAILED',
|
|
144
144
|
cause: `${input.command.join(' ')} exited ${result.code}`,
|
|
145
145
|
fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
|
|
146
|
-
docs:
|
|
146
|
+
docs: ERROR_DOCS_URL,
|
|
147
147
|
},
|
|
148
148
|
],
|
|
149
149
|
data: {
|
package/src/cmd-db-backfill.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { CommandResult } from './output';
|
|
|
27
27
|
import { findingFrom } from './output';
|
|
28
28
|
import type { ParsedArgs } from './parse';
|
|
29
29
|
import { flagBool, flagString } from './parse';
|
|
30
|
+
import { quoteArg } from './shell-quote';
|
|
30
31
|
|
|
31
32
|
/** Which of the four questions an invocation asked. `pass` is `<name>` and `--all` both. */
|
|
32
33
|
type BackfillShape = 'list' | 'pending' | 'pass';
|
|
@@ -73,11 +74,23 @@ function readShape(args: ParsedArgs): { readonly shape: BackfillShape; readonly
|
|
|
73
74
|
// `--name` is a FILTER under `--list` and a target everywhere else, so it selects a shape only
|
|
74
75
|
// where `--list` is absent. Two spellings of one target are still two, and are refused.
|
|
75
76
|
const target = list ? undefined : (positional ?? named);
|
|
77
|
+
// Under `--list` the positional is DROPPED by the line above, and until 2026-08 nothing said so:
|
|
78
|
+
// `x db backfill cleanup --list` printed the whole ledger and reported `ok: true` while the
|
|
79
|
+
// operator had named one sweep. `--name` is the filter's one spelling here, so this is a refusal
|
|
80
|
+
// and not a second reading of the argument — the same rule every other shape in this function
|
|
81
|
+
// follows, and the same argv one flag over (`--pending cleanup`) has always been refused.
|
|
82
|
+
if (list && positional !== undefined) {
|
|
83
|
+
return refuseShape(
|
|
84
|
+
'name',
|
|
85
|
+
`x db backfill --list filters the ledger with --name, so the positional "${positional}" selects nothing — it is a pass target in every other shape`,
|
|
86
|
+
`x db backfill --list --name ${quoteArg(positional)} --json`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
76
89
|
if (!list && positional !== undefined && named !== undefined) {
|
|
77
90
|
return refuseShape(
|
|
78
91
|
'name',
|
|
79
92
|
`x db backfill names two backfills ("${positional}" and "${named}") — a pass sweeps the positional or --name, never both`,
|
|
80
|
-
`x db backfill ${positional} --write --json`,
|
|
93
|
+
`x db backfill ${quoteArg(positional)} --write --json`,
|
|
81
94
|
);
|
|
82
95
|
}
|
|
83
96
|
const asked = [
|
package/src/cmd-db-branch.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `x db branch ls` used to clone a database called `ls`, because the argument was the name.
|
|
4
4
|
// The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
|
|
5
5
|
|
|
6
|
-
import { nearestName } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
|
|
7
7
|
import { createPostgresClient, type DbClient } from '@ultimat3/db';
|
|
8
8
|
import type { CommandContext } from './command';
|
|
9
9
|
import type { BranchRow } from './db-branch';
|
|
@@ -215,6 +215,6 @@ function notABranch(services: DevServices, name: string): CommandResult {
|
|
|
215
215
|
code: 'X_DB_BRANCH_FAILED',
|
|
216
216
|
cause: `"${name}" is not a branch of this database, so nothing was dropped (it would be ${target})`,
|
|
217
217
|
fix: LIST_FIX,
|
|
218
|
-
docs:
|
|
218
|
+
docs: ERROR_DOCS_URL,
|
|
219
219
|
});
|
|
220
220
|
}
|
package/src/cmd-deploy.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// anything that runs containers can execute.
|
|
4
4
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
7
|
import { requireAppRoot } from './app-root';
|
|
7
8
|
import type { CliCommand, CommandContext } from './command';
|
|
8
9
|
import { BadFlagError, UnknownCommandError } from './errors';
|
|
@@ -31,6 +32,13 @@ import { quoteArg } from './shell-quote';
|
|
|
31
32
|
* honours. Both compose definitions carry it — `docker/docker-compose.prod.yml`'s `backfill` and
|
|
32
33
|
* the one `templates/scaffold-container.ts` scaffolds, which also gates on `migrate` completing.
|
|
33
34
|
* This paragraph said they "still owe" both for as long as they have had them.
|
|
35
|
+
*
|
|
36
|
+
* COMPOSE-ONLY, and `backfill` is why that has to be written down. `docker/helm`'s `roles:` map is
|
|
37
|
+
* `web|sync|worker|scheduler|replicator` plus a `migrate` Job — there is no `backfill` object in
|
|
38
|
+
* the chart at all — and the helm plan is one `helm upgrade --install`, so this list describes
|
|
39
|
+
* neither the objects a chart deploy creates nor the steps it runs. Every reader of it is therefore
|
|
40
|
+
* inside the compose branch; the SUMMARY reads `plan.steps` instead, or `x deploy --method helm`
|
|
41
|
+
* reports a post-deploy sweep to an operator that nothing will ever run.
|
|
34
42
|
*/
|
|
35
43
|
export const DEPLOY_ROLES = ['migrate', 'web', 'sync', 'worker', 'scheduler', 'backfill'] as const;
|
|
36
44
|
|
|
@@ -202,11 +210,14 @@ export const deployCommand: CliCommand = {
|
|
|
202
210
|
env: { ...plan.env },
|
|
203
211
|
steps: plan.steps.map((step) => ({ role: step.role, command: step.command.join(' ') })),
|
|
204
212
|
};
|
|
213
|
+
// The roles THIS plan has, never the compose list: on `--method helm` there is one step,
|
|
214
|
+
// `all`, and naming `backfill` there promises an operator a sweep the chart cannot run.
|
|
215
|
+
const roles = plan.steps.map((step) => step.role).join(',');
|
|
205
216
|
if (flagBool(ctx.args, 'dry-run')) {
|
|
206
217
|
return {
|
|
207
218
|
ok: true,
|
|
208
219
|
command: 'deploy',
|
|
209
|
-
summary: msg('cli.deploy.plan', { images: 1, roles
|
|
220
|
+
summary: msg('cli.deploy.plan', { images: 1, roles }),
|
|
210
221
|
data: planJson,
|
|
211
222
|
lines: plan.steps.map(
|
|
212
223
|
(step) => ` ${step.role.padEnd(10)} ${stepLine(plan.env, step.command)}`,
|
|
@@ -225,7 +236,7 @@ export const deployCommand: CliCommand = {
|
|
|
225
236
|
code: 'X_DEPLOY_FAILED',
|
|
226
237
|
cause: `role "${step.role}" step exited ${result.code}`,
|
|
227
238
|
fix: `${stepLine(plan.env, step.command)} # run it directly to see the full output`,
|
|
228
|
-
docs:
|
|
239
|
+
docs: ERROR_DOCS_URL,
|
|
229
240
|
},
|
|
230
241
|
],
|
|
231
242
|
data: planJson,
|
|
@@ -235,7 +246,7 @@ export const deployCommand: CliCommand = {
|
|
|
235
246
|
return {
|
|
236
247
|
ok: true,
|
|
237
248
|
command: 'deploy',
|
|
238
|
-
summary: msg('cli.deploy.plan', { images: 1, roles
|
|
249
|
+
summary: msg('cli.deploy.plan', { images: 1, roles }),
|
|
239
250
|
data: planJson,
|
|
240
251
|
};
|
|
241
252
|
},
|
package/src/cmd-dev.ts
CHANGED
|
@@ -199,6 +199,9 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
199
199
|
// declaration, and `x dev` and `serve.ts` must not be able to disagree about where the app's
|
|
200
200
|
// sign-in page is.
|
|
201
201
|
signInPath: await loadSignInPath(options.root),
|
|
202
|
+
// The same seam `serve.ts` passes: the app's own error page is a FILE, so the root is what
|
|
203
|
+
// `startWeb` needs to find one.
|
|
204
|
+
root: options.root,
|
|
202
205
|
// The one document this process serves that the app did not write; `startRoles` covers the
|
|
203
206
|
// app's own surfaces itself. `x dev` sends the policy report-only, so an uncovered `<style>`
|
|
204
207
|
// here is a console report rather than a blank page — which is how this reached production.
|
package/src/cmd-docs.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// should not have to guess which of 29 packages holds the answer, and it must never be handed a
|
|
5
5
|
// URL: `node_modules` already contains every doc, because the published artifact IS the source.
|
|
6
6
|
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import type { DocEntry, DocHit } from '@ultimat3/manifest';
|
|
8
9
|
import { nearestTopics, scanInstalledDocs, searchDocs } from '@ultimat3/manifest';
|
|
9
10
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -22,7 +23,7 @@ const unresolvedFinding = (): Finding => ({
|
|
|
22
23
|
code: 'X_CLI_UNEXPECTED',
|
|
23
24
|
cause: '@ultimat3/core does not resolve from the installed CLI, so no docs could be read',
|
|
24
25
|
fix: 'bun install && x doctor --json',
|
|
25
|
-
docs:
|
|
26
|
+
docs: ERROR_DOCS_URL,
|
|
26
27
|
at: import.meta.dir,
|
|
27
28
|
});
|
|
28
29
|
|
package/src/cmd-doctor.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
|
|
7
|
+
import { ERROR_DOCS_URL, tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
|
|
8
8
|
import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
|
|
9
9
|
import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
|
|
10
10
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -49,12 +49,10 @@ export interface DoctorProbe {
|
|
|
49
49
|
snapshots(): Promise<readonly Finding[]>;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const docs = (code: string): string => `https://ultimate.dev/errors/${code}`;
|
|
53
|
-
|
|
54
52
|
const finding = (code: string, cause: string, fix: string, at?: string): Finding =>
|
|
55
53
|
at === undefined
|
|
56
|
-
? { code, cause, fix, docs:
|
|
57
|
-
: { code, cause, fix, docs:
|
|
54
|
+
? { code, cause, fix, docs: ERROR_DOCS_URL }
|
|
55
|
+
: { code, cause, fix, docs: ERROR_DOCS_URL, at };
|
|
58
56
|
|
|
59
57
|
export const OFFLINE_FALLBACK = 'apps/web/app/offline.tsx';
|
|
60
58
|
|
package/src/cmd-env.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { checkEnv, ENV_EXAMPLE_PATH, maskedEnvValues } from '@ultimat3/core';
|
|
7
|
+
import { checkEnv, ENV_EXAMPLE_PATH, ERROR_DOCS_URL, maskedEnvValues } from '@ultimat3/core';
|
|
8
8
|
import { ENV_SCHEMA_EXPORT, envExampleFor, loadEnvSchema } from './app-env';
|
|
9
9
|
import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
|
|
10
10
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -54,7 +54,7 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
|
|
|
54
54
|
code: 'X_ENV_MISSING',
|
|
55
55
|
cause: `${issue.key} is ${issue.reason} (expected ${issue.expected})`,
|
|
56
56
|
fix: issue.fix,
|
|
57
|
-
docs:
|
|
57
|
+
docs: ERROR_DOCS_URL,
|
|
58
58
|
at: ENV_EXAMPLE_PATH,
|
|
59
59
|
}));
|
|
60
60
|
return {
|
package/src/cmd-fix.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (`docs/architecture/02-boundaries.md`) — a caller runs the printed edit, or the generated
|
|
4
4
|
// `git mv`, itself.
|
|
5
5
|
|
|
6
|
-
import { nearestName } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
|
|
7
7
|
import { appImportGraph, readAppSources } from './app-boundaries';
|
|
8
8
|
import { requireAppRoot } from './app-root';
|
|
9
9
|
import type { BoundaryCut } from './boundary-cuts';
|
|
@@ -18,8 +18,6 @@ export { planBoundaryCuts };
|
|
|
18
18
|
|
|
19
19
|
export const FIX_SUBCOMMANDS = ['boundary'] as const;
|
|
20
20
|
|
|
21
|
-
const docsUrl = (code: string): string => `https://ultimate.dev/errors/${code}`;
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Accept either an app-root-relative path or a suffix that matches exactly one scanned file —
|
|
25
23
|
* an agent copying the path out of a `fix:` line has the short form
|
|
@@ -81,7 +79,7 @@ const findingForCut = (cut: BoundaryCut): Finding => ({
|
|
|
81
79
|
code: cut.code,
|
|
82
80
|
cause: cut.cause,
|
|
83
81
|
fix: cut.edit,
|
|
84
|
-
docs:
|
|
82
|
+
docs: ERROR_DOCS_URL,
|
|
85
83
|
at: cut.at,
|
|
86
84
|
});
|
|
87
85
|
|