@ultimat3/cli 23.0.0 → 25.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 (296) hide show
  1. package/CLAUDE.md +28 -13
  2. package/README.md +32 -7
  3. package/package.json +29 -29
  4. package/src/admin-catalog-keys.ts +2 -2
  5. package/src/affected.ts +30 -36
  6. package/src/api-registration.ts +89 -0
  7. package/src/app-artifacts.ts +1 -1
  8. package/src/app-auth.ts +4 -27
  9. package/src/app-boundaries.ts +17 -4
  10. package/src/app-config-load.ts +51 -0
  11. package/src/app-env.ts +26 -25
  12. package/src/app-load.ts +38 -14
  13. package/src/app-manifest.ts +4 -10
  14. package/src/app-mcp.ts +31 -46
  15. package/src/app-name.ts +52 -0
  16. package/src/app-permissions-borrowed.ts +123 -0
  17. package/src/app-permissions-site.ts +48 -0
  18. package/src/app-permissions.ts +7 -17
  19. package/src/app-root.ts +9 -5
  20. package/src/bin.ts +3 -7
  21. package/src/browser-launcher.ts +4 -2
  22. package/src/budgets-charged.ts +60 -0
  23. package/src/budgets-scan.ts +76 -0
  24. package/src/budgets-unmeasured.ts +91 -0
  25. package/src/budgets.ts +117 -154
  26. package/src/cdp-shot-driver.ts +20 -1
  27. package/src/cdp-shot-watch.ts +35 -31
  28. package/src/ci-log.ts +0 -0
  29. package/src/cmd-auth-spec.ts +16 -0
  30. package/src/cmd-auth.ts +65 -0
  31. package/src/cmd-build-spec.ts +7 -1
  32. package/src/cmd-build.ts +151 -22
  33. package/src/cmd-db-backfill.ts +26 -13
  34. package/src/cmd-db-branch.ts +2 -2
  35. package/src/cmd-db.ts +10 -6
  36. package/src/cmd-deploy-compose.ts +94 -0
  37. package/src/cmd-deploy-drain.ts +48 -0
  38. package/src/cmd-deploy-helm.ts +7 -6
  39. package/src/cmd-deploy-start-first.ts +178 -0
  40. package/src/cmd-deploy.ts +158 -36
  41. package/src/cmd-dev.ts +18 -356
  42. package/src/cmd-doctor-spec.ts +2 -9
  43. package/src/cmd-doctor.ts +57 -80
  44. package/src/cmd-env.ts +55 -16
  45. package/src/cmd-generate.ts +89 -32
  46. package/src/cmd-help.ts +19 -6
  47. package/src/cmd-i18n.ts +21 -17
  48. package/src/cmd-jobs-spec.ts +7 -22
  49. package/src/cmd-jobs.ts +33 -140
  50. package/src/cmd-manifest-spec.ts +1 -1
  51. package/src/cmd-manifest.ts +6 -25
  52. package/src/cmd-mcp.ts +71 -22
  53. package/src/cmd-new.ts +36 -8
  54. package/src/cmd-planned.ts +24 -8
  55. package/src/cmd-routes.ts +15 -5
  56. package/src/cmd-secrets.ts +47 -6
  57. package/src/cmd-shot-matrix.ts +22 -7
  58. package/src/cmd-shot-spec.ts +10 -2
  59. package/src/cmd-shot.ts +46 -41
  60. package/src/cmd-tasks.ts +2 -2
  61. package/src/cmd-test-spec.ts +1 -7
  62. package/src/cmd-test.ts +14 -30
  63. package/src/cmd-verify.ts +4 -4
  64. package/src/coverage-lcov.ts +4 -2
  65. package/src/db-backfill.ts +2 -2
  66. package/src/db-replay-engine.ts +4 -10
  67. package/src/db-subscribes.ts +27 -16
  68. package/src/dev-boot.ts +290 -0
  69. package/src/dev-child-watch.ts +63 -12
  70. package/src/dev-dashboard-guard.ts +76 -0
  71. package/src/dev-dashboard.ts +56 -30
  72. package/src/dev-lock.ts +100 -31
  73. package/src/dev-rebuild.ts +80 -0
  74. package/src/dev-restart-retire.ts +46 -0
  75. package/src/dev-route-table.ts +30 -8
  76. package/src/dev-supervisor.ts +114 -22
  77. package/src/dev-traces.ts +53 -20
  78. package/src/dispatch.ts +17 -13
  79. package/src/doctor-app-url.ts +39 -0
  80. package/src/doctor-auth.ts +95 -0
  81. package/src/doctor-offline.ts +6 -5
  82. package/src/document-options.ts +7 -0
  83. package/src/drift-replica-identity.ts +42 -0
  84. package/src/drift.ts +29 -6
  85. package/src/error-catalog.ts +6 -9
  86. package/src/error-codes.ts +29 -3
  87. package/src/error-page-csp.ts +14 -15
  88. package/src/error-unthrown.ts +29 -11
  89. package/src/errors.ts +54 -83
  90. package/src/exec.ts +8 -0
  91. package/src/favicon.ts +2 -2
  92. package/src/fix-command.ts +9 -1
  93. package/src/framework-env.ts +148 -0
  94. package/src/framework-schema-apply.ts +78 -0
  95. package/src/framework-schema-stamp.ts +96 -0
  96. package/src/framework-schema.ts +25 -3
  97. package/src/generate-catalog-locales.ts +79 -0
  98. package/src/generate-files.ts +4 -3
  99. package/src/generate-kinds.ts +18 -0
  100. package/src/generate-plural.ts +50 -0
  101. package/src/generate-shadow.ts +95 -0
  102. package/src/generate-write.ts +47 -13
  103. package/src/gh-target.ts +12 -7
  104. package/src/hold.ts +56 -1
  105. package/src/i18n-audit.ts +25 -9
  106. package/src/i18n-index.ts +5 -6
  107. package/src/i18n-registration.ts +7 -9
  108. package/src/import-scan.ts +28 -3
  109. package/src/index.ts +2 -6
  110. package/src/island-bundle.ts +57 -79
  111. package/src/island-capture.ts +1 -1
  112. package/src/island-duplicate-refusal.ts +57 -0
  113. package/src/island-duplicates.ts +221 -0
  114. package/src/island-entry-missing.ts +64 -0
  115. package/src/island-harness-route.ts +20 -4
  116. package/src/island-harness-script.ts +23 -2
  117. package/src/island-identity.ts +17 -14
  118. package/src/island-package-dedupe.ts +150 -0
  119. package/src/island-realtime.ts +36 -16
  120. package/src/island-routes.ts +2 -2
  121. package/src/island-runtime.ts +241 -0
  122. package/src/island-sources.ts +105 -0
  123. package/src/island-states-load.ts +6 -0
  124. package/src/island-store.ts +90 -6
  125. package/src/jobs-json.ts +0 -26
  126. package/src/jobs-report.ts +36 -20
  127. package/src/local-cli-handoff.ts +90 -0
  128. package/src/loopback-host.ts +13 -0
  129. package/src/manifest-staleness.ts +62 -0
  130. package/src/mcp-errors.ts +20 -4
  131. package/src/mcp-host.ts +22 -14
  132. package/src/mcp-test-run.ts +38 -0
  133. package/src/mcp-ui-inspect.ts +3 -2
  134. package/src/mcp-ui-interact.ts +3 -2
  135. package/src/mcp-ui.ts +8 -7
  136. package/src/measure-scope.ts +15 -10
  137. package/src/messages.ts +29 -16
  138. package/src/metrics-endpoint.ts +28 -3
  139. package/src/migration-extensions.ts +1 -1
  140. package/src/otlp-export.ts +2 -2
  141. package/src/output.ts +24 -0
  142. package/src/page-navigation.ts +7 -23
  143. package/src/page-speculation.ts +19 -31
  144. package/src/parse.ts +0 -8
  145. package/src/permission-grants.ts +3 -3
  146. package/src/posix-path.ts +30 -0
  147. package/src/prerender-locales.ts +6 -4
  148. package/src/prerender-out.ts +34 -7
  149. package/src/prerender-report.ts +58 -0
  150. package/src/prerender-served.ts +52 -0
  151. package/src/prerender.ts +71 -103
  152. package/src/pwa-artifacts.ts +33 -94
  153. package/src/pwa-manifest-locales.ts +9 -9
  154. package/src/registry.ts +2 -0
  155. package/src/retry-memo.ts +1 -1
  156. package/src/revalidated-response.ts +4 -3
  157. package/src/role-realtime.ts +7 -1
  158. package/src/role-replicator.ts +124 -29
  159. package/src/role-start.ts +69 -85
  160. package/src/role-sync.ts +119 -25
  161. package/src/root-env.ts +157 -28
  162. package/src/route-islands.ts +69 -0
  163. package/src/runtime-admin.ts +4 -1
  164. package/src/runtime-assets.ts +26 -9
  165. package/src/runtime-bindings.ts +27 -7
  166. package/src/runtime-cache.ts +46 -55
  167. package/src/runtime-hooks.ts +2 -1
  168. package/src/runtime-idempotency-scope.ts +23 -0
  169. package/src/runtime-isr-outcome.ts +100 -0
  170. package/src/runtime-isr.ts +36 -0
  171. package/src/runtime-jobs.ts +76 -41
  172. package/src/runtime-live-feed.ts +1 -1
  173. package/src/runtime-mail.ts +26 -0
  174. package/src/runtime-mfa-warning.ts +29 -0
  175. package/src/runtime-notify-retention.ts +10 -46
  176. package/src/runtime-overrides.ts +4 -4
  177. package/src/runtime-purge.ts +38 -16
  178. package/src/runtime-queue.ts +43 -22
  179. package/src/runtime-realtime.ts +11 -45
  180. package/src/runtime-render.ts +136 -114
  181. package/src/runtime-replica.ts +3 -8
  182. package/src/runtime-services.ts +49 -26
  183. package/src/runtime-storage-upload.ts +2 -2
  184. package/src/runtime-storage.ts +13 -8
  185. package/src/schema-diff.ts +16 -0
  186. package/src/schema-drift.ts +45 -15
  187. package/src/secrets-rotation.ts +8 -9
  188. package/src/seo-routes.ts +80 -9
  189. package/src/serve-boot.ts +51 -12
  190. package/src/serve-drain.ts +11 -33
  191. package/src/serve-env.ts +9 -6
  192. package/src/serve-retire.ts +95 -0
  193. package/src/serve-web.ts +31 -15
  194. package/src/serve.ts +27 -10
  195. package/src/server-barrels.ts +6 -3
  196. package/src/shot-browser.ts +1 -1
  197. package/src/shot-locale.ts +6 -33
  198. package/src/shot-settle.ts +3 -3
  199. package/src/shot-theme.ts +1 -1
  200. package/src/shot-viewport.ts +152 -0
  201. package/src/signal-shred.ts +26 -12
  202. package/src/site-asset-routes.ts +2 -2
  203. package/src/site-assets.ts +3 -5
  204. package/src/site-config.ts +9 -48
  205. package/src/site-seo.ts +7 -5
  206. package/src/sitemap-lastmod.ts +5 -0
  207. package/src/solid-loader.ts +12 -3
  208. package/src/source-files.ts +19 -3
  209. package/src/static-report.ts +5 -0
  210. package/src/stored-object-headers.ts +43 -0
  211. package/src/style-routes.ts +2 -2
  212. package/src/sw-artifacts.ts +8 -8
  213. package/src/sw-precache-plan.ts +34 -4
  214. package/src/sync-authenticator.ts +3 -3
  215. package/src/sync-url.ts +3 -3
  216. package/src/tasks-facts.ts +3 -29
  217. package/src/templates/action.ts +7 -4
  218. package/src/templates/backfill.ts +4 -4
  219. package/src/templates/entity.ts +2 -2
  220. package/src/templates/github/ci.yml.ts +28 -18
  221. package/src/templates/github/image.yml.ts +92 -0
  222. package/src/templates/github/runner.ts +18 -0
  223. package/src/templates/guard-bare-error.ts +16 -2
  224. package/src/templates/guard-unzoned-date.ts +56 -6
  225. package/src/templates/index.ts +1 -1
  226. package/src/templates/job-body-test.ts +2 -2
  227. package/src/templates/job.ts +6 -6
  228. package/src/templates/naming.ts +28 -0
  229. package/src/templates/query.ts +9 -8
  230. package/src/templates/resource-page.ts +9 -7
  231. package/src/templates/resource-service.ts +2 -2
  232. package/src/templates/route.ts +26 -14
  233. package/src/templates/scaffold-api.ts +2 -3
  234. package/src/templates/scaffold-app.ts +8 -3
  235. package/src/templates/scaffold-auth.ts +205 -70
  236. package/src/templates/scaffold-bin.ts +122 -0
  237. package/src/templates/scaffold-claude-commands.ts +11 -11
  238. package/src/templates/scaffold-container-compose.ts +10 -1
  239. package/src/templates/scaffold-container.ts +16 -8
  240. package/src/templates/scaffold-dashboard-bare.ts +8 -4
  241. package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
  242. package/src/templates/scaffold-dashboard-example.ts +112 -172
  243. package/src/templates/scaffold-dashboard-shared.ts +62 -1
  244. package/src/templates/scaffold-dashboard.ts +9 -5
  245. package/src/templates/scaffold-db-client.ts +4 -2
  246. package/src/templates/scaffold-db-package.ts +2 -2
  247. package/src/templates/scaffold-docs.ts +20 -77
  248. package/src/templates/scaffold-domain-package.ts +0 -4
  249. package/src/templates/scaffold-entries.ts +4 -3
  250. package/src/templates/scaffold-env.ts +6 -3
  251. package/src/templates/scaffold-gitattributes.ts +59 -0
  252. package/src/templates/scaffold-helm-fences.ts +76 -0
  253. package/src/templates/scaffold-helm-templates.ts +100 -19
  254. package/src/templates/scaffold-helm.ts +62 -5
  255. package/src/templates/scaffold-http.ts +1 -1
  256. package/src/templates/scaffold-i18n.ts +15 -2
  257. package/src/templates/scaffold-icon.ts +2 -2
  258. package/src/templates/scaffold-repo.ts +27 -16
  259. package/src/templates/scaffold-shell.ts +55 -37
  260. package/src/test-counts.ts +2 -6
  261. package/src/test-ignores.ts +25 -0
  262. package/src/test-only-shipped.ts +193 -0
  263. package/src/test-passes.ts +0 -15
  264. package/src/test-select.ts +7 -15
  265. package/src/test-shards.ts +40 -95
  266. package/src/test-workers.ts +26 -4
  267. package/src/theme-boot.ts +8 -33
  268. package/src/theme-brand.ts +66 -0
  269. package/src/trusted-hops-error.ts +17 -0
  270. package/src/trusted-hops.ts +49 -0
  271. package/src/unscanned-admin.ts +2 -2
  272. package/src/unthrown-codes.ts +26 -0
  273. package/src/verify-checks.ts +29 -48
  274. package/src/verify-coverage-run.ts +3 -2
  275. package/src/verify-deadline.ts +207 -33
  276. package/src/verify-merge.ts +108 -43
  277. package/src/verify-part-step.ts +98 -0
  278. package/src/verify-role-load-probe.ts +12 -2
  279. package/src/verify-role-load.ts +9 -4
  280. package/src/verify-run.ts +32 -8
  281. package/src/verify-shard.ts +13 -2
  282. package/src/verify-stalled.ts +152 -0
  283. package/src/verify-step.ts +2 -0
  284. package/src/verify-test-run.ts +3 -2
  285. package/src/verify-tests.ts +5 -3
  286. package/src/worker-bundle.ts +2 -2
  287. package/src/workspace-checks.ts +53 -7
  288. package/src/workspace-graph.ts +3 -2
  289. package/src/write-line.ts +82 -7
  290. package/src/browser-launcher-fake-html.ts +0 -80
  291. package/src/browser-launcher-fake.ts +0 -168
  292. package/src/build-errors.ts +0 -54
  293. package/src/jobs-drain.ts +0 -163
  294. package/src/scaffold-typecheck.ts +0 -227
  295. package/src/thrown-by.ts +0 -24
  296. package/src/verify-errors.ts +0 -58
@@ -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.
@@ -31,19 +40,52 @@ image:
31
40
 
32
41
  # Non-secret configuration. Secrets come from an existing Secret, never from this file:
33
42
  # kubectl create secret generic ${app.kebab}-secrets --from-literal=DATABASE_URL=...
43
+ #
44
+ # APP_URL — the pages' public origin — is what the sync node admits a websocket from. With the
45
+ # ingress below enabled the chart SETS it on the sync role from ingress.host (https when
46
+ # ingress.tls), because an undeclared node behind a TLS ingress also admits the plain-http page.
47
+ # Set it here when the pages are served from another host or port than that (a CDN domain, a
48
+ # SYNC_URL of its own): this value wins, and without it every websocket from those pages is
49
+ # 403 X_SOCKET_ORIGIN_REFUSED.
34
50
  env:
35
51
  NODE_ENV: production
36
52
 
37
- 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
54
+
55
+ # The release-wide Secret above is every role's DEFAULT: \`existingSecret\` under a role
56
+ # (roles.web.existingSecret, migrate.existingSecret) names that role's own, so the migrate Job's
57
+ # schema-owning DATABASE_URL need not sit on a web pod.
58
+
59
+ # The /tmp every pod mounts (the root filesystem is read-only), bounded: past this the kubelet
60
+ # evicts the pod that filled it, never its neighbours for node disk pressure.
61
+ tmp:
62
+ sizeLimit: 512Mi
63
+
64
+ # One NetworkPolicy per serving role (templates/networkpolicy.yaml): \`http\` from httpFrom,
65
+ # \`metrics\` from metricsFrom, nothing else. Inert on a CNI that does not enforce NetworkPolicy.
66
+ # httpFrom empty = any source; narrow it to your ingress controller's namespace, e.g.
67
+ # [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
68
+ # metricsFrom default: any pod in the cluster and no address outside it
69
+ # egress OFF while empty; a list turns on egress filtering with exactly those rules —
70
+ # the database, NATS, the object store and DNS are your addresses, so list all of them
71
+ networkPolicy:
72
+ enabled: true
73
+ httpFrom: []
74
+ metricsFrom:
75
+ - namespaceSelector: {}
76
+ egress: []
38
77
 
39
78
  # The scrape listener every serving role opens, on its own port and never the app's: the ingress
40
79
  # routes / to web, so /metrics beside /healthz on 3000 is /metrics on the internet. This is
41
80
  # METRICS_PORT in the container; move one and the other follows.
42
81
  metricsPort: 9090
43
82
 
83
+ # The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
44
84
  podSecurityContext:
45
85
  runAsNonRoot: true
46
- runAsUser: 65532
86
+ runAsUser: ${RUNTIME_UID}
87
+ runAsGroup: ${RUNTIME_UID}
88
+ fsGroup: ${RUNTIME_UID}
47
89
  seccompProfile: { type: RuntimeDefault }
48
90
 
49
91
  securityContext:
@@ -52,10 +94,20 @@ securityContext:
52
94
  capabilities: { drop: [ALL] }
53
95
 
54
96
  # The stop sequence: out of the Service, then this preStop sleep (Kubernetes 1.30+ only), then
55
- # SIGTERM, then the framework's readiness grace and drain. terminationGracePeriodSeconds in
56
- # 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
57
103
  drain:
58
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
59
111
 
60
112
  # The release phase. Runs to completion before any serving role starts.
61
113
  migrate:
@@ -112,6 +164,11 @@ roles:
112
164
  worker:
113
165
  enabled: true
114
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
115
172
  resources:
116
173
  requests: { cpu: 200m, memory: 256Mi }
117
174
  limits: { memory: 1Gi }
@@ -153,7 +210,7 @@ ingress:
153
210
  `;
154
211
 
155
212
  /**
156
- * The chart for a new app: five kinds of object, all of them core Kubernetes. A ServiceMonitor and
213
+ * The chart for a new app: six kinds of object, all of them core Kubernetes. A ServiceMonitor and
157
214
  * a PodDisruptionBudget are deliberately absent — the first needs a CRD `helm install` fails on
158
215
  * where no Prometheus operator is installed, and both are cluster policy rather than app topology.
159
216
  */
@@ -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
@@ -41,10 +43,18 @@ const BIOME_VERSION = '2.5.8';
41
43
  */
42
44
  const TYPESCRIPT_VERSION = '^7.0.2';
43
45
 
46
+ /**
47
+ * The Solid runtime every island and `@ultimat3/ui` component renders with, exact for the reason
48
+ * `BIOME_VERSION` is: the CLI compiles islands with `babel-preset-solid ^1.9.15`, and a scaffold
49
+ * that installed an older runtime than the compiler targets drifted as a buried literal (1.9.14
50
+ * beside the repository's 1.9.15). `scaffold-repo.test.ts` holds it equal to the root manifest's.
51
+ */
52
+ const SOLID_VERSION = '1.9.15';
53
+
44
54
  // `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
45
55
  // (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
46
56
  // BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
47
- // 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
48
58
  // floor admitting a runtime the app's own first command refuses. Interpolated, the two cannot
49
59
  // disagree; `scripts/bun-pin.test.ts` reads the emitted string as one more pin site, and
50
60
  // `github/ci.yml.ts` pins the workflow's Bun to the same constant.
@@ -62,9 +72,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
62
72
  "packages/*"
63
73
  ],
64
74
  "scripts": {
65
- "setup": "bin/setup",
75
+ "setup": "${binScript(BIN_SCRIPTS.setup)}",
66
76
  "dev": "x dev",
67
- "check": "bin/check",
77
+ "check": "${binScript(BIN_SCRIPTS.check)}",
68
78
  "verify": "x verify",
69
79
  "typecheck": "tsc -p . --pretty",
70
80
  "lint": "biome check .",
@@ -82,6 +92,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
82
92
  "dependencies": {
83
93
  "@ultimat3/action": "^${version}",
84
94
  "@ultimat3/admin": "^${version}",
95
+ "@ultimat3/auth": "^${version}",
85
96
  "@ultimat3/cache": "^${version}",
86
97
  "@ultimat3/cli": "^${version}",
87
98
  "@ultimat3/core": "^${version}",
@@ -99,7 +110,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
99
110
  "@ultimat3/schema": "^${version}",
100
111
  "@ultimat3/seo": "^${version}",
101
112
  "@ultimat3/ui": "^${version}",
102
- "solid-js": "1.9.14"
113
+ "solid-js": "${SOLID_VERSION}"
103
114
  },
104
115
  "engines": {
105
116
  "bun": ">=${REQUIRED_BUN}"
@@ -176,9 +187,8 @@ export const env = defineEnv(envSchema);`;
176
187
  * author has no use for a comment about keys their config does not name.
177
188
  *
178
189
  * `tier` is the one that got through, and it says what the list was worth. It accepted
179
- * `'channels' | 'live-queries' | 'local-first'`, so `'local-first'` read as a durable client store
180
- * that does not exist (`createOpfsLocalStore` throws `X_NOT_IMPLEMENTED`) — silent, and shaped like
181
- * 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()`
182
192
  * topic, a `live: true` query, a local store. `scaffold-config.test.ts` no longer holds a list of
183
193
  * dead names to remember; it resolves every key path this literal writes against what `defineConfig`
184
194
  * really returns, so the fourteenth deletion fails there with no edit here.
@@ -195,10 +205,8 @@ ${envDeclaration()}
195
205
 
196
206
  export const config = defineConfig({
197
207
  name: '${app.kebab}',
198
- locales: ['en'],
199
- defaultLocale: 'en',
200
- defaultTimeZone: 'UTC',
201
- 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.
202
210
  // Env KEYS, never the value: the same image deploys to every environment. The database is
203
211
  // configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
204
212
  // The tiers ARE the cache selection: add 'redis' to build the shared rung, which reads
@@ -215,7 +223,7 @@ export const config = defineConfig({
215
223
  enabled: true,
216
224
  // The document an offline navigation gets when the cache has no answer, and the path
217
225
  // \`apps/web/site/offline/page.tsx\` serves. Required once \`enabled\` is true: an installable
218
- // 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.
219
227
  offline: { fallback: '/offline' },
220
228
  name: '${titleCase(app.raw)}',
221
229
  colors: {
@@ -227,7 +235,7 @@ export const config = defineConfig({
227
235
  // it, and a choice stored by the theme toggle wins over it on every later visit; 'system' follows
228
236
  // the OS.
229
237
  theme: { defaultMode: 'dark' },
230
- ai: { mcp: { expose: true, path: '/mcp' } },
238
+ ai: { mcp: { expose: true } },
231
239
  });
232
240
 
233
241
  /**
@@ -287,7 +295,8 @@ const biome = (): string => `{
287
295
  "rules": {
288
296
  "preset": "recommended",
289
297
  "suspicious": { "noExplicitAny": "error" },
290
- "correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" }
298
+ "correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" },
299
+ "style": { "useNodejsImportProtocol": "error" }
291
300
  }
292
301
  },
293
302
  "javascript": {
@@ -356,7 +365,7 @@ export const SCAFFOLD_COVERAGE_EXCLUDE: readonly CoverageExclude[] = [
356
365
  },
357
366
  {
358
367
  glob: 'apps/*/server.ts',
359
- 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',
360
369
  },
361
370
  {
362
371
  glob: 'apps/*/prerender.ts',
@@ -462,6 +471,8 @@ export function repoFiles(
462
471
  { path: VERIFY_FLOOR_FILE, contents: verifyFloor(example) },
463
472
  { path: 'types/scss.d.ts', contents: scssTypes() },
464
473
  { path: '.gitignore', contents: gitignore() },
474
+ // Every text file LF whatever `core.autocrlf` says — a Windows checkout is otherwise CRLF.
475
+ rootGitattributesFile(),
465
476
  { path: '.env.development', contents: envDevelopment() },
466
477
  // Committed, and generated: `x env example` rewrites this file from `envSchema`, and the
467
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);
@@ -272,17 +286,11 @@ const toggleIsland =
272
286
  // browser downloads. Named by SPECIFIER from the page, never by import:
273
287
  // const ThemeSwitch = island({ src: '../../shared/theme-toggle.island.tsx', props: ['locale'] });
274
288
  //
275
- // The control itself is the catalog's \`ThemeToggle\`; this file is the runtime it needs and the
276
- // one decision the framework cannot make for it — see \`bootedEnv\` below.
289
+ // The control itself is the catalog's \`ThemeToggle\`; this file is only the runtime it needs. It
290
+ // reads the browser on its own: the theme the boot stamped on \`<html>\`, then \`theme.defaultMode\`
291
+ // (stamped beside it), then the OS.
277
292
 
278
- import {
279
- browserThemeEnv,
280
- setSolidRuntime,
281
- THEME_ATTRIBUTE,
282
- type ThemeEnv,
283
- ThemeToggle,
284
- UiProvider,
285
- } from '@ultimat3/ui';
293
+ import { setSolidRuntime, ThemeToggle, UiProvider } from '@ultimat3/ui';
286
294
  import {
287
295
  createContext,
288
296
  createEffect,
@@ -298,17 +306,6 @@ export interface ThemeToggleIslandProps {
298
306
  readonly locale: string;
299
307
  }
300
308
 
301
- /**
302
- * With no stored choice the catalog's \`resolveTheme\` asks the OS — but the document already wears
303
- * the theme the framework's boot script stamped from \`theme.defaultMode\` (dark, in this app), so
304
- * a toggle that asked the OS would show one theme while the page wore another. The boot's verdict
305
- * is on \`<html>\` before this runs: read it once, and let a stored choice win as it does there.
306
- */
307
- export const bootedEnv = (): ThemeEnv => {
308
- const booted = document.documentElement.getAttribute(THEME_ATTRIBUTE) === 'dark';
309
- return { ...browserThemeEnv(), prefersDark: () => booted };
310
- };
311
-
312
309
  /**
313
310
  * The one export the hydration runtime calls. \`setSolidRuntime\` comes FIRST and is not optional:
314
311
  * \`@ultimat3/ui\` imports types from solid-js and never a runtime, so the reactive graph a
@@ -318,12 +315,11 @@ export const bootedEnv = (): ThemeEnv => {
318
315
  */
319
316
  export function mount(el: HTMLElement, props: ThemeToggleIslandProps): void {
320
317
  setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
321
- const env = bootedEnv();
322
318
  el.textContent = '';
323
319
  render(
324
320
  () => (
325
321
  <UiProvider locale={props.locale}>
326
- <ThemeToggle mode="toggle" env={env} />
322
+ <ThemeToggle mode="toggle" />
327
323
  </UiProvider>
328
324
  ),
329
325
  el,
@@ -353,15 +349,14 @@ export const themeToggleStates = defineIslandStates({
353
349
 
354
350
  const toggleTest =
355
351
  (): string => `// The toggle the browser actually runs: built with the same \`buildIslands\` as \`x build\`, mounted
356
- // against a DOM small enough to read, and clicked. What it pins is the one decision this island
357
- // makes — the boot's verdict on \`<html>\` is what the control starts from, and a click writes the
358
- // key the boot script reads back on the next load.
352
+ // against a DOM small enough to read, and clicked. What it pins: the boot's verdict on \`<html>\` is
353
+ // what the control starts from, and a click writes the key the boot script reads back next load.
359
354
 
360
355
  import { join } from 'node:path';
361
356
  import { buildIslands } from '@ultimat3/cli';
357
+ import { THEME_STORAGE_KEY } from '@ultimat3/core';
362
358
  import { describeIslandState, expect, test } from '@ultimat3/testing';
363
- import { THEME_ATTRIBUTE, THEME_STORAGE_KEY } from '@ultimat3/ui';
364
- import { bootedEnv } from './theme-toggle.island';
359
+ import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE } from '@ultimat3/ui';
365
360
  import { themeToggleStates } from './theme-toggle.island.states';
366
361
 
367
362
  /** What the island reads through \`browserThemeEnv\`: storage and the OS query, both fakes. */
@@ -371,11 +366,19 @@ const localStorage = {
371
366
  setItem: (key: string, value: string): void => void stored.set(key, value),
372
367
  removeItem: (key: string): void => void stored.delete(key),
373
368
  };
369
+ /** One OS query, shared by every \`matchMedia\` call: \`flipOs\` changes it and tells the listeners. */
370
+ const os = { dark: false, listeners: new Set<() => void>() };
374
371
  const matchMedia = (): Record<string, unknown> => ({
375
- matches: false,
376
- addEventListener: () => {},
377
- removeEventListener: () => {},
372
+ get matches(): boolean {
373
+ return os.dark;
374
+ },
375
+ addEventListener: (_type: string, listener: () => void) => void os.listeners.add(listener),
376
+ removeEventListener: (_type: string, listener: () => void) => void os.listeners.delete(listener),
378
377
  });
378
+ const flipOs = (dark: boolean): void => {
379
+ os.dark = dark;
380
+ for (const listener of os.listeners) listener();
381
+ };
379
382
 
380
383
  // The state \`x shot --island theme-toggle\` photographs is the state this mounts: one manifest,
381
384
  // so the picture and the assertion cannot be of two different components.
@@ -404,12 +407,27 @@ describeIslandState(themeToggleStates, 'default', island, (mounted) => {
404
407
  expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
405
408
  });
406
409
 
407
- test('the env the island builds starts from the theme stamped on <html>', () => {
408
- // The fake document is still installed here, so this reads exactly what \`mount\` read.
410
+ test('with nothing stored, a click flips the theme the boot stamped — not the OS', () => {
411
+ // The OS fake says light. The boot stamped this app's \`defaultMode\`, dark, so the visible
412
+ // theme is dark and the first click must go light; a toggle that asked the OS would write the
413
+ // dark already on screen and look dead.
414
+ stored.clear();
415
+ mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'dark');
409
416
  mounted().documentElement.setAttribute(THEME_ATTRIBUTE, 'dark');
410
- expect(bootedEnv().prefersDark()).toBe(true);
411
- mounted().documentElement.setAttribute(THEME_ATTRIBUTE, 'light');
412
- expect(bootedEnv().prefersDark()).toBe(false);
417
+ expect(mounted().fire('button', 'click')).toBe(true);
418
+ expect(stored.get(THEME_STORAGE_KEY)).toBe('light');
419
+ expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('light');
420
+ });
421
+
422
+ test("an OS flip is followed while nothing is stored and the app's default is 'system'", () => {
423
+ stored.clear();
424
+ mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'system');
425
+ flipOs(true);
426
+ expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
427
+ // A fixed default is the app's own opinion: the OS does not override it.
428
+ mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'dark');
429
+ flipOs(false);
430
+ expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
413
431
  });
414
432
  });
415
433
  `;
@@ -5,6 +5,8 @@
5
5
  import type { ExecResult } from './exec';
6
6
  import { execOutput } from './exec';
7
7
  import { parseBunTest } from './mcp-test-output';
8
+ // One reader of bun's `N errors` line, shared with MCP `tests.run` — never a second regex.
9
+ import { errorsIn } from './mcp-test-run';
8
10
 
9
11
  export interface TestCounts {
10
12
  /** Tests that executed — passed plus failed. A failed test is a test that ran. */
@@ -20,12 +22,6 @@ export interface TestCounts {
20
22
  readonly errors?: number;
21
23
  }
22
24
 
23
- /** Bun's own `N error` / `N errors` summary line: the last one, as `parseBunTest` reads the rest. */
24
- const errorsIn = (output: string): number => {
25
- const last = [...output.matchAll(/^\s*(\d+)\s+errors?\s*$/gm)].at(-1);
26
- return last === undefined ? 0 : Number.parseInt(last[1] ?? '0', 10);
27
- };
28
-
29
25
  /**
30
26
  * Summed across every process the step spawned, because a step is one line in the gate whether it
31
27
  * ran on one worker or eight.
@@ -0,0 +1,25 @@
1
+ // The ONE policy for which paths are never a test the gate runs here, projected twice: the path
2
+ // predicate `discoverTests` filters by and the `--path-ignore-patterns` the serial steps pass to
3
+ // `bun test`. A leaf, so `test-select.ts` and `verify-tests.ts` (which import each other) share it.
4
+
5
+ /** Build output and installed packages: never a test, at ANY depth — a workspace's `dist/` too. */
6
+ const IGNORED_ANYWHERE = ['dist', 'node_modules'] as const;
7
+
8
+ /**
9
+ * Nested projects with their own gate (`examples/`, `dummy/`, run by
10
+ * `scripts/reference-app-gate.ts`) and the root `build/` — skipped AT THE ROOT only. Matched at any
11
+ * depth they swallowed an app's own slice: `x g resource build` lost 14 of its 16 test files and
12
+ * `x g resource example` its `examples/` page tests, from `x test` and the gate alike.
13
+ */
14
+ const IGNORED_AT_ROOT = ['build', 'examples', 'dummy'] as const;
15
+
16
+ /** Whether a root-relative POSIX path is excluded from every test selection. */
17
+ export const isIgnoredTestPath = (path: string): boolean =>
18
+ IGNORED_ANYWHERE.some((dir) => `/${path}`.includes(`/${dir}/`)) ||
19
+ IGNORED_AT_ROOT.some((dir) => path.startsWith(`${dir}/`));
20
+
21
+ /** The same policy as `bun test --path-ignore-patterns` globs, relative to the run's root. */
22
+ export const IGNORED_TEST_PATTERNS: readonly string[] = [
23
+ ...IGNORED_ANYWHERE.map((dir) => `**/${dir}/**`),
24
+ ...IGNORED_AT_ROOT.map((dir) => `${dir}/**`),
25
+ ];