@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.
- package/CLAUDE.md +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +57 -79
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -33
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/naming.ts +28 -0
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- 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 {
|
|
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,
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
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
|
};
|
package/src/cmd-planned.ts
CHANGED
|
@@ -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):
|
|
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
|
|
142
|
-
|
|
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
|
|
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 `
|
|
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 {
|
|
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
|
|
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 =
|
|
100
|
+
const routes = describePages().filter(
|
|
91
101
|
(route) => surface === undefined || route.surface === surface,
|
|
92
102
|
);
|
|
93
103
|
return {
|
package/src/cmd-secrets.ts
CHANGED
|
@@ -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. */
|
package/src/cmd-shot-matrix.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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} · ${
|
|
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
|
})),
|
package/src/cmd-shot-spec.ts
CHANGED
|
@@ -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 ×
|
|
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 {
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
};
|
package/src/cmd-test-spec.ts
CHANGED
|
@@ -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] [--
|
|
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',
|