@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,21 @@
|
|
|
1
|
+
// `x doctor`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-doctor.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
/** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
|
|
7
|
+
export const DEFAULT_DOCTOR_PORT = 3000;
|
|
8
|
+
|
|
9
|
+
export const doctorSpec: CommandSpec = {
|
|
10
|
+
name: 'doctor',
|
|
11
|
+
summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
|
|
12
|
+
usage: 'x doctor [--port 3000] [--json]',
|
|
13
|
+
flags: [
|
|
14
|
+
{
|
|
15
|
+
name: 'port',
|
|
16
|
+
type: 'string',
|
|
17
|
+
summary: 'port to test',
|
|
18
|
+
default: String(DEFAULT_DOCTOR_PORT),
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
package/src/cmd-doctor.ts
CHANGED
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
} from '@ultimat3/db';
|
|
20
20
|
import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
|
|
21
21
|
import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
|
|
22
|
+
import { DEFAULT_DOCTOR_PORT, doctorSpec } from './cmd-doctor-spec';
|
|
22
23
|
import type { CliCommand, CommandContext } from './command';
|
|
23
24
|
import { checkMigrationSnapshots } from './db-snapshot';
|
|
24
|
-
import { syncPortFor } from './dev-sync';
|
|
25
25
|
import type { OfflineFallbackFact } from './doctor-offline';
|
|
26
26
|
import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
|
|
27
27
|
import { intFlagOr, PORT_RANGE, portPairAfter } from './flag-number';
|
|
@@ -31,6 +31,7 @@ import type { CommandResult, Finding } from './output';
|
|
|
31
31
|
import { findingFrom } from './output';
|
|
32
32
|
import type { ParsedArgs } from './parse';
|
|
33
33
|
import { portFree } from './port-probe';
|
|
34
|
+
import { syncPortFor } from './role-sync';
|
|
34
35
|
import { checkMigrationDrift } from './schema-drift';
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -129,9 +130,6 @@ export const embeddedDatabaseFinding = (fact: EmbeddedDatabase): Finding | undef
|
|
|
129
130
|
/** The file `x doctor` reports missing, and the one the reader creates. */
|
|
130
131
|
export const ENV_DEVELOPMENT = '.env.development';
|
|
131
132
|
|
|
132
|
-
/** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
|
|
133
|
-
const DEFAULT_DOCTOR_PORT = 3000;
|
|
134
|
-
|
|
135
133
|
/**
|
|
136
134
|
* Both ports `x dev` binds, each labelled with the role that wants it. `x dev --port 3999` printed
|
|
137
135
|
* `web listening on 3999`, then died on 4000 as `X_CLI_UNEXPECTED` with a caught `Error` rendered
|
|
@@ -144,7 +142,7 @@ const DEFAULT_DOCTOR_PORT = 3000;
|
|
|
144
142
|
* N+1 is still not a runnable command. It did not move both until 2026-09 — the line was
|
|
145
143
|
* `neighbouringPort(probe.port)`, which for the sync finding IS the port the finding is about, and
|
|
146
144
|
* a docblock claiming otherwise is how it survived. `portPairAfter` is the one reader of that rule
|
|
147
|
-
* and `
|
|
145
|
+
* and `role-sync.ts`'s own refusal shares it.
|
|
148
146
|
*
|
|
149
147
|
* The sync port is `syncPortFor`, never `neighbouringPort` again: that helper answers 65534 for a
|
|
150
148
|
* web port of 65535 — BELOW the web port, and a port `x dev` never binds — where the boot refuses
|
|
@@ -400,19 +398,7 @@ export function probeFor(cwd: string, bunVersion: string, port: number): DoctorP
|
|
|
400
398
|
}
|
|
401
399
|
|
|
402
400
|
export const doctorCommand: CliCommand = {
|
|
403
|
-
spec:
|
|
404
|
-
name: 'doctor',
|
|
405
|
-
summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
|
|
406
|
-
usage: 'x doctor [--port 3000] [--json]',
|
|
407
|
-
flags: [
|
|
408
|
-
{
|
|
409
|
-
name: 'port',
|
|
410
|
-
type: 'string',
|
|
411
|
-
summary: 'port to test',
|
|
412
|
-
default: String(DEFAULT_DOCTOR_PORT),
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
},
|
|
401
|
+
spec: doctorSpec,
|
|
416
402
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
417
403
|
const port = doctorPort(ctx.args);
|
|
418
404
|
const findings = await runDoctor(probeFor(ctx.cwd, ctx.bunVersion, port));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `x env`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-env.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import { ENV_SCHEMA_EXPORT } from './app-env';
|
|
5
|
+
import { APP_CONFIG_FILE } from './app-root';
|
|
6
|
+
import type { CommandSpec } from './parse';
|
|
7
|
+
|
|
8
|
+
export const envSpec: CommandSpec = {
|
|
9
|
+
name: 'env',
|
|
10
|
+
summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
|
|
11
|
+
usage: 'x env [check|example] [--json]',
|
|
12
|
+
requiresApp: true,
|
|
13
|
+
subcommands: ['check', 'example'],
|
|
14
|
+
// The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
|
|
15
|
+
// framework already tells its reader to run.
|
|
16
|
+
defaultSubcommand: 'check',
|
|
17
|
+
flags: [],
|
|
18
|
+
};
|
package/src/cmd-env.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { checkEnv, ENV_EXAMPLE_PATH, ERROR_DOCS_URL, maskedEnvValues } from '@ultimat3/core';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { envExampleFor, loadEnvSchema } from './app-env';
|
|
9
|
+
import { requireAppRoot } from './app-root';
|
|
10
|
+
import { envSpec } from './cmd-env-spec';
|
|
10
11
|
import type { CliCommand, CommandContext } from './command';
|
|
11
12
|
import { EnvSchemaMissingError } from './errors';
|
|
12
13
|
import { msg } from './messages';
|
|
@@ -73,17 +74,7 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export const envCommand: CliCommand = {
|
|
76
|
-
spec:
|
|
77
|
-
name: 'env',
|
|
78
|
-
summary: `the typed environment declared by ${ENV_SCHEMA_EXPORT} in ${APP_CONFIG_FILE}`,
|
|
79
|
-
usage: 'x env [check|example] [--json]',
|
|
80
|
-
requiresApp: true,
|
|
81
|
-
subcommands: ['check', 'example'],
|
|
82
|
-
// The bare `x env` answers the question the fix line on every `X_ENV_MISSING` in this
|
|
83
|
-
// framework already tells its reader to run.
|
|
84
|
-
defaultSubcommand: 'check',
|
|
85
|
-
flags: [],
|
|
86
|
-
},
|
|
77
|
+
spec: envSpec,
|
|
87
78
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
88
79
|
// `subcommand`, never `positionals[0]`: the parser has already lifted a declared subcommand
|
|
89
80
|
// out of the positionals, so reading the array here matches nothing and every invocation
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// `x errors`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-errors.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const ERRORS_SUBCOMMANDS = ['explain', 'list'] as const;
|
|
7
|
+
|
|
8
|
+
export const errorsSpec: CommandSpec = {
|
|
9
|
+
name: 'errors',
|
|
10
|
+
summary: 'an X_* code, explained: cause, runnable fix, docs URL',
|
|
11
|
+
usage: 'x errors [explain <CODE>|list] [--json]',
|
|
12
|
+
subcommands: ERRORS_SUBCOMMANDS,
|
|
13
|
+
// `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
|
|
14
|
+
// which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
|
|
15
|
+
// to a caller who meant to explain one — see `MissingPositionalError`'s own note.
|
|
16
|
+
defaultSubcommand: 'explain',
|
|
17
|
+
// `x errors X_PERMISSION_UNKNOWN` is the form every reader tries first — `x help` prints
|
|
18
|
+
// `errors an X_* code, explained`, which reads as exactly that — and it answered
|
|
19
|
+
// `X_CLI_UNKNOWN_COMMAND … fix: x help`, which leads back to the line that suggested it.
|
|
20
|
+
// Safe to declare here and nowhere else so far: the only thing that is not `explain` or
|
|
21
|
+
// `list` in this slot is a code, and a near miss of either is still refused (#F16).
|
|
22
|
+
defaultSubcommandTakesPositional: true,
|
|
23
|
+
};
|
package/src/cmd-errors.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { nearestName, singleLine } from '@ultimat3/core';
|
|
|
5
5
|
// a plausible-sounding explanation an agent would then act on.
|
|
6
6
|
|
|
7
7
|
import type { ErrorExplanation } from '@ultimat3/mcp';
|
|
8
|
+
import { errorsSpec } from './cmd-errors-spec';
|
|
8
9
|
import type { CliCommand, CommandContext } from './command';
|
|
9
10
|
import type { ErrorCatalog } from './error-catalog';
|
|
10
11
|
import { loadErrorCatalog } from './error-catalog';
|
|
@@ -14,7 +15,7 @@ import { explainErrorCode, explainEveryErrorCode } from './mcp-errors';
|
|
|
14
15
|
import { msg } from './messages';
|
|
15
16
|
import type { CommandResult, JsonValue } from './output';
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
+
export { ERRORS_SUBCOMMANDS } from './cmd-errors-spec';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* `site` is the throw site as DATA, and it is why the `fix:` for a code whose fix is built at run
|
|
@@ -85,22 +86,7 @@ function listAll(catalog: ErrorCatalog): CommandResult {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export const errorsCommand: CliCommand = {
|
|
88
|
-
spec:
|
|
89
|
-
name: 'errors',
|
|
90
|
-
summary: 'an X_* code, explained: cause, runnable fix, docs URL',
|
|
91
|
-
usage: 'x errors [explain <CODE>|list] [--json]',
|
|
92
|
-
subcommands: ERRORS_SUBCOMMANDS,
|
|
93
|
-
// `explain`, deliberately: the bare `x errors` then answers with `MissingPositionalError`,
|
|
94
|
-
// which names `<CODE>` and hands back a real invocation. `list` would silently print 200 rows
|
|
95
|
-
// to a caller who meant to explain one — see `MissingPositionalError`'s own note.
|
|
96
|
-
defaultSubcommand: 'explain',
|
|
97
|
-
// `x errors X_PERMISSION_UNKNOWN` is the form every reader tries first — `x help` prints
|
|
98
|
-
// `errors an X_* code, explained`, which reads as exactly that — and it answered
|
|
99
|
-
// `X_CLI_UNKNOWN_COMMAND … fix: x help`, which leads back to the line that suggested it.
|
|
100
|
-
// Safe to declare here and nowhere else so far: the only thing that is not `explain` or
|
|
101
|
-
// `list` in this slot is a code, and a near miss of either is still refused (#F16).
|
|
102
|
-
defaultSubcommandTakesPositional: true,
|
|
103
|
-
},
|
|
89
|
+
spec: errorsSpec,
|
|
104
90
|
// `async` is load-bearing: a synchronous throw would escape every caller that awaits the
|
|
105
91
|
// promise this signature promises, including the dispatcher's own error path.
|
|
106
92
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `x fix`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-fix.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const FIX_SUBCOMMANDS = ['boundary'] as const;
|
|
7
|
+
|
|
8
|
+
export const fixSpec: CommandSpec = {
|
|
9
|
+
name: 'fix',
|
|
10
|
+
// Says "plan" in the one line `x help` prints. The name is kept — five packages' `fix:` lines
|
|
11
|
+
// cite `x fix boundary <file>` and renaming a shipped command breaks every one of them — so
|
|
12
|
+
// the honest move is to stop the summary from promising a repair the command never performs.
|
|
13
|
+
summary: 'plan the minimal cut for an import that crossed a surface boundary (never rewrites)',
|
|
14
|
+
usage: 'x fix boundary <file> [--json]',
|
|
15
|
+
requiresApp: true,
|
|
16
|
+
subcommands: FIX_SUBCOMMANDS,
|
|
17
|
+
defaultSubcommand: 'boundary',
|
|
18
|
+
};
|
package/src/cmd-fix.ts
CHANGED
|
@@ -8,16 +8,17 @@ import { appImportGraph, readAppSources } from './app-boundaries';
|
|
|
8
8
|
import { requireAppRoot } from './app-root';
|
|
9
9
|
import type { BoundaryCut } from './boundary-cuts';
|
|
10
10
|
import { planBoundaryCuts } from './boundary-cuts';
|
|
11
|
+
import { fixSpec } from './cmd-fix-spec';
|
|
11
12
|
import type { CliCommand, CommandContext } from './command';
|
|
12
13
|
import { BadFlagError, FixTargetUnknownError, MissingPositionalError } from './errors';
|
|
13
14
|
import { msg } from './messages';
|
|
14
15
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
15
16
|
|
|
17
|
+
export { FIX_SUBCOMMANDS } from './cmd-fix-spec';
|
|
18
|
+
|
|
16
19
|
export type { BoundaryCut };
|
|
17
20
|
export { planBoundaryCuts };
|
|
18
21
|
|
|
19
|
-
export const FIX_SUBCOMMANDS = ['boundary'] as const;
|
|
20
|
-
|
|
21
22
|
/**
|
|
22
23
|
* Accept either an app-root-relative path or a suffix that matches exactly one scanned file —
|
|
23
24
|
* an agent copying the path out of a `fix:` line has the short form
|
|
@@ -88,17 +89,7 @@ const editCount = (cuts: readonly BoundaryCut[]): number =>
|
|
|
88
89
|
new Set(cuts.map((cut) => JSON.stringify([cut.edge.from, cut.edge.to]))).size;
|
|
89
90
|
|
|
90
91
|
export const fixCommand: CliCommand = {
|
|
91
|
-
spec:
|
|
92
|
-
name: 'fix',
|
|
93
|
-
// Says "plan" in the one line `x help` prints. The name is kept — five packages' `fix:` lines
|
|
94
|
-
// cite `x fix boundary <file>` and renaming a shipped command breaks every one of them — so
|
|
95
|
-
// the honest move is to stop the summary from promising a repair the command never performs.
|
|
96
|
-
summary: 'plan the minimal cut for an import that crossed a surface boundary (never rewrites)',
|
|
97
|
-
usage: 'x fix boundary <file> [--json]',
|
|
98
|
-
requiresApp: true,
|
|
99
|
-
subcommands: FIX_SUBCOMMANDS,
|
|
100
|
-
defaultSubcommand: 'boundary',
|
|
101
|
-
},
|
|
92
|
+
spec: fixSpec,
|
|
102
93
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
103
94
|
const root = requireAppRoot('fix', ctx.cwd).dir;
|
|
104
95
|
// Refused before the scan, never defaulted to `''`: an empty string reached `resolveTarget` as
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `x generate`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-generate.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import { GENERATORS } from './generate-kinds';
|
|
5
|
+
import type { CommandSpec } from './parse';
|
|
6
|
+
|
|
7
|
+
export const generateSpec: CommandSpec = {
|
|
8
|
+
name: 'g',
|
|
9
|
+
aliases: ['generate'],
|
|
10
|
+
summary: 'scaffold a primitive with its passing test',
|
|
11
|
+
// Projected from `GENERATORS`, never restated: the literal that used to live here had already
|
|
12
|
+
// drifted — it omitted `backfill` — and a usage line that can disagree with the list it
|
|
13
|
+
// describes is exactly the second source of truth axiom 2 forbids.
|
|
14
|
+
usage: `x g ${GENERATORS.join('|')} <name> [--feature f]`,
|
|
15
|
+
// Declared from the SAME constant `readKind` validates against: without it `fix-command.ts`
|
|
16
|
+
// has no set to judge the word after `x g`, and two shipped `@ultimat3/admin` fix lines said
|
|
17
|
+
// `x g migration` — a generator that has never existed — straight through the `errors` gate.
|
|
18
|
+
positionalChoices: GENERATORS,
|
|
19
|
+
requiresApp: true,
|
|
20
|
+
flags: [
|
|
21
|
+
{ name: 'feature', type: 'string', summary: 'feature slice to write into' },
|
|
22
|
+
{ name: 'surface', type: 'string', summary: 'site | app', default: 'app' },
|
|
23
|
+
{ name: 'live', type: 'boolean', summary: 'subscribable query' },
|
|
24
|
+
{ name: 'admin', type: 'boolean', summary: 'resource: also emit the admin override' },
|
|
25
|
+
{ name: 'locales', type: 'string', summary: 'comma-separated locales, default en' },
|
|
26
|
+
{ name: 'at', type: 'string', summary: 'island, admin:page: directory to write into' },
|
|
27
|
+
{ name: 'permission', type: 'string', summary: 'admin:page: the permission it needs' },
|
|
28
|
+
{ name: 'force', type: 'boolean', summary: 'overwrite existing files' },
|
|
29
|
+
{ name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
|
|
30
|
+
],
|
|
31
|
+
};
|
package/src/cmd-generate.ts
CHANGED
|
@@ -4,19 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { MANIFEST_FILENAME } from '@ultimat3/manifest';
|
|
7
|
-
import {
|
|
7
|
+
import { registerGeneratedPrimitives } from './api-registration';
|
|
8
|
+
import { writeAppArtifacts } from './app-artifacts';
|
|
9
|
+
import { appManifest } from './app-manifest';
|
|
8
10
|
import { requireAppRoot } from './app-root';
|
|
11
|
+
import { generateSpec } from './cmd-generate-spec';
|
|
9
12
|
import type { CliCommand, CommandContext } from './command';
|
|
13
|
+
import { invocationOf } from './command';
|
|
14
|
+
import { assertFeatureExists } from './generate-feature';
|
|
10
15
|
import { generate, sliceDir } from './generate-files';
|
|
16
|
+
import { grantGeneratedPermissions } from './generate-grants';
|
|
11
17
|
import type { Generator } from './generate-kinds';
|
|
12
|
-
import {
|
|
18
|
+
import { readFeature, readKind, readName, readPermission, readSurface } from './generate-kinds';
|
|
13
19
|
import { containedPath, writeFiles } from './generate-write';
|
|
14
20
|
import { resolveCatalogModule } from './i18n-audit';
|
|
15
21
|
import { syncI18nIndex } from './i18n-index';
|
|
22
|
+
import { reproducedFlags } from './invocation-flags';
|
|
16
23
|
import { msg } from './messages';
|
|
17
24
|
import type { CommandResult, Finding } from './output';
|
|
18
25
|
import { flagBool, flagList, flagString } from './parse';
|
|
19
|
-
import {
|
|
26
|
+
import { quoteArg } from './shell-quote';
|
|
27
|
+
import { kebab, resolveLocales } from './templates';
|
|
20
28
|
|
|
21
29
|
// One import path for the generator, unchanged by the split: `index.ts`, `x new` and the scaffold
|
|
22
30
|
// fixture reach the kinds, the pure file list and the writer through this module, and a second path
|
|
@@ -29,36 +37,12 @@ export type { WriteReport } from './generate-write';
|
|
|
29
37
|
export { dedupe, writeFiles } from './generate-write';
|
|
30
38
|
|
|
31
39
|
export const generateCommand: CliCommand = {
|
|
32
|
-
spec:
|
|
33
|
-
name: 'g',
|
|
34
|
-
aliases: ['generate'],
|
|
35
|
-
summary: 'scaffold a primitive with its passing test',
|
|
36
|
-
// Projected from `GENERATORS`, never restated: the literal that used to live here had already
|
|
37
|
-
// drifted — it omitted `backfill` — and a usage line that can disagree with the list it
|
|
38
|
-
// describes is exactly the second source of truth axiom 2 forbids.
|
|
39
|
-
usage: `x g ${GENERATORS.join('|')} <name> [--feature f]`,
|
|
40
|
-
// Declared from the SAME constant `readKind` validates against: without it `fix-command.ts`
|
|
41
|
-
// has no set to judge the word after `x g`, and two shipped `@ultimat3/admin` fix lines said
|
|
42
|
-
// `x g migration` — a generator that has never existed — straight through the `errors` gate.
|
|
43
|
-
positionalChoices: GENERATORS,
|
|
44
|
-
requiresApp: true,
|
|
45
|
-
flags: [
|
|
46
|
-
{ name: 'feature', type: 'string', summary: 'feature slice to write into' },
|
|
47
|
-
{ name: 'surface', type: 'string', summary: 'site | app', default: 'app' },
|
|
48
|
-
{ name: 'live', type: 'boolean', summary: 'subscribable query' },
|
|
49
|
-
{ name: 'admin', type: 'boolean', summary: 'resource: also emit the admin override' },
|
|
50
|
-
{ name: 'locales', type: 'string', summary: 'comma-separated locales, default en' },
|
|
51
|
-
{ name: 'at', type: 'string', summary: 'island, admin:page: directory to write into' },
|
|
52
|
-
{ name: 'permission', type: 'string', summary: 'admin:page: the permission it needs' },
|
|
53
|
-
{ name: 'force', type: 'boolean', summary: 'overwrite existing files' },
|
|
54
|
-
{ name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
40
|
+
spec: generateSpec,
|
|
57
41
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
58
42
|
const root = requireAppRoot('g', ctx.cwd).dir;
|
|
59
43
|
const kind = readKind(ctx.args.positionals[0]);
|
|
60
44
|
const name = readName(ctx.args.positionals[1], kind);
|
|
61
|
-
const featureFlag = flagString(ctx.args, 'feature');
|
|
45
|
+
const featureFlag = readFeature(flagString(ctx.args, 'feature'), kind);
|
|
62
46
|
// Both flags are resolved before a single file is planned: a bad surface or a locale that is
|
|
63
47
|
// really a path fails here, with nothing written and nothing to undo.
|
|
64
48
|
const surface = readSurface(flagString(ctx.args, 'surface'), kind, name);
|
|
@@ -72,7 +56,9 @@ export const generateCommand: CliCommand = {
|
|
|
72
56
|
// imports `useT()` from is a fact about THIS app, and `generate` is a pure function.
|
|
73
57
|
const catalogModule = await resolveCatalogModule(root);
|
|
74
58
|
// Read for the same reason: which errors the slice declares is written on THIS app's disk.
|
|
75
|
-
const slice = sliceDir(surface, featureFlag ?? name);
|
|
59
|
+
const slice = sliceDir(surface, kebab(featureFlag ?? name));
|
|
60
|
+
// A named slice that is not there is refused, never invented (X_FEATURE_UNKNOWN).
|
|
61
|
+
assertFeatureExists(root, kind, featureFlag, slice);
|
|
76
62
|
const sliceErrors = await readSliceErrors(root, kind, slice);
|
|
77
63
|
// Same reason again: whether `job`/`task` may assume the tenant-scoped shape is a fact about
|
|
78
64
|
// THIS feature's own `entity.ts`/`repo.ts`, not a default the template gets to assume.
|
|
@@ -102,16 +88,27 @@ export const generateCommand: CliCommand = {
|
|
|
102
88
|
lines: files.map((file) => msg('cli.file.added', { path: file.path })),
|
|
103
89
|
};
|
|
104
90
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
// The caller's own invocation, EVERY flag it set included: without `--feature` the fix wrote
|
|
92
|
+
// a second slice beside the one that conflicted.
|
|
93
|
+
const invocation = [
|
|
94
|
+
invocationOf(ctx, 'g'),
|
|
95
|
+
kind,
|
|
96
|
+
quoteArg(name),
|
|
97
|
+
...reproducedFlags(generateCommand.spec, ctx.args),
|
|
98
|
+
].join(' ');
|
|
99
|
+
const report = await writeFiles(root, files, flagBool(ctx.args, 'force'), invocation);
|
|
100
|
+
// The two edits a generated primitive needs outside its own slice, performed rather than left
|
|
101
|
+
// as findings: a declared permission granted to a role, and a job listed in `defineApi`.
|
|
102
|
+
// Before the manifest load below, so the projection sees both.
|
|
103
|
+
const edited = [
|
|
104
|
+
...(await grantGeneratedPermissions(root, report.written)),
|
|
105
|
+
...(await registerGeneratedPrimitives(root, report.written)),
|
|
106
|
+
];
|
|
111
107
|
// A locale's catalog existing on disk and the app being able to select it are two different
|
|
112
108
|
// facts — see `syncI18nIndex`. Runs before the manifest load below so a route or resource
|
|
113
109
|
// this same invocation just wrote never gets projected against a stale catalog registration.
|
|
114
|
-
|
|
110
|
+
const indexSync =
|
|
111
|
+
report.written.length > 0 ? await syncI18nIndex(root) : { registered: true, findings: [] };
|
|
115
112
|
// Facts, not prose: every `x g` run leaves the route/action/entity/job/policy table current,
|
|
116
113
|
// the same guarantee `x manifest` makes on its own — an agent reading it after `x g` never
|
|
117
114
|
// sees a resource that exists on disk but not in the manifest.
|
|
@@ -124,18 +121,23 @@ export const generateCommand: CliCommand = {
|
|
|
124
121
|
// partial load would replace the compatibility contract with a subset of the app. The scaffold
|
|
125
122
|
// stays on disk — only the projection is withheld, and the load failures travel as findings.
|
|
126
123
|
const loadFailures: Finding[] = [];
|
|
124
|
+
// `openapi.json` rides with it (`writeAppArtifacts`): refreshing one contract and not the other
|
|
125
|
+
// made this command's own output fail the `contract-diff` step.
|
|
126
|
+
const artifacts: string[] = [];
|
|
127
127
|
if (report.written.length > 0 && existsSync(containedPath(root, MANIFEST_FILENAME))) {
|
|
128
128
|
const { manifest, findings } = await appManifest(root);
|
|
129
129
|
if (findings.length === 0) {
|
|
130
|
-
|
|
130
|
+
artifacts.push(
|
|
131
|
+
...(await writeAppArtifacts(root, manifest, { openapi: true, onlyExisting: true })),
|
|
132
|
+
);
|
|
131
133
|
buildId = manifest.buildId;
|
|
132
134
|
} else loadFailures.push(...findings);
|
|
133
135
|
}
|
|
134
|
-
const findings = [...report.conflicts, ...loadFailures];
|
|
136
|
+
const findings = [...report.conflicts, ...indexSync.findings, ...loadFailures];
|
|
135
137
|
// One list behind all three renderings. The manifest was printed as a `+` line while the count
|
|
136
138
|
// beside it came from `report.written` alone, so `x g island` said "wrote 2 file(s)" over three
|
|
137
139
|
// lines — and `--json` carried the shorter list, which is the drift `--json` exists to prevent.
|
|
138
|
-
const written = [...report.written, ...
|
|
140
|
+
const written = [...report.written, ...edited, ...artifacts];
|
|
139
141
|
return {
|
|
140
142
|
ok: findings.length === 0,
|
|
141
143
|
command: 'g',
|
|
@@ -166,7 +168,7 @@ async function readSliceErrors(
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
/**
|
|
169
|
-
* `job` and `
|
|
171
|
+
* `job`, `task`, `action` and `mutator`: the slice's `entity.ts`/`repo.ts` as they stand on disk, absent when the
|
|
170
172
|
* generator's kind is neither or the file does not exist yet. `readSliceErrors`'s reason —
|
|
171
173
|
* whichever generator reads it decides on THIS app's disk, not on a default the template assumes.
|
|
172
174
|
*/
|
|
@@ -176,7 +178,9 @@ async function readSliceFile(
|
|
|
176
178
|
slice: string,
|
|
177
179
|
name: 'entity.ts' | 'repo.ts',
|
|
178
180
|
): Promise<string | undefined> {
|
|
179
|
-
if (kind !== 'job' && kind !== 'task')
|
|
181
|
+
if (kind !== 'job' && kind !== 'task' && kind !== 'action' && kind !== 'mutator') {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
180
184
|
const file = containedPath(root, `${slice}/${name}`);
|
|
181
185
|
return existsSync(file) ? await Bun.file(file).text() : undefined;
|
|
182
186
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `x i18n`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-i18n.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const I18N_SUBCOMMANDS = ['check', 'add', 'sync'] as const;
|
|
7
|
+
|
|
8
|
+
export const i18nSpec: CommandSpec = {
|
|
9
|
+
name: 'i18n',
|
|
10
|
+
summary: 'catalogs: add a locale, sync keys, check for gaps',
|
|
11
|
+
usage: 'x i18n [check|add <locale>|sync <locale>] [--json]',
|
|
12
|
+
requiresApp: true,
|
|
13
|
+
subcommands: I18N_SUBCOMMANDS,
|
|
14
|
+
// The bare `x i18n` audits; `add` and `sync` write catalogs and must be asked for.
|
|
15
|
+
defaultSubcommand: 'check',
|
|
16
|
+
};
|
package/src/cmd-i18n.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { Catalog } from '@ultimat3/i18n';
|
|
|
13
13
|
import { auditCatalogs, catalogKeys } from '@ultimat3/i18n';
|
|
14
14
|
import { loadApp } from './app-load';
|
|
15
15
|
import { requireAppRoot } from './app-root';
|
|
16
|
+
import { i18nSpec } from './cmd-i18n-spec';
|
|
16
17
|
import type { CliCommand, CommandContext } from './command';
|
|
17
18
|
import { BadFlagError, CatalogExistsError, MissingPositionalError } from './errors';
|
|
18
19
|
import {
|
|
@@ -36,7 +37,7 @@ import type { CommandResult, Finding, JsonValue } from './output';
|
|
|
36
37
|
import { renderTable } from './table';
|
|
37
38
|
import { catalogPath, resolveLocales } from './templates/locales';
|
|
38
39
|
|
|
39
|
-
export
|
|
40
|
+
export { I18N_SUBCOMMANDS } from './cmd-i18n-spec';
|
|
40
41
|
|
|
41
42
|
/** `ExtractReport` is plain JSON by construction — same idiom as `cmd-registries.ts`'s `asJson`. */
|
|
42
43
|
const asJson = (value: object): Record<string, JsonValue> => value as Record<string, JsonValue>;
|
|
@@ -192,7 +193,7 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
|
|
|
192
193
|
// answered `X_CATALOG_UNREGISTERED` with a fix naming an edit that had already been made. Same
|
|
193
194
|
// writer `x g --locales` already uses, so a catalog on disk and a selectable locale can never be
|
|
194
195
|
// two different sets (#F4).
|
|
195
|
-
const registered = await syncI18nIndex(root);
|
|
196
|
+
const { registered, findings: indexFindings } = await syncI18nIndex(root);
|
|
196
197
|
|
|
197
198
|
const keys = catalogKeys(seeded).length;
|
|
198
199
|
return {
|
|
@@ -202,7 +203,7 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
|
|
|
202
203
|
ok: true,
|
|
203
204
|
command: 'i18n',
|
|
204
205
|
summary: msg('cli.i18n.added', { locale, keys, from: from ?? locale }),
|
|
205
|
-
findings: app.findings,
|
|
206
|
+
findings: [...indexFindings, ...app.findings],
|
|
206
207
|
// `registered` is false only for an app with no `packages/i18n` at all — a fact a caller has
|
|
207
208
|
// to be able to read, because it is the one case where the locale is on disk and unselectable.
|
|
208
209
|
data: { locale, from: from ?? locale, keys, path, registered },
|
|
@@ -270,7 +271,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
|
|
|
270
271
|
// hand-created file, a `git merge` — is exactly the unregistered locale the gate refuses, and
|
|
271
272
|
// this is the command its `fix:` names. Re-deriving an index that is already correct writes the
|
|
272
273
|
// same bytes.
|
|
273
|
-
const registered = await syncI18nIndex(root);
|
|
274
|
+
const { registered, findings: indexFindings } = await syncI18nIndex(root);
|
|
274
275
|
|
|
275
276
|
const total = catalogKeys(merged).length;
|
|
276
277
|
return {
|
|
@@ -282,7 +283,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
|
|
|
282
283
|
// The keys themselves, raw — `runCheck` lists gaps the same way, because a key is a value an
|
|
283
284
|
// author copies and never prose the catalog owns.
|
|
284
285
|
lines: seeded ? added.map((key) => ` ${key}`) : [],
|
|
285
|
-
findings: app.findings,
|
|
286
|
+
findings: [...indexFindings, ...app.findings],
|
|
286
287
|
data: {
|
|
287
288
|
locale,
|
|
288
289
|
from: from ?? locale,
|
|
@@ -298,15 +299,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
|
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
export const i18nCommand: CliCommand = {
|
|
301
|
-
spec:
|
|
302
|
-
name: 'i18n',
|
|
303
|
-
summary: 'catalogs: add a locale, sync keys, check for gaps',
|
|
304
|
-
usage: 'x i18n [check|add <locale>|sync <locale>] [--json]',
|
|
305
|
-
requiresApp: true,
|
|
306
|
-
subcommands: I18N_SUBCOMMANDS,
|
|
307
|
-
// The bare `x i18n` audits; `add` and `sync` write catalogs and must be asked for.
|
|
308
|
-
defaultSubcommand: 'check',
|
|
309
|
-
},
|
|
302
|
+
spec: i18nSpec,
|
|
310
303
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
311
304
|
const root = requireAppRoot('i18n', ctx.cwd).dir;
|
|
312
305
|
const sub = ctx.args.subcommand ?? 'check';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// `x jobs`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-jobs.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const JOBS_SUBCOMMANDS = ['ls', 'show', 'retry', 'cancel', 'drain'] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The drivers a drain may move work ONTO — every one of them durable, and that is the whole rule.
|
|
10
|
+
* Closed, and read three ways: the flag summary, the refusal, and the `memory` case below.
|
|
11
|
+
*/
|
|
12
|
+
export const DRAIN_TARGETS = ['redis', 'nats'] as const;
|
|
13
|
+
|
|
14
|
+
export const jobsSpec: CommandSpec = {
|
|
15
|
+
name: 'jobs',
|
|
16
|
+
summary: 'list, show, retry, cancel and drain the job queue',
|
|
17
|
+
usage:
|
|
18
|
+
'x jobs [ls|show <id>|retry <id>|cancel <id>|drain --to <driver>] [--queue q] [--state s] [--limit n] [--from-step name] [--reason text] [--to driver] [--dry-run] [--json]',
|
|
19
|
+
requiresApp: true,
|
|
20
|
+
subcommands: JOBS_SUBCOMMANDS,
|
|
21
|
+
// The bare `x jobs` lists; it never retries, cancels or drains anything.
|
|
22
|
+
defaultSubcommand: 'ls',
|
|
23
|
+
flags: [
|
|
24
|
+
{ name: 'queue', type: 'string', summary: 'filter by queue name' },
|
|
25
|
+
{ name: 'state', type: 'string', summary: 'filter by job state' },
|
|
26
|
+
{ name: 'limit', type: 'string', summary: 'max rows to return' },
|
|
27
|
+
{ name: 'name', type: 'string', summary: 'filter by job name' },
|
|
28
|
+
// Each of these is read by ONE subcommand — `retryJob`, `cancelJob`, `runDrain` — and says
|
|
29
|
+
// so in its own summary. The scope is what makes the parser refuse it anywhere else instead
|
|
30
|
+
// of accepting it and ignoring it: `x db gen --dry-run` parsed and wrote the migration.
|
|
31
|
+
{
|
|
32
|
+
name: 'from-step',
|
|
33
|
+
type: 'string',
|
|
34
|
+
summary: 'retry: drop this step so it re-executes',
|
|
35
|
+
subcommands: ['retry'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'reason',
|
|
39
|
+
type: 'string',
|
|
40
|
+
summary: 'cancel: why, recorded on the job',
|
|
41
|
+
subcommands: ['cancel'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'to',
|
|
45
|
+
type: 'string',
|
|
46
|
+
summary: `drain: target driver — ${DRAIN_TARGETS.join(', ')}`,
|
|
47
|
+
subcommands: ['drain'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'dry-run',
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
summary: 'drain: report the plan, move nothing',
|
|
53
|
+
subcommands: ['drain'],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|