@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
package/src/cmd-new.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  // Interactive-free: every choice is a flag with a default, because an agent cannot answer prompts.
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
- import { chmod } from 'node:fs/promises';
7
- import { isAbsolute, join, resolve } from 'node:path';
6
+ import { isAbsolute, join, relative, resolve } from 'node:path';
8
7
  import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
8
+ import { AppNameInvalidError, appNameIssue } from './app-name';
9
9
  import { dedupe } from './cmd-generate';
10
10
  import { newSpec } from './cmd-new-spec';
11
11
  import type { CliCommand, CommandContext } from './command';
@@ -18,7 +18,7 @@ import type { CommandResult } from './output';
18
18
  import { flagBool, flagString } from './parse';
19
19
  import { quoteArg } from './shell-quote';
20
20
  import type { GeneratedFile } from './templates';
21
- import { appFiles, EXECUTABLE_FILES, names, repoFiles, resourceFiles } from './templates';
21
+ import { appFiles, names, repoFiles, resourceFiles } from './templates';
22
22
  import { loadVersion } from './version-loader';
23
23
 
24
24
  export interface NewAppOptions {
@@ -102,6 +102,15 @@ export async function initRepository(runner: Runner, dir: string): Promise<Repos
102
102
  return { initialized: true, committed: true, problem: null };
103
103
  }
104
104
 
105
+ /** `path` relative to `cwd` when it lies under it, else absolute — never a `../../` walk. */
106
+ const displayPath = (cwd: string, path: string): string => {
107
+ const rel = relative(cwd, path);
108
+ return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? path : rel;
109
+ };
110
+
111
+ /** The `cd` operand of the next-steps line: the target as typed from `cwd`, pasteable verbatim. */
112
+ const cdTarget = (cwd: string, target: string): string => quoteArg(displayPath(cwd, target));
113
+
105
114
  /** Pure: the complete file list for a new app, so `--dry-run` and the test see the same thing. */
106
115
  export function planNewApp(options: NewAppOptions): readonly GeneratedFile[] {
107
116
  const app = names(options.name);
@@ -145,13 +154,12 @@ export interface WrittenApp {
145
154
  * a migration whose snapshot never existed is what made the app's first two database commands
146
155
  * refuse each other — `x db migrate` naming `x db gen`, and `x db gen` refusing a sidecar version
147
156
  * control never had. The consequence is deliberate: `x verify`'s `drift` step is red on a pristine
148
- * scaffold until `x db gen "initial"` runs — which `bin/setup` does, and which is what
157
+ * scaffold until `x db gen "initial"` runs — which `bun run setup` does, and which is what
149
158
  * `cli.new.done` tells the author to run.
150
159
  */
151
160
  export async function writeNewApp(target: string, options: NewAppOptions): Promise<WrittenApp> {
152
161
  const files = planNewApp(options);
153
162
  for (const file of files) await Bun.write(join(target, file.path), file.contents);
154
- for (const path of EXECUTABLE_FILES) await chmod(join(target, path), 0o755);
155
163
  return { dir: target, files: files.map((file) => file.path) };
156
164
  }
157
165
 
@@ -210,6 +218,18 @@ export const newCommand: CliCommand = {
210
218
  flags: callerFlags(ctx),
211
219
  });
212
220
  }
221
+ // After the slug exists and before anything is planned: the slug is `app.config.ts`'s `name`,
222
+ // and a name core's `defineConfig` refuses is an app that throws at its own first import.
223
+ const issue = appNameIssue(app.kebab);
224
+ if (issue !== undefined) {
225
+ throw new AppNameInvalidError({
226
+ name: raw,
227
+ slug: app.kebab,
228
+ issue,
229
+ invocation: invocationOf(ctx, 'new'),
230
+ flags: callerFlags(ctx),
231
+ });
232
+ }
213
233
  const target = resolve(parentDir(ctx.cwd, flagString(ctx.args, 'dir')), app.kebab);
214
234
  const options: NewAppOptions = { name: raw, example: ctx.args.flags.get('example') !== false };
215
235
 
@@ -218,9 +238,17 @@ export const newCommand: CliCommand = {
218
238
  return {
219
239
  ok: true,
220
240
  command: 'new',
221
- summary: msg('cli.new.done', { name: app.kebab }),
241
+ summary: msg('cli.new.dryRun', {
242
+ name: app.kebab,
243
+ dir: target,
244
+ cd: cdTarget(ctx.cwd, target),
245
+ }),
222
246
  data: { dir: target, files: files.map((file) => file.path), dryRun: true },
223
- lines: files.map((file) => msg('cli.file.added', { path: `${app.kebab}/${file.path}` })),
247
+ // Where each file WOULD land, as the caller would reach it: relative to the cwd, `--dir`
248
+ // included — `${app.kebab}/…` named the wrong place whenever `--dir` was set.
249
+ lines: files.map((file) =>
250
+ msg('cli.file.added', { path: displayPath(ctx.cwd, join(target, file.path)) }),
251
+ ),
224
252
  };
225
253
  }
226
254
  if (existsSync(target) && !flagBool(ctx.args, 'force')) {
@@ -263,7 +291,7 @@ export const newCommand: CliCommand = {
263
291
  return {
264
292
  ok: true,
265
293
  command: 'new',
266
- summary: msg('cli.new.done', { name: app.kebab }),
294
+ summary: msg('cli.new.done', { name: app.kebab, cd: cdTarget(ctx.cwd, target) }),
267
295
  data: { dir: written.dir, files: written.files, git },
268
296
  lines,
269
297
  };
@@ -3,8 +3,8 @@
3
3
  // does not exist", which is false and sends an agent looking for a typo. X_NOT_IMPLEMENTED plus a
4
4
  // fix naming the closest shipped command says the true thing, and `x help` lists them honestly.
5
5
 
6
+ import { NotImplementedError } from '@ultimat3/core';
6
7
  import type { CliCommand } from './command';
7
- import { CliNotImplementedError } from './errors';
8
8
  import type { CommandResult } from './output';
9
9
  import type { CommandSpec } from './parse';
10
10
 
@@ -127,6 +127,15 @@ export const PLANNED_SUBCOMMANDS: readonly PlannedSubcommand[] = [
127
127
  subcommand: 'studio',
128
128
  fix: 'x dev # then the db panel at /_x: schema, rows, and a guarded SQL console',
129
129
  },
130
+ // Planned because Postgres is the one durable job driver: there is nowhere to drain to. The two
131
+ // `--to` values, `redis` and `nats`, were `X_NOT_IMPLEMENTED` stubs on every method, so a drain
132
+ // leased the production batch for five minutes, failed each enqueue and nacked it back. 25.0.0
133
+ // deleted both stubs and the unreachable drain body; a real second driver writes a new one.
134
+ {
135
+ command: 'jobs',
136
+ subcommand: 'drain',
137
+ fix: 'x jobs ls --json # the queue as it stands; Postgres is the only durable driver, so there is nowhere to drain to',
138
+ },
130
139
  ];
131
140
 
132
141
  /**
@@ -134,18 +143,28 @@ export const PLANNED_SUBCOMMANDS: readonly PlannedSubcommand[] = [
134
143
  * the error rather than throwing it, because a `run` that throws synchronously escapes the
135
144
  * promise its signature promises — the caller does `throw plannedSubcommand(...)`.
136
145
  */
137
- export function plannedSubcommand(command: string, subcommand: string): CliNotImplementedError {
146
+ export function plannedSubcommand(command: string, subcommand: string): NotImplementedError {
138
147
  const planned = PLANNED_SUBCOMMANDS.find(
139
148
  (entry) => entry.command === command && entry.subcommand === subcommand,
140
149
  );
141
- return new CliNotImplementedError({
142
- feature: `x ${command} ${subcommand}`,
150
+ return new NotImplementedError({
151
+ cause: `x ${command} ${subcommand} is not implemented in this build`,
143
152
  // `x help <command>`, not `x <command> --help`: for a command that declares subcommands and no
144
153
  // default, the latter is refused by the parser before help is ever rendered.
145
154
  fix: planned?.fix ?? `x help ${command}`,
146
155
  });
147
156
  }
148
157
 
158
+ /**
159
+ * What every invocation of a planned command answers — its `run`, and the dispatcher when the
160
+ * parser refused a flag before any `run` was reached. One builder, so the two cannot drift.
161
+ */
162
+ export const plannedCommandError = (planned: PlannedCommand): NotImplementedError =>
163
+ new NotImplementedError({
164
+ cause: `x ${planned.name} is not implemented in this build`,
165
+ fix: planned.fix,
166
+ });
167
+
149
168
  const specFor = (planned: PlannedCommand): CommandSpec => ({
150
169
  name: planned.name,
151
170
  summary: `${planned.summary} (planned)`,
@@ -167,10 +186,7 @@ const toCommand = (planned: PlannedCommand): CliCommand => ({
167
186
  // `async` is load-bearing: a synchronous throw would escape every caller that awaits the
168
187
  // promise this signature promises, including the dispatcher's own error path.
169
188
  async run(): Promise<CommandResult> {
170
- throw new CliNotImplementedError({
171
- feature: `x ${planned.name}`,
172
- fix: planned.fix,
173
- });
189
+ throw plannedCommandError(planned);
174
190
  },
175
191
  });
176
192
 
package/src/cmd-routes.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  // `x routes` — the route table as a table, or as JSON. Replaces grepping a router directory, which
2
2
  // is what an agent does when the framework has no answer to "what URLs exist".
3
3
  //
4
- // The rows are `@ultimat3/render`'s own `describeRoutes()`: the CLI prints the route table, it
4
+ // The rows are `@ultimat3/render`'s own `describePages()`: the CLI prints the route table, it
5
5
  // does not keep a second one.
6
6
 
7
7
  import type { RouteDescriptor, Surface } from '@ultimat3/render';
8
- import { describeRoutes, SURFACES } from '@ultimat3/render';
8
+ import { describePages, SURFACES } from '@ultimat3/render';
9
9
  import { loadApp } from './app-load';
10
10
  import { requireAppRoot } from './app-root';
11
11
  import { routesSpec } from './cmd-routes-spec';
12
12
  import type { CliCommand, CommandContext } from './command';
13
- import { BadFlagError } from './errors';
13
+ import { BadFlagError, UnknownCommandError } from './errors';
14
14
  import { msg } from './messages';
15
15
  import type { CommandResult, JsonValue } from './output';
16
16
  import { flagString } from './parse';
@@ -52,7 +52,7 @@ const routeJson = (routes: readonly RouteDescriptor[]): JsonValue =>
52
52
  render: route.mode,
53
53
  hydrate: route.hydrate,
54
54
  offline: route.offline,
55
- budget: { js: route.budgetJs, lcp: route.budgetLcp },
55
+ budget: { js: route.budgetJs },
56
56
  // Absent on a file route, never `null`: only a mounted route has the fact.
57
57
  ...(route.mount === undefined
58
58
  ? {}
@@ -83,11 +83,21 @@ export const routesCommand: CliCommand = {
83
83
  spec: routesSpec,
84
84
  async run(ctx: CommandContext): Promise<CommandResult> {
85
85
  const root = requireAppRoot('routes', ctx.cwd).dir;
86
+ // No positional: `x routes list --json` ran only because a stray word was ignored, which is
87
+ // how a retired spelling stayed in the route-miss fix lines. Refused before the app loads.
88
+ const stray = ctx.args.positionals[0];
89
+ if (stray !== undefined) {
90
+ throw new UnknownCommandError({
91
+ path: `routes ${stray}`,
92
+ known: ['routes'],
93
+ suggestion: 'routes --json',
94
+ });
95
+ }
86
96
  // Read before the app is loaded: a typo must not cost a boot to report, the rule `x mcp`'s
87
97
  // `--transport` already follows.
88
98
  const surface = readSurfaceFilter(flagString(ctx.args, 'surface'));
89
99
  const { findings } = await loadApp(root);
90
- const routes = describeRoutes().filter(
100
+ const routes = describePages().filter(
91
101
  (route) => surface === undefined || route.surface === surface,
92
102
  );
93
103
  return {
@@ -25,6 +25,7 @@ import {
25
25
  SecretsPlaintextInvalidError,
26
26
  secretsFileExists,
27
27
  serializeSecretValues,
28
+ UltimateError,
28
29
  writeMasterKeyFile,
29
30
  writeSecretsFile,
30
31
  } from '@ultimat3/core';
@@ -33,12 +34,7 @@ import { requireAppRoot } from './app-root';
33
34
  import { appSecrets, dropRetired, retire, retiredKeyIds } from './cmd-secrets-ring';
34
35
  import { secretsSpec } from './cmd-secrets-spec';
35
36
  import type { CliCommand, CommandContext } from './command';
36
- import {
37
- MissingPositionalError,
38
- SecretsEditFailedError,
39
- SecretsEditorMissingError,
40
- SecretsExistsError,
41
- } from './errors';
37
+ import { MissingPositionalError } from './errors';
42
38
  import { msg } from './messages';
43
39
  import type { CommandResult, JsonValue } from './output';
44
40
  import { flagString } from './parse';
@@ -46,6 +42,51 @@ import { recoverRotation, rotateMasterKey } from './secrets-rotation';
46
42
  import { shredOnSignal } from './signal-shred';
47
43
  import { renderTable } from './table';
48
44
 
45
+ /**
46
+ * `x secrets edit` decrypts into a buffer and hands it to `$EDITOR`. There is no fallback editor:
47
+ * guessing one and opening a decrypted file in it is the last place a surprise belongs.
48
+ */
49
+ export class SecretsEditorMissingError extends UltimateError {
50
+ constructor(input: { vars: readonly string[] }) {
51
+ super({
52
+ code: 'X_SECRETS_EDITOR_MISSING',
53
+ cause: `x secrets edit opens the decrypted secrets in an editor and none of ${input.vars.join(', ')} is set`,
54
+ fix: 'EDITOR=nano x secrets edit',
55
+ });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The editor exited non-zero — a crash, or a deliberate abort. The buffer is discarded either way
61
+ * and the committed file is left exactly as it was: resealing a buffer whose editor failed would
62
+ * commit whatever half-written state the crash left behind.
63
+ */
64
+ export class SecretsEditFailedError extends UltimateError {
65
+ constructor(input: { editor: string; code: number }) {
66
+ super({
67
+ code: 'X_SECRETS_EDIT_FAILED',
68
+ cause: `"${input.editor}" exited ${input.code}, so the decrypted buffer was discarded and the committed secrets file was not rewritten`,
69
+ fix: 'x secrets edit',
70
+ });
71
+ }
72
+ }
73
+
74
+ /**
75
+ * `x secrets init` would overwrite a file that already exists. `X_GENERATE_CONFLICT` is this
76
+ * package's own code for exactly that, and a second name for "a generator would clobber something"
77
+ * is the duplication the code registry exists to prevent. Losing a master key is unrecoverable —
78
+ * the committed file it opens is then ciphertext nobody can read again.
79
+ */
80
+ export class SecretsExistsError extends UltimateError {
81
+ constructor(input: { path: string; fix: string }) {
82
+ super({
83
+ code: 'X_GENERATE_CONFLICT',
84
+ cause: `${input.path} already exists, and x secrets init would replace it`,
85
+ fix: input.fix,
86
+ });
87
+ }
88
+ }
89
+
49
90
  export { SECRETS_SUBCOMMANDS } from './cmd-secrets-spec';
50
91
 
51
92
  /** In order of precedence. `VISUAL` outranks `EDITOR` by POSIX convention on an interactive tty. */
@@ -5,14 +5,17 @@
5
5
 
6
6
  // why: no Bun native joins a path.
7
7
  import { join, relative } from 'node:path';
8
+ import { escapeHtml } from '@ultimat3/core';
8
9
  import { routeEntries } from '@ultimat3/render';
9
10
  import { loadApp } from './app-load';
10
11
  import type { ShotRun } from './cmd-shot';
12
+ import { BadFlagError } from './errors';
11
13
  import type { CommandResult } from './output';
12
14
  import { localizedShotPath } from './shot-locale';
13
15
  import type { ShotServer } from './shot-server';
14
16
  import { SHOT_DIR } from './shot-server';
15
17
  import type { ShotArtifacts } from './shot-verdict';
18
+ import { viewportDir } from './shot-viewport';
16
19
 
17
20
  export const MATRIX_DIR = join(SHOT_DIR, 'matrix');
18
21
  export const MATRIX_INDEX = 'index.html';
@@ -20,7 +23,10 @@ export const MATRIX_INDEX = 'index.html';
20
23
  /** The two themes the boot honours from storage. */
21
24
  export const MATRIX_THEMES: readonly ('light' | 'dark')[] = ['light', 'dark'];
22
25
 
23
- /** A phone and a desktop, each with the height its class of device is photographed at. */
26
+ /**
27
+ * A phone and a desktop, each with the height its class of device is photographed at. The default
28
+ * only: `x shot --matrix --viewport <w>x<h>[,…]` replaces the pair (`shot-viewport.ts`).
29
+ */
24
30
  export const MATRIX_VIEWPORTS: readonly { readonly width: number; readonly height: number }[] = [
25
31
  { width: 390, height: 844 },
26
32
  { width: 1440, height: 900 },
@@ -34,10 +40,20 @@ export interface MatrixCell {
34
40
  readonly viewport: { readonly width: number; readonly height: number };
35
41
  /** What the browser opens: the route, prefixed for a non-default locale. */
36
42
  readonly path: string;
37
- /** Relative to the matrix directory: `<route-slug>/<locale>/<theme>-<width>`. */
43
+ /** Relative to the matrix directory: `<route-slug>/<locale>/<theme>-<width>x<height>`. */
38
44
  readonly dir: string;
39
45
  }
40
46
 
47
+ /** The matrix photographs ROUTES; `--island`/`--all-islands` beside it is two subjects. */
48
+ export function refuseMatrixWithComponent(): never {
49
+ throw new BadFlagError({
50
+ flag: 'matrix',
51
+ command: 'shot',
52
+ reason: 'photographs every site route; --island and --all-islands photograph components',
53
+ fix: 'x shot --matrix --json',
54
+ });
55
+ }
56
+
41
57
  /** A route a matrix can photograph without inventing a parameter: no `:param`, no `*rest`. */
42
58
  export const isMatrixRoute = (path: string): boolean => !/[:*[]/.test(path);
43
59
 
@@ -78,7 +94,8 @@ export function planShotMatrix(input: MatrixPlanInput): readonly MatrixCell[] {
78
94
  theme,
79
95
  viewport,
80
96
  path: localizedShotPath(route, locale, input.defaultLocale),
81
- dir: join(slug(route), locale, `${theme}-${String(viewport.width)}`),
97
+ // The whole size, never the width alone: `--viewport 390x844,390x664` is two pictures.
98
+ dir: join(slug(route), locale, `${theme}-${viewportDir(viewport)}`),
82
99
  });
83
100
  }
84
101
  }
@@ -94,9 +111,6 @@ export interface MatrixShot {
94
111
  readonly image: string;
95
112
  }
96
113
 
97
- const escapeHtml = (text: string): string =>
98
- text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
99
-
100
114
  /**
101
115
  * The contact sheet. System colours only (`Canvas`, `CanvasText`, `Mark`): a generated review page
102
116
  * has no design tokens to read, and a literal colour would be the one this repo refuses.
@@ -108,7 +122,7 @@ export function contactSheet(shots: readonly MatrixShot[]): string {
108
122
  .filter((shot) => shot.cell.route === route)
109
123
  .map((shot) => {
110
124
  const { locale, theme, viewport, path } = shot.cell;
111
- const label = `${locale} · ${theme} · ${String(viewport.width)}px${shot.ok ? '' : ' · FAILED'}`;
125
+ const label = `${locale} · ${theme} · ${viewportDir(viewport)}${shot.ok ? '' : ' · FAILED'}`;
112
126
  return (
113
127
  `<figure${shot.ok ? '' : ' class="failed"'}><a href="${escapeHtml(shot.image)}">` +
114
128
  `<img src="${escapeHtml(shot.image)}" alt="${escapeHtml(`${path} ${label}`)}" loading="lazy" ` +
@@ -193,6 +207,7 @@ export async function runShotMatrix(run: MatrixRun): Promise<CommandResult> {
193
207
  locale: shot.cell.locale,
194
208
  theme: shot.cell.theme,
195
209
  width: shot.cell.viewport.width,
210
+ height: shot.cell.viewport.height,
196
211
  image: shot.image,
197
212
  ok: shot.ok,
198
213
  })),
@@ -7,7 +7,7 @@ export const shotSpec: CommandSpec = {
7
7
  name: 'shot',
8
8
  summary: 'photograph one route, one island in a state it declares, or every island in the app',
9
9
  usage:
10
- 'x shot <route> [--locale <l>] [--theme light|dark] | --matrix [<route>] | --island <name> [--state <id>] | --all-islands [--cookie <name=value>] [--expect-status 404] [--port 0] [--out <dir>] [--settle 2000] [--json]',
10
+ 'x shot <route> [--locale <l>] [--theme light|dark] [--viewport 390x844,1440x900] | --matrix [<route>] | --island <name> [--state <id>] | --all-islands [--cookie <name=value>] [--expect-status 404] [--port 0] [--out <dir>] [--settle 2000] [--json]',
11
11
  requiresApp: true,
12
12
  flags: [
13
13
  { name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
@@ -51,7 +51,15 @@ export const shotSpec: CommandSpec = {
51
51
  name: 'matrix',
52
52
  type: 'boolean',
53
53
  summary:
54
- 'every site route × locale × light/dark × 390/1440 px into .x/shot/matrix/, plus an index.html contact sheet',
54
+ 'every site route × locale × light/dark × 390x844/1440x900 (or --viewport) into .x/shot/matrix/, plus an index.html contact sheet',
55
+ },
56
+ // One flag, read as a list, rather than a repeatable one: the parser keeps the last value of a
57
+ // repeated flag, so `--viewport a --viewport b` would photograph `b` alone and say nothing.
58
+ {
59
+ name: 'viewport',
60
+ type: 'string',
61
+ summary:
62
+ '<w>x<h>[,<w>x<h>] — a route once per size into <out>/<w>x<h>/; with --matrix, its sizes',
55
63
  },
56
64
  // A FLAG on `x shot` and never a second command: photographing a route and photographing a
57
65
  // component are one job with two subjects, and a parallel command would be the second path
package/src/cmd-shot.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { mkdirSync } from 'node:fs';
9
9
  import { join, resolve } from 'node:path';
10
10
  import { renderFixShellArg } from '@ultimat3/core';
11
+ import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
11
12
  import { IDLE_HYDRATE_TIMEOUT_MS } from '@ultimat3/render';
12
13
  import { requireAppRoot } from './app-root';
13
14
  import { appBrowser } from './browser-launcher';
@@ -23,7 +24,13 @@ import {
23
24
  refuseSweepWithRoute,
24
25
  refuseSweepWithState,
25
26
  } from './cmd-shot-island';
26
- import { MATRIX_DIR, matrixRoutes, planShotMatrix, runShotMatrix } from './cmd-shot-matrix';
27
+ import {
28
+ MATRIX_DIR,
29
+ matrixRoutes,
30
+ planShotMatrix,
31
+ refuseMatrixWithComponent,
32
+ runShotMatrix,
33
+ } from './cmd-shot-matrix';
27
34
  import { shotSpec } from './cmd-shot-spec';
28
35
  import type { CliCommand, CommandContext } from './command';
29
36
  import { BadFlagError, MissingPositionalError } from './errors';
@@ -33,12 +40,7 @@ import type { ParsedArgs } from './parse';
33
40
  import { flagBool, flagString } from './parse';
34
41
  import { shotBrowserChoice } from './shot-browser';
35
42
  import { readCookieFlag, type ShotCookiePair, shotCookies } from './shot-cookie';
36
- import {
37
- acceptLanguageHeaders,
38
- loadShotLocales,
39
- localizedShotPath,
40
- readLocaleFlag,
41
- } from './shot-locale';
43
+ import { acceptLanguageHeaders, localizedShotPath, readLocaleFlag } from './shot-locale';
42
44
  import type { BootDevServer, ShotServer } from './shot-server';
43
45
  import { allowHostsFrom, devServerFor, SHOT_DIR } from './shot-server';
44
46
  import { SETTLE_POLL_MS, settleIslands } from './shot-settle';
@@ -53,21 +55,12 @@ import {
53
55
  shotSummary,
54
56
  verdictJson,
55
57
  } from './shot-verdict';
58
+ import { parseViewports, refuseViewportWithComponent, runShotViewports } from './shot-viewport';
56
59
 
57
60
  /** Kernel-picked by default: :3000 is usually another project's dev server, not a free port. */
58
61
  const DEFAULT_PORT = 0;
59
62
 
60
- /**
61
- * How long the page is left alone after `load` before it is photographed: exactly the
62
- * `requestIdleCallback` deadline `@ultimat3/render`'s hydration runtime gives an `idle` island —
63
- * shoot sooner and the verdict reports `booted: 0` for a page that hydrates perfectly. READ from
64
- * that runtime rather than restated, because two copies of one number that must agree is the drift
65
- * axiom 2 refuses: the settle window is not "2 seconds", it is "the deadline the runtime uses".
66
- */
67
- export const DEFAULT_SETTLE_MS = IDLE_HYDRATE_TIMEOUT_MS;
68
-
69
- // Re-exported, not re-declared: `cmd-shot.test.ts` and the island path both name them, and a
70
- // second declaration of a path or an allow-list rule is a second answer.
63
+ // Re-exported, not re-declared: a second declaration of a path or a host rule is a second answer.
71
64
  export type { BootDevServer, ShotServer };
72
65
  export { allowHostsFrom, devServerFor, SHOT_DIR };
73
66
 
@@ -339,6 +332,14 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
339
332
  }
340
333
  }
341
334
 
335
+ /**
336
+ * Where the artifacts land. A relative `--out` is a path the caller typed from where they stand,
337
+ * so it resolves against the cwd — `x build --out`'s rule. Against the app root it landed
338
+ * somewhere else whenever `x shot` ran from `apps/web`.
339
+ */
340
+ export const shotOutDir = (cwd: string, out: string | undefined, fallback: string): string =>
341
+ out === undefined ? fallback : resolve(cwd, out);
342
+
342
343
  export const shotResult = (artifacts: ShotArtifacts): CommandResult => ({
343
344
  ok: artifacts.verdict.ok,
344
345
  command: 'shot',
@@ -364,7 +365,8 @@ export const shotCommand: CliCommand = {
364
365
  const sweep = flagBool(ctx.args, 'all-islands');
365
366
  const matrix = flagBool(ctx.args, 'matrix');
366
367
  const cookies = readCookieFlag(flagString(ctx.args, 'cookie'));
367
- const app = await loadShotLocales(root);
368
+ const viewports = parseViewports(flagString(ctx.args, 'viewport'));
369
+ const app = await appLocaleSet(root);
368
370
  const locale = readLocaleFlag(flagString(ctx.args, 'locale'), app);
369
371
  // Every ambiguous pair refused BY NAME, before a value is read: a reader who typed two
370
372
  // subjects has a belief about which one runs, and half of them would be wrong.
@@ -377,14 +379,9 @@ export const shotCommand: CliCommand = {
377
379
  }
378
380
  const component = sweep || (island !== undefined && island !== '');
379
381
  // The matrix photographs ROUTES; a component beside it is two subjects.
380
- if (matrix && component) {
381
- throw new BadFlagError({
382
- flag: 'matrix',
383
- command: 'shot',
384
- reason: 'photographs every site route; --island and --all-islands photograph components',
385
- fix: 'x shot --matrix --json',
386
- });
387
- }
382
+ if (matrix && component) refuseMatrixWithComponent();
383
+ // An island state declares its own size, so a size asked for beside one is a second answer.
384
+ if (component && viewports !== undefined) refuseViewportWithComponent();
388
385
  // An island is photographed in BOTH themes by the harness, which owns its `data-theme` and
389
386
  // carries no boot script — so a theme asked for beside one is a request nothing could honour.
390
387
  if (component && locale !== undefined) {
@@ -406,7 +403,12 @@ export const shotCommand: CliCommand = {
406
403
  // `--matrix` alone is every site route; a route beside it narrows the matrix to that one.
407
404
  const route = component || (matrix && positional === undefined) ? '' : readRoute(positional);
408
405
  const port = intFlag(ctx.args, 'port', PORT_RANGE.min, DEFAULT_PORT, PORT_RANGE.max);
409
- const settleMs = intFlag(ctx.args, 'settle', 0, DEFAULT_SETTLE_MS);
406
+ // How long the page is left alone after `load` before it is photographed: exactly the
407
+ // `requestIdleCallback` deadline `@ultimat3/render`'s hydration runtime gives an `idle` island —
408
+ // shoot sooner and the verdict reports `booted: 0` for a page that hydrates perfectly. The
409
+ // runtime's own constant, under its own name: the settle window is not "2 seconds", it is "the
410
+ // deadline the runtime uses", and a second name for it is a second import path.
411
+ const settleMs = intFlag(ctx.args, 'settle', 0, IDLE_HYDRATE_TIMEOUT_MS);
410
412
  const timeoutMs = intFlag(ctx.args, 'timeout', 1, DEFAULT_PAGE_TIMEOUT_MS);
411
413
  // Read only when given: its absence means "any 2xx", which no single default number can say.
412
414
  const expectStatus =
@@ -426,7 +428,8 @@ export const shotCommand: CliCommand = {
426
428
  const boot = (): Promise<ShotServer> => devServerFor(root, ctx.env, port);
427
429
  const shared = {
428
430
  root,
429
- ...(out === undefined ? {} : { out }),
431
+ // Absolute here, so the island sweep's own `resolve(root, out)` keeps the caller's path.
432
+ ...(out === undefined ? {} : { out: shotOutDir(ctx.cwd, out, root) }),
430
433
  settleMs,
431
434
  timeoutMs,
432
435
  ...(executablePath === undefined ? {} : { executablePath }),
@@ -464,22 +467,24 @@ export const shotCommand: CliCommand = {
464
467
  locales: locale === undefined ? app.locales : [locale],
465
468
  defaultLocale: app.defaultLocale,
466
469
  ...(theme === undefined ? {} : { themes: [theme] }),
470
+ ...(viewports === undefined ? {} : { viewports }),
467
471
  });
468
- const outDir = out === undefined ? join(root, MATRIX_DIR) : resolve(root, out);
472
+ const outDir = shotOutDir(ctx.cwd, out, join(root, MATRIX_DIR));
469
473
  return runShotMatrix({ cells, outDir, boot, shoot: runShot, base });
470
474
  }
471
475
  const shown = locale ?? app.defaultLocale;
472
476
  const path = localizedShotPath(route, shown, app.defaultLocale);
473
- return shotResult(
474
- await runShot({
475
- ...base,
476
- route: path,
477
- outDir: out === undefined ? join(root, SHOT_DIR, shotSlug(path)) : resolve(root, out),
478
- boot,
479
- // Pinned whether or not `--locale` was given: the default locale, never the box's own.
480
- acceptLanguage: shown,
481
- ...(theme === undefined ? {} : { colorScheme: theme }),
482
- }),
483
- );
477
+ const single = {
478
+ ...base,
479
+ route: path,
480
+ // Pinned whether or not `--locale` was given: the default locale, never the box's own.
481
+ acceptLanguage: shown,
482
+ ...(theme === undefined ? {} : { colorScheme: theme }),
483
+ };
484
+ const outDir = shotOutDir(ctx.cwd, out, join(root, SHOT_DIR, shotSlug(path)));
485
+ if (viewports !== undefined) {
486
+ return runShotViewports({ viewports, outDir, boot, shoot: runShot, base: single });
487
+ }
488
+ return shotResult(await runShot({ ...single, outDir, boot }));
484
489
  },
485
490
  };
package/src/cmd-tasks.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { nearestName, systemClock } from '@ultimat3/core';
8
8
  import type { JobDriver, TaskFire, TaskHandle } from '@ultimat3/jobs';
9
9
  import type { CronPhrases } from '@ultimat3/time';
10
+ import { fromEpochMs, isoInZone } from '@ultimat3/time';
10
11
  import { loadApp } from './app-load';
11
12
  import { requireAppRoot } from './app-root';
12
13
  import { tasksSpec } from './cmd-tasks-spec';
@@ -19,7 +20,6 @@ import { flagString } from './parse';
19
20
  import { renderTable } from './table';
20
21
  import {
21
22
  findTaskHandle,
22
- isoInZone,
23
23
  knownTaskNames,
24
24
  listTaskFacts,
25
25
  parseCountFlag,
@@ -58,7 +58,7 @@ const lastFireOf = (fires: ReadonlyMap<string, TaskFire>, name: string, tz: stri
58
58
  if (fire === undefined) return NEVER_FIRED;
59
59
  return {
60
60
  lastMs: fire.occurrenceMs,
61
- last: isoInZone(fire.occurrenceMs, tz),
61
+ last: isoInZone(fromEpochMs(fire.occurrenceMs), tz),
62
62
  lastFiredAtMs: fire.firedAt,
63
63
  };
64
64
  };
@@ -10,7 +10,7 @@ export const testSpec: CommandSpec = {
10
10
  name: 'test',
11
11
  summary:
12
12
  'run one test type — or the whole suite — across N workers, one isolated database per worker',
13
- usage: `x test [${TEST_TYPES.join('|')}] [--filter path[,path…]] [--allow-empty] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--isolate] [--json] [-- <bun test flags>]`,
13
+ usage: `x test [${TEST_TYPES.join('|')}] [--filter path[,path…]] [--allow-empty] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--isolate] [--json] [-- <bun test flags>]`,
14
14
  positionalChoices: TEST_TYPES,
15
15
  // The one command that hands a tail to another tool — `bun test` — and the reason
16
16
  // `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
@@ -28,12 +28,6 @@ export const testSpec: CommandSpec = {
28
28
  summary:
29
29
  'a fresh global per test file (bun test --isolate) — off by default since 22.7; x.verify.json "isolate": true makes it the repo default',
30
30
  },
31
- {
32
- name: 'worker',
33
- type: 'string',
34
- summary:
35
- 'run only shard I of an N-way split of the selection, serially — one CI job\u2019s share',
36
- },
37
31
  {
38
32
  name: 'filter',
39
33
  type: 'string',