@ultimat3/cli 22.15.0 → 24.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 +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
package/src/budgets.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
12
12
|
import type { Manifest, RouteFact } from '@ultimat3/manifest';
|
|
13
13
|
import {
|
|
14
|
+
describeRoutes,
|
|
14
15
|
formatBytes,
|
|
15
16
|
parseByteBudget,
|
|
16
17
|
SPECULATION_RULES_TYPE,
|
|
@@ -38,15 +39,6 @@ export interface RouteStats {
|
|
|
38
39
|
* older build simply did not count it, and `checkBudgets` reads `jsBytes` either way.
|
|
39
40
|
*/
|
|
40
41
|
readonly frameworkJsBytes?: number;
|
|
41
|
-
/**
|
|
42
|
-
* **Written by nothing, `As of 2026-08`.** `apps/web/prerender.ts` is the only producer of this
|
|
43
|
-
* file and it emits static HTML — there is no browser in the build to observe a paint. So the
|
|
44
|
-
* comparison in `checkBudgets` below is reachable only for an app that writes its own stats, and
|
|
45
|
-
* `x new` no longer scaffolds an `lcp` budget for exactly that reason: a budget the build cannot
|
|
46
|
-
* weigh passes silently the moment a stats row exists, which is the false green this file's
|
|
47
|
-
* header is about. `RouteBudget.lcp` still accepts one — that key is `@ultimat3/render`'s.
|
|
48
|
-
*/
|
|
49
|
-
readonly lcpMs?: number;
|
|
50
42
|
/** Import chain that pulled the heaviest module into this route. */
|
|
51
43
|
readonly heaviestChain?: readonly string[];
|
|
52
44
|
}
|
|
@@ -66,20 +58,32 @@ export interface BuildStats {
|
|
|
66
58
|
* `examples/dummy` was charged 250 B for `/x-sw-register.js` by a file written before
|
|
67
59
|
* `FRAMEWORK_SCRIPTS` exempted it. `2`: that exemption and the page-boot decision (ledger #28).
|
|
68
60
|
* `3`: an island entry's imported chunks are charged, each once per document (shared chunks).
|
|
61
|
+
* `4`: the exempt theme script's bytes moved — it also stamps `data-theme-default` (plan 101, 09).
|
|
69
62
|
*/
|
|
70
|
-
export const BUILD_STATS_RULES =
|
|
63
|
+
export const BUILD_STATS_RULES = 4;
|
|
71
64
|
|
|
72
65
|
const chainOf = (stats: RouteStats): string =>
|
|
73
66
|
stats.heaviestChain === undefined ? 'unknown import chain' : stats.heaviestChain.join(' -> ');
|
|
74
67
|
|
|
75
68
|
const jsBudgetOf = (route: RouteFact): number | null => parseByteBudget(route.budget?.js);
|
|
76
69
|
|
|
77
|
-
/** Which
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
/** Which file declares a route, by URL — the loaded app's route table, read when the step runs. */
|
|
71
|
+
export type RouteFileOf = (url: string) => string | undefined;
|
|
72
|
+
|
|
73
|
+
const declaredFileOf: RouteFileOf = (url) =>
|
|
74
|
+
describeRoutes().find((route) => route.path === url && route.mount === undefined)?.file;
|
|
75
|
+
|
|
76
|
+
const KB = 1024;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The paste, not the advice: the file, the literal to set (the measured bytes rounded UP to a
|
|
80
|
+
* whole kb, so the raise is the smallest that clears) and the comment `bun run budget-raises`
|
|
81
|
+
* reads above it. The other way out stays beside it — a raise is axiom 9's to allow, not to force.
|
|
82
|
+
*/
|
|
83
|
+
function exceededJsFix(url: string, file: string | undefined, bytes: number): string {
|
|
84
|
+
const where = file ?? `the file declaring ${url}`;
|
|
85
|
+
const kb = Math.ceil(bytes / KB);
|
|
86
|
+
return `edit ${where} — set budget: { js: '${String(kb)}kb' } with // measured: ${String(bytes)} B (x build --target static) — why: <the function it buys> directly above it; or x routes --json for the chain and move the heavy import behind hydrate: 'interaction'`;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
/**
|
|
@@ -169,11 +173,15 @@ function ownCodeFinding(url: string, unmeasured: readonly UnmeasuredRoute[]): Fi
|
|
|
169
173
|
* each failure's code when it had one. Optional because the report is written beside the stats
|
|
170
174
|
* and can be absent for the same reason; with it, a route whose measurement failed on a code in
|
|
171
175
|
* `REPORTED_BY_OWN_CODE` is reported under that code, with the build's own cause and fix.
|
|
176
|
+
*
|
|
177
|
+
* `fileOf` names the file an `X_BUDGET_EXCEEDED` fix edits: the step runs after the app loaded,
|
|
178
|
+
* so render's route table already holds it. A test hands its own.
|
|
172
179
|
*/
|
|
173
180
|
export function checkBudgets(
|
|
174
181
|
manifest: Manifest,
|
|
175
182
|
stats: BuildStats | undefined,
|
|
176
183
|
unmeasured: readonly UnmeasuredRoute[] = [],
|
|
184
|
+
fileOf: RouteFileOf = declaredFileOf,
|
|
177
185
|
): readonly Finding[] {
|
|
178
186
|
const byPath = new Map((stats?.routes ?? []).map((route) => [route.path, route]));
|
|
179
187
|
// Routes no build can weigh by construction (`UnmeasuredRoute.weighable`): printed by the step,
|
|
@@ -186,17 +194,11 @@ export function checkBudgets(
|
|
|
186
194
|
if (unweighable.has(route.url)) continue;
|
|
187
195
|
const measured = byPath.get(route.url);
|
|
188
196
|
const js = jsBudgetOf(route);
|
|
189
|
-
const lcp = route.budget?.lcp;
|
|
190
197
|
if (measured === undefined) {
|
|
191
|
-
if (js !== null
|
|
198
|
+
if (js !== null) {
|
|
192
199
|
findings.push(
|
|
193
200
|
ownCodeFinding(route.url, unmeasured) ??
|
|
194
|
-
unmeasuredFinding(
|
|
195
|
-
route.url,
|
|
196
|
-
declaredBudgets(js, lcp),
|
|
197
|
-
stats !== undefined,
|
|
198
|
-
stats?.stale === true,
|
|
199
|
-
),
|
|
201
|
+
unmeasuredFinding(route.url, 'JS', stats !== undefined, stats?.stale === true),
|
|
200
202
|
);
|
|
201
203
|
}
|
|
202
204
|
continue;
|
|
@@ -205,16 +207,7 @@ export function checkBudgets(
|
|
|
205
207
|
findings.push({
|
|
206
208
|
code: 'X_BUDGET_EXCEEDED',
|
|
207
209
|
cause: `${route.url} ships ${formatBytes(measured.jsBytes)} of JS (minified, uncompressed) over a ${formatBytes(js)} budget via ${chainOf(measured)}`,
|
|
208
|
-
fix:
|
|
209
|
-
docs: ERROR_DOCS_URL,
|
|
210
|
-
at: route.url,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
if (lcp !== undefined && measured.lcpMs !== undefined && measured.lcpMs > lcp) {
|
|
214
|
-
findings.push({
|
|
215
|
-
code: 'X_BUDGET_EXCEEDED',
|
|
216
|
-
cause: `${route.url} LCP ${measured.lcpMs}ms over the ${lcp}ms budget`,
|
|
217
|
-
fix: `raise the budget in defineRoute, or switch render to 'isr' to serve it prebuilt`,
|
|
210
|
+
fix: exceededJsFix(route.url, fileOf(route.url), measured.jsBytes),
|
|
218
211
|
docs: ERROR_DOCS_URL,
|
|
219
212
|
at: route.url,
|
|
220
213
|
});
|
package/src/cdp-shot-driver.ts
CHANGED
|
@@ -91,7 +91,9 @@ export function cdpShotDriver(options: CdpShotDriverOptions): ShotDriver {
|
|
|
91
91
|
throw new CdpBrowserMissingError({ tried: CHROME_CANDIDATES });
|
|
92
92
|
}
|
|
93
93
|
const launched = await launch(options.executablePath, timeoutMs);
|
|
94
|
-
|
|
94
|
+
// Awaited by `close` below: `x shot` exits right after its last picture, and a reap still
|
|
95
|
+
// running then is a Chrome child left behind and a profile directory it re-creates.
|
|
96
|
+
return { connection: launched.connection, end: () => launched.close() };
|
|
95
97
|
};
|
|
96
98
|
|
|
97
99
|
return {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `x auth`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-auth.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const AUTH_SUBCOMMANDS = ['seal-mfa'] as const;
|
|
7
|
+
|
|
8
|
+
export const authSpec: CommandSpec = {
|
|
9
|
+
name: 'auth',
|
|
10
|
+
summary: 'one-shot maintenance of the auth tables: seal every plaintext second-factor secret',
|
|
11
|
+
usage: 'x auth seal-mfa [--json]',
|
|
12
|
+
requiresApp: true,
|
|
13
|
+
// No default: the one subcommand WRITES, and a bare `x auth` must not.
|
|
14
|
+
subcommands: [...AUTH_SUBCOMMANDS],
|
|
15
|
+
flags: [],
|
|
16
|
+
};
|
package/src/cmd-auth.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `x auth seal-mfa` — seal every `x_users.mfa_secret` still stored in the clear, in place, under
|
|
2
|
+
// the app's master key. The one-shot an upgrade onto sealed second-factor secrets owes: readers
|
|
3
|
+
// refuse a plaintext value (`X_MFA_SECRET_UNSEALED`), and this is the command that refusal names.
|
|
4
|
+
// Idempotent — a sealed value is counted and left alone — so an interrupted run is finished by
|
|
5
|
+
// running it again.
|
|
6
|
+
|
|
7
|
+
import type { MfaSecretStore } from '@ultimat3/auth';
|
|
8
|
+
import { BuiltinAdapter, sealMfaSecrets } from '@ultimat3/auth';
|
|
9
|
+
import { requireAppRoot } from './app-root';
|
|
10
|
+
import { authSpec } from './cmd-auth-spec';
|
|
11
|
+
import type { CliCommand, CommandContext } from './command';
|
|
12
|
+
import { ok } from './command';
|
|
13
|
+
import { msg } from './messages';
|
|
14
|
+
import type { CommandResult } from './output';
|
|
15
|
+
import { resolveServices } from './runtime-bindings';
|
|
16
|
+
import { startQueue } from './runtime-queue';
|
|
17
|
+
|
|
18
|
+
export interface OpenAuthStore extends MfaSecretStore {
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The app's own database, opened the way every other command opens it (`startQueue`: the
|
|
24
|
+
* framework's tables are applied first, so `x_users` exists) and ALWAYS released — a CLI that
|
|
25
|
+
* exits holding the PGlite lock breaks the next command run against this app.
|
|
26
|
+
*/
|
|
27
|
+
async function openAppAuthStore(root: string, ctx: CommandContext): Promise<OpenAuthStore> {
|
|
28
|
+
const queue = await startQueue(resolveServices(root, ctx.env), undefined, ctx.env);
|
|
29
|
+
return { adapter: new BuiltinAdapter(queue.db), close: () => queue.stop() };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `open` is the seam a test supplies a store through; the shipped command opens the app's. */
|
|
33
|
+
export const authCommandOver = (
|
|
34
|
+
open: (root: string, ctx: CommandContext) => Promise<OpenAuthStore>,
|
|
35
|
+
): CliCommand => ({
|
|
36
|
+
spec: authSpec,
|
|
37
|
+
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
38
|
+
const root = requireAppRoot('auth', ctx.cwd).dir;
|
|
39
|
+
const store = await open(root, ctx);
|
|
40
|
+
try {
|
|
41
|
+
// The key is read from the command's own environment and the app's `.secrets.key`: with
|
|
42
|
+
// neither, the first seal refuses (`X_SEAL_KEY_MISSING`) before any row is written.
|
|
43
|
+
const report = await sealMfaSecrets(store, { root, env: ctx.env });
|
|
44
|
+
return ok(
|
|
45
|
+
'auth',
|
|
46
|
+
msg('cli.auth.sealed', {
|
|
47
|
+
sealed: report.sealed,
|
|
48
|
+
already: report.alreadySealed,
|
|
49
|
+
skipped: report.skipped,
|
|
50
|
+
}),
|
|
51
|
+
{
|
|
52
|
+
data: {
|
|
53
|
+
sealed: report.sealed,
|
|
54
|
+
alreadySealed: report.alreadySealed,
|
|
55
|
+
skipped: report.skipped,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
} finally {
|
|
60
|
+
await store.close();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const authCommand: CliCommand = authCommandOver(openAppAuthStore);
|
package/src/cmd-build-spec.ts
CHANGED
|
@@ -5,12 +5,19 @@ import type { CommandSpec } from './parse';
|
|
|
5
5
|
|
|
6
6
|
export const buildSpec: CommandSpec = {
|
|
7
7
|
name: 'build',
|
|
8
|
-
summary:
|
|
8
|
+
summary:
|
|
9
|
+
'build a container image, a single binary, a prerendered static site, or the store an image boots from',
|
|
9
10
|
usage:
|
|
10
|
-
'x build --target docker|binary|static [--tag name] [--out path] [--no-preflight] [--json]',
|
|
11
|
+
'x build --target docker|binary|static|prebuilt [--tag name] [--out path] [--no-preflight] [--json]',
|
|
11
12
|
requiresApp: true,
|
|
12
13
|
flags: [
|
|
13
|
-
{
|
|
14
|
+
{
|
|
15
|
+
name: 'target',
|
|
16
|
+
type: 'string',
|
|
17
|
+
summary:
|
|
18
|
+
'docker | binary | static | prebuilt — prebuilt is the line a Dockerfile runs inside the image build',
|
|
19
|
+
default: 'docker',
|
|
20
|
+
},
|
|
14
21
|
{ name: 'tag', type: 'string', summary: 'image tag (docker target)' },
|
|
15
22
|
{ name: 'out', type: 'string', summary: 'output path (binary and static targets)' },
|
|
16
23
|
{
|
package/src/cmd-build.ts
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
// `x build --target docker|binary|static` —
|
|
2
|
-
//
|
|
1
|
+
// `x build --target docker|binary|static|prebuilt` — no platform primitives. Deploy anywhere means
|
|
2
|
+
// "anywhere that runs a container or a binary"; nothing here knows the name of a cloud. `prebuilt`
|
|
3
|
+
// is the docker target's other half: the line the Dockerfile runs INSIDE the image build.
|
|
3
4
|
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
5
6
|
import { join, resolve } from 'node:path';
|
|
6
|
-
import { ERROR_DOCS_URL, frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
7
|
+
import { ERROR_DOCS_URL, frameworkVersion, UltimateError, VERSION_DEFINE } from '@ultimat3/core';
|
|
7
8
|
import { requireAppRoot } from './app-root';
|
|
8
9
|
import { buildSpec } from './cmd-build-spec';
|
|
9
10
|
import { runVerify } from './cmd-verify';
|
|
10
11
|
import type { CliCommand, CommandContext } from './command';
|
|
11
12
|
import { externalArgs } from './compile-externals';
|
|
12
|
-
import {
|
|
13
|
+
import { BadFlagError, UnknownCommandError } from './errors';
|
|
13
14
|
import type { ExecResult } from './exec';
|
|
14
15
|
import { execOutput } from './exec';
|
|
15
|
-
import { prepareImage } from './image-prepare';
|
|
16
16
|
import { msg } from './messages';
|
|
17
17
|
import type { CommandResult } from './output';
|
|
18
|
+
import type { ParsedArgs } from './parse';
|
|
18
19
|
import { flagString } from './parse';
|
|
20
|
+
import { PREBUILT_DIR } from './serve-prebuilt-paths';
|
|
21
|
+
import { quoteArg } from './shell-quote';
|
|
19
22
|
import type { StaticReport } from './static-report';
|
|
20
23
|
import {
|
|
21
24
|
readStaticReport,
|
|
@@ -23,11 +26,30 @@ import {
|
|
|
23
26
|
renderStaticReport,
|
|
24
27
|
staticReportData,
|
|
25
28
|
} from './static-report';
|
|
29
|
+
import type { VerifyStepName } from './verify-step';
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A build target names an entry file the app does not have. `x build` refuses before it spawns the
|
|
33
|
+
* builder: `bun build`'s own "module not found" says nothing about which file an Ultimate app is
|
|
34
|
+
* supposed to own, and `docker build`'s says nothing about which target wanted it.
|
|
35
|
+
*/
|
|
36
|
+
export class BuildEntryMissingError extends UltimateError {
|
|
37
|
+
constructor(input: { target: string; entry: string }) {
|
|
38
|
+
super({
|
|
39
|
+
code: 'X_BUILD_ENTRY_MISSING',
|
|
40
|
+
cause: `x build --target ${input.target} builds from ${input.entry}, and the app does not have it`,
|
|
41
|
+
fix: `x new scratch-app --dry-run --json # its file list carries ${input.entry}; copy that file into this app`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const BUILD_TARGETS = ['docker', 'binary', 'static', 'prebuilt'] as const;
|
|
28
47
|
|
|
29
48
|
export type BuildTarget = (typeof BUILD_TARGETS)[number];
|
|
30
49
|
|
|
50
|
+
/** The targets whose builder is a subprocess. `prebuilt` is this process, inside `docker build`. */
|
|
51
|
+
export type SpawnedTarget = Exclude<BuildTarget, 'prebuilt'>;
|
|
52
|
+
|
|
31
53
|
export function readTarget(raw: string | undefined): BuildTarget {
|
|
32
54
|
const targets: readonly string[] = BUILD_TARGETS;
|
|
33
55
|
if (raw === undefined) return 'docker';
|
|
@@ -48,6 +70,8 @@ export const BUILD_ENTRY: Readonly<Record<BuildTarget, string>> = {
|
|
|
48
70
|
docker: 'docker/Dockerfile',
|
|
49
71
|
binary: 'apps/web/server.ts',
|
|
50
72
|
static: 'apps/web/prerender.ts',
|
|
73
|
+
// What boots from the store: an image with no server entry has nothing to prebuild for.
|
|
74
|
+
prebuilt: 'apps/web/server.ts',
|
|
51
75
|
};
|
|
52
76
|
|
|
53
77
|
/** Absolute path of the target's entry, or the error that names the file and what writes it. */
|
|
@@ -109,7 +133,7 @@ export function staticArgs(root: string, out: string): readonly string[] {
|
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
export function argsFor(
|
|
112
|
-
target:
|
|
136
|
+
target: SpawnedTarget,
|
|
113
137
|
paths: {
|
|
114
138
|
readonly root: string;
|
|
115
139
|
readonly tag: string;
|
|
@@ -184,8 +208,77 @@ export function buildResult(input: {
|
|
|
184
208
|
};
|
|
185
209
|
}
|
|
186
210
|
|
|
211
|
+
/** The flags only some targets read. */
|
|
212
|
+
const TARGET_SCOPED_FLAGS = ['tag', 'out', 'preflight'] as const;
|
|
213
|
+
|
|
214
|
+
type TargetScopedFlag = (typeof TARGET_SCOPED_FLAGS)[number];
|
|
215
|
+
|
|
216
|
+
/** The optional flags each target READS. One table, so a flag cannot be read by one and ignored. */
|
|
217
|
+
const TARGET_FLAGS: ReadonlyMap<BuildTarget, readonly TargetScopedFlag[]> = new Map([
|
|
218
|
+
['docker', ['tag', 'preflight']],
|
|
219
|
+
['binary', ['out', 'preflight']],
|
|
220
|
+
['static', ['out', 'preflight']],
|
|
221
|
+
// The boot reads ONE place, and the image holds no devDependencies to run a gate with.
|
|
222
|
+
['prebuilt', []],
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
/** Why a target takes no `--<flag>`, in the words its refusal prints. */
|
|
226
|
+
const UNREAD_BECAUSE: Readonly<Record<TargetScopedFlag, string>> = {
|
|
227
|
+
tag: 'only the docker target tags an image',
|
|
228
|
+
out: 'the docker target writes an image, not a path',
|
|
229
|
+
preflight: 'the prebuilt target runs no gate: `x verify` ran before `docker build` was called',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A flag the chosen target never reads is refused before the gate runs. Accepted and dropped, it
|
|
234
|
+
* printed a green build: `--out dist` on `docker` and `--tag` on `binary` read as if they landed.
|
|
235
|
+
*/
|
|
236
|
+
function refuseUnreadFlags(args: ParsedArgs, target: BuildTarget): void {
|
|
237
|
+
for (const flag of TARGET_SCOPED_FLAGS) {
|
|
238
|
+
if (!args.flags.has(flag) || TARGET_FLAGS.get(target)?.includes(flag) === true) continue;
|
|
239
|
+
throw new BadFlagError({
|
|
240
|
+
flag,
|
|
241
|
+
command: 'build',
|
|
242
|
+
reason:
|
|
243
|
+
target === 'prebuilt' && flag !== 'preflight'
|
|
244
|
+
? `the prebuilt target writes ${PREBUILT_DIR}, the one place a container boots from, and takes no --${flag}`
|
|
245
|
+
: `--target ${target} never reads it: ${UNREAD_BECAUSE[flag]}`,
|
|
246
|
+
fix: `x build --target ${quoteArg(target)}`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* `x build --target prebuilt`: the island chunks and compiled stylesheets, written by the process
|
|
253
|
+
* `docker build` runs after the source is copied in. No gate and no subprocess — the image holds
|
|
254
|
+
* no devDependencies to typecheck with, and `x verify` ran before `docker build` was called.
|
|
255
|
+
*
|
|
256
|
+
* A module that would not import fails the image build rather than warning into a build log
|
|
257
|
+
* nobody reads: its stylesheets are missing from the store, and every pod would compile them.
|
|
258
|
+
*/
|
|
259
|
+
async function buildPrebuilt(root: string): Promise<CommandResult> {
|
|
260
|
+
const started = Bun.nanoseconds();
|
|
261
|
+
const { prebuildImage } = await import('./image-prepare');
|
|
262
|
+
const built = await prebuildImage(root);
|
|
263
|
+
const ok = built.findings.length === 0;
|
|
264
|
+
return {
|
|
265
|
+
ok,
|
|
266
|
+
command: 'build',
|
|
267
|
+
summary: msg(ok ? 'cli.build.done' : 'cli.build.failed', { target: 'prebuilt' }),
|
|
268
|
+
findings: built.findings,
|
|
269
|
+
data: {
|
|
270
|
+
target: 'prebuilt',
|
|
271
|
+
artifact: built.dir,
|
|
272
|
+
durationMs: Math.round((Bun.nanoseconds() - started) / 1e6),
|
|
273
|
+
islands: built.islands,
|
|
274
|
+
stylesheets: built.stylesheets,
|
|
275
|
+
},
|
|
276
|
+
lines: [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
187
280
|
/** The static steps `x build` runs before it builds, and `--no-preflight` leaves to the gate. */
|
|
188
|
-
export const PREFLIGHT_STEPS: readonly
|
|
281
|
+
export const PREFLIGHT_STEPS: readonly VerifyStepName[] = [
|
|
189
282
|
'typecheck',
|
|
190
283
|
'lint',
|
|
191
284
|
'boundaries',
|
|
@@ -203,6 +296,8 @@ export const buildCommand: CliCommand = {
|
|
|
203
296
|
// typecheck, and eight seconds of `tsc` ahead of "that file does not exist" is eight seconds
|
|
204
297
|
// an agent spends on the wrong question.
|
|
205
298
|
requireEntry(root, target);
|
|
299
|
+
refuseUnreadFlags(ctx.args, target);
|
|
300
|
+
if (target === 'prebuilt') return buildPrebuilt(root);
|
|
206
301
|
|
|
207
302
|
// Run static verify steps before building — unless the caller is a gate that runs the same
|
|
208
303
|
// six steps itself right after (`bin/check`: `x build --no-preflight && x verify`), where the
|
|
@@ -227,9 +322,10 @@ export const buildCommand: CliCommand = {
|
|
|
227
322
|
? join(root, '.x', target === 'static' ? 'static' : 'app')
|
|
228
323
|
: resolve(ctx.cwd, outFlag);
|
|
229
324
|
const tag = flagString(ctx.args, 'tag') ?? 'ultimate-app:dev';
|
|
230
|
-
// The docker target stamps the manifest's build id into the image,
|
|
231
|
-
//
|
|
232
|
-
const buildId =
|
|
325
|
+
// The docker target stamps the manifest's build id into the image, so no role derives one at
|
|
326
|
+
// boot. The island chunks and stylesheets are the image build's own (`--target prebuilt`).
|
|
327
|
+
const buildId =
|
|
328
|
+
target === 'docker' ? await (await import('./image-prepare')).imageBuildId(root) : undefined;
|
|
233
329
|
const command = argsFor(target, {
|
|
234
330
|
root,
|
|
235
331
|
tag,
|
package/src/cmd-db-backfill.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { BadFlagError } from './errors';
|
|
|
23
23
|
import { withJobDriver } from './jobs-driver';
|
|
24
24
|
import { backfillToJson } from './jobs-json';
|
|
25
25
|
import { msg } from './messages';
|
|
26
|
-
import type { CommandResult } from './output';
|
|
26
|
+
import type { CommandResult, Finding } from './output';
|
|
27
27
|
import { findingFrom } from './output';
|
|
28
28
|
import type { ParsedArgs } from './parse';
|
|
29
29
|
import { flagBool, flagString } from './parse';
|
|
@@ -171,15 +171,22 @@ async function runBackfillList(ctx: CommandContext, root: string): Promise<Comma
|
|
|
171
171
|
* The alarm the framework did not have. Non-zero when anything is unswept, so a cron or a deploy
|
|
172
172
|
* check can read the exit code — a `--json` nobody has to parse to know something is wrong.
|
|
173
173
|
* `loadApp` first: importing the app's modules IS the declaration, and a diff run without it
|
|
174
|
-
* would report a clean database against an empty declaration list.
|
|
174
|
+
* would report a clean database against an empty declaration list. Its findings fail the run for
|
|
175
|
+
* the same reason: a module that would not import may be the one declaring the unswept backfill.
|
|
175
176
|
*/
|
|
176
177
|
async function runBackfillPending(ctx: CommandContext, root: string): Promise<CommandResult> {
|
|
177
|
-
await loadApp(root);
|
|
178
|
+
const { findings: loadFindings } = await loadApp(root);
|
|
178
179
|
const environment = resolveEnvironment({ env: ctx.env });
|
|
179
180
|
return withJobDriver(root, ctx, async (driver) => {
|
|
180
181
|
const report = await pendingReport(driver, environment);
|
|
182
|
+
const findings = [
|
|
183
|
+
...report.pending.map((row) =>
|
|
184
|
+
findingFrom(new BackfillPendingError({ backfill: row.name, environment })),
|
|
185
|
+
),
|
|
186
|
+
...loadFindings,
|
|
187
|
+
];
|
|
181
188
|
return {
|
|
182
|
-
ok:
|
|
189
|
+
ok: findings.length === 0,
|
|
183
190
|
command: 'db',
|
|
184
191
|
summary:
|
|
185
192
|
report.pending.length === 0
|
|
@@ -188,9 +195,7 @@ async function runBackfillPending(ctx: CommandContext, root: string): Promise<Co
|
|
|
188
195
|
count: report.pending.length,
|
|
189
196
|
declared: report.rows.length,
|
|
190
197
|
}),
|
|
191
|
-
findings
|
|
192
|
-
findingFrom(new BackfillPendingError({ backfill: row.name, environment })),
|
|
193
|
-
),
|
|
198
|
+
findings,
|
|
194
199
|
lines: report.rows.length === 0 ? [] : renderPendingTable(report).map((line) => ` ${line}`),
|
|
195
200
|
data: pendingToJson(report),
|
|
196
201
|
};
|
|
@@ -208,7 +213,7 @@ async function runBackfillPass(
|
|
|
208
213
|
root: string,
|
|
209
214
|
names: readonly string[] | 'all',
|
|
210
215
|
): Promise<CommandResult> {
|
|
211
|
-
await loadApp(root);
|
|
216
|
+
const { findings: loadFindings } = await loadApp(root);
|
|
212
217
|
const environment = resolveEnvironment({ env: ctx.env });
|
|
213
218
|
const write = flagBool(ctx.args, 'write');
|
|
214
219
|
return withJobDriver(root, ctx, async (driver) => {
|
|
@@ -220,16 +225,24 @@ async function runBackfillPass(
|
|
|
220
225
|
environment,
|
|
221
226
|
appliedMigrations: await readAppliedMigrations(),
|
|
222
227
|
});
|
|
223
|
-
return backfillPassResult(rows, write);
|
|
228
|
+
return backfillPassResult(rows, write, loadFindings);
|
|
224
229
|
});
|
|
225
230
|
}
|
|
226
231
|
|
|
227
232
|
/**
|
|
228
233
|
* A blocked or deduped name is a finding and a non-zero exit, and every OTHER name still ran —
|
|
229
|
-
* that isolation is what stops one wedged cleanup blocking every later one forever.
|
|
234
|
+
* that isolation is what stops one wedged cleanup blocking every later one forever. A module that
|
|
235
|
+
* would not import fails the pass too: `--all` over it plans every backfill but the unloaded ones.
|
|
230
236
|
*/
|
|
231
|
-
function backfillPassResult(
|
|
232
|
-
|
|
237
|
+
function backfillPassResult(
|
|
238
|
+
rows: readonly BackfillPlanRow[],
|
|
239
|
+
write: boolean,
|
|
240
|
+
loadFindings: readonly Finding[],
|
|
241
|
+
): CommandResult {
|
|
242
|
+
const findings = [
|
|
243
|
+
...rows.flatMap((row) => (row.finding === null ? [] : [row.finding])),
|
|
244
|
+
...loadFindings,
|
|
245
|
+
];
|
|
233
246
|
// Counted per action, never derived from the total: a deduped pass is neither enqueued nor
|
|
234
247
|
// blocked, and `rows.length - enqueued` reported it as blocked while `--json` reported it as
|
|
235
248
|
// deduped. `planToJson` is the same list, so the two renders now add up to the same run.
|