@ultimat3/cli 24.0.0 → 25.1.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 (243) hide show
  1. package/CLAUDE.md +15 -8
  2. package/README.md +30 -5
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +3 -1
  6. package/src/app-boundaries.ts +17 -4
  7. package/src/app-config-load.ts +3 -3
  8. package/src/app-env.ts +21 -14
  9. package/src/app-load.ts +38 -14
  10. package/src/app-manifest.ts +2 -2
  11. package/src/app-mcp.ts +25 -27
  12. package/src/app-name.ts +52 -0
  13. package/src/app-permissions-borrowed.ts +2 -8
  14. package/src/app-permissions-site.ts +31 -7
  15. package/src/app-root.ts +9 -5
  16. package/src/bin.ts +3 -7
  17. package/src/browser-launcher.ts +4 -2
  18. package/src/budgets-charged.ts +60 -0
  19. package/src/budgets-scan.ts +76 -0
  20. package/src/budgets-unmeasured.ts +91 -0
  21. package/src/budgets.ts +115 -121
  22. package/src/cdp-shot-driver.ts +17 -0
  23. package/src/cdp-shot-watch.ts +35 -31
  24. package/src/ci-log.ts +0 -0
  25. package/src/cmd-auth.ts +2 -2
  26. package/src/cmd-build-spec.ts +7 -1
  27. package/src/cmd-build.ts +89 -9
  28. package/src/cmd-db-backfill.ts +1 -1
  29. package/src/cmd-db-branch.ts +2 -2
  30. package/src/cmd-db.ts +5 -2
  31. package/src/cmd-deploy-compose.ts +94 -0
  32. package/src/cmd-deploy-drain.ts +48 -0
  33. package/src/cmd-deploy-start-first.ts +178 -0
  34. package/src/cmd-deploy.ts +158 -36
  35. package/src/cmd-dev.ts +18 -357
  36. package/src/cmd-doctor.ts +16 -66
  37. package/src/cmd-env.ts +29 -14
  38. package/src/cmd-generate.ts +12 -2
  39. package/src/cmd-i18n.ts +11 -8
  40. package/src/cmd-jobs-spec.ts +7 -22
  41. package/src/cmd-jobs.ts +18 -137
  42. package/src/cmd-mcp.ts +25 -7
  43. package/src/cmd-new.ts +36 -8
  44. package/src/cmd-planned.ts +9 -0
  45. package/src/cmd-routes.ts +3 -3
  46. package/src/cmd-shot-matrix.ts +21 -4
  47. package/src/cmd-shot-spec.ts +10 -2
  48. package/src/cmd-shot.ts +35 -39
  49. package/src/cmd-test.ts +1 -1
  50. package/src/coverage-lcov.ts +4 -2
  51. package/src/db-backfill.ts +2 -2
  52. package/src/db-replay-engine.ts +4 -10
  53. package/src/db-subscribes.ts +19 -16
  54. package/src/dev-boot.ts +290 -0
  55. package/src/dev-child-watch.ts +63 -12
  56. package/src/dev-dashboard-guard.ts +1 -1
  57. package/src/dev-dashboard.ts +5 -1
  58. package/src/dev-lock.ts +96 -24
  59. package/src/dev-rebuild.ts +80 -0
  60. package/src/dev-restart-retire.ts +46 -0
  61. package/src/dev-route-table.ts +30 -8
  62. package/src/dev-supervisor.ts +114 -22
  63. package/src/dev-traces.ts +53 -20
  64. package/src/dispatch.ts +14 -7
  65. package/src/doctor-auth.ts +4 -4
  66. package/src/doctor-offline.ts +6 -5
  67. package/src/document-options.ts +7 -0
  68. package/src/drift-replica-identity.ts +2 -2
  69. package/src/drift.ts +29 -6
  70. package/src/error-codes.ts +6 -0
  71. package/src/error-page-csp.ts +14 -15
  72. package/src/exec.ts +8 -0
  73. package/src/favicon.ts +2 -2
  74. package/src/fix-command.ts +9 -1
  75. package/src/framework-env.ts +148 -0
  76. package/src/framework-schema.ts +13 -2
  77. package/src/generate-catalog-locales.ts +79 -0
  78. package/src/generate-kinds.ts +3 -17
  79. package/src/generate-plural.ts +50 -0
  80. package/src/generate-shadow.ts +50 -1
  81. package/src/hold.ts +56 -1
  82. package/src/i18n-audit.ts +21 -8
  83. package/src/i18n-index.ts +5 -6
  84. package/src/i18n-registration.ts +7 -9
  85. package/src/import-scan.ts +28 -3
  86. package/src/index.ts +1 -3
  87. package/src/island-bundle.ts +54 -64
  88. package/src/island-capture.ts +1 -1
  89. package/src/island-duplicate-refusal.ts +57 -0
  90. package/src/island-duplicates.ts +221 -0
  91. package/src/island-entry-missing.ts +64 -0
  92. package/src/island-harness-route.ts +20 -4
  93. package/src/island-harness-script.ts +23 -2
  94. package/src/island-identity.ts +17 -14
  95. package/src/island-package-dedupe.ts +150 -0
  96. package/src/island-realtime.ts +36 -16
  97. package/src/island-routes.ts +2 -2
  98. package/src/island-runtime.ts +241 -0
  99. package/src/island-sources.ts +105 -0
  100. package/src/island-states-load.ts +6 -0
  101. package/src/island-store.ts +90 -6
  102. package/src/jobs-json.ts +0 -26
  103. package/src/jobs-report.ts +32 -16
  104. package/src/local-cli-handoff.ts +90 -0
  105. package/src/mcp-errors.ts +8 -1
  106. package/src/mcp-host.ts +6 -5
  107. package/src/mcp-ui-inspect.ts +3 -2
  108. package/src/mcp-ui-interact.ts +3 -2
  109. package/src/mcp-ui.ts +8 -7
  110. package/src/measure-scope.ts +15 -10
  111. package/src/messages.ts +27 -16
  112. package/src/metrics-endpoint.ts +3 -2
  113. package/src/migration-extensions.ts +1 -1
  114. package/src/otlp-export.ts +2 -2
  115. package/src/output.ts +18 -0
  116. package/src/page-speculation.ts +14 -11
  117. package/src/parse.ts +0 -8
  118. package/src/permission-grants.ts +3 -3
  119. package/src/posix-path.ts +30 -0
  120. package/src/prerender-locales.ts +6 -4
  121. package/src/prerender-report.ts +58 -0
  122. package/src/prerender-served.ts +52 -0
  123. package/src/prerender.ts +63 -98
  124. package/src/pwa-artifacts.ts +8 -1
  125. package/src/pwa-manifest-locales.ts +9 -9
  126. package/src/retry-memo.ts +1 -1
  127. package/src/role-realtime.ts +7 -1
  128. package/src/role-replicator.ts +110 -31
  129. package/src/role-start.ts +66 -87
  130. package/src/role-sync.ts +65 -6
  131. package/src/role-wake.ts +2 -3
  132. package/src/root-env.ts +157 -28
  133. package/src/route-islands.ts +69 -0
  134. package/src/runtime-admin.ts +4 -1
  135. package/src/runtime-assets.ts +15 -2
  136. package/src/runtime-bindings.ts +23 -3
  137. package/src/runtime-cache.ts +8 -8
  138. package/src/runtime-hooks.ts +2 -1
  139. package/src/runtime-isr-outcome.ts +100 -0
  140. package/src/runtime-isr.ts +2 -2
  141. package/src/runtime-jobs.ts +78 -18
  142. package/src/runtime-live-feed.ts +1 -1
  143. package/src/runtime-mail.ts +26 -0
  144. package/src/runtime-mfa-warning.ts +2 -2
  145. package/src/runtime-overrides.ts +3 -3
  146. package/src/runtime-purge.ts +1 -1
  147. package/src/runtime-queue.ts +19 -30
  148. package/src/runtime-realtime.ts +5 -3
  149. package/src/runtime-render.ts +77 -82
  150. package/src/runtime-replica.ts +3 -8
  151. package/src/runtime-services.ts +36 -15
  152. package/src/runtime-storage-upload.ts +2 -2
  153. package/src/runtime-storage.ts +3 -3
  154. package/src/schema-diff.ts +1 -1
  155. package/src/secrets-rotation.ts +8 -9
  156. package/src/seo-routes.ts +2 -2
  157. package/src/serve-boot.ts +22 -11
  158. package/src/serve-drain.ts +3 -1
  159. package/src/serve-retire.ts +95 -0
  160. package/src/serve-web.ts +19 -8
  161. package/src/serve.ts +27 -10
  162. package/src/server-barrels.ts +6 -3
  163. package/src/shot-browser.ts +1 -1
  164. package/src/shot-locale.ts +6 -15
  165. package/src/shot-settle.ts +3 -3
  166. package/src/shot-theme.ts +1 -1
  167. package/src/shot-viewport.ts +152 -0
  168. package/src/signal-shred.ts +26 -12
  169. package/src/site-asset-routes.ts +2 -2
  170. package/src/site-seo.ts +6 -4
  171. package/src/source-files.ts +13 -3
  172. package/src/static-report.ts +5 -0
  173. package/src/style-routes.ts +2 -2
  174. package/src/sw-artifacts.ts +8 -8
  175. package/src/sw-precache-plan.ts +34 -4
  176. package/src/sync-authenticator.ts +3 -3
  177. package/src/sync-url.ts +1 -1
  178. package/src/templates/action.ts +4 -4
  179. package/src/templates/backfill.ts +4 -4
  180. package/src/templates/entity.ts +2 -2
  181. package/src/templates/github/ci.yml.ts +28 -18
  182. package/src/templates/github/image.yml.ts +92 -0
  183. package/src/templates/github/runner.ts +18 -0
  184. package/src/templates/guard-bare-error.ts +16 -2
  185. package/src/templates/guard-unzoned-date.ts +56 -6
  186. package/src/templates/index.ts +1 -1
  187. package/src/templates/job-body-test.ts +2 -2
  188. package/src/templates/job.ts +6 -6
  189. package/src/templates/query.ts +9 -8
  190. package/src/templates/resource-page.ts +9 -7
  191. package/src/templates/resource-service.ts +2 -2
  192. package/src/templates/route.ts +23 -8
  193. package/src/templates/scaffold-api.ts +2 -3
  194. package/src/templates/scaffold-app.ts +2 -1
  195. package/src/templates/scaffold-auth.ts +205 -70
  196. package/src/templates/scaffold-bin.ts +122 -0
  197. package/src/templates/scaffold-claude-commands.ts +11 -11
  198. package/src/templates/scaffold-container.ts +12 -6
  199. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  200. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  201. package/src/templates/scaffold-dashboard-example.ts +112 -172
  202. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  203. package/src/templates/scaffold-dashboard.ts +9 -5
  204. package/src/templates/scaffold-db-package.ts +2 -2
  205. package/src/templates/scaffold-docs.ts +20 -77
  206. package/src/templates/scaffold-domain-package.ts +0 -4
  207. package/src/templates/scaffold-entries.ts +4 -3
  208. package/src/templates/scaffold-env.ts +6 -3
  209. package/src/templates/scaffold-gitattributes.ts +59 -0
  210. package/src/templates/scaffold-helm-templates.ts +74 -18
  211. package/src/templates/scaffold-helm.ts +31 -4
  212. package/src/templates/scaffold-http.ts +1 -1
  213. package/src/templates/scaffold-i18n.ts +15 -2
  214. package/src/templates/scaffold-icon.ts +2 -2
  215. package/src/templates/scaffold-repo.ts +18 -15
  216. package/src/templates/scaffold-shell.ts +17 -2
  217. package/src/test-only-shipped.ts +193 -0
  218. package/src/test-select.ts +2 -1
  219. package/src/test-shards.ts +1 -4
  220. package/src/theme-boot.ts +2 -2
  221. package/src/theme-brand.ts +66 -0
  222. package/src/trusted-hops.ts +49 -0
  223. package/src/unscanned-admin.ts +2 -2
  224. package/src/verify-checks.ts +12 -2
  225. package/src/verify-coverage-run.ts +3 -2
  226. package/src/verify-deadline.ts +87 -10
  227. package/src/verify-merge.ts +48 -16
  228. package/src/verify-part-step.ts +10 -2
  229. package/src/verify-role-load-probe.ts +12 -2
  230. package/src/verify-role-load.ts +9 -4
  231. package/src/verify-run.ts +3 -0
  232. package/src/verify-step.ts +2 -0
  233. package/src/verify-test-run.ts +3 -2
  234. package/src/verify-tests.ts +0 -1
  235. package/src/worker-bundle.ts +2 -2
  236. package/src/workspace-checks.ts +53 -7
  237. package/src/workspace-graph.ts +3 -2
  238. package/src/write-line.ts +82 -7
  239. package/src/browser-launcher-fake-html.ts +0 -80
  240. package/src/browser-launcher-fake.ts +0 -168
  241. package/src/jobs-drain.ts +0 -230
  242. package/src/scaffold-typecheck.ts +0 -227
  243. package/src/thrown-by.ts +0 -24
@@ -8,6 +8,15 @@
8
8
  import type { GeneratedFile, NameSet } from './naming';
9
9
  import { helmTemplateFiles } from './scaffold-helm-templates';
10
10
 
11
+ /**
12
+ * The uid AND gid the app's process runs as, in the image and in every pod: the `bun` user of the
13
+ * `oven/bun:1.4-alpine` base the scaffolded Dockerfile pins (`id bun` → `uid=1000 gid=1000`). One
14
+ * number, read by both templates, because two of them drifted — the chart ran distroless'
15
+ * `nonroot` (65532), a uid that image does not have. Pinned to the measurement by
16
+ * `scaffold-container.test.ts`.
17
+ */
18
+ export const RUNTIME_UID = 1000;
19
+
11
20
  /**
12
21
  * `appVersion` is the default image tag (`image.tag: ""` below), so it must name a tag that can
13
22
  * exist. `x deploy --image <ref>` overrides both keys, which is the path that actually ships.
@@ -41,7 +50,7 @@ image:
41
50
  env:
42
51
  NODE_ENV: production
43
52
 
44
- existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET, ULTIMATE_CURSOR_SECRET
53
+ existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET, and .env.example's Framework section
45
54
 
46
55
  # The release-wide Secret above is every role's DEFAULT: \`existingSecret\` under a role
47
56
  # (roles.web.existingSecret, migrate.existingSecret) names that role's own, so the migrate Job's
@@ -71,9 +80,12 @@ networkPolicy:
71
80
  # METRICS_PORT in the container; move one and the other follows.
72
81
  metricsPort: 9090
73
82
 
83
+ # The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
74
84
  podSecurityContext:
75
85
  runAsNonRoot: true
76
- runAsUser: 65532
86
+ runAsUser: ${RUNTIME_UID}
87
+ runAsGroup: ${RUNTIME_UID}
88
+ fsGroup: ${RUNTIME_UID}
77
89
  seccompProfile: { type: RuntimeDefault }
78
90
 
79
91
  securityContext:
@@ -82,10 +94,20 @@ securityContext:
82
94
  capabilities: { drop: [ALL] }
83
95
 
84
96
  # The stop sequence: out of the Service, then this preStop sleep (Kubernetes 1.30+ only), then
85
- # SIGTERM, then the framework's readiness grace and drain. terminationGracePeriodSeconds in
86
- # templates/deployments.yaml is sized to all three, so move them together.
97
+ # SIGTERM, then the framework's readiness grace and drain. Each Deployment's
98
+ # terminationGracePeriodSeconds is DERIVED from this block per role — never set it by hand.
99
+ # readinessGraceSeconds app.config.ts drain.readinessGraceMs / 1000 (web and sync only)
100
+ # deadlineSeconds app.config.ts drain.deadlineMs / 1000 — x deploy --method helm sets
101
+ # both from app.config.ts, so a raised budget raises the grace with it
102
+ # teardownMarginSeconds the release after the drain, the telemetry flush, the pool close
87
103
  drain:
88
104
  preStopSleepSeconds: 5
105
+ readinessGraceSeconds: 5
106
+ deadlineSeconds: 25
107
+ teardownMarginSeconds: 10
108
+
109
+ # How long a new pod must stay Ready before a rollout counts it and stops an old one.
110
+ minReadySeconds: 10
89
111
 
90
112
  # The release phase. Runs to completion before any serving role starts.
91
113
  migrate:
@@ -142,6 +164,11 @@ roles:
142
164
  worker:
143
165
  enabled: true
144
166
  replicas: 2
167
+ # The retire, OFF at 0. Set to the longest a held job may run, in seconds, and the worker's
168
+ # preStop sends its PID 1 SIGUSR2: stop claiming, finish every held job, abort nothing, exit 0
169
+ # — for a job whose side effect must happen at most once and may outlast drain.deadlineSeconds.
170
+ # Added to this role's terminationGracePeriodSeconds, which the kubelet counts the preStop against.
171
+ retireSeconds: 0
145
172
  resources:
146
173
  requests: { cpu: 200m, memory: 256Mi }
147
174
  limits: { memory: 1Gi }
@@ -13,7 +13,7 @@ const httpConfig = (
13
13
  app: NameSet,
14
14
  ): string => `// What this app declares about HTTP. Module scope IS the wiring: the boot scan imports every
15
15
  // module under \`apps/*\` before a listener binds, and \`x dev\` and the container both read the
16
- // configured value back at start — the same seam \`app/auth/dev-actor.ts\` installs through.
16
+ // configured value back at start — the same seam \`app/auth/authenticator.ts\` installs through.
17
17
  //
18
18
  // The BOOT lays its own facts over whatever this says: \`port\`, \`hostname\`, \`dev\`, \`buildId\`,
19
19
  // \`signInPath\`, \`trustProxy\`, \`trustedProxyHops\` and \`rateLimit.scope\` are all facts about the
@@ -2,6 +2,7 @@
2
2
  // blessed typed-catalog shape (modelled on examples/dummy/packages/i18n/src/index.ts) — split out
3
3
  // of scaffold-repo.ts to stay under the file-size ceiling.
4
4
 
5
+ import { APP_CATALOGS_PATH } from '@ultimat3/i18n/app-catalogs';
5
6
  import { catalogJson } from './catalog-json';
6
7
  import type { GeneratedFile, NameSet } from './naming';
7
8
  import { camel } from './naming';
@@ -121,7 +122,7 @@ const i18nCatalog = (app: NameSet): string =>
121
122
  'site.home.secondary': 'Open the admin',
122
123
  'site.home.f1Title': 'One command to a running app',
123
124
  'site.home.f1Body':
124
- 'bin/setup installs, migrates and seeds; bin/dev runs every role in one process.',
125
+ 'bun run setup installs, migrates and seeds; bun run dev runs every role in one process.',
125
126
  'site.home.f2Title': 'Policies, not flags',
126
127
  'site.home.f2Body':
127
128
  'Every route, action and query declares who may call it, and one system decides.',
@@ -149,6 +150,18 @@ const i18nCatalog = (app: NameSet): string =>
149
150
  'app.dashboard.columnTitle': 'Title',
150
151
  'app.dashboard.columnCreated': 'Created',
151
152
  'app.dashboard.emptyTitle': 'No posts yet',
153
+ 'app.dashboard.statPaid': 'Paid posts',
154
+ 'app.dashboard.hintPaid': 'priced above zero',
155
+ 'app.dashboard.chartKey': 'Day',
156
+ 'app.dashboard.chartSeries': 'Posts',
157
+ 'app.dashboard.mixTitle': 'Free and paid',
158
+ 'app.dashboard.mixLabel': 'Posts by price, free against paid',
159
+ 'app.dashboard.mixFree': 'Free',
160
+ 'app.dashboard.mixPaid': 'Paid',
161
+ 'app.dashboard.mixTotal': 'posts',
162
+ 'app.dashboard.mixKey': 'Price',
163
+ 'app.dashboard.gaugeLabel': 'Created this week, of all posts',
164
+ 'app.dashboard.columnPrice': 'Price',
152
165
  'app.dashboard.statRoutes': 'Routes',
153
166
  'app.dashboard.hintRoutes': 'registered at boot',
154
167
  'app.dashboard.statLocales': 'Locales',
@@ -203,7 +216,7 @@ export function i18nFiles(app: NameSet, version: string): readonly GeneratedFile
203
216
  '**/*.ts',
204
217
  'catalogs/**/*',
205
218
  ]),
206
- { path: 'packages/i18n/src/index.ts', contents: i18nIndex(['en']) },
219
+ { path: APP_CATALOGS_PATH, contents: i18nIndex(['en']) },
207
220
  { path: 'packages/i18n/src/index.test.ts', contents: i18nTest() },
208
221
  { path: 'packages/i18n/catalogs/en.json', contents: i18nCatalog(app), merge: 'json' },
209
222
  ];
@@ -4,7 +4,7 @@
4
4
  // never turn it into the fourteen `ICON_MATRIX` PNGs the generated web manifest declares.
5
5
 
6
6
  import type { Raster } from '@ultimat3/core';
7
- import { createRaster, encodeImage } from '@ultimat3/core';
7
+ import { blankRaster, encodeImage } from '@ultimat3/core';
8
8
 
9
9
  /** `@ultimat3/pwa`'s own contract (`requireSourceIcon`'s fix line): square, 1024 or larger. */
10
10
  const ICON_SIZE = 1024;
@@ -48,7 +48,7 @@ function paintMark(raster: Raster, inset: number): void {
48
48
  * so `x new` output never depends on run order or the clock.
49
49
  */
50
50
  export function icon(): Uint8Array {
51
- const raster = createRaster(ICON_SIZE, ICON_SIZE, 'scaffold-icon');
51
+ const raster = blankRaster(ICON_SIZE, ICON_SIZE, 'scaffold-icon');
52
52
  paintMark(raster, Math.round(ICON_SIZE * MASKABLE_PADDING));
53
53
  return encodeImage(raster, 'png');
54
54
  }
@@ -12,11 +12,13 @@ import { VERIFY_FLOOR_FILE } from '../verify-floor';
12
12
  import type { VerifyStepName } from '../verify-step';
13
13
  import type { GeneratedFile, NameSet } from './naming';
14
14
  import { titleCase } from './naming';
15
+ import { BIN_SCRIPTS, binScript } from './scaffold-bin';
15
16
  import { dbPackageFiles } from './scaffold-db-package';
16
17
  import { docsFiles } from './scaffold-docs';
17
18
  import { domainPackageFiles } from './scaffold-domain-package';
18
19
  import { envExampleSource, envSchemaSource } from './scaffold-env';
19
20
  import { PWA_COLORS } from './scaffold-errors';
21
+ import { rootGitattributesFile } from './scaffold-gitattributes';
20
22
  import { scaffoldGuardFiles } from './scaffold-guards';
21
23
  import { i18nFiles } from './scaffold-i18n';
22
24
  import { mcpPackageFiles } from './scaffold-mcp-package';
@@ -28,7 +30,7 @@ import { uiPackageFiles } from './scaffold-ui-package';
28
30
  * config as out of date on every `bun run lint`. A formatter is a build input — a range that floats
29
31
  * is a `lint` step whose verdict depends on the day the app was installed.
30
32
  */
31
- const BIOME_VERSION = '2.5.8';
33
+ const BIOME_VERSION = '2.5.15';
32
34
 
33
35
  /**
34
36
  * The checker a scaffolded app typechecks with, and it must not lag the one the FRAMEWORK is built
@@ -52,7 +54,7 @@ const SOLID_VERSION = '1.9.15';
52
54
  // `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
53
55
  // (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
54
56
  // BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
55
- // next line of `bin/setup` died `X_BUN_VERSION: Bun 1.3.14 is older than the required 1.4.0` — a
57
+ // next line of `bun run setup` died `X_BUN_VERSION: Bun 1.3.14 is older than the required 1.4.0` — a
56
58
  // floor admitting a runtime the app's own first command refuses. Interpolated, the two cannot
57
59
  // disagree; `scripts/bun-pin.test.ts` reads the emitted string as one more pin site, and
58
60
  // `github/ci.yml.ts` pins the workflow's Bun to the same constant.
@@ -70,9 +72,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
70
72
  "packages/*"
71
73
  ],
72
74
  "scripts": {
73
- "setup": "bin/setup",
75
+ "setup": "${binScript(BIN_SCRIPTS.setup)}",
74
76
  "dev": "x dev",
75
- "check": "bin/check",
77
+ "check": "${binScript(BIN_SCRIPTS.check)}",
76
78
  "verify": "x verify",
77
79
  "typecheck": "tsc -p . --pretty",
78
80
  "lint": "biome check .",
@@ -90,6 +92,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
90
92
  "dependencies": {
91
93
  "@ultimat3/action": "^${version}",
92
94
  "@ultimat3/admin": "^${version}",
95
+ "@ultimat3/auth": "^${version}",
93
96
  "@ultimat3/cache": "^${version}",
94
97
  "@ultimat3/cli": "^${version}",
95
98
  "@ultimat3/core": "^${version}",
@@ -184,9 +187,8 @@ export const env = defineEnv(envSchema);`;
184
187
  * author has no use for a comment about keys their config does not name.
185
188
  *
186
189
  * `tier` is the one that got through, and it says what the list was worth. It accepted
187
- * `'channels' | 'live-queries' | 'local-first'`, so `'local-first'` read as a durable client store
188
- * that does not exist (`createOpfsLocalStore` throws `X_NOT_IMPLEMENTED`) — silent, and shaped like
189
- * a capability. Which realtime tier an app is on is decided by what it DECLARES: a `channel()`
190
+ * `'channels' | 'live-queries' | 'local-first'`, and nothing read the value — silent, and shaped
191
+ * like a capability. Which realtime tier an app is on is decided by what it DECLARES: a `channel()`
190
192
  * topic, a `live: true` query, a local store. `scaffold-config.test.ts` no longer holds a list of
191
193
  * dead names to remember; it resolves every key path this literal writes against what `defineConfig`
192
194
  * really returns, so the fourteenth deletion fails there with no edit here.
@@ -203,10 +205,8 @@ ${envDeclaration()}
203
205
 
204
206
  export const config = defineConfig({
205
207
  name: '${app.kebab}',
206
- locales: ['en'],
207
- defaultLocale: 'en',
208
- defaultTimeZone: 'UTC',
209
- defaultCurrency: 'USD',
208
+ // Locales are declared by the catalogs (\`defineCatalogs\` in \`packages/i18n/src/index.ts\`); no
209
+ // zone or currency default exists — every format call names its zone, every \`Money\` its currency.
210
210
  // Env KEYS, never the value: the same image deploys to every environment. The database is
211
211
  // configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
212
212
  // The tiers ARE the cache selection: add 'redis' to build the shared rung, which reads
@@ -223,7 +223,7 @@ export const config = defineConfig({
223
223
  enabled: true,
224
224
  // The document an offline navigation gets when the cache has no answer, and the path
225
225
  // \`apps/web/site/offline/page.tsx\` serves. Required once \`enabled\` is true: an installable
226
- // app that shows the browser's error page offline is the failure the block exists to prevent.
226
+ // app that shows the browser error page offline is the failure the block exists to prevent.
227
227
  offline: { fallback: '/offline' },
228
228
  name: '${titleCase(app.raw)}',
229
229
  colors: {
@@ -235,7 +235,7 @@ export const config = defineConfig({
235
235
  // it, and a choice stored by the theme toggle wins over it on every later visit; 'system' follows
236
236
  // the OS.
237
237
  theme: { defaultMode: 'dark' },
238
- ai: { mcp: { expose: true, path: '/mcp' } },
238
+ ai: { mcp: { expose: true } },
239
239
  });
240
240
 
241
241
  /**
@@ -295,7 +295,8 @@ const biome = (): string => `{
295
295
  "rules": {
296
296
  "preset": "recommended",
297
297
  "suspicious": { "noExplicitAny": "error" },
298
- "correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" }
298
+ "correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" },
299
+ "style": { "useNodejsImportProtocol": "error" }
299
300
  }
300
301
  },
301
302
  "javascript": {
@@ -364,7 +365,7 @@ export const SCAFFOLD_COVERAGE_EXCLUDE: readonly CoverageExclude[] = [
364
365
  },
365
366
  {
366
367
  glob: 'apps/*/server.ts',
367
- why: 'the container entry point: it boots the roles and lives as long as the process — bin/dev and the image exercise it',
368
+ why: 'the container entry point: it boots the roles and lives as long as the process — bun run dev and the image exercise it',
368
369
  },
369
370
  {
370
371
  glob: 'apps/*/prerender.ts',
@@ -470,6 +471,8 @@ export function repoFiles(
470
471
  { path: VERIFY_FLOOR_FILE, contents: verifyFloor(example) },
471
472
  { path: 'types/scss.d.ts', contents: scssTypes() },
472
473
  { path: '.gitignore', contents: gitignore() },
474
+ // Every text file LF whatever `core.autocrlf` says — a Windows checkout is otherwise CRLF.
475
+ rootGitattributesFile(),
473
476
  { path: '.env.development', contents: envDevelopment() },
474
477
  // Committed, and generated: `x env example` rewrites this file from `envSchema`, and the
475
478
  // gate's `manifest` step fails with X_ENV_EXAMPLE_DRIFT when the two stop agreeing. A
@@ -90,7 +90,7 @@ export function Shell(props: ShellProps): JSX.Element {
90
90
  <span class={styles.env}>
91
91
  {/* The pulse is a CSS opacity animation: alive without a byte of script. */}
92
92
  <span class={styles.envDot} aria-hidden="true" />
93
- {environment}
93
+ <span class={styles.envLabel}>{environment}</span>
94
94
  </span>
95
95
  )}
96
96
  {props.actions}
@@ -125,9 +125,13 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
125
125
  justify-content: space-between;
126
126
  gap: tokens.space(4);
127
127
  inline-size: 100%;
128
+ // Free to shrink below its content: on a 390px phone the brand gives way, never the page.
129
+ min-inline-size: 0;
128
130
  }
129
131
 
132
+ // The controls keep their size; the brand is what truncates.
130
133
  .end {
134
+ flex: none;
131
135
  display: inline-flex;
132
136
  align-items: center;
133
137
  gap: tokens.space(3);
@@ -136,6 +140,7 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
136
140
  .brand {
137
141
  display: inline-flex;
138
142
  align-items: center;
143
+ min-inline-size: 0;
139
144
  gap: tokens.space(3);
140
145
  color: tokens.role('fg-strong');
141
146
 
@@ -146,6 +151,7 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
146
151
 
147
152
  // One accent tile. The only saturated surface in the chrome, so the eye has exactly one anchor.
148
153
  .mark {
154
+ flex: none;
149
155
  display: inline-grid;
150
156
  place-items: center;
151
157
  inline-size: tokens.space(8);
@@ -159,6 +165,7 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
159
165
 
160
166
  .wordmark {
161
167
  @include tokens.data-text;
168
+ @include tokens.truncate;
162
169
 
163
170
  font-size: tokens.text('sm');
164
171
  font-weight: tokens.weight('medium');
@@ -178,6 +185,13 @@ const shellStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
178
185
  font-size: tokens.text('xs');
179
186
  }
180
187
 
188
+ // Below sm the pill is its dot: the word stays readable to assistive tech, not to the layout.
189
+ .envLabel {
190
+ @include tokens.respond-down(sm) {
191
+ @include tokens.visually-hidden;
192
+ }
193
+ }
194
+
181
195
  .envDot {
182
196
  inline-size: tokens.space(2);
183
197
  block-size: tokens.space(2);
@@ -340,8 +354,9 @@ const toggleTest =
340
354
 
341
355
  import { join } from 'node:path';
342
356
  import { buildIslands } from '@ultimat3/cli';
357
+ import { THEME_STORAGE_KEY } from '@ultimat3/core';
343
358
  import { describeIslandState, expect, test } from '@ultimat3/testing';
344
- import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE, THEME_STORAGE_KEY } from '@ultimat3/ui';
359
+ import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE } from '@ultimat3/ui';
345
360
  import { themeToggleStates } from './theme-toggle.island.states';
346
361
 
347
362
  /** What the island reads through \`browserThemeEnv\`: storage and the OS query, both fakes. */
@@ -0,0 +1,193 @@
1
+ // `package-shape`'s third rule of "test code does not ship": a `src/` module only tests (and
2
+ // fixtures) import is test code under a name `files` cannot see. The tarball excludes `*.test.ts`
3
+ // and `*-fixture.ts`, so a fake, a probe or a `bun:test` helper named anything else rode into
4
+ // every install — 20 of them across eleven packages until 2026-10. The defect is the name.
5
+
6
+ import { ERROR_DOCS_URL, renderFixShellArg } from '@ultimat3/core';
7
+ import type { ClosureHost } from './import-closure';
8
+ import { resolveSpec } from './import-closure';
9
+ import type { Finding } from './output';
10
+ import { FIXTURE_SUFFIX } from './publish-closure';
11
+ import { eachSourceFile, isGenerated, isTest } from './source-files';
12
+
13
+ /** A published package: its directory under `packages/` and the files its manifest names. */
14
+ export interface ShippedPackage {
15
+ readonly dir: string;
16
+ /** Package-relative (`src/index.ts`), and may be a glob (`src/icons/glyphs/*.ts`). */
17
+ readonly entries: readonly string[];
18
+ }
19
+
20
+ export interface TestOnlyModule {
21
+ readonly path: string;
22
+ /** Every file that imports or names it — all of them tests, fixtures or test-only modules. */
23
+ readonly importers: readonly string[];
24
+ }
25
+
26
+ /**
27
+ * Every `src/` file a manifest names anywhere — `exports`, `bin`, the `browser` map, a `scripts`
28
+ * command (`bun run src/icons/build-icons.ts`). Read off the whole document rather than field by
29
+ * field: a module the manifest names is shipped on purpose, whichever field names it.
30
+ */
31
+ export function entryPatternsOf(manifest: unknown): readonly string[] {
32
+ const text = JSON.stringify(manifest ?? null);
33
+ const found = [...text.matchAll(/(?:\.\/)?(src\/[\w./*-]+?\.tsx?)(?![\w.])/g)];
34
+ return [...new Set(found.map((match) => match[1] ?? ''))].sort();
35
+ }
36
+
37
+ // Type-only imports count: the package ships `.ts` source, so a consumer's `tsc` opens the file a
38
+ // shipped `import type` names. One pattern for `from '.'`, `import('.')` and `import '.'`, relative
39
+ // specifiers only: over-matching an importer only ever clears a module, never flags one.
40
+ const RELATIVE = /(?:\bfrom|\bimport)\s*\(?\s*(['"])(\.\.?\/[^'"]*)\1/g;
41
+ /**
42
+ * A template-literal `import(\`./drivers/${n}\`)`: the static head up to the first `${` names a
43
+ * DIRECTORY prefix, and any module under it may be the one loaded. With no `${` the closing
44
+ * backtick ends it and it is an ordinary specifier.
45
+ */
46
+ const TEMPLATE = /\bimport\s*\(\s*`(\.\.?\/[^`$]*)(\$\{|`)/g;
47
+ /** A file named in a string — `join(import.meta.dir, 'probe.ts')`, `new URL('./worker.ts', …)`. */
48
+ const NAMED = /['"`/]([\w.-]+\.tsx?)['"`]/g;
49
+
50
+ /**
51
+ * Whether the match at `index` sits on a line that IS a comment — `//`, a doc comment's `*`, a
52
+ * `/*` opener. Line-wise and not core's `stripComments`, which is exact and costs ~1.1 s over the
53
+ * repo's 6,000 files (measured 2026-10-05); asked only of a match, never of every line. The lines
54
+ * this skips are where a commented-out or quoted-in-prose import lives, and one it keeps can only
55
+ * clear a module, never flag one.
56
+ */
57
+ function onCommentLine(text: string, index: number): boolean {
58
+ let at = text.lastIndexOf('\n', index - 1) + 1;
59
+ while (text[at] === ' ' || text[at] === '\t') at += 1;
60
+ const head = text.slice(at, at + 2);
61
+ return head === '//' || head === '/*' || head.startsWith('*');
62
+ }
63
+
64
+ /** `from`'s directory joined with a relative `head`, `.` and `..` folded; a trailing `/` kept. */
65
+ function prefixFrom(from: string, head: string): string {
66
+ const parts = from.split('/').slice(0, -1);
67
+ const segments = head.split('/');
68
+ const last = segments.pop() ?? '';
69
+ for (const segment of segments) {
70
+ if (segment === '..') parts.pop();
71
+ else if (segment !== '.' && segment !== '') parts.push(segment);
72
+ }
73
+ return `${parts.join('/')}/${last}`;
74
+ }
75
+
76
+ const isFixture = (path: string): boolean => path.endsWith(FIXTURE_SUFFIX);
77
+ const baseName = (path: string): string => path.slice(path.lastIndexOf('/') + 1);
78
+
79
+ const add = (map: Map<string, Set<string>>, key: string, value: string): void => {
80
+ const set = map.get(key);
81
+ if (set === undefined) map.set(key, new Set([value]));
82
+ else set.add(value);
83
+ };
84
+
85
+ /** Who imports each file, and which files name each basename in a string. One pass per file. */
86
+ function referrersOf(sources: ReadonlyMap<string, string>): {
87
+ readonly imports: ReadonlyMap<string, ReadonlySet<string>>;
88
+ readonly named: ReadonlyMap<string, ReadonlySet<string>>;
89
+ } {
90
+ const host: ClosureHost = { read: (path) => sources.get(path), alias: () => undefined };
91
+ const imports = new Map<string, Set<string>>();
92
+ const named = new Map<string, Set<string>>();
93
+ const prefixes: { readonly prefix: string; readonly from: string }[] = [];
94
+ for (const [from, text] of sources) {
95
+ for (const match of text.matchAll(RELATIVE)) {
96
+ if (onCommentLine(text, match.index)) continue;
97
+ const target = resolveSpec(host, from, match[2] ?? '');
98
+ if (target !== undefined && target !== from) add(imports, target, from);
99
+ }
100
+ for (const match of text.matchAll(TEMPLATE)) {
101
+ if (onCommentLine(text, match.index)) continue;
102
+ const head = match[1] ?? '';
103
+ if (match[2] === '`') {
104
+ const target = resolveSpec(host, from, head);
105
+ if (target !== undefined && target !== from) add(imports, target, from);
106
+ } else prefixes.push({ prefix: prefixFrom(from, head), from });
107
+ }
108
+ for (const match of text.matchAll(NAMED)) {
109
+ if (!onCommentLine(text, match.index)) add(named, match[1] ?? '', from);
110
+ }
111
+ }
112
+ // Few computed imports in any repo, so a scan of every path per prefix costs nothing measurable.
113
+ for (const { prefix, from } of prefixes) {
114
+ for (const path of sources.keys()) {
115
+ if (path !== from && path.startsWith(prefix)) add(imports, path, from);
116
+ }
117
+ }
118
+ return { imports, named };
119
+ }
120
+
121
+ /**
122
+ * The shipped `.ts` modules of the given packages whose every importer is a test, a fixture or
123
+ * another such module — a greatest fixed point, so a helper only a fake imports is caught with the
124
+ * fake. A module nobody references is not this rule's question, and an entry point is shipped by
125
+ * definition. `.tsx` is not judged: `FIXTURE_EXCLUSION` matches `-fixture.ts` alone.
126
+ */
127
+ export function testOnlyModules(
128
+ sources: ReadonlyMap<string, string>,
129
+ packages: readonly ShippedPackage[],
130
+ ): readonly TestOnlyModule[] {
131
+ const { imports, named } = referrersOf(sources);
132
+ const entryGlobs = new Map(
133
+ packages.map((pkg) => [
134
+ pkg.dir,
135
+ pkg.entries.map((entry) => new Bun.Glob(`packages/${pkg.dir}/${entry}`)),
136
+ ]),
137
+ );
138
+ const referrers = new Map<string, ReadonlySet<string>>();
139
+ for (const path of sources.keys()) {
140
+ const dir = /^packages\/([^/]+)\/src\//.exec(path)?.[1];
141
+ const globs = dir === undefined ? undefined : entryGlobs.get(dir);
142
+ if (globs === undefined || !path.endsWith('.ts') || isGenerated(path)) continue;
143
+ if (isTest(path) || isFixture(path) || globs.some((glob) => glob.match(path))) continue;
144
+ const from = new Set([...(imports.get(path) ?? []), ...(named.get(baseName(path)) ?? [])]);
145
+ from.delete(path);
146
+ if (from.size > 0) referrers.set(path, from);
147
+ }
148
+ const testOnly = new Set(referrers.keys());
149
+ const live = (path: string): boolean => !isTest(path) && !isFixture(path) && !testOnly.has(path);
150
+ for (let changed = true; changed; ) {
151
+ changed = false;
152
+ for (const path of testOnly) {
153
+ if (![...(referrers.get(path) ?? [])].some(live)) continue;
154
+ testOnly.delete(path);
155
+ changed = true;
156
+ }
157
+ }
158
+ return [...testOnly].sort().map((path) => ({
159
+ path,
160
+ importers: [...(referrers.get(path) ?? [])].sort(),
161
+ }));
162
+ }
163
+
164
+ /** `X_PACKAGE_TEST_ONLY_SHIPPED`, its fix the registered rename with the real paths in it. */
165
+ export const testOnlyShippedFinding = (module: TestOnlyModule): Finding => {
166
+ const shown = module.importers.slice(0, 3).join(', ');
167
+ const more = module.importers.length > 3 ? ` and ${module.importers.length - 3} more` : '';
168
+ return {
169
+ code: 'X_PACKAGE_TEST_ONLY_SHIPPED',
170
+ cause: `${module.path} is imported only by tests and fixtures (${shown}${more}), and "files" publishes it — test scaffolding in every install`,
171
+ fix: `git mv ${renderFixShellArg(module.path, '<the module above>')} ${renderFixShellArg(module.path.replace(/\.ts$/, FIXTURE_SUFFIX), '<its name>-fixture.ts')} # and update its importers`,
172
+ docs: ERROR_DOCS_URL,
173
+ at: module.path,
174
+ meta: { importers: [...module.importers] },
175
+ };
176
+ };
177
+
178
+ /**
179
+ * The rule over a repo on disk. Every source file is a possible importer — a script reading a
180
+ * package's module by relative path is a shipped use of it — so the walk is the gate's own
181
+ * (`eachSourceFile`), tests included. A repo with no published package reads nothing.
182
+ */
183
+ export async function checkTestOnlyShipped(
184
+ root: string,
185
+ packages: readonly ShippedPackage[],
186
+ ): Promise<readonly Finding[]> {
187
+ if (packages.length === 0) return [];
188
+ const paths: string[] = [];
189
+ for await (const path of eachSourceFile(root)) if (!isGenerated(path)) paths.push(path);
190
+ const texts = await Promise.all(paths.map((path) => Bun.file(`${root}/${path}`).text()));
191
+ const sources = new Map(paths.map((path, index) => [path, texts[index] ?? '']));
192
+ return testOnlyModules(sources, packages).map(testOnlyShippedFinding);
193
+ }
@@ -6,12 +6,13 @@ import { join } from 'node:path';
6
6
  // Bun ships no equivalent: `join` builds the host-separator path from the scan root to a hit.
7
7
  // Sizing is Bun's own (`Bun.file().size`), so nothing here reaches for `node:fs`.
8
8
  import { nearestName, renderFixShellArg } from '@ultimat3/core';
9
+ import { TEST_TYPES } from '@ultimat3/testing';
9
10
  import { BadFlagError } from './errors';
10
11
  import type { ParsedArgs } from './parse';
11
12
  import { flagString } from './parse';
12
13
  import { isIgnoredTestPath } from './test-ignores';
13
14
  import type { TestType } from './verify-tests';
14
- import { ownerOf, TEST_TYPES } from './verify-tests';
15
+ import { ownerOf } from './verify-tests';
15
16
 
16
17
  export interface TestFile {
17
18
  readonly path: string;
@@ -52,9 +52,6 @@ import type { CommandResult, Finding, JsonValue, StepResult } from './output';
52
52
  import { quoteArg } from './shell-quote';
53
53
  import { testEnvOverrides } from './test-dotenv';
54
54
 
55
- /** `@ultimat3/testing`'s `ISOLATED_ENV` — the one spelling, which that package's preload reads. */
56
- export const ISOLATED_TEST_ENV = ISOLATED_ENV;
57
-
58
55
  /** Bun's per-file durations, cached under the run's root and refreshed by every parallel run. */
59
56
  export const TEST_TIMINGS_FILE = '.x/test-timings.json';
60
57
 
@@ -258,7 +255,7 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
258
255
  ...testEnvOverrides(options.root, options.env ?? Bun.env),
259
256
  // Only an isolated run: the preload clears `Bun.plugin` handlers after each file, which in a
260
257
  // shared global would leave the next `.tsx` compiling with Bun's classic factory.
261
- ...(isolatedRun(options) ? { [ISOLATED_TEST_ENV]: '1' } : {}),
258
+ ...(isolatedRun(options) ? { [ISOLATED_ENV]: '1' } : {}),
262
259
  };
263
260
  const passthrough = options.passthrough ?? [];
264
261
  const watching = passthrough.includes('--watch');
package/src/theme-boot.ts CHANGED
@@ -22,8 +22,8 @@ export interface ThemeBoot {
22
22
  /**
23
23
  * Tag and hash from ONE body: `themeScriptBody` is what `themeScript` inlines, so a policy hashed
24
24
  * here admits the script the document carries and not a restatement of it. The storage key is
25
- * render's `THEME_STORAGE_KEY`, which `theme-boot.test.ts` pins equal to `@ultimat3/ui`'s — the
26
- * toggle writes the key the boot reads.
25
+ * `@ultimat3/core`'s `THEME_STORAGE_KEY`, the one `@ultimat3/ui`'s toggle writes — so the toggle
26
+ * writes the key the boot reads.
27
27
  */
28
28
  export function themeBoot(mode: ThemeMode): ThemeBoot {
29
29
  const options = { fallback: mode };
@@ -0,0 +1,66 @@
1
+ // The app's brand, declared once as `export const brand = defineTheme(…)` in
2
+ // `apps/web/shared/theme.ts`, read here and nowhere else: the `<style>` every document the boot
3
+ // renders carries after its stylesheet, and the body `style-src` admits. No module, no brand: an
4
+ // unthemed app emits no tag and extends no policy.
5
+
6
+ // why: Bun exposes no path-join primitive, and the module path is app-root-relative.
7
+ import { join } from 'node:path';
8
+ import { ConfigInvalidError, describeValue } from '@ultimat3/core';
9
+ import { renderHead } from '@ultimat3/render';
10
+
11
+ /** The conventional module, beside `shared/global.ts` — one fixed path, as `API_INDEX` is. */
12
+ export const APP_THEME_MODULE = 'apps/web/shared/theme.ts';
13
+ /** The named export the module declares — `@ultimat3/ui`'s README spells it `brand`. */
14
+ export const APP_THEME_EXPORT = 'brand';
15
+
16
+ export interface ThemeBrand {
17
+ /** The rendered `<style>`, for `DocumentOptions.brandHead`. */
18
+ readonly head: string;
19
+ /** Exactly the text between the tags — the body a boot hands `inlineStyleSources`. */
20
+ readonly style: string;
21
+ }
22
+
23
+ /** Code to paste, as `loadAppConfig`'s is: the module path is already in the cause. */
24
+ const FIX = `export const ${APP_THEME_EXPORT} = defineTheme({ preset: 'scifi' });`;
25
+
26
+ /**
27
+ * `undefined` when the app has no theme module or its brand renders no CSS. A module that will not
28
+ * import throws as it did, so a palette `defineTheme()` refuses fails the boot with
29
+ * `X_UI_CONTRAST_INSUFFICIENT` (or its token code) and not a rewording of it.
30
+ */
31
+ export async function loadThemeBrand(root: string): Promise<ThemeBrand | undefined> {
32
+ const path = join(root, APP_THEME_MODULE);
33
+ if (!(await Bun.file(path).exists())) return undefined;
34
+ const module = (await import(path)) as Record<string, unknown>;
35
+ const css = brandCss(path, module);
36
+ if (css.length === 0) return undefined;
37
+ // `renderHead` escapes `</` in a style body, which would make the tag's text differ from the
38
+ // hashed one — so a body that needs escaping is refused above rather than escaped here.
39
+ return { head: renderHead([{ kind: 'style', key: 'style:brand', content: css }]), style: css };
40
+ }
41
+
42
+ /**
43
+ * The brand's stylesheet, read structurally: `@ultimat3/ui` is a tier below and this package does
44
+ * not import it, so a `Brand` is `{ css: string }`. `defineTheme()` never emits `<` — its values
45
+ * are validated against grammars that exclude it — so one here came from a hand-built object, and
46
+ * it is the one character that could end the element and start markup.
47
+ */
48
+ function brandCss(path: string, module: Record<string, unknown>): string {
49
+ const brand = Object.hasOwn(module, APP_THEME_EXPORT) ? module[APP_THEME_EXPORT] : undefined;
50
+ const css = typeof brand === 'object' && brand !== null && 'css' in brand ? brand.css : undefined;
51
+ if (typeof css !== 'string') {
52
+ throw new ConfigInvalidError({
53
+ cause: `${path} exports no ${APP_THEME_EXPORT} built by defineTheme() (${describeValue(brand)}), so no document can carry the app's theme — declare it with defineTheme from @ultimat3/ui`,
54
+ fix: FIX,
55
+ meta: { key: APP_THEME_EXPORT },
56
+ });
57
+ }
58
+ if (css.includes('<')) {
59
+ throw new ConfigInvalidError({
60
+ cause: `${path}'s ${APP_THEME_EXPORT}.css contains "<", which defineTheme() never emits and which could close the <style> element it is inlined in`,
61
+ fix: FIX,
62
+ meta: { key: APP_THEME_EXPORT },
63
+ });
64
+ }
65
+ return css;
66
+ }