@ultimat3/cli 1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/package.json +60 -0
  4. package/src/app-agents-md.ts +27 -0
  5. package/src/app-boundaries.ts +206 -0
  6. package/src/app-evals.ts +74 -0
  7. package/src/app-load.ts +136 -0
  8. package/src/app-manifest.ts +137 -0
  9. package/src/app-openapi.ts +12 -0
  10. package/src/app-root.ts +57 -0
  11. package/src/bin.ts +17 -0
  12. package/src/boundary-cuts.ts +219 -0
  13. package/src/budgets.ts +92 -0
  14. package/src/cmd-build.ts +109 -0
  15. package/src/cmd-db.ts +187 -0
  16. package/src/cmd-deploy.ts +124 -0
  17. package/src/cmd-dev.ts +286 -0
  18. package/src/cmd-doctor.ts +178 -0
  19. package/src/cmd-errors.ts +99 -0
  20. package/src/cmd-fix.ts +126 -0
  21. package/src/cmd-generate.ts +434 -0
  22. package/src/cmd-help.ts +94 -0
  23. package/src/cmd-i18n.ts +212 -0
  24. package/src/cmd-jobs.ts +237 -0
  25. package/src/cmd-manifest.ts +97 -0
  26. package/src/cmd-mcp.ts +176 -0
  27. package/src/cmd-new.ts +133 -0
  28. package/src/cmd-planned.ts +119 -0
  29. package/src/cmd-policy.ts +136 -0
  30. package/src/cmd-registries.ts +195 -0
  31. package/src/cmd-routes.ts +73 -0
  32. package/src/cmd-tasks.ts +151 -0
  33. package/src/cmd-test.ts +109 -0
  34. package/src/cmd-verify.ts +265 -0
  35. package/src/command.ts +33 -0
  36. package/src/dev-assets.ts +177 -0
  37. package/src/dev-dashboard.ts +242 -0
  38. package/src/dev-hooks.ts +51 -0
  39. package/src/dev-policy.ts +82 -0
  40. package/src/dev-queue.ts +109 -0
  41. package/src/dev-render.ts +129 -0
  42. package/src/dev-replicator.ts +92 -0
  43. package/src/dev-roles.ts +246 -0
  44. package/src/dev-runtime.ts +203 -0
  45. package/src/dev-services.ts +75 -0
  46. package/src/dev-traces.ts +141 -0
  47. package/src/dispatch.ts +98 -0
  48. package/src/drift.ts +86 -0
  49. package/src/error-catalog.ts +156 -0
  50. package/src/error-contract.ts +212 -0
  51. package/src/errors.ts +367 -0
  52. package/src/exec.ts +70 -0
  53. package/src/hold.ts +48 -0
  54. package/src/i18n-audit.ts +183 -0
  55. package/src/index.ts +179 -0
  56. package/src/jobs-drain.ts +151 -0
  57. package/src/jobs-json.ts +134 -0
  58. package/src/jobs-report.ts +132 -0
  59. package/src/jobs-table.ts +34 -0
  60. package/src/json-merge.ts +40 -0
  61. package/src/mcp-db-target.ts +50 -0
  62. package/src/mcp-errors.ts +99 -0
  63. package/src/mcp-host.ts +282 -0
  64. package/src/mcp-test-output.ts +57 -0
  65. package/src/messages.ts +119 -0
  66. package/src/output.ts +174 -0
  67. package/src/parse.ts +243 -0
  68. package/src/policy-facts.ts +196 -0
  69. package/src/policy-fixture.ts +71 -0
  70. package/src/registry.ts +73 -0
  71. package/src/scaffold-fixture.ts +69 -0
  72. package/src/scaffold-typecheck.ts +240 -0
  73. package/src/source-files.ts +38 -0
  74. package/src/table.ts +19 -0
  75. package/src/tasks-facts.ts +113 -0
  76. package/src/templates/action.ts +193 -0
  77. package/src/templates/admin.ts +46 -0
  78. package/src/templates/catalog-json.ts +17 -0
  79. package/src/templates/entity.ts +157 -0
  80. package/src/templates/index.ts +23 -0
  81. package/src/templates/job.ts +148 -0
  82. package/src/templates/locales.ts +93 -0
  83. package/src/templates/naming.ts +97 -0
  84. package/src/templates/policy.ts +120 -0
  85. package/src/templates/query.ts +116 -0
  86. package/src/templates/resource.ts +199 -0
  87. package/src/templates/route.ts +138 -0
  88. package/src/templates/scaffold-app.ts +320 -0
  89. package/src/templates/scaffold-docs.ts +156 -0
  90. package/src/templates/scaffold-i18n.ts +149 -0
  91. package/src/templates/scaffold-icon.ts +54 -0
  92. package/src/templates/scaffold-package-shape.ts +49 -0
  93. package/src/templates/scaffold-repo.ts +427 -0
  94. package/src/test-select.ts +130 -0
  95. package/src/test-shards.ts +188 -0
  96. package/src/thrown-by.ts +24 -0
  97. package/src/ts-scan.ts +217 -0
  98. package/src/verify-step.ts +83 -0
  99. package/src/verify-tests.ts +166 -0
  100. package/src/version-loader.ts +16 -0
  101. package/src/workspace-checks.ts +288 -0
@@ -0,0 +1,265 @@
1
+ // `x verify` — the contract. Every check is a named step with its own pass/fail and duration, the
2
+ // same list in the terminal and in --json, and a non-zero exit if any step fails. Green means
3
+ // shippable (axiom 5): one step list, no second checklist, no CI-only step, and no way to narrow
4
+ // the run — `--only` and `--skip` would make "green" mean whatever the caller chose.
5
+
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import type { Manifest } from '@ultimat3/manifest';
9
+ import {
10
+ AGENTS_MD_FILENAME,
11
+ assertNoDrift,
12
+ MANIFEST_FILENAME,
13
+ verifyContract,
14
+ } from '@ultimat3/manifest';
15
+ import { checkAgentsMd } from './app-agents-md';
16
+ import { checkAppBoundaries } from './app-boundaries';
17
+ import { appManifest, readAppManifest } from './app-manifest';
18
+ import { OPENAPI_FILE, openApiJson } from './app-openapi';
19
+ import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
20
+ import { checkBudgets, readBuildStats } from './budgets';
21
+ import type { CliCommand, CommandContext } from './command';
22
+ import { checkDrift } from './drift';
23
+ import { checkErrorFixes } from './error-contract';
24
+ import { msg } from './messages';
25
+ import type { CommandResult, Finding, StepResult } from './output';
26
+ import { findingFrom } from './output';
27
+ import type { StepOutcome, VerifyContext, VerifyStep, VerifyStepName } from './verify-step';
28
+ import { fromExec, fromFindings, hostFindings, passed } from './verify-step';
29
+ import { TEST_STEPS } from './verify-tests';
30
+ import { checkFileSizes, checkPackageShape, hasWorkspacePackages } from './workspace-checks';
31
+
32
+ /** The whole contract, in cost order. Every check the framework knows how to make lives here. */
33
+ export const VERIFY_STEPS: readonly VerifyStep[] = [
34
+ {
35
+ name: 'typecheck',
36
+ summary: 'tsc -b across every project the root references',
37
+ async run(ctx) {
38
+ const result = await ctx.runner(['bunx', 'tsc', '-b', '--pretty', 'false'], {
39
+ cwd: ctx.root,
40
+ });
41
+ return fromExec(result, {
42
+ code: 'X_TYPECHECK_FAILED',
43
+ cause: 'the project does not typecheck',
44
+ fix: 'bunx tsc -b --pretty false',
45
+ });
46
+ },
47
+ },
48
+ {
49
+ name: 'lint',
50
+ summary: 'biome: no any, no default exports, no raw colours',
51
+ async run(ctx) {
52
+ const result = await ctx.runner(['bunx', 'biome', 'check', '.'], { cwd: ctx.root });
53
+ return fromExec(result, {
54
+ code: 'X_LINT_FAILED',
55
+ cause: 'biome reported problems',
56
+ fix: 'bunx biome check --write .',
57
+ });
58
+ },
59
+ },
60
+ {
61
+ name: 'boundaries',
62
+ summary: 'surface, layer and package-tier imports',
63
+ run: async (ctx) =>
64
+ fromFindings([
65
+ ...(await checkAppBoundaries(ctx.root)),
66
+ ...(await hostFindings(ctx, 'boundaries')),
67
+ ]),
68
+ },
69
+ {
70
+ name: 'filesize',
71
+ summary: 'one file, one job',
72
+ run: async (ctx) => fromFindings(await checkFileSizes(ctx.root)),
73
+ },
74
+ {
75
+ name: 'package-shape',
76
+ summary: 'every package ships the same contract files',
77
+ applies: (ctx) => hasWorkspacePackages(ctx.root),
78
+ run: async (ctx) => fromFindings(await checkPackageShape(ctx.root)),
79
+ },
80
+ {
81
+ name: 'errors',
82
+ summary: 'every X_* code has a runnable fix and a docs page',
83
+ // The fix-line half runs anywhere source does. The docs half needs a reference page to check
84
+ // against, and which file that is belongs to the host repo — hence `hostFindings`.
85
+ run: async (ctx) =>
86
+ fromFindings([...(await checkErrorFixes(ctx.root)), ...(await hostFindings(ctx, 'errors'))]),
87
+ },
88
+ ...TEST_STEPS,
89
+ {
90
+ name: 'drift',
91
+ summary: 'schema vs migrations',
92
+ // Only an app owns migrations; a package monorepo's `packages/db` is the driver, not a schema.
93
+ applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
94
+ run: async (ctx) => fromFindings(await checkDrift(ctx.root)),
95
+ },
96
+ {
97
+ name: 'contract-diff',
98
+ summary: 'the published contract vs the committed manifest',
99
+ // Either file is a published contract on its own: `openapi.json` generates the typed client,
100
+ // so gating on the manifest alone let a stale spec ship a wrong client unchecked.
101
+ applies: async (ctx) =>
102
+ existsSync(join(ctx.root, MANIFEST_FILENAME)) || existsSync(join(ctx.root, OPENAPI_FILE)),
103
+ async run(ctx) {
104
+ const committed = await readAppManifest(ctx.root);
105
+ const { manifest, findings } = await appManifest(ctx.root);
106
+ return fromFindings([
107
+ ...findings,
108
+ ...(committed === undefined ? [] : contractFindings(committed, manifest)),
109
+ ...(await specFindings(ctx.root, manifest)),
110
+ ]);
111
+ },
112
+ },
113
+ {
114
+ name: 'budgets',
115
+ summary: 'per-route JS bytes and LCP',
116
+ async run(ctx) {
117
+ const stats = await readBuildStats(ctx.root);
118
+ if (stats === undefined) return passed;
119
+ const { manifest } = await appManifest(ctx.root);
120
+ return fromFindings(checkBudgets(manifest, stats));
121
+ },
122
+ },
123
+ {
124
+ name: 'manifest',
125
+ summary: 'the two files an agent reads: generated facts, hand-written conventions',
126
+ // No `applies`. The drift half has nothing to compare against until `x manifest` has run
127
+ // once, and says so by finding nothing — but `AGENTS.md` is required of every repo the gate
128
+ // runs in, so the step always has a question to answer and must never report as skipped.
129
+ async run(ctx) {
130
+ const agents = await checkAgentsMd(ctx.root);
131
+ const findings = [
132
+ ...(await driftFindings(ctx.root)),
133
+ ...agents.findings,
134
+ ...(await hostFindings(ctx, 'manifest')),
135
+ ];
136
+ // Warnings are not findings: `AGENTS.md` tabulating a route table is a smell a human
137
+ // judges, not a build error. They ride in `output`, which `--json` carries verbatim.
138
+ const output = agents.warnings.map((warning) => `${AGENTS_MD_FILENAME}: ${warning}`);
139
+ return {
140
+ ok: findings.length === 0,
141
+ findings,
142
+ ...(output.length === 0 ? {} : { output: output.join('\n') }),
143
+ };
144
+ },
145
+ },
146
+ {
147
+ name: 'roadmap',
148
+ summary: "every roadmap milestone's status marker matches what is actually on disk",
149
+ // A generated app ships no `docs/idea/14-roadmap.md` — only the framework monorepo does, so
150
+ // only a host that registers this check has anything for the step to verify.
151
+ applies: async (ctx) => ctx.hostChecks?.roadmap !== undefined,
152
+ run: async (ctx) => fromFindings(await hostFindings(ctx, 'roadmap')),
153
+ },
154
+ ];
155
+
156
+ /** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
157
+ async function driftFindings(root: string): Promise<readonly Finding[]> {
158
+ const path = join(root, MANIFEST_FILENAME);
159
+ if (!existsSync(path)) return [];
160
+ const { manifest, findings } = await appManifest(root);
161
+ try {
162
+ await assertNoDrift({ manifest, path });
163
+ return findings;
164
+ } catch (error) {
165
+ return [...findings, { ...findingFrom(error), at: MANIFEST_FILENAME }];
166
+ }
167
+ }
168
+
169
+ /** A breaking change is allowed — with a major bump. `verifyContract` is the one that decides. */
170
+ function contractFindings(before: Manifest, after: Manifest): readonly Finding[] {
171
+ try {
172
+ verifyContract({ before, after });
173
+ return [];
174
+ } catch (error) {
175
+ return [{ ...findingFrom(error), at: MANIFEST_FILENAME }];
176
+ }
177
+ }
178
+
179
+ /** The typed client is generated from `openapi.json`, so a stale spec ships a wrong client. */
180
+ async function specFindings(root: string, manifest: Manifest): Promise<readonly Finding[]> {
181
+ const path = join(root, OPENAPI_FILE);
182
+ if (!existsSync(path)) return [];
183
+ if ((await Bun.file(path).text()) === openApiJson(manifest)) return [];
184
+ return [
185
+ {
186
+ code: 'X_MANIFEST_STALE',
187
+ cause: `${OPENAPI_FILE} does not match the actions the code registers`,
188
+ fix: 'x manifest',
189
+ docs: 'https://ultimate.dev/errors/X_MANIFEST_STALE',
190
+ at: OPENAPI_FILE,
191
+ },
192
+ ];
193
+ }
194
+
195
+ /**
196
+ * Run every step in order, never bailing early: an agent fixing three things at once needs all
197
+ * three findings from one run, not one per round-trip.
198
+ */
199
+ export async function runVerify(
200
+ steps: readonly VerifyStep[],
201
+ ctx: VerifyContext,
202
+ ): Promise<CommandResult> {
203
+ const results: StepResult[] = [];
204
+ for (const step of steps) {
205
+ const applies = step.applies === undefined ? true : await step.applies(ctx);
206
+ if (!applies) {
207
+ results.push({ name: step.name, ok: true, durationMs: 0, skipped: true, findings: [] });
208
+ continue;
209
+ }
210
+ const started = performance.now();
211
+ const outcome = await step.run(ctx).catch(
212
+ (error: unknown): StepOutcome => ({
213
+ ok: false,
214
+ findings: [findingOf(error, step.name)],
215
+ }),
216
+ );
217
+ results.push({
218
+ name: step.name,
219
+ ok: outcome.ok,
220
+ durationMs: Math.round(performance.now() - started),
221
+ findings: outcome.findings,
222
+ ...(outcome.output === undefined ? {} : { output: outcome.output }),
223
+ });
224
+ }
225
+ const failedSteps = results.filter((step) => !step.ok).map((step) => step.name);
226
+ const totalMs = results.reduce((sum, step) => sum + step.durationMs, 0);
227
+ return {
228
+ ok: failedSteps.length === 0,
229
+ command: 'verify',
230
+ summary:
231
+ failedSteps.length === 0
232
+ ? msg('cli.verify.pass', { count: results.length, ms: totalMs })
233
+ : msg('cli.verify.fail', { failed: failedSteps.length, count: results.length }),
234
+ steps: results,
235
+ data: { failed: failedSteps, durationMs: totalMs },
236
+ exitCode: failedSteps.length === 0 ? 0 : 1,
237
+ };
238
+ }
239
+
240
+ function findingOf(error: unknown, step: string): Finding {
241
+ const cause = error instanceof Error ? error.message : String(error);
242
+ return {
243
+ code: 'X_VERIFY_FAILED',
244
+ cause: `step "${step}" threw: ${cause}`,
245
+ fix: 'x verify --json',
246
+ docs: 'https://ultimate.dev/errors/X_VERIFY_FAILED',
247
+ };
248
+ }
249
+
250
+ export const verifyCommand: CliCommand = {
251
+ spec: {
252
+ name: 'verify',
253
+ summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
254
+ usage: 'x verify [--json]',
255
+ requiresApp: true,
256
+ flags: [],
257
+ },
258
+ async run(ctx: CommandContext): Promise<CommandResult> {
259
+ const root = requireAppRoot('verify', ctx.cwd).dir;
260
+ return runVerify(VERIFY_STEPS, { root, runner: ctx.runner });
261
+ },
262
+ };
263
+
264
+ export const verifyStepNames = (): readonly VerifyStepName[] =>
265
+ VERIFY_STEPS.map((step) => step.name);
package/src/command.ts ADDED
@@ -0,0 +1,33 @@
1
+ // The shape every command implements. A command is a spec (for parsing and help) plus a pure-ish
2
+ // `run` that returns a `CommandResult` — it never writes to stdout and never calls process.exit,
3
+ // so the dispatcher owns rendering and the exit code, and every command is directly testable.
4
+
5
+ import type { Runner } from './exec';
6
+ import type { CommandResult } from './output';
7
+ import type { CommandSpec, ParsedArgs } from './parse';
8
+
9
+ export interface CommandContext {
10
+ readonly args: ParsedArgs;
11
+ /** Already resolved: `--cwd` applied, app root not yet required. */
12
+ readonly cwd: string;
13
+ readonly runner: Runner;
14
+ readonly env: Readonly<Record<string, string | undefined>>;
15
+ readonly bunVersion: string;
16
+ }
17
+
18
+ export interface CliCommand {
19
+ readonly spec: CommandSpec;
20
+ run(ctx: CommandContext): Promise<CommandResult>;
21
+ }
22
+
23
+ export const ok = (
24
+ command: string,
25
+ summary: string,
26
+ extra: Partial<CommandResult> = {},
27
+ ): CommandResult => ({ ok: true, command, summary, ...extra });
28
+
29
+ export const failed = (
30
+ command: string,
31
+ summary: string,
32
+ extra: Partial<CommandResult> = {},
33
+ ): CommandResult => ({ ok: false, command, summary, ...extra });
@@ -0,0 +1,177 @@
1
+ // Projecting the framework's one image pipeline onto the routes `x dev` serves. Three packages
2
+ // declare what an image is — `@ultimat3/seo` a variant URL, `@ultimat3/storage` a variant key,
3
+ // `@ultimat3/pwa` the icons a web manifest promises — and `@ultimat3/core`'s pipeline owns every
4
+ // pixel, so this file picks the two base paths they hang off and decides nothing else.
5
+
6
+ // `join` is `node:`-only by necessity: Bun exposes no path-join primitive, and `ICON_SOURCE` is
7
+ // app-root-relative, so resolving it against the root is string work no `Bun.file` overload does.
8
+ import { join } from 'node:path';
9
+ import { probeImage } from '@ultimat3/core';
10
+ import type { Route, UltimateRequest } from '@ultimat3/http';
11
+ import { applyCacheHeaders } from '@ultimat3/http';
12
+ import type { IconPlan } from '@ultimat3/pwa';
13
+ import { BuiltinImagePipeline, PwaIconMissingError, planIcons } from '@ultimat3/pwa';
14
+ import type { ImageQuery } from '@ultimat3/seo';
15
+ import { builtinImageDriver, parseImageQuery } from '@ultimat3/seo';
16
+ import type { ImageFormat, ImageTransform, Storage } from '@ultimat3/storage';
17
+ import { IMAGE_FORMATS, variantKey } from '@ultimat3/storage';
18
+
19
+ /**
20
+ * The one source image every generated icon derives from. `x new` scaffolds it, `x doctor` checks
21
+ * it and this file reads it — one constant, because a second spelling is an app that passes the
22
+ * diagnostic and still serves no icons. PNG, not SVG: core's pipeline decodes PNG and JPEG only.
23
+ */
24
+ export const ICON_SOURCE = 'apps/web/site/icon.png';
25
+
26
+ /** Where `planIcons` writes, and therefore the paths the generated web manifest names. */
27
+ export const ICON_BASE_PATH = '/icons';
28
+
29
+ /** Storage-backed images. `responsiveImage({ src: '/media/<key>' })` mints its variants under it. */
30
+ export const MEDIA_BASE_PATH = '/media';
31
+
32
+ /**
33
+ * Variants are content-addressed by `variantKey`, so a URL that answers once answers forever with
34
+ * the same bytes — the immutable hint is a fact about the key, not an optimism about the source.
35
+ */
36
+ const imageResponse = (bytes: Uint8Array, contentType: string): Response =>
37
+ applyCacheHeaders(
38
+ // Copied, not passed through: a `Uint8Array<ArrayBufferLike>` may be backed by a
39
+ // `SharedArrayBuffer`, which `Response` does not accept, and copying is what makes that true
40
+ // by construction rather than by a cast that would only silence it.
41
+ new Response(new Uint8Array(bytes), { headers: { 'content-type': contentType } }),
42
+ { mode: 'immutable' },
43
+ );
44
+
45
+ const isImageFormat = (value: string): value is ImageFormat =>
46
+ (IMAGE_FORMATS as readonly string[]).includes(value);
47
+
48
+ /**
49
+ * `exactOptionalPropertyTypes` makes an explicit `undefined` a different answer from an absent
50
+ * key, and `variantKey` reads presence — so a spread, never an assignment.
51
+ */
52
+ function storageTransform(query: ImageQuery, format: ImageFormat | undefined): ImageTransform {
53
+ return {
54
+ ...(query.width === undefined ? {} : { width: query.width }),
55
+ ...(format === undefined ? {} : { format }),
56
+ ...(query.quality === undefined ? {} : { quality: query.quality }),
57
+ };
58
+ }
59
+
60
+ /**
61
+ * A cache hit costs one `exists` and one `get`; a miss costs a decode. The source is read once
62
+ * either way and handed to the driver rather than fetched again — `read` exists so seo never has
63
+ * to guess whether a `src` is a path, a key or a URL, and here it is unambiguously a storage key.
64
+ */
65
+ async function transformedVariant(
66
+ storage: Storage,
67
+ key: string,
68
+ query: ImageQuery,
69
+ ): Promise<Response> {
70
+ const disk = storage.disk();
71
+ // A format storage cannot name has no variant key, so it cannot be cached. The driver refuses
72
+ // it with core's `X_IMAGE_UNSUPPORTED`; refusing it here too would give one bad URL two codes.
73
+ const format =
74
+ query.format !== undefined && isImageFormat(query.format) ? query.format : undefined;
75
+ const cacheable = query.format === undefined || format !== undefined;
76
+ const cached = cacheable ? variantKey(key, storageTransform(query, format)) : undefined;
77
+ if (cached !== undefined && (await disk.exists(cached))) {
78
+ const hit = await disk.get(cached);
79
+ return imageResponse(hit.bytes, hit.object.contentType);
80
+ }
81
+
82
+ const source = await disk.get(key);
83
+ const variant = await builtinImageDriver({ read: async () => source.bytes }).transform({
84
+ src: key,
85
+ // A header read, not a decode: `?f=webp` alone still needs a width, and the source's own is
86
+ // the only one that does not resize an image the caller never asked to resize.
87
+ width: query.width ?? probeImage(source.bytes).width,
88
+ ...(query.format === undefined ? {} : { format: query.format }),
89
+ ...(query.quality === undefined ? {} : { quality: query.quality }),
90
+ });
91
+ if (cached !== undefined) {
92
+ await disk.put(cached, variant.bytes, { contentType: variant.contentType });
93
+ }
94
+ return imageResponse(variant.bytes, variant.contentType);
95
+ }
96
+
97
+ async function mediaResponse(request: UltimateRequest, storage: Storage): Promise<Response> {
98
+ const key = request.params['key'] ?? '';
99
+ const query = parseImageQuery(request.url.searchParams);
100
+ if (query !== null) return transformedVariant(storage, key, query);
101
+ // No transform asked for: the object itself, still under the storage key's own safety checks.
102
+ const read = await storage.disk().get(key);
103
+ return imageResponse(read.bytes, read.object.contentType);
104
+ }
105
+
106
+ /**
107
+ * Rendered once per process, not per request: the fourteen matrix entries are pure functions of
108
+ * one source file, and re-encoding a 512px PNG on every hit would be work no caller can observe.
109
+ */
110
+ function iconRenderer(root: string): (plan: IconPlan, path: string) => Promise<Uint8Array> {
111
+ const pipeline = new BuiltinImagePipeline();
112
+ const rendered = new Map<string, Promise<Uint8Array>>();
113
+ const sourceBytes = async (): Promise<Uint8Array> => {
114
+ const file = Bun.file(join(root, ICON_SOURCE));
115
+ if (!(await file.exists())) {
116
+ throw new PwaIconMissingError(
117
+ `${ICON_SOURCE} does not exist, so every icon the web manifest declares is unbacked and ` +
118
+ 'the app is not installable',
119
+ // The same edit `x doctor` reports for the same condition, in `@ultimat3/pwa`'s own words.
120
+ // `x new` was here and takes an app name, so it could never run inside the broken app.
121
+ `add a 1024x1024 square PNG at ${ICON_SOURCE}`,
122
+ );
123
+ }
124
+ return file.bytes();
125
+ };
126
+ return async (plan, path) => {
127
+ const entry = plan.entries.find((candidate) => candidate.outputPath === path);
128
+ if (entry === undefined) {
129
+ throw new PwaIconMissingError(
130
+ `${path} is not in the icon matrix, so no transform describes it`,
131
+ `request one of ${plan.entries.map((one) => one.outputPath).join(', ')}`,
132
+ );
133
+ }
134
+ const existing = rendered.get(path);
135
+ if (existing !== undefined) return existing;
136
+ const bytes = sourceBytes().then((source) => pipeline.resize(source, entry.transform));
137
+ rendered.set(path, bytes);
138
+ // A failed render must not be remembered — the next request comes after the source was added.
139
+ bytes.catch(() => rendered.delete(path));
140
+ return bytes;
141
+ };
142
+ }
143
+
144
+ export interface AssetRoutesOptions {
145
+ /** App root. The source icon is resolved against it; storage keys never are. */
146
+ readonly root: string;
147
+ readonly storage: Storage;
148
+ }
149
+
150
+ /**
151
+ * The icon routes mount whether or not the source exists, and a missing source is refused on the
152
+ * wire with `X_PWA_ICON_MISSING` and its fix — a route that silently disappears is a 404 whose
153
+ * meaning an agent has to guess. Deliberately NOT a boot finding: `x doctor` already reports this
154
+ * exact condition with this exact code, and a second reporter of one condition is the duplication
155
+ * this package's own rule forbids. `x dev` owns the runtime half, the diagnostic owns the other.
156
+ */
157
+ export function assetRoutes(options: AssetRoutesOptions): readonly Route[] {
158
+ const plan = planIcons({ sourceIcon: ICON_SOURCE, outDir: ICON_BASE_PATH });
159
+ const render = iconRenderer(options.root);
160
+
161
+ const routes: Route[] = plan.entries.map((entry) => ({
162
+ method: 'GET',
163
+ path: entry.outputPath,
164
+ meta: { name: `assets.icon.${entry.spec.filename}`, auth: 'public', tags: ['assets'] },
165
+ handler: async (request: UltimateRequest): Promise<Response> =>
166
+ imageResponse(await render(plan, request.pathname), 'image/png'),
167
+ }));
168
+ routes.push({
169
+ method: 'GET',
170
+ path: `${MEDIA_BASE_PATH}/*key`,
171
+ meta: { name: 'assets.media', auth: 'public', tags: ['assets'] },
172
+ handler: async (request: UltimateRequest): Promise<Response> =>
173
+ mediaResponse(request, options.storage),
174
+ });
175
+
176
+ return routes;
177
+ }