@ultimat3/cli 20.2.1 → 22.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 +84 -1582
- package/README.md +40 -2
- package/package.json +31 -31
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-env.ts +2 -2
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +56 -13
- package/src/build-errors.ts +54 -0
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +117 -42
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +78 -122
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +119 -0
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +33 -28
- package/src/error-pages.ts +19 -7
- package/src/error-unthrown.ts +130 -0
- package/src/errors.ts +22 -29
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -334
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +38 -11
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +98 -0
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +2 -2
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/live-routes.ts +82 -42
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +30 -26
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/page-sync.ts +54 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/realtime-browser-probe-fixture.ts +2 -2
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +23 -11
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-render.ts → runtime-render.ts} +119 -20
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -338
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/sw-artifacts.ts +13 -3
- package/src/sync-url.ts +31 -0
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +76 -30
- package/src/templates/resource.ts +10 -4
- package/src/templates/route.ts +3 -0
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +25 -145
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-env.ts +6 -0
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +41 -0
- package/src/verify-run.ts +115 -50
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +22 -30
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/worker-bundle.ts +192 -0
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -100
- package/src/cdp-connection.ts +0 -211
- package/src/cdp-e2e-page.ts +0 -209
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -139
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -97
- package/src/e2e-errors.ts +0 -103
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -150
- package/src/e2e-selection.ts +0 -182
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// `x routes`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-routes.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const routesSpec: CommandSpec = {
|
|
7
|
+
name: 'routes',
|
|
8
|
+
summary: 'the route table: path, surface, render mode, hydrate, offline',
|
|
9
|
+
usage: 'x routes [--surface site|app|api|shared] [--json]',
|
|
10
|
+
requiresApp: true,
|
|
11
|
+
flags: [{ name: 'surface', type: 'string', summary: 'filter by surface' }],
|
|
12
|
+
};
|
package/src/cmd-routes.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { RouteDescriptor, Surface } from '@ultimat3/render';
|
|
|
8
8
|
import { describeRoutes, SURFACES } from '@ultimat3/render';
|
|
9
9
|
import { loadApp } from './app-load';
|
|
10
10
|
import { requireAppRoot } from './app-root';
|
|
11
|
+
import { routesSpec } from './cmd-routes-spec';
|
|
11
12
|
import type { CliCommand, CommandContext } from './command';
|
|
12
13
|
import { BadFlagError } from './errors';
|
|
13
14
|
import { msg } from './messages';
|
|
@@ -65,13 +66,7 @@ export function readSurfaceFilter(raw: string | undefined): Surface | undefined
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export const routesCommand: CliCommand = {
|
|
68
|
-
spec:
|
|
69
|
-
name: 'routes',
|
|
70
|
-
summary: 'the route table: path, surface, render mode, hydrate, offline',
|
|
71
|
-
usage: 'x routes [--surface site|app|api|shared] [--json]',
|
|
72
|
-
requiresApp: true,
|
|
73
|
-
flags: [{ name: 'surface', type: 'string', summary: 'filter by surface' }],
|
|
74
|
-
},
|
|
69
|
+
spec: routesSpec,
|
|
75
70
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
76
71
|
const root = requireAppRoot('routes', ctx.cwd).dir;
|
|
77
72
|
// Read before the app is loaded: a typo must not cost a boot to report, the rule `x mcp`'s
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// `x secrets`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-secrets.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import { ENV_SCHEMA_EXPORT } from './app-env';
|
|
5
|
+
import type { CommandSpec } from './parse';
|
|
6
|
+
|
|
7
|
+
export const SECRETS_SUBCOMMANDS = ['show', 'init', 'edit', 'set', 'rotate'] as const;
|
|
8
|
+
|
|
9
|
+
export const secretsSpec: CommandSpec = {
|
|
10
|
+
name: 'secrets',
|
|
11
|
+
summary: `the committed encrypted secrets, decrypted into the ${ENV_SCHEMA_EXPORT} variables of the same names`,
|
|
12
|
+
usage: 'x secrets [show|init|edit|set <NAME>|rotate] [--json]',
|
|
13
|
+
requiresApp: true,
|
|
14
|
+
subcommands: [...SECRETS_SUBCOMMANDS],
|
|
15
|
+
// The bare `x secrets` answers without a key ever leaving the file. Declared, not inherited
|
|
16
|
+
// from the array's order — `init`, `edit`, `set` and `rotate` all write.
|
|
17
|
+
defaultSubcommand: 'show',
|
|
18
|
+
flags: [],
|
|
19
|
+
};
|
package/src/cmd-secrets.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from '@ultimat3/core';
|
|
31
31
|
import { ENV_SCHEMA_EXPORT, loadEnvSchema } from './app-env';
|
|
32
32
|
import { requireAppRoot } from './app-root';
|
|
33
|
+
import { secretsSpec } from './cmd-secrets-spec';
|
|
33
34
|
import type { CliCommand, CommandContext } from './command';
|
|
34
35
|
import {
|
|
35
36
|
MissingPositionalError,
|
|
@@ -39,9 +40,11 @@ import {
|
|
|
39
40
|
} from './errors';
|
|
40
41
|
import { msg } from './messages';
|
|
41
42
|
import type { CommandResult, JsonValue } from './output';
|
|
43
|
+
import { recoverRotation, rotateMasterKey } from './secrets-rotation';
|
|
44
|
+
import { shredOnSignal } from './signal-shred';
|
|
42
45
|
import { renderTable } from './table';
|
|
43
46
|
|
|
44
|
-
export
|
|
47
|
+
export { SECRETS_SUBCOMMANDS } from './cmd-secrets-spec';
|
|
45
48
|
|
|
46
49
|
/** In order of precedence. `VISUAL` outranks `EDITOR` by POSIX convention on an interactive tty. */
|
|
47
50
|
export const EDITOR_VARS = ['VISUAL', 'EDITOR'] as const;
|
|
@@ -74,9 +77,10 @@ interface Session {
|
|
|
74
77
|
readonly key: MasterKeyRef;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
/** The root and its key — after finishing or abandoning a rotation a crash interrupted. */
|
|
81
|
+
const open = async (ctx: CommandContext, subcommand: string): Promise<Session> => {
|
|
78
82
|
const root = requireAppRoot(`secrets ${subcommand}`, ctx.cwd).dir;
|
|
79
|
-
return { root, key: requireMasterKey(root, ctx.env) };
|
|
83
|
+
return { root, key: await recoverRotation(root, requireMasterKey(root, ctx.env)) };
|
|
80
84
|
};
|
|
81
85
|
|
|
82
86
|
const names = (values: SecretValues): readonly string[] => Object.keys(values).sort();
|
|
@@ -148,7 +152,7 @@ async function init(ctx: CommandContext): Promise<CommandResult> {
|
|
|
148
152
|
* of the two harms. Leaving decrypted values on disk so they can be recovered is the larger one.
|
|
149
153
|
*/
|
|
150
154
|
async function edit(ctx: CommandContext, io: SecretsIo): Promise<CommandResult> {
|
|
151
|
-
const { root, key } = open(ctx, 'edit');
|
|
155
|
+
const { root, key } = await open(ctx, 'edit');
|
|
152
156
|
const editor = EDITOR_VARS.map((name) => ctx.env[name]).find(
|
|
153
157
|
(value): value is string => value !== undefined && value.trim().length > 0,
|
|
154
158
|
);
|
|
@@ -157,9 +161,9 @@ async function edit(ctx: CommandContext, io: SecretsIo): Promise<CommandResult>
|
|
|
157
161
|
const dir = await mkdtemp(join(tmpdir(), 'ultimate-secrets-'));
|
|
158
162
|
const buffer = join(dir, 'secrets.json');
|
|
159
163
|
const shred = (): void => rmSync(dir, { recursive: true, force: true });
|
|
160
|
-
// A `finally` does not run for a signal, and Ctrl-C inside an editor kills this process too
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
// A `finally` does not run for a signal, and Ctrl-C inside an editor kills this process too —
|
|
165
|
+
// after the shred, which `shredOnSignal` runs and then re-raises the signal for.
|
|
166
|
+
const unlisten = shredOnSignal(shred);
|
|
163
167
|
try {
|
|
164
168
|
await Bun.write(buffer, serializeSecretValues(before));
|
|
165
169
|
// `$EDITOR` may carry flags (`code --wait`), and there is no shell here to split them.
|
|
@@ -195,8 +199,7 @@ async function edit(ctx: CommandContext, io: SecretsIo): Promise<CommandResult>
|
|
|
195
199
|
};
|
|
196
200
|
} finally {
|
|
197
201
|
shred();
|
|
198
|
-
|
|
199
|
-
process.off('SIGTERM', shred);
|
|
202
|
+
unlisten();
|
|
200
203
|
}
|
|
201
204
|
}
|
|
202
205
|
|
|
@@ -217,7 +220,7 @@ async function set(ctx: CommandContext, io: SecretsIo): Promise<CommandResult> {
|
|
|
217
220
|
example: 'printf %s "$TOKEN" | x secrets set STRIPE_KEY --json',
|
|
218
221
|
});
|
|
219
222
|
}
|
|
220
|
-
const { root, key } = open(ctx, 'set');
|
|
223
|
+
const { root, key } = await open(ctx, 'set');
|
|
221
224
|
const before = await readSecretsFile(root, key);
|
|
222
225
|
// Exactly one trailing newline, because `echo` adds one and a secret with a stray `\n` fails
|
|
223
226
|
// against the service it authenticates to with an error that names nothing.
|
|
@@ -242,23 +245,18 @@ async function set(ctx: CommandContext, io: SecretsIo): Promise<CommandResult> {
|
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
/**
|
|
245
|
-
* A new master key over the same values
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* a file it cannot open — `X_SECRETS_KEY_MISMATCH`, whose fix restores the file from git.
|
|
248
|
+
* A new master key over the same values: staged beside the old one, the file sealed with it, then
|
|
249
|
+
* made live (`rotateMasterKey`). Sealed first with the key file written last, a crash between the
|
|
250
|
+
* two left a committed file no key on disk could open.
|
|
249
251
|
*/
|
|
250
252
|
async function rotate(ctx: CommandContext): Promise<CommandResult> {
|
|
251
|
-
const { root, key } = open(ctx, 'rotate');
|
|
253
|
+
const { root, key } = await open(ctx, 'rotate');
|
|
252
254
|
const values = await readSecretsFile(root, key);
|
|
253
255
|
const previous = await masterKeyIdOf(key);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
source: 'file',
|
|
257
|
-
at: masterKeyPath(root),
|
|
258
|
-
};
|
|
259
|
-
await writeSecretsFile(root, values, next);
|
|
256
|
+
// Ignored BEFORE any key file is written, staged one included; the order inside is
|
|
257
|
+
// `secrets-rotation.ts`'s: stage the key, seal, then rename it live.
|
|
260
258
|
await ensureIgnored(root);
|
|
261
|
-
|
|
259
|
+
const next = await rotateMasterKey(root, values);
|
|
262
260
|
const keyId = await masterKeyIdOf(next);
|
|
263
261
|
return {
|
|
264
262
|
ok: true,
|
|
@@ -286,7 +284,7 @@ async function rotate(ctx: CommandContext): Promise<CommandResult> {
|
|
|
286
284
|
* `x secrets edit`. A `--reveal` flag would be a second path that `--json` could not honour.
|
|
287
285
|
*/
|
|
288
286
|
async function show(ctx: CommandContext): Promise<CommandResult> {
|
|
289
|
-
const { root, key } = open(ctx, 'show');
|
|
287
|
+
const { root, key } = await open(ctx, 'show');
|
|
290
288
|
const values = await readSecretsFile(root, key);
|
|
291
289
|
const schema = await loadEnvSchema(root);
|
|
292
290
|
const summaries = describeSecrets(values);
|
|
@@ -337,17 +335,7 @@ async function show(ctx: CommandContext): Promise<CommandResult> {
|
|
|
337
335
|
|
|
338
336
|
export function createSecretsCommand(io: SecretsIo): CliCommand {
|
|
339
337
|
return {
|
|
340
|
-
spec:
|
|
341
|
-
name: 'secrets',
|
|
342
|
-
summary: `the committed encrypted secrets, decrypted into the ${ENV_SCHEMA_EXPORT} variables of the same names`,
|
|
343
|
-
usage: 'x secrets [show|init|edit|set <NAME>|rotate] [--json]',
|
|
344
|
-
requiresApp: true,
|
|
345
|
-
subcommands: [...SECRETS_SUBCOMMANDS],
|
|
346
|
-
// The bare `x secrets` answers without a key ever leaving the file. Declared, not inherited
|
|
347
|
-
// from the array's order — `init`, `edit`, `set` and `rotate` all write.
|
|
348
|
-
defaultSubcommand: 'show',
|
|
349
|
-
flags: [],
|
|
350
|
-
},
|
|
338
|
+
spec: secretsSpec,
|
|
351
339
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
352
340
|
switch (ctx.args.subcommand ?? 'show') {
|
|
353
341
|
case 'init':
|
package/src/cmd-shot-island.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
// why: no Bun native joins or resolves a path; `--out` is resolved against the app root.
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
|
-
import type { ScrapeDriver } from '@ultimat3/scraping';
|
|
13
12
|
import type { IslandStatesManifest, IslandViewport } from '@ultimat3/testing';
|
|
14
13
|
import { findIslandStates } from '@ultimat3/testing';
|
|
15
14
|
import { appBrowser } from './browser-launcher';
|
|
15
|
+
import type { ShotDriver } from './browser-launcher-port';
|
|
16
16
|
import { BadFlagError } from './errors';
|
|
17
17
|
import type { IslandBrowser } from './island-shot';
|
|
18
18
|
import { ISLAND_SHOT_DIR, runIslandShot, runIslandSweep } from './island-shot';
|
|
@@ -39,19 +39,18 @@ export function islandBrowser(input: {
|
|
|
39
39
|
readonly executablePath?: string | undefined;
|
|
40
40
|
readonly cdpUrl?: string | undefined;
|
|
41
41
|
}): IslandBrowser {
|
|
42
|
-
const byViewport = new Map<string, Promise<
|
|
43
|
-
return (viewport: IslandViewport): Promise<
|
|
42
|
+
const byViewport = new Map<string, Promise<ShotDriver>>();
|
|
43
|
+
return (viewport: IslandViewport): Promise<ShotDriver> => {
|
|
44
44
|
const key = `${viewport.width}x${viewport.height}`;
|
|
45
|
-
const held =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return started;
|
|
45
|
+
const held =
|
|
46
|
+
byViewport.get(key) ??
|
|
47
|
+
appBrowser({
|
|
48
|
+
...(input.executablePath === undefined ? {} : { executablePath: input.executablePath }),
|
|
49
|
+
...(input.cdpUrl === undefined ? {} : { cdpUrl: input.cdpUrl }),
|
|
50
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
51
|
+
});
|
|
52
|
+
byViewport.set(key, held);
|
|
53
|
+
return held;
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
56
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// `x shot`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-shot.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const shotSpec: CommandSpec = {
|
|
7
|
+
name: 'shot',
|
|
8
|
+
summary: 'photograph one route, one island in a state it declares, or every island in the app',
|
|
9
|
+
usage:
|
|
10
|
+
'x shot <route> | --island <name> [--state <id>] | --all-islands [--port 0] [--out <dir>] [--settle 2000] [--json]',
|
|
11
|
+
requiresApp: true,
|
|
12
|
+
flags: [
|
|
13
|
+
{ name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
|
|
14
|
+
{ name: 'out', type: 'string', summary: 'where shot.png and verdict.json are written' },
|
|
15
|
+
{ name: 'full', type: 'boolean', summary: 'whole page, not the fold', default: true },
|
|
16
|
+
{ name: 'settle', type: 'string', summary: 'ms to wait after load before capturing' },
|
|
17
|
+
{ name: 'timeout', type: 'string', summary: 'ms one navigation may take' },
|
|
18
|
+
{ name: 'browser', type: 'string', summary: 'Chrome or Chromium binary to launch' },
|
|
19
|
+
{
|
|
20
|
+
name: 'cdp-url',
|
|
21
|
+
type: 'string',
|
|
22
|
+
summary: 'attach to a browser somebody else is running (a provider session, a sidecar)',
|
|
23
|
+
},
|
|
24
|
+
{ name: 'allow-hosts', type: 'string', summary: 'extra hosts the page may request' },
|
|
25
|
+
{
|
|
26
|
+
name: 'theme',
|
|
27
|
+
type: 'string',
|
|
28
|
+
summary: "light or dark, stored as the visitor's choice; absent is the app's own default",
|
|
29
|
+
},
|
|
30
|
+
// A FLAG on `x shot` and never a second command: photographing a route and photographing a
|
|
31
|
+
// component are one job with two subjects, and a parallel command would be the second path
|
|
32
|
+
// axiom 1 refuses.
|
|
33
|
+
{
|
|
34
|
+
name: 'island',
|
|
35
|
+
type: 'string',
|
|
36
|
+
summary: 'photograph one island in every state it declares',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'state',
|
|
40
|
+
type: 'string',
|
|
41
|
+
summary: 'one declared state of that island, not all of them',
|
|
42
|
+
},
|
|
43
|
+
// Its own SPELLING and never `--island` with no value: the parser refuses a bare `--island`
|
|
44
|
+
// ("expects a value") and `--island=` is an empty name, so "every island" had no form a
|
|
45
|
+
// reader could type that could not be read as a mistyped one. A boolean cannot be confused
|
|
46
|
+
// with a name, and `x shot --all-islands` says what it does beside `x shot --island <name>`.
|
|
47
|
+
{
|
|
48
|
+
name: 'all-islands',
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
summary: 'every island in the app, in every state it declares, plus an index.md',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
package/src/cmd-shot.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import { mkdirSync } from 'node:fs';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { IDLE_HYDRATE_TIMEOUT_MS } from '@ultimat3/render';
|
|
11
|
-
import type { ColorScheme, ScrapeDriver, ScrapePage, ScrapeSession } from '@ultimat3/scraping';
|
|
12
|
-
import { DEFAULT_PAGE_TIMEOUT_MS, systemScrapeClock } from '@ultimat3/scraping';
|
|
13
11
|
import { requireAppRoot } from './app-root';
|
|
14
12
|
import { appBrowser } from './browser-launcher';
|
|
13
|
+
import type { ShotColorScheme, ShotDriver, ShotPage, ShotSession } from './browser-launcher-port';
|
|
14
|
+
import { DEFAULT_PAGE_TIMEOUT_MS, systemShotClock } from './cdp-shot-clock';
|
|
15
15
|
import {
|
|
16
16
|
islandShot,
|
|
17
17
|
islandShotResult,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
refuseSweepWithRoute,
|
|
23
23
|
refuseSweepWithState,
|
|
24
24
|
} from './cmd-shot-island';
|
|
25
|
+
import { shotSpec } from './cmd-shot-spec';
|
|
25
26
|
import type { CliCommand, CommandContext } from './command';
|
|
26
27
|
import { BadFlagError, MissingPositionalError } from './errors';
|
|
27
28
|
import { intFlagOr, PORT_RANGE } from './flag-number';
|
|
@@ -181,7 +182,7 @@ const intFlag = (
|
|
|
181
182
|
export interface ShotRun {
|
|
182
183
|
readonly route: string;
|
|
183
184
|
readonly outDir: string;
|
|
184
|
-
readonly driver:
|
|
185
|
+
readonly driver: ShotDriver;
|
|
185
186
|
readonly boot: () => Promise<ShotServer>;
|
|
186
187
|
readonly settleMs: number;
|
|
187
188
|
readonly timeoutMs: number;
|
|
@@ -200,7 +201,7 @@ export interface ShotRun {
|
|
|
200
201
|
* neither: the box's own preference and the app's own default — what `x shot` has always done,
|
|
201
202
|
* and the point of `defaultMode` — and `ui.shot` names one explicitly for exactly that reason.
|
|
202
203
|
*/
|
|
203
|
-
readonly colorScheme?:
|
|
204
|
+
readonly colorScheme?: ShotColorScheme | undefined;
|
|
204
205
|
readonly now?: (() => Date) | undefined;
|
|
205
206
|
/**
|
|
206
207
|
* Something to do with the page AFTER the islands settled and BEFORE the picture — `ui.inspect`
|
|
@@ -209,7 +210,7 @@ export interface ShotRun {
|
|
|
209
210
|
* caller who never calls it gets the count from the first settle.
|
|
210
211
|
*/
|
|
211
212
|
readonly act?:
|
|
212
|
-
| ((page:
|
|
213
|
+
| ((page: ShotPage, settle: () => Promise<IslandCount | null>) => Promise<void>)
|
|
213
214
|
| undefined;
|
|
214
215
|
}
|
|
215
216
|
|
|
@@ -226,7 +227,7 @@ const quietly = async (stop: () => Promise<void>): Promise<void> => {
|
|
|
226
227
|
*/
|
|
227
228
|
export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
|
|
228
229
|
const server = await options.boot();
|
|
229
|
-
let session:
|
|
230
|
+
let session: ShotSession | undefined;
|
|
230
231
|
try {
|
|
231
232
|
const requestedUrl = new URL(options.route, server.url).toString();
|
|
232
233
|
session = await options.driver.open({
|
|
@@ -235,7 +236,7 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
|
|
|
235
236
|
// inside your network is the widest SSRF surface an app can own, and a screenshot command is
|
|
236
237
|
// not the place to open it by default. Every refusal lands in the verdict's `refused` count.
|
|
237
238
|
rules: { allowHosts: allowHostsFrom(server.url, options.extraHosts) },
|
|
238
|
-
clock:
|
|
239
|
+
clock: systemShotClock,
|
|
239
240
|
timeoutMs: options.timeoutMs,
|
|
240
241
|
});
|
|
241
242
|
const page = session.page;
|
|
@@ -312,54 +313,7 @@ export const shotResult = (artifacts: ShotArtifacts): CommandResult => ({
|
|
|
312
313
|
});
|
|
313
314
|
|
|
314
315
|
export const shotCommand: CliCommand = {
|
|
315
|
-
spec:
|
|
316
|
-
name: 'shot',
|
|
317
|
-
summary: 'photograph one route, one island in a state it declares, or every island in the app',
|
|
318
|
-
usage:
|
|
319
|
-
'x shot <route> | --island <name> [--state <id>] | --all-islands [--port 0] [--out <dir>] [--settle 2000] [--json]',
|
|
320
|
-
requiresApp: true,
|
|
321
|
-
flags: [
|
|
322
|
-
{ name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
|
|
323
|
-
{ name: 'out', type: 'string', summary: 'where shot.png and verdict.json are written' },
|
|
324
|
-
{ name: 'full', type: 'boolean', summary: 'whole page, not the fold', default: true },
|
|
325
|
-
{ name: 'settle', type: 'string', summary: 'ms to wait after load before capturing' },
|
|
326
|
-
{ name: 'timeout', type: 'string', summary: 'ms one navigation may take' },
|
|
327
|
-
{ name: 'browser', type: 'string', summary: 'browser executable puppeteer-core launches' },
|
|
328
|
-
{
|
|
329
|
-
name: 'cdp-url',
|
|
330
|
-
type: 'string',
|
|
331
|
-
summary: 'attach to a browser somebody else is running (a provider session, a sidecar)',
|
|
332
|
-
},
|
|
333
|
-
{ name: 'allow-hosts', type: 'string', summary: 'extra hosts the page may request' },
|
|
334
|
-
{
|
|
335
|
-
name: 'theme',
|
|
336
|
-
type: 'string',
|
|
337
|
-
summary: "light or dark, stored as the visitor's choice; absent is the app's own default",
|
|
338
|
-
},
|
|
339
|
-
// A FLAG on `x shot` and never a second command: photographing a route and photographing a
|
|
340
|
-
// component are one job with two subjects, and a parallel command would be the second path
|
|
341
|
-
// axiom 1 refuses.
|
|
342
|
-
{
|
|
343
|
-
name: 'island',
|
|
344
|
-
type: 'string',
|
|
345
|
-
summary: 'photograph one island in every state it declares',
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
name: 'state',
|
|
349
|
-
type: 'string',
|
|
350
|
-
summary: 'one declared state of that island, not all of them',
|
|
351
|
-
},
|
|
352
|
-
// Its own SPELLING and never `--island` with no value: the parser refuses a bare `--island`
|
|
353
|
-
// ("expects a value") and `--island=` is an empty name, so "every island" had no form a
|
|
354
|
-
// reader could type that could not be read as a mistyped one. A boolean cannot be confused
|
|
355
|
-
// with a name, and `x shot --all-islands` says what it does beside `x shot --island <name>`.
|
|
356
|
-
{
|
|
357
|
-
name: 'all-islands',
|
|
358
|
-
type: 'boolean',
|
|
359
|
-
summary: 'every island in the app, in every state it declares, plus an index.md',
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
},
|
|
316
|
+
spec: shotSpec,
|
|
363
317
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
364
318
|
const root = requireAppRoot('shot', ctx.cwd).dir;
|
|
365
319
|
// Every value read before anything boots: a typo must not cost a browser and a dev server to
|
|
@@ -396,8 +350,7 @@ export const shotCommand: CliCommand = {
|
|
|
396
350
|
// Which browser this run gets — start one here, or attach to one somebody else is running.
|
|
397
351
|
// Decided by `shot-browser.ts` over plain inputs, and decided HERE, before a dev server or a
|
|
398
352
|
// provider session exists to pay for a typo. It also PROBES for an installed Chrome and refuses
|
|
399
|
-
// when there is none
|
|
400
|
-
// a library throw one embedded Postgres later.
|
|
353
|
+
// when there is none, so a missing browser costs no embedded Postgres boot.
|
|
401
354
|
const { cdpUrl, executablePath } = shotBrowserChoice({
|
|
402
355
|
cdpFlag: flagString(ctx.args, 'cdp-url'),
|
|
403
356
|
browserFlag: flagString(ctx.args, 'browser'),
|
|
@@ -423,10 +376,8 @@ export const shotCommand: CliCommand = {
|
|
|
423
376
|
await islandShot({ ...shared, island, ...(state === undefined ? {} : { state }) }),
|
|
424
377
|
);
|
|
425
378
|
}
|
|
426
|
-
//
|
|
427
|
-
// an embedded Postgres to be told to run `bun add -d puppeteer-core`.
|
|
379
|
+
// Built before the boot, for the same reason.
|
|
428
380
|
const driver = await appBrowser({
|
|
429
|
-
root,
|
|
430
381
|
...(executablePath === undefined ? {} : { executablePath }),
|
|
431
382
|
...(cdpUrl === undefined ? {} : { cdpUrl }),
|
|
432
383
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// `x tasks`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-tasks.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const tasksSpec: CommandSpec = {
|
|
7
|
+
name: 'tasks',
|
|
8
|
+
summary: 'cron tasks, their timezone and their next run',
|
|
9
|
+
usage: 'x tasks [list|show <name>] [--count n] [--json]',
|
|
10
|
+
requiresApp: true,
|
|
11
|
+
subcommands: ['list', 'show'],
|
|
12
|
+
defaultSubcommand: 'list',
|
|
13
|
+
flags: [
|
|
14
|
+
{
|
|
15
|
+
name: 'count',
|
|
16
|
+
type: 'string',
|
|
17
|
+
summary: 'show: how many upcoming occurrences to list',
|
|
18
|
+
subcommands: ['show'],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
package/src/cmd-tasks.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { TaskHandle } from '@ultimat3/jobs';
|
|
|
8
8
|
import type { CronPhrases } from '@ultimat3/time';
|
|
9
9
|
import { loadApp } from './app-load';
|
|
10
10
|
import { requireAppRoot } from './app-root';
|
|
11
|
+
import { tasksSpec } from './cmd-tasks-spec';
|
|
11
12
|
import type { CliCommand, CommandContext } from './command';
|
|
12
13
|
import { BadFlagError, DeclarationUnknownError } from './errors';
|
|
13
14
|
import { msg } from './messages';
|
|
@@ -130,22 +131,7 @@ function runShow(ctx: CommandContext, nowMs: number, findings: readonly Finding[
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
export const tasksCommand: CliCommand = {
|
|
133
|
-
spec:
|
|
134
|
-
name: 'tasks',
|
|
135
|
-
summary: 'cron tasks, their timezone and their next run',
|
|
136
|
-
usage: 'x tasks [list|show <name>] [--count n] [--json]',
|
|
137
|
-
requiresApp: true,
|
|
138
|
-
subcommands: ['list', 'show'],
|
|
139
|
-
defaultSubcommand: 'list',
|
|
140
|
-
flags: [
|
|
141
|
-
{
|
|
142
|
-
name: 'count',
|
|
143
|
-
type: 'string',
|
|
144
|
-
summary: 'show: how many upcoming occurrences to list',
|
|
145
|
-
subcommands: ['show'],
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
},
|
|
134
|
+
spec: tasksSpec,
|
|
149
135
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
150
136
|
const root = requireAppRoot('tasks', ctx.cwd).dir;
|
|
151
137
|
const { findings } = await loadApp(root);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// `x test`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-test.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import { TEST_TYPES } from '@ultimat3/testing/test-types';
|
|
5
|
+
import { DEFAULT_BASE } from './affected';
|
|
6
|
+
import type { CommandSpec } from './parse';
|
|
7
|
+
import { SERIAL_TYPES, WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
|
|
8
|
+
|
|
9
|
+
export const testSpec: CommandSpec = {
|
|
10
|
+
name: 'test',
|
|
11
|
+
summary:
|
|
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 text] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--json] [-- <bun test flags>]`,
|
|
14
|
+
positionalChoices: TEST_TYPES,
|
|
15
|
+
// The one command that hands a tail to another tool — `bun test` — and the reason
|
|
16
|
+
// `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
|
|
17
|
+
// dropped both, so a run that measured no coverage reported exactly what a coverage run does.
|
|
18
|
+
passthrough: true,
|
|
19
|
+
flags: [
|
|
20
|
+
{
|
|
21
|
+
name: 'workers',
|
|
22
|
+
type: 'string',
|
|
23
|
+
summary: `bun worker count (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'worker',
|
|
27
|
+
type: 'string',
|
|
28
|
+
summary:
|
|
29
|
+
'run only shard I of an N-way split of the selection, serially — one CI job\u2019s share',
|
|
30
|
+
},
|
|
31
|
+
{ name: 'filter', type: 'string', summary: 'only files whose path contains this substring' },
|
|
32
|
+
{
|
|
33
|
+
name: 'sample',
|
|
34
|
+
type: 'string',
|
|
35
|
+
summary:
|
|
36
|
+
'run at most N files of the selected type — a fast signal for the eval loop, never a gate',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'affected',
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
summary: 'only the workspaces a diff touches, and everything that depends on one of them',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'base',
|
|
45
|
+
type: 'string',
|
|
46
|
+
summary: `--affected: git ref to diff against, merge-base style (default: ${DEFAULT_BASE})`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'dirty',
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
summary: '--affected: also count uncommitted work, whichever agent in this checkout made it',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
package/src/cmd-test.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AffectedScope } from './affected';
|
|
9
9
|
import { affectedScope, affectedScopeJson, DEFAULT_BASE, inScope } from './affected';
|
|
10
|
+
import { testSpec } from './cmd-test-spec';
|
|
10
11
|
import type { CliCommand, CommandContext } from './command';
|
|
11
12
|
import { ok } from './command';
|
|
12
13
|
import { BadFlagError, NoTestFilesError } from './errors';
|
|
@@ -18,9 +19,9 @@ import { flagBool, flagString } from './parse';
|
|
|
18
19
|
import { quoteArg } from './shell-quote';
|
|
19
20
|
import { discoverTests, missingSelection, readSample, readType, sampleFiles } from './test-select';
|
|
20
21
|
import { runShards } from './test-shards';
|
|
21
|
-
import { defaultWorkers,
|
|
22
|
+
import { defaultWorkers, SERIAL_TYPES, WORKER_CEILING } from './test-workers';
|
|
22
23
|
import type { TestType } from './verify-tests';
|
|
23
|
-
import {
|
|
24
|
+
import { TEST_TYPES } from './verify-tests';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* `--workers` and `--shard`. `Number.parseInt` alone accepted `4abc` and `4.9` as four, while
|
|
@@ -100,53 +101,7 @@ const withScope = (result: CommandResult, scope: AffectedScope): CommandResult =
|
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
export const testCommand: CliCommand = {
|
|
103
|
-
spec:
|
|
104
|
-
name: 'test',
|
|
105
|
-
summary:
|
|
106
|
-
'run one test type — or the whole suite — across N workers, one isolated database per worker',
|
|
107
|
-
usage: `x test [${TEST_TYPES.join('|')}] [--filter text] [--sample N] [--affected [--base ref] [--dirty]] [--workers N] [--worker I] [--json] [-- <bun test flags>]`,
|
|
108
|
-
positionalChoices: TEST_TYPES,
|
|
109
|
-
// The one command that hands a tail to another tool — `bun test` — and the reason
|
|
110
|
-
// `CommandSpec.passthrough` exists: `x test unit -- --coverage --bail` parsed both flags and
|
|
111
|
-
// dropped both, so a run that measured no coverage reported exactly what a coverage run does.
|
|
112
|
-
passthrough: true,
|
|
113
|
-
flags: [
|
|
114
|
-
{
|
|
115
|
-
name: 'workers',
|
|
116
|
-
type: 'string',
|
|
117
|
-
summary: `bun worker count (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING}); clamped to the file count, and to 1 for ${SERIAL_TYPES.join(' and ')}`,
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
name: 'worker',
|
|
121
|
-
type: 'string',
|
|
122
|
-
summary:
|
|
123
|
-
'run only shard I of an N-way split of the selection, serially — one CI job\u2019s share',
|
|
124
|
-
},
|
|
125
|
-
{ name: 'filter', type: 'string', summary: 'only files whose path contains this substring' },
|
|
126
|
-
{
|
|
127
|
-
name: 'sample',
|
|
128
|
-
type: 'string',
|
|
129
|
-
summary:
|
|
130
|
-
'run at most N files of the selected type — a fast signal for the eval loop, never a gate',
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: 'affected',
|
|
134
|
-
type: 'boolean',
|
|
135
|
-
summary: 'only the workspaces a diff touches, and everything that depends on one of them',
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: 'base',
|
|
139
|
-
type: 'string',
|
|
140
|
-
summary: `--affected: git ref to diff against, merge-base style (default: ${DEFAULT_BASE})`,
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: 'dirty',
|
|
144
|
-
type: 'boolean',
|
|
145
|
-
summary:
|
|
146
|
-
'--affected: also count uncommitted work, whichever agent in this checkout made it',
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
},
|
|
104
|
+
spec: testSpec,
|
|
150
105
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
151
106
|
const type = readOnlyType(ctx.args.positionals);
|
|
152
107
|
const filter = flagString(ctx.args, 'filter');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `x verify`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-verify.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
import { WORKER_CEILING, WORKER_FLOOR, WORKER_OVERSUBSCRIBE } from './test-workers';
|
|
6
|
+
|
|
7
|
+
export const verifySpec: CommandSpec = {
|
|
8
|
+
name: 'verify',
|
|
9
|
+
summary: 'the gate: typecheck, lint, boundaries, all tests, drift, contract, budgets',
|
|
10
|
+
usage: 'x verify [--only <step>] [--workers N] [--json]',
|
|
11
|
+
requiresApp: true,
|
|
12
|
+
// Two flags, and only one of them narrows. `--workers` changes how wide the test steps
|
|
13
|
+
// spread, never which steps run. `--only` runs one step and says so in both renderers —
|
|
14
|
+
// never silently, which is the whole of what makes it safe to have.
|
|
15
|
+
flags: [
|
|
16
|
+
{
|
|
17
|
+
name: 'workers',
|
|
18
|
+
type: 'string',
|
|
19
|
+
summary: `test processes per parallel step (default: ${WORKER_OVERSUBSCRIBE}x CPUs, min ${WORKER_FLOOR}, max ${WORKER_CEILING})`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'only',
|
|
23
|
+
type: 'string',
|
|
24
|
+
summary:
|
|
25
|
+
'run ONE step by name — an iteration loop, NOT A GATE RUN; the gate is this command with no flag',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|