@ultimat3/cli 23.0.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.
Files changed (149) hide show
  1. package/CLAUDE.md +15 -7
  2. package/README.md +3 -3
  3. package/package.json +28 -28
  4. package/src/affected.ts +27 -35
  5. package/src/api-registration.ts +89 -0
  6. package/src/app-artifacts.ts +1 -1
  7. package/src/app-auth.ts +4 -27
  8. package/src/app-config-load.ts +51 -0
  9. package/src/app-env.ts +5 -11
  10. package/src/app-manifest.ts +2 -8
  11. package/src/app-mcp.ts +10 -23
  12. package/src/app-permissions-borrowed.ts +129 -0
  13. package/src/app-permissions-site.ts +24 -0
  14. package/src/app-permissions.ts +7 -17
  15. package/src/budgets.ts +4 -35
  16. package/src/cdp-shot-driver.ts +3 -1
  17. package/src/cmd-auth-spec.ts +16 -0
  18. package/src/cmd-auth.ts +65 -0
  19. package/src/cmd-build.ts +64 -15
  20. package/src/cmd-db-backfill.ts +25 -12
  21. package/src/cmd-db.ts +6 -5
  22. package/src/cmd-deploy-helm.ts +7 -6
  23. package/src/cmd-deploy.ts +4 -4
  24. package/src/cmd-dev.ts +9 -8
  25. package/src/cmd-doctor-spec.ts +2 -9
  26. package/src/cmd-doctor.ts +41 -14
  27. package/src/cmd-env.ts +26 -2
  28. package/src/cmd-generate.ts +79 -32
  29. package/src/cmd-help.ts +19 -6
  30. package/src/cmd-i18n.ts +10 -9
  31. package/src/cmd-jobs.ts +18 -6
  32. package/src/cmd-manifest-spec.ts +1 -1
  33. package/src/cmd-manifest.ts +6 -25
  34. package/src/cmd-mcp.ts +46 -15
  35. package/src/cmd-planned.ts +15 -8
  36. package/src/cmd-routes.ts +12 -2
  37. package/src/cmd-secrets.ts +47 -6
  38. package/src/cmd-shot-matrix.ts +1 -3
  39. package/src/cmd-shot.ts +12 -3
  40. package/src/cmd-tasks.ts +2 -2
  41. package/src/cmd-test-spec.ts +1 -7
  42. package/src/cmd-test.ts +13 -29
  43. package/src/cmd-verify.ts +4 -4
  44. package/src/db-subscribes.ts +12 -4
  45. package/src/dev-dashboard-guard.ts +76 -0
  46. package/src/dev-dashboard.ts +51 -29
  47. package/src/dev-lock.ts +4 -7
  48. package/src/dispatch.ts +3 -6
  49. package/src/doctor-app-url.ts +39 -0
  50. package/src/doctor-auth.ts +95 -0
  51. package/src/drift-replica-identity.ts +42 -0
  52. package/src/error-catalog.ts +6 -9
  53. package/src/error-codes.ts +23 -3
  54. package/src/error-unthrown.ts +29 -11
  55. package/src/errors.ts +54 -83
  56. package/src/framework-schema-apply.ts +78 -0
  57. package/src/framework-schema-stamp.ts +96 -0
  58. package/src/framework-schema.ts +13 -2
  59. package/src/generate-files.ts +4 -3
  60. package/src/generate-kinds.ts +33 -1
  61. package/src/generate-shadow.ts +46 -0
  62. package/src/generate-write.ts +47 -13
  63. package/src/gh-target.ts +12 -7
  64. package/src/i18n-audit.ts +4 -1
  65. package/src/index.ts +1 -3
  66. package/src/island-bundle.ts +3 -15
  67. package/src/jobs-drain.ts +82 -15
  68. package/src/jobs-report.ts +4 -4
  69. package/src/loopback-host.ts +13 -0
  70. package/src/manifest-staleness.ts +62 -0
  71. package/src/mcp-errors.ts +12 -3
  72. package/src/mcp-host.ts +16 -9
  73. package/src/mcp-test-run.ts +38 -0
  74. package/src/messages.ts +2 -0
  75. package/src/metrics-endpoint.ts +25 -1
  76. package/src/output.ts +6 -0
  77. package/src/page-navigation.ts +7 -23
  78. package/src/page-speculation.ts +5 -20
  79. package/src/prerender-out.ts +34 -7
  80. package/src/prerender.ts +8 -5
  81. package/src/pwa-artifacts.ts +26 -94
  82. package/src/registry.ts +2 -0
  83. package/src/revalidated-response.ts +4 -3
  84. package/src/role-replicator.ts +16 -0
  85. package/src/role-start.ts +9 -4
  86. package/src/role-sync.ts +55 -20
  87. package/src/runtime-assets.ts +11 -7
  88. package/src/runtime-bindings.ts +4 -4
  89. package/src/runtime-cache.ts +38 -47
  90. package/src/runtime-idempotency-scope.ts +23 -0
  91. package/src/runtime-isr.ts +36 -0
  92. package/src/runtime-jobs.ts +8 -33
  93. package/src/runtime-mfa-warning.ts +29 -0
  94. package/src/runtime-notify-retention.ts +10 -46
  95. package/src/runtime-overrides.ts +1 -1
  96. package/src/runtime-purge.ts +38 -16
  97. package/src/runtime-queue.ts +31 -10
  98. package/src/runtime-realtime.ts +9 -45
  99. package/src/runtime-render.ts +62 -35
  100. package/src/runtime-services.ts +20 -17
  101. package/src/runtime-storage.ts +10 -5
  102. package/src/schema-diff.ts +16 -0
  103. package/src/schema-drift.ts +45 -15
  104. package/src/seo-routes.ts +80 -9
  105. package/src/serve-boot.ts +34 -6
  106. package/src/serve-drain.ts +9 -33
  107. package/src/serve-env.ts +9 -6
  108. package/src/serve-web.ts +12 -7
  109. package/src/shot-locale.ts +5 -23
  110. package/src/site-assets.ts +3 -5
  111. package/src/site-config.ts +9 -48
  112. package/src/site-seo.ts +1 -1
  113. package/src/sitemap-lastmod.ts +5 -0
  114. package/src/solid-loader.ts +12 -3
  115. package/src/source-files.ts +6 -0
  116. package/src/stored-object-headers.ts +43 -0
  117. package/src/sync-url.ts +2 -2
  118. package/src/tasks-facts.ts +3 -29
  119. package/src/templates/action.ts +3 -0
  120. package/src/templates/naming.ts +28 -0
  121. package/src/templates/route.ts +3 -6
  122. package/src/templates/scaffold-app.ts +6 -2
  123. package/src/templates/scaffold-container-compose.ts +10 -1
  124. package/src/templates/scaffold-container.ts +4 -2
  125. package/src/templates/scaffold-db-client.ts +4 -2
  126. package/src/templates/scaffold-helm-fences.ts +76 -0
  127. package/src/templates/scaffold-helm-templates.ts +32 -7
  128. package/src/templates/scaffold-helm.ts +31 -1
  129. package/src/templates/scaffold-repo.ts +9 -1
  130. package/src/templates/scaffold-shell.ts +39 -36
  131. package/src/test-counts.ts +2 -6
  132. package/src/test-ignores.ts +25 -0
  133. package/src/test-passes.ts +0 -15
  134. package/src/test-select.ts +5 -14
  135. package/src/test-shards.ts +41 -93
  136. package/src/test-workers.ts +26 -4
  137. package/src/theme-boot.ts +6 -31
  138. package/src/trusted-hops-error.ts +17 -0
  139. package/src/unthrown-codes.ts +26 -0
  140. package/src/verify-checks.ts +17 -46
  141. package/src/verify-deadline.ts +130 -33
  142. package/src/verify-merge.ts +60 -27
  143. package/src/verify-part-step.ts +90 -0
  144. package/src/verify-run.ts +29 -8
  145. package/src/verify-shard.ts +13 -2
  146. package/src/verify-stalled.ts +152 -0
  147. package/src/verify-tests.ts +5 -2
  148. package/src/build-errors.ts +0 -54
  149. package/src/verify-errors.ts +0 -58
@@ -0,0 +1,129 @@
1
+ // The `policy` step's provenance question: every permission the app's OWN actions, queries and
2
+ // routes require is one the app ITSELF declares. `defineAdmin()` declares `<entity>:read|write|
3
+ // delete` for every mounted entity, so an app rule naming one passed in the web process and threw
4
+ // X_PERMISSION_UNKNOWN in any process that never mounted the admin — with nothing saying so.
5
+
6
+ // why: Bun ships no path API, and `relative` renders a frame's absolute path against the app root.
7
+ import { relative } from 'node:path';
8
+ import { listActions } from '@ultimat3/action';
9
+ import { type Policy, permissionDeclarationSites, policyPermissions } from '@ultimat3/policy';
10
+ import { listQueries } from '@ultimat3/query';
11
+ import { routeEntries } from '@ultimat3/render';
12
+ import { frameFile, siteFile } from './app-permissions-site';
13
+ import type { Finding } from './output';
14
+
15
+ /** One permission one app declaration requires. */
16
+ export interface AppRule {
17
+ readonly permission: string;
18
+ /** `action createPost`, `query postList`, or the route file. */
19
+ readonly by: string;
20
+ }
21
+
22
+ /** An app rule whose permission no app module declares, and the file that did declare it. */
23
+ export interface BorrowedPermission extends AppRule {
24
+ /**
25
+ * Root-relative paths of EVERY declaring module (`node_modules/…` or `../…`), distinct and
26
+ * sorted: naming only the first told the reader one package declared it when two did.
27
+ */
28
+ readonly from: readonly string[];
29
+ }
30
+
31
+ /**
32
+ * EVERY permission in each tree, composites included — unlike the grant question, which judges
33
+ * only a bare `can()`: an `or()` branch is still a `can()` that asserted its name, so it needs the
34
+ * app's own declaration as much as a bare one does. A screen the admin MOUNTS is not here: those
35
+ * are the package's rules, and declaring their names is the package's job.
36
+ */
37
+ export function appRules(): readonly AppRule[] {
38
+ const named = (by: string, policy: Policy<unknown, unknown>): readonly AppRule[] =>
39
+ policyPermissions(policy).map((permission) => ({ permission, by }));
40
+ return [
41
+ ...listActions().flatMap((target) => named(`action ${target.name}`, target.policy)),
42
+ ...listQueries().flatMap((target) => named(`query ${target.name}`, target.policy)),
43
+ ...routeEntries().flatMap((entry) => {
44
+ const permission = entry.config.policy?.permission;
45
+ return permission === undefined ? [] : [{ permission, by: entry.file }];
46
+ }),
47
+ ];
48
+ }
49
+
50
+ /** An app module's root-relative path, or `undefined` for a dependency's or an unreadable frame. */
51
+ const appFile = (root: string, site: string): string | undefined => {
52
+ const file = siteFile(root, site);
53
+ return file === undefined || file.split('/').includes('node_modules') ? undefined : file;
54
+ };
55
+
56
+ /**
57
+ * Provenance by the declaring module's PATH, which is where the call ran — never by the name's
58
+ * shape: `posts:read` is the same string whether `defineAdmin()` or the app declared it. A name
59
+ * with any unreadable site is one whose origin nobody can state, so it is not judged — and that
60
+ * covers an UNKNOWN name too, which has no site at all: it is X_PERMISSION_UNKNOWN's, asked first,
61
+ * and a second finding would be a second instruction for one fault.
62
+ */
63
+ export function borrowedPermissions(
64
+ root: string,
65
+ rules: readonly AppRule[] = appRules(),
66
+ declaredAt: Readonly<Record<string, readonly string[]>> = permissionDeclarationSites(),
67
+ ): readonly BorrowedPermission[] {
68
+ const found = new Map<string, BorrowedPermission>();
69
+ for (const rule of rules) {
70
+ const sites = Object.hasOwn(declaredAt, rule.permission) ? declaredAt[rule.permission] : [];
71
+ const files = (sites ?? []).map(frameFile);
72
+ if (files.length === 0 || files.some((file) => file === undefined)) continue;
73
+ if ((sites ?? []).some((site) => appFile(root, site) !== undefined)) continue;
74
+ const from = [
75
+ ...new Set(
76
+ files.flatMap((file) =>
77
+ file === undefined ? [] : [relative(root, file).replaceAll('\\', '/')],
78
+ ),
79
+ ),
80
+ ].sort();
81
+ found.set(`${rule.permission}\u0000${rule.by}`, { ...rule, from });
82
+ }
83
+ return [...found.values()].sort((a, b) =>
84
+ a.permission === b.permission
85
+ ? a.by.localeCompare(b.by)
86
+ : a.permission.localeCompare(b.permission),
87
+ );
88
+ }
89
+
90
+ /**
91
+ * The app file its permissions are declared in — where the missing name is added. Read off the
92
+ * recorded sites, never a scaffold path; none under the root is `undefined`, never a guess.
93
+ */
94
+ export function permissionsFile(root: string): string | undefined {
95
+ for (const sites of Object.values(permissionDeclarationSites())) {
96
+ for (const site of sites) {
97
+ const file = appFile(root, site);
98
+ if (file !== undefined) return file;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+
104
+ /** A TS string literal for the name — quoted the house way unless the name itself needs escaping. */
105
+ const literal = (name: string): string =>
106
+ /^[\w:.*-]+$/.test(name) ? `'${name}'` : JSON.stringify(name);
107
+
108
+ export function borrowedFinding(
109
+ entry: BorrowedPermission,
110
+ declaredIn: string | undefined,
111
+ ): Finding {
112
+ const [only, ...more] = entry.from;
113
+ const declarers =
114
+ more.length === 0 ? `only ${only} declares it` : `only ${entry.from.join(', ')} declare it`;
115
+ const where =
116
+ declaredIn === undefined
117
+ ? 'paste this call into an app module'
118
+ : `paste this call into ${declaredIn} (definePermissions() merges, so a second call is fine)`;
119
+ const finding: Finding = {
120
+ code: 'X_PERMISSION_BORROWED',
121
+ cause:
122
+ `${entry.by} requires ${literal(entry.permission)}, and ${declarers} — no app module does, ` +
123
+ "so the rule holds only in a process that ran that package's declaration (defineAdmin()) " +
124
+ `first; ${where}, then x verify --only policy`,
125
+ // A call to paste, never prose: `add …` read as a command answered `add: command not found`.
126
+ fix: `definePermissions([${literal(entry.permission)}])`,
127
+ };
128
+ return declaredIn === undefined ? finding : { ...finding, at: declaredIn };
129
+ }
@@ -0,0 +1,24 @@
1
+ // Single responsibility: a declaration site — a stack FRAME `@ultimat3/policy` recorded for a
2
+ // `defineRoles()` or `definePermissions()` call — reduced to the file it names, absolute or
3
+ // app-root-relative. Both `policy` step questions key their findings by it.
4
+
5
+ // why: Bun ships no path API, and `relative` is what turns a stack frame's absolute path into
6
+ // the app-root-relative one every finding is keyed by.
7
+ import { relative } from 'node:path';
8
+
9
+ /** The absolute source path a frame (`at foo (/abs/path.ts:3:1)`) names, or `undefined`. */
10
+ export function frameFile(site: string): string | undefined {
11
+ return /\(?(\/[^():]+\.[cm]?tsx?)(?::\d+)?(?::\d+)?\)?/.exec(site)?.[1];
12
+ }
13
+
14
+ /**
15
+ * The frame's file relative to the app root, or `undefined` outside it: a locator nobody can act on
16
+ * is worse than none. The frame is the only place the declaring module's name exists by the time
17
+ * the declaration runs.
18
+ */
19
+ export function siteFile(root: string, site: string): string | undefined {
20
+ const absolute = frameFile(site);
21
+ if (absolute === undefined) return undefined;
22
+ const rel = relative(root, absolute).replaceAll('\\', '/');
23
+ return rel.startsWith('..') || rel.length === 0 ? undefined : rel;
24
+ }
@@ -7,9 +7,6 @@
7
7
  // required it on `/dashboard`, declared it nowhere, and served HTTP 500 on two of its three routes
8
8
  // with `x verify` green (#F1). This is the question nothing asked.
9
9
 
10
- // why: Bun ships no path API, and `relative` is what turns a stack frame's absolute path into
11
- // the app-root-relative one every finding is keyed by.
12
- import { relative } from 'node:path';
13
10
  import {
14
11
  grantMatches,
15
12
  isKnownPermission,
@@ -21,6 +18,8 @@ import {
21
18
  import { routeEntries } from '@ultimat3/render';
22
19
  import { definedStorage } from '@ultimat3/storage';
23
20
  import { loadApp } from './app-load';
21
+ import { borrowedFinding, borrowedPermissions, permissionsFile } from './app-permissions-borrowed';
22
+ import { siteFile } from './app-permissions-site';
24
23
  import type { DuplicateProbe } from './duplicate-packages';
25
24
  import { duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
26
25
  import type { Finding } from './output';
@@ -45,19 +44,6 @@ export interface PermissionReference {
45
44
  /** What this check needs of a boot — the seam `i18n-registration.ts` already established. */
46
45
  export type AppLoader = (root: string) => Promise<{ readonly findings: readonly Finding[] }>;
47
46
 
48
- /**
49
- * A role's declaration site is a stack FRAME (`at foo (/abs/path.ts:3:1)`), which is the only
50
- * place the declaring module's name exists by the time `defineRoles` runs. Reduced to the
51
- * app-relative path an agent opens, or dropped: a locator nobody can act on is worse than none.
52
- */
53
- export function siteFile(root: string, site: string): string | undefined {
54
- const match = /\(?(\/[^():]+\.[cm]?tsx?)(?::\d+)?(?::\d+)?\)?/.exec(site);
55
- const absolute = match?.[1];
56
- if (absolute === undefined) return undefined;
57
- const rel = relative(root, absolute).replaceAll('\\', '/');
58
- return rel.startsWith('..') || rel.length === 0 ? undefined : rel;
59
- }
60
-
61
47
  /** Every permission a registered role grants, with where that role was declared. */
62
48
  export function grantedReferences(root: string): readonly PermissionReference[] {
63
49
  const sites = roleDeclarationSites();
@@ -198,7 +184,11 @@ export async function policyFindings(
198
184
  unknown.length === 0
199
185
  ? ungrantedRequirements().map((rule) => ungrantedFinding(rule, rolesAt))
200
186
  : [];
201
- const findings = [...duplicated, ...unknown, ...ungranted];
187
+ // Asked of KNOWN names only (`borrowedPermissions` skips the rest), so it rides beside the
188
+ // unknown half rather than behind it: one fault, one finding.
189
+ const declaredIn = permissionsFile(root);
190
+ const borrowed = borrowedPermissions(root).map((entry) => borrowedFinding(entry, declaredIn));
191
+ const findings = [...duplicated, ...unknown, ...borrowed, ...ungranted];
202
192
  return findings.length === 0 ? [] : [...findings, ...app.findings];
203
193
  }
204
194
 
package/src/budgets.ts CHANGED
@@ -39,15 +39,6 @@ export interface RouteStats {
39
39
  * older build simply did not count it, and `checkBudgets` reads `jsBytes` either way.
40
40
  */
41
41
  readonly frameworkJsBytes?: number;
42
- /**
43
- * **Written by nothing, `As of 2026-08`.** `apps/web/prerender.ts` is the only producer of this
44
- * file and it emits static HTML — there is no browser in the build to observe a paint. So the
45
- * comparison in `checkBudgets` below is reachable only for an app that writes its own stats, and
46
- * `x new` no longer scaffolds an `lcp` budget for exactly that reason: a budget the build cannot
47
- * weigh passes silently the moment a stats row exists, which is the false green this file's
48
- * header is about. `RouteBudget.lcp` still accepts one — that key is `@ultimat3/render`'s.
49
- */
50
- readonly lcpMs?: number;
51
42
  /** Import chain that pulled the heaviest module into this route. */
52
43
  readonly heaviestChain?: readonly string[];
53
44
  }
@@ -67,8 +58,9 @@ export interface BuildStats {
67
58
  * `examples/dummy` was charged 250 B for `/x-sw-register.js` by a file written before
68
59
  * `FRAMEWORK_SCRIPTS` exempted it. `2`: that exemption and the page-boot decision (ledger #28).
69
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).
70
62
  */
71
- export const BUILD_STATS_RULES = 3;
63
+ export const BUILD_STATS_RULES = 4;
72
64
 
73
65
  const chainOf = (stats: RouteStats): string =>
74
66
  stats.heaviestChain === undefined ? 'unknown import chain' : stats.heaviestChain.join(' -> ');
@@ -94,14 +86,6 @@ function exceededJsFix(url: string, file: string | undefined, bytes: number): st
94
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'`;
95
87
  }
96
88
 
97
- /** Which budgets the route declared, for a cause line that names what went unmeasured. */
98
- function declaredBudgets(js: number | null, lcp: number | undefined): string {
99
- const labels: string[] = [];
100
- if (js !== null) labels.push('JS');
101
- if (lcp !== undefined) labels.push('LCP');
102
- return labels.join(' and ');
103
- }
104
-
105
89
  /**
106
90
  * Compare declared budgets against measured stats. A declared budget with no measurement is a
107
91
  * finding, never a pass: a route that clears the gate without ever being weighed is exactly the
@@ -210,17 +194,11 @@ export function checkBudgets(
210
194
  if (unweighable.has(route.url)) continue;
211
195
  const measured = byPath.get(route.url);
212
196
  const js = jsBudgetOf(route);
213
- const lcp = route.budget?.lcp;
214
197
  if (measured === undefined) {
215
- if (js !== null || lcp !== undefined) {
198
+ if (js !== null) {
216
199
  findings.push(
217
200
  ownCodeFinding(route.url, unmeasured) ??
218
- unmeasuredFinding(
219
- route.url,
220
- declaredBudgets(js, lcp),
221
- stats !== undefined,
222
- stats?.stale === true,
223
- ),
201
+ unmeasuredFinding(route.url, 'JS', stats !== undefined, stats?.stale === true),
224
202
  );
225
203
  }
226
204
  continue;
@@ -234,15 +212,6 @@ export function checkBudgets(
234
212
  at: route.url,
235
213
  });
236
214
  }
237
- if (lcp !== undefined && measured.lcpMs !== undefined && measured.lcpMs > lcp) {
238
- findings.push({
239
- code: 'X_BUDGET_EXCEEDED',
240
- cause: `${route.url} LCP ${measured.lcpMs}ms over the ${lcp}ms budget`,
241
- fix: `raise the budget in defineRoute, or switch render to 'isr' to serve it prebuilt`,
242
- docs: ERROR_DOCS_URL,
243
- at: route.url,
244
- });
245
- }
246
215
  }
247
216
  return findings;
248
217
  }
@@ -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
- return { connection: launched.connection, end: async () => launched.close() };
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
+ };
@@ -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.ts CHANGED
@@ -4,19 +4,21 @@
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
6
  import { join, resolve } from 'node:path';
7
- import { ERROR_DOCS_URL, frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
7
+ import { ERROR_DOCS_URL, frameworkVersion, UltimateError, VERSION_DEFINE } from '@ultimat3/core';
8
8
  import { requireAppRoot } from './app-root';
9
9
  import { buildSpec } from './cmd-build-spec';
10
10
  import { runVerify } from './cmd-verify';
11
11
  import type { CliCommand, CommandContext } from './command';
12
12
  import { externalArgs } from './compile-externals';
13
- import { BadFlagError, BuildEntryMissingError, UnknownCommandError } from './errors';
13
+ import { BadFlagError, UnknownCommandError } from './errors';
14
14
  import type { ExecResult } from './exec';
15
15
  import { execOutput } from './exec';
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';
19
20
  import { PREBUILT_DIR } from './serve-prebuilt-paths';
21
+ import { quoteArg } from './shell-quote';
20
22
  import type { StaticReport } from './static-report';
21
23
  import {
22
24
  readStaticReport,
@@ -24,6 +26,22 @@ import {
24
26
  renderStaticReport,
25
27
  staticReportData,
26
28
  } from './static-report';
29
+ import type { VerifyStepName } from './verify-step';
30
+
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
+ }
27
45
 
28
46
  export const BUILD_TARGETS = ['docker', 'binary', 'static', 'prebuilt'] as const;
29
47
 
@@ -190,6 +208,46 @@ export function buildResult(input: {
190
208
  };
191
209
  }
192
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
+
193
251
  /**
194
252
  * `x build --target prebuilt`: the island chunks and compiled stylesheets, written by the process
195
253
  * `docker build` runs after the source is copied in. No gate and no subprocess — the image holds
@@ -198,17 +256,7 @@ export function buildResult(input: {
198
256
  * A module that would not import fails the image build rather than warning into a build log
199
257
  * nobody reads: its stylesheets are missing from the store, and every pod would compile them.
200
258
  */
201
- async function buildPrebuilt(ctx: CommandContext, root: string): Promise<CommandResult> {
202
- // The boot reads ONE place; a flag that seemed to move it would write a store no pod opens.
203
- for (const flag of ['tag', 'out']) {
204
- if (flagString(ctx.args, flag) === undefined) continue;
205
- throw new BadFlagError({
206
- flag,
207
- command: 'build',
208
- reason: `the prebuilt target writes ${PREBUILT_DIR}, the one place a container boots from, and takes no --${flag}`,
209
- fix: 'x build --target prebuilt',
210
- });
211
- }
259
+ async function buildPrebuilt(root: string): Promise<CommandResult> {
212
260
  const started = Bun.nanoseconds();
213
261
  const { prebuildImage } = await import('./image-prepare');
214
262
  const built = await prebuildImage(root);
@@ -230,7 +278,7 @@ async function buildPrebuilt(ctx: CommandContext, root: string): Promise<Command
230
278
  }
231
279
 
232
280
  /** The static steps `x build` runs before it builds, and `--no-preflight` leaves to the gate. */
233
- export const PREFLIGHT_STEPS: readonly string[] = [
281
+ export const PREFLIGHT_STEPS: readonly VerifyStepName[] = [
234
282
  'typecheck',
235
283
  'lint',
236
284
  'boundaries',
@@ -248,7 +296,8 @@ export const buildCommand: CliCommand = {
248
296
  // typecheck, and eight seconds of `tsc` ahead of "that file does not exist" is eight seconds
249
297
  // an agent spends on the wrong question.
250
298
  requireEntry(root, target);
251
- if (target === 'prebuilt') return buildPrebuilt(ctx, root);
299
+ refuseUnreadFlags(ctx.args, target);
300
+ if (target === 'prebuilt') return buildPrebuilt(root);
252
301
 
253
302
  // Run static verify steps before building — unless the caller is a gate that runs the same
254
303
  // six steps itself right after (`bin/check`: `x build --no-preflight && x verify`), where the
@@ -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: report.pending.length === 0,
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: report.pending.map((row) =>
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(rows: readonly BackfillPlanRow[], write: boolean): CommandResult {
232
- const findings = rows.flatMap((row) => (row.finding === null ? [] : [row.finding]));
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.
package/src/cmd-db.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  // directory tree. `node:path` for `join` — Bun exposes no path joiner.
12
12
  import { rm } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
- import { resolveEnvironment } from '@ultimat3/core';
14
+ import { NotImplementedError, resolveEnvironment } from '@ultimat3/core';
15
15
  import { type DriftReport, driftError, withTransaction } from '@ultimat3/db';
16
16
  import { postgresDriver } from '@ultimat3/entity';
17
17
  import { requireAppRoot } from './app-root';
@@ -40,7 +40,7 @@ import {
40
40
  selectSeeds,
41
41
  } from './db-seed';
42
42
  import { DevAlreadyRunningError, liveDevLock } from './dev-lock';
43
- import { CliNotImplementedError, MissingSubcommandError, UnknownCommandError } from './errors';
43
+ import { MissingSubcommandError, UnknownCommandError } from './errors';
44
44
  import { withJobDriver } from './jobs-driver';
45
45
  import { msg } from './messages';
46
46
  import type { CommandResult, Finding } from './output';
@@ -254,8 +254,8 @@ async function runMigrate(
254
254
  async function runReset(ctx: CommandContext, root: string): Promise<CommandResult> {
255
255
  const services = resolveServices(root, ctx.env);
256
256
  if (services.db.mode === 'external') {
257
- throw new CliNotImplementedError({
258
- feature: 'x db reset against an external Postgres',
257
+ throw new NotImplementedError({
258
+ cause: 'x db reset against an external Postgres is not implemented in this build',
259
259
  fix: 'drop and recreate the database yourself, then run: x db migrate',
260
260
  });
261
261
  }
@@ -301,7 +301,8 @@ async function runSeed(ctx: CommandContext, root: string): Promise<CommandResult
301
301
  });
302
302
  if (chosen.length === 0) {
303
303
  return {
304
- ok: true,
304
+ // A seed module that would not import may hold the seed that was asked for.
305
+ ok: discovery.findings.length === 0,
305
306
  command: 'db',
306
307
  summary: msg('cli.db.seed.none'),
307
308
  findings: discovery.findings,
@@ -8,8 +8,7 @@
8
8
  // why: Bun exposes no path-join primitive; the chart path is handed to helm as one joined string.
9
9
  import { join } from 'node:path';
10
10
  import { UltimateError } from '@ultimat3/core';
11
- import { APP_CONFIG_EXPORT } from './app-auth';
12
- import { APP_CONFIG_FILE } from './app-root';
11
+ import { loadAppConfig } from './app-config-load';
13
12
  import { BadFlagError } from './errors';
14
13
 
15
14
  /** Long enough for a migration a pre-upgrade hook must finish; helm's own 5m default is not. */
@@ -67,16 +66,18 @@ export function readLabel(flag: 'namespace' | 'release', raw: string): string {
67
66
  */
68
67
  export async function readReleaseName(root: string, flag: string | undefined): Promise<string> {
69
68
  if (flag !== undefined) return readLabel('release', flag);
70
- const module = (await import(join(root, APP_CONFIG_FILE))) as Record<string, unknown>;
71
- const config = module[APP_CONFIG_EXPORT];
72
- const name = isRecord(config) ? config['name'] : undefined;
69
+ const config = await loadAppConfig(root);
70
+ // `unknown`, not the declared `string`: core reads an absent name as `"undefined"` and lets it by.
71
+ const name: unknown = config?.name;
73
72
  if (typeof name === 'string' && name.length <= RELEASE_MAX && LABEL.test(name)) return name;
74
73
  throw new UltimateError({
75
74
  code: 'X_CONFIG_INVALID',
76
75
  cause:
77
76
  typeof name === 'string'
78
77
  ? `app.config.ts names the app "${name.slice(0, 80)}", which is not a helm release name (a DNS-1123 label of at most ${RELEASE_MAX} characters)`
79
- : 'app.config.ts exports no config.name, and x deploy --method helm names the release after it',
78
+ : config === undefined
79
+ ? `${root} has no app.config.ts, and x deploy --method helm names the release after its config.name`
80
+ : 'app.config.ts exports no config.name, and x deploy --method helm names the release after it',
80
81
  fix: 'name the release explicitly: x deploy --method helm --release my-app --json',
81
82
  });
82
83
  }