@ultimat3/cli 1.2.0 → 3.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 +761 -0
- package/README.md +42 -9
- package/package.json +25 -23
- package/src/api-routes.ts +16 -0
- package/src/app-auth.ts +32 -0
- package/src/app-entities.ts +18 -0
- package/src/app-env.ts +103 -0
- package/src/app-load.ts +20 -3
- package/src/bin.ts +4 -3
- package/src/budgets.ts +134 -9
- package/src/cmd-build.ts +69 -21
- package/src/cmd-db-branch.ts +219 -0
- package/src/cmd-db.ts +458 -153
- package/src/cmd-deploy.ts +59 -6
- package/src/cmd-dev.ts +92 -18
- package/src/cmd-docs.ts +167 -0
- package/src/cmd-doctor.ts +74 -10
- package/src/cmd-env.ts +95 -0
- package/src/cmd-errors.ts +33 -13
- package/src/cmd-fix.ts +5 -1
- package/src/cmd-generate.ts +146 -111
- package/src/cmd-help.ts +16 -5
- package/src/cmd-i18n.ts +2 -0
- package/src/cmd-jobs.ts +47 -33
- package/src/cmd-mcp.ts +11 -2
- package/src/cmd-new.ts +14 -8
- package/src/cmd-planned.ts +55 -10
- package/src/cmd-policy.ts +1 -0
- package/src/cmd-registries.ts +3 -0
- package/src/cmd-secrets.ts +368 -0
- package/src/cmd-tasks.ts +1 -0
- package/src/cmd-test.ts +29 -24
- package/src/cmd-verify.ts +197 -25
- package/src/db-backfill.ts +401 -0
- package/src/db-branch.ts +269 -0
- package/src/db-destructive.ts +29 -0
- package/src/db-finding.ts +28 -0
- package/src/db-generate.ts +144 -0
- package/src/db-seed.ts +294 -0
- package/src/db-snapshot.ts +24 -0
- package/src/dev-assets.ts +108 -23
- package/src/dev-cache.ts +122 -0
- package/src/dev-dashboard.ts +19 -4
- package/src/dev-hooks.ts +27 -2
- package/src/dev-n-plus-one.ts +191 -0
- package/src/dev-queue.ts +105 -19
- package/src/dev-render.ts +158 -26
- package/src/dev-roles-fixture.ts +67 -0
- package/src/dev-roles.ts +167 -78
- package/src/dev-runtime.ts +117 -40
- package/src/dev-services.ts +15 -0
- package/src/dev-storage.ts +247 -0
- package/src/dev-sync.ts +107 -0
- package/src/dev-traces.ts +37 -7
- package/src/dispatch.ts +4 -2
- package/src/document-styles.ts +54 -0
- package/src/drift.ts +78 -10
- package/src/error-catalog.ts +8 -18
- package/src/error-codes.ts +192 -0
- package/src/error-contract.ts +29 -7
- package/src/error-fixes.ts +114 -0
- package/src/errors.ts +201 -138
- package/src/exec.ts +42 -8
- package/src/fix-command.ts +268 -0
- package/src/flag-number.ts +67 -0
- package/src/framework-scope.ts +49 -0
- package/src/generate-kinds.ts +97 -0
- package/src/guards.ts +186 -0
- package/src/index.ts +92 -15
- package/src/island-bundle.ts +166 -0
- package/src/island-routes.ts +50 -0
- package/src/jobs-driver.ts +33 -0
- package/src/jobs-json.ts +24 -0
- package/src/jobs-report.ts +17 -4
- package/src/mcp-db-target.ts +52 -27
- package/src/mcp-errors.ts +128 -19
- package/src/mcp-host.ts +44 -25
- package/src/messages.ts +93 -2
- package/src/metrics-endpoint.ts +64 -16
- package/src/migrations.ts +37 -4
- package/src/otlp-export.ts +64 -0
- package/src/output.ts +46 -16
- package/src/parse.ts +41 -3
- package/src/policy-facts.ts +38 -6
- package/src/policy-fixture.ts +14 -7
- package/src/prerender.ts +111 -2
- package/src/registry.ts +21 -3
- package/src/runtime-overrides.ts +66 -0
- package/src/safe-url-label.ts +24 -0
- package/src/scaffold-fixture.ts +10 -0
- package/src/scaffold-typecheck.ts +16 -38
- package/src/serve.ts +185 -13
- package/src/shell-quote.ts +15 -0
- package/src/source-files.ts +4 -0
- package/src/statement-loop.ts +74 -0
- package/src/style-csp.ts +18 -0
- package/src/sync-authenticator.ts +59 -0
- package/src/templates/action.ts +15 -30
- package/src/templates/admin-page.ts +103 -0
- package/src/templates/admin.ts +11 -7
- package/src/templates/backfill.ts +212 -0
- package/src/templates/entity.ts +72 -31
- package/src/templates/guard.ts +143 -0
- package/src/templates/index.ts +12 -1
- package/src/templates/island.ts +67 -0
- package/src/templates/job.ts +53 -13
- package/src/templates/naming.ts +17 -1
- package/src/templates/policy.ts +35 -28
- package/src/templates/query.ts +24 -5
- package/src/templates/resource.ts +19 -11
- package/src/templates/route.ts +90 -15
- package/src/templates/scaffold-app.ts +142 -45
- package/src/templates/scaffold-claude-agents.ts +149 -0
- package/src/templates/scaffold-claude-commands.ts +221 -0
- package/src/templates/scaffold-claude.ts +134 -0
- package/src/templates/scaffold-container.ts +46 -2
- package/src/templates/scaffold-db-package.ts +91 -0
- package/src/templates/scaffold-docs.ts +24 -5
- package/src/templates/scaffold-domain-package.ts +90 -0
- package/src/templates/scaffold-env.ts +87 -0
- package/src/templates/scaffold-i18n.ts +4 -1
- package/src/templates/scaffold-mcp-package.ts +49 -0
- package/src/templates/scaffold-package-shape.ts +25 -4
- package/src/templates/scaffold-repo.ts +116 -257
- package/src/templates/scaffold-roles.ts +68 -0
- package/src/templates/scaffold-ui-package.ts +56 -0
- package/src/templates/slice-foundation.ts +88 -0
- package/src/templates/wrap.ts +95 -0
- package/src/test-counts.ts +35 -0
- package/src/test-select.ts +30 -15
- package/src/test-shards.ts +20 -11
- package/src/test-workers.ts +50 -0
- package/src/ts-scan.ts +284 -15
- package/src/tsconfig-references.ts +103 -0
- package/src/verify-floor.ts +133 -0
- package/src/verify-step.ts +19 -0
- package/src/verify-test-run.ts +72 -0
- package/src/verify-tests.ts +160 -71
- package/src/version-loader.ts +20 -3
- package/src/workspace-checks.ts +87 -16
- package/src/write-line.ts +34 -0
package/src/mcp-db-target.ts
CHANGED
|
@@ -1,36 +1,64 @@
|
|
|
1
|
-
// Which database the MCP dev host is pointed at
|
|
2
|
-
// decides from
|
|
3
|
-
// database somebody else is using
|
|
1
|
+
// Which database the MCP dev host is pointed at: whether it is a branch, and whether this is
|
|
2
|
+
// production. `db.migrate` decides from these two alone, so both readings have to be exact — a
|
|
3
|
+
// wrong `branch` is a migration against a database somebody else is using, and a `production` that
|
|
4
|
+
// is always false is a refusal that never runs.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// `node:path` for the joiner Bun has no equivalent of — the same reason `db-branch.ts` reaches for
|
|
7
|
+
// it, and the state directory it builds a path under is a real one on disk.
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { tryResolveEnvironment } from '@ultimat3/core';
|
|
6
10
|
import { pgliteDataDir } from '@ultimat3/db';
|
|
7
11
|
import type { DatabaseTarget } from '@ultimat3/mcp';
|
|
8
|
-
import
|
|
12
|
+
import { branchNameOf, pgliteBranchName } from './db-branch';
|
|
13
|
+
import type { DevServices, Env } from './dev-services';
|
|
14
|
+
import { safeUrlLabel } from './safe-url-label';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
* `production`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* `production` was the literal `false` in both arms until `As of 2026-08`, and this is the only
|
|
18
|
+
* place a `DatabaseTarget` is ever built — so `assertBranchDatabase`'s FIRST refusal, the one
|
|
19
|
+
* meaning "production is never migratable from MCP at all", could not run for any database this
|
|
20
|
+
* CLI produced. A production database refused it only incidentally, because its name lacked
|
|
21
|
+
* `_branch_`; one named `shop_branch_hotfix`, or a container whose data directory is
|
|
22
|
+
* `pgdata-hotfix`, read as a branch and was migratable through an MCP tool call.
|
|
23
|
+
*
|
|
24
|
+
* The environment is read the way `x doctor` reads it (`cmd-doctor.ts`), through core's one key —
|
|
25
|
+
* so `production` is a fact about the deploy and `branch` stays a fact about the database, and
|
|
26
|
+
* `@ultimat3/mcp` keeps receiving two answers rather than re-deriving either.
|
|
15
27
|
*/
|
|
16
|
-
export function databaseTarget(services: DevServices): DatabaseTarget {
|
|
28
|
+
export function databaseTarget(services: DevServices, env: Env): DatabaseTarget {
|
|
17
29
|
const url = services.db.url;
|
|
30
|
+
const production = isProduction(env);
|
|
18
31
|
return services.db.mode === 'embedded'
|
|
19
|
-
? { label: url, branch: pgliteBranch(url, services.stateDir), production
|
|
20
|
-
: {
|
|
32
|
+
? { label: url, branch: pgliteBranch(url, services.stateDir), production }
|
|
33
|
+
: {
|
|
34
|
+
// An external `DATABASE_URL` may carry credentials, and this string gets printed.
|
|
35
|
+
label: safeUrlLabel(url, 'external database'),
|
|
36
|
+
branch: postgresBranch(url),
|
|
37
|
+
production,
|
|
38
|
+
};
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The non-throwing read, because `ULTIMATE_ENV` is in no env schema — nothing validates it at boot,
|
|
43
|
+
* and a tool call is not where a typo should surface as a crash (`cmd-doctor.ts` chose the same
|
|
44
|
+
* variant for the same reason).
|
|
45
|
+
*
|
|
46
|
+
* `undefined` is answered **true**, and that is the whole point of using it here. It means exactly
|
|
47
|
+
* one thing: `ULTIMATE_ENV` is set to something that is not an environment. Reading a typo as "not
|
|
48
|
+
* production" would let the single misconfiguration this guard exists to survive defeat it — so an
|
|
49
|
+
* environment that cannot be read is treated as the most dangerous one it could be.
|
|
50
|
+
*/
|
|
51
|
+
const isProduction = (env: Env): boolean => {
|
|
52
|
+
const environment = tryResolveEnvironment({ env });
|
|
53
|
+
return environment === undefined || environment === 'production';
|
|
54
|
+
};
|
|
32
55
|
|
|
33
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* `x db branch create <name>` names an external clone `<source>_branch_<name>`. Both readings come
|
|
58
|
+
* from `db-branch.ts` — the module that also WRITES those names — because a target that disagrees
|
|
59
|
+
* with `x db branch ls` about what a branch is would let `db.migrate` run against a shared
|
|
60
|
+
* database on the strength of a naming rule one of the two had drifted away from.
|
|
61
|
+
*/
|
|
34
62
|
function postgresBranch(url: string): string | null {
|
|
35
63
|
let database: string;
|
|
36
64
|
try {
|
|
@@ -38,13 +66,10 @@ function postgresBranch(url: string): string | null {
|
|
|
38
66
|
} catch {
|
|
39
67
|
return null;
|
|
40
68
|
}
|
|
41
|
-
return
|
|
69
|
+
return branchNameOf(database);
|
|
42
70
|
}
|
|
43
71
|
|
|
44
72
|
/** `branchPglite` copies `<stateDir>/pgdata` to `<stateDir>/pgdata-<name>`; the dev dir is no branch. */
|
|
45
73
|
function pgliteBranch(url: string, stateDir: string): string | null {
|
|
46
|
-
|
|
47
|
-
const dev = join(stateDir, 'pgdata');
|
|
48
|
-
if (dir === dev || basename(dir) === basename(dev)) return null;
|
|
49
|
-
return dir.startsWith(`${dev}-`) ? dir.slice(dev.length + 1) : null;
|
|
74
|
+
return pgliteBranchName(pgliteDataDir(url), join(stateDir, 'pgdata'));
|
|
50
75
|
}
|
package/src/mcp-errors.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { describeErrorCode, hasErrorCode, listErrorCodes } from '@ultimat3/core';
|
|
6
6
|
import type { ErrorExplanation } from '@ultimat3/mcp';
|
|
7
|
-
import type { CliErrorCode } from './
|
|
8
|
-
import { CLI_ERROR_CODES, docsFor } from './
|
|
7
|
+
import type { CliErrorCode } from './error-codes';
|
|
8
|
+
import { CLI_ERROR_CODES, docsFor } from './error-codes';
|
|
9
|
+
import { codeFixes, codeFixScan } from './error-fixes';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* One runnable command per CLI code. Typed over `CliErrorCode`, so a new code fails the build.
|
|
@@ -18,17 +19,25 @@ import { CLI_ERROR_CODES, docsFor } from './errors';
|
|
|
18
19
|
*/
|
|
19
20
|
const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
20
21
|
X_CLI_UNKNOWN_COMMAND: 'x help --json',
|
|
21
|
-
|
|
22
|
+
// Runnable first, the narrowing behind a `#`: `x help <command> --json` pasted into a shell
|
|
23
|
+
// is a redirect, not a command, and this table is copied verbatim by whoever reads it.
|
|
24
|
+
X_CLI_BAD_FLAG: 'x help --json # then narrow to the command the cause names',
|
|
22
25
|
X_VERIFY_FAILED: 'x verify --json',
|
|
23
26
|
X_NOT_IN_APP: 'x new myapp --json && cd myapp',
|
|
24
27
|
X_BUN_VERSION: 'bun upgrade',
|
|
25
28
|
X_NOT_IMPLEMENTED: 'x doctor --json',
|
|
26
|
-
|
|
29
|
+
// Core's three env codes, answered by the command that covers each. `X_CONFIG_INVALID` gets
|
|
30
|
+
// `x doctor` rather than `x env check`: its causes are env *and* `app.config.ts` fields, and
|
|
31
|
+
// `x env check` on a config the app cannot boot on would throw this same code straight back.
|
|
32
|
+
X_CONFIG_INVALID: 'x doctor --json',
|
|
33
|
+
X_ENV_MISSING: 'x env check --json',
|
|
34
|
+
X_ENV_EXAMPLE_DRIFT: 'x env example --json',
|
|
35
|
+
X_TEST_NO_FILES: 'x test --json # from the repo root, or pass --cwd to it',
|
|
27
36
|
X_TEST_SHARD_FAILED: 'x test --workers 1 --json',
|
|
28
|
-
X_SCAFFOLD_PATH_ESCAPE: 'x g route
|
|
37
|
+
X_SCAFFOLD_PATH_ESCAPE: 'x g route posts --json # a path with no ".." segment',
|
|
29
38
|
X_GENERATE_JSON_INVALID:
|
|
30
39
|
'bun test packages/cli/src/cmd-generate.test.ts # the error names the template to fix',
|
|
31
|
-
X_APP_PACKAGE_INVALID: 'bun pm pkg set name
|
|
40
|
+
X_APP_PACKAGE_INVALID: 'bun pm pkg set name=my-app version=0.1.0',
|
|
32
41
|
X_ERROR_CODE_UNKNOWN: 'x errors list --json',
|
|
33
42
|
X_DECLARATION_UNKNOWN: 'x actions list --json',
|
|
34
43
|
X_JOB_UNKNOWN: 'x jobs ls --json',
|
|
@@ -41,20 +50,41 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
|
41
50
|
X_TYPECHECK_FAILED: 'bunx tsc -b --pretty false',
|
|
42
51
|
X_LINT_FAILED: 'bunx biome check --write .',
|
|
43
52
|
X_TEST_FAILED: 'x test --json # the finding carries the exact bun test invocation that failed',
|
|
53
|
+
// The same two edits `vanishedSuiteFinding` names, verbatim, so both surfaces of this code hand
|
|
54
|
+
// an agent one instruction. Neither edit is scripted here on purpose: a command that rewrites
|
|
55
|
+
// x.verify.json is the gate editing its own ratchet, which is the false green the floor closes.
|
|
56
|
+
X_VERIFY_SUITE_VANISHED:
|
|
57
|
+
'x verify --json # restore the suite, or drop its name from x.verify.json in the commit that says why',
|
|
44
58
|
X_FILE_TOO_LONG: 'x verify --json # the finding names the file to split',
|
|
45
|
-
X_PACKAGE_SHAPE: 'bun run
|
|
59
|
+
X_PACKAGE_SHAPE: 'bun run verify --json # every finding carries its own new-package.ts command',
|
|
60
|
+
// NOT `bunx tsc -b`: an unreferenced package is one `tsc -b` skips by definition, so it exits 0
|
|
61
|
+
// while the finding stands — a fix that runs clean and changes nothing is the failure axiom 4
|
|
62
|
+
// exists to prevent. The gate is what re-emits the finding, whose own `fix:` carries the exact
|
|
63
|
+
// `{ "path": … }` entry; the `tsc -b` that then reports the type errors the package had been
|
|
64
|
+
// hiding is a step of the same run.
|
|
65
|
+
X_PACKAGE_UNREFERENCED:
|
|
66
|
+
'x verify --json # the package-shape finding carries the tsconfig.json entry to add',
|
|
46
67
|
X_RELEASE_VERSION_SKEW: 'bun run scripts/release.ts --bump patch --dry-run --json',
|
|
68
|
+
// Two real remedies and the command cannot know which one this deployment wants, so it names
|
|
69
|
+
// the one that inspects the binding rather than guessing between a volume and a bucket.
|
|
70
|
+
X_STORAGE_UNWRITABLE: 'x doctor --json',
|
|
71
|
+
X_STORAGE_SECRET_DEV: 'export STORAGE_SIGNING_SECRET="$(openssl rand -hex 32)"',
|
|
47
72
|
X_MANIFEST_STALE: 'x manifest --json',
|
|
48
|
-
|
|
73
|
+
// `--target static`, not a bare `x build`: `--target` defaults to `docker`, and only the static
|
|
74
|
+
// target runs `apps/web/prerender.ts` — the one caller of `writeBuildStats`. Without the flag
|
|
75
|
+
// this fix builds an image, writes no `.x/build-stats.json`, and the next `x verify` reports the
|
|
76
|
+
// same code. Byte-identical to `checkBudgets`'s own finding, which is the other half of the pair.
|
|
77
|
+
X_BUDGET_UNMEASURED: 'x build --target static --json && x verify --json',
|
|
49
78
|
X_BUILD_FAILED: 'x build --json # the finding names the failing step',
|
|
50
79
|
X_BUILD_ENTRY_MISSING:
|
|
51
|
-
'x new
|
|
80
|
+
'x new scratch-app --dry-run --json # the file list names every entry a build needs',
|
|
52
81
|
X_DEPLOY_FAILED: 'x deploy --json # the finding carries the command to re-run directly',
|
|
53
82
|
// The container's own environment, so the answer is the run that sets it — never an `x` command,
|
|
54
83
|
// which is not what is running when a `ROLE=wroker` pod refuses to boot.
|
|
55
|
-
X_ROLE_UNKNOWN: 'docker run -e ROLE=web
|
|
56
|
-
X_PORT_INVALID: 'docker run -e PORT=3000
|
|
57
|
-
|
|
84
|
+
X_ROLE_UNKNOWN: 'docker run -e ROLE=web my-app:latest',
|
|
85
|
+
X_PORT_INVALID: 'docker run -e PORT=3000 my-app:latest',
|
|
86
|
+
X_RUNTIME_DRIVER_SPLIT: 'x dev --json # the boot names the driver the app installed twice',
|
|
87
|
+
X_GENERATE_CONFLICT: 'x g route posts --force --json',
|
|
58
88
|
X_PORT_IN_USE: 'x dev --port 3001 --json',
|
|
59
89
|
// Not `x db status`: there is no such subcommand (`x db` is gen, migrate, reset, studio, branch),
|
|
60
90
|
// so the fix answered a failed step with X_CLI_UNKNOWN_COMMAND. `x doctor` is what reports
|
|
@@ -63,20 +93,99 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
|
63
93
|
X_DB_MIGRATE_FAILED: 'x doctor --json # cause carries the Postgres error verbatim',
|
|
64
94
|
X_DB_BRANCH_FAILED: 'x db branch ls --json',
|
|
65
95
|
X_DB_STUDIO_FAILED: 'x doctor --json',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
// Runnable first, the narrowing behind a `#`, exactly as X_CLI_UNKNOWN_COMMAND above: naming
|
|
97
|
+
// the tier IS the consent, and which seed to consent to is the one thing this table cannot
|
|
98
|
+
// know — a bare `x db seed --tier dev` would seed every dev fixture in production to answer a
|
|
99
|
+
// refusal about one. The dry run is what lists them, and the raised error's own `fix:` already
|
|
100
|
+
// carries the fully named invocation. `ULTIMATE_SEED_TIER=<tier>` is the other half of the
|
|
101
|
+
// consent and stays in the cause: it is the answer only for a container with a fixed argv.
|
|
102
|
+
X_SEED_ENVIRONMENT:
|
|
103
|
+
'x db seed --dry-run --json # then name the tier: x db seed <name> --tier dev --json',
|
|
104
|
+
X_BOUNDARY_SITE_TO_APP:
|
|
105
|
+
'x verify --json # then: x fix boundary <the file the finding names> --json',
|
|
106
|
+
X_BOUNDARY_SHARED_LEAF:
|
|
107
|
+
'x verify --json # then: x fix boundary <the file the finding names> --json',
|
|
108
|
+
X_BOUNDARY_APP_TO_API:
|
|
109
|
+
'x verify --json # then: x fix boundary <the file the finding names> --json',
|
|
110
|
+
X_BOUNDARY_ROUTE_TO_DB:
|
|
111
|
+
'x verify --json # then: x fix boundary <the file the finding names> --json',
|
|
112
|
+
X_BOUNDARY_SERVICE_TO_HTTP:
|
|
113
|
+
'x verify --json # then: x fix boundary <the file the finding names> --json',
|
|
114
|
+
// The app's own guards. All three are reported by the gate and by nothing else, so the runnable
|
|
115
|
+
// half is the gate — the narrowing behind the `#` is the edit, because only the finding knows
|
|
116
|
+
// which file in `guards/` is the one to open.
|
|
117
|
+
X_GUARD_INVALID: 'x verify --json # then export a `guard` from the file the finding names',
|
|
118
|
+
X_GUARD_FAILED: 'x verify --json # the cause carries the throw the guard raised, verbatim',
|
|
119
|
+
X_GUARD_FINDING_INVALID:
|
|
120
|
+
'x verify --json # then give the finding an X_ code, a cause and a fix naming a command',
|
|
121
|
+
// `EDITOR=` inline rather than `export`: the variable is only needed for the one invocation, and
|
|
122
|
+
// an agent copying this line gets a working command instead of a shell it has to keep.
|
|
123
|
+
X_SECRETS_EDITOR_MISSING: 'EDITOR=nano x secrets edit',
|
|
124
|
+
X_SECRETS_EDIT_FAILED: 'x secrets show --json # then re-open the buffer: x secrets edit',
|
|
125
|
+
// Render's code, thrown here by the bundler half: the cause names the specifier and the file it
|
|
126
|
+
// resolved to, and `x g island` is what puts that file where the page already says it is.
|
|
127
|
+
X_ISLAND_INVALID: 'x routes --json # the cause names the src; then: x g island <name>',
|
|
71
128
|
};
|
|
72
129
|
|
|
73
130
|
const isCliCode = (code: string): code is CliErrorCode =>
|
|
74
131
|
(CLI_ERROR_CODES as readonly string[]).includes(code);
|
|
75
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The fix a code's own throw site writes, for every code this table does not own.
|
|
135
|
+
*
|
|
136
|
+
* The table above stays the answer for `CliErrorCode` and only for it: those lines are typed,
|
|
137
|
+
* build-enforced, and several of them are deliberately NOT the throw site's wording (the comments
|
|
138
|
+
* above say which and why). Everywhere else the throw site is the definition and this is a
|
|
139
|
+
* projection of it — one `fix:`, written once, `x errors explain` and the raised error agreeing by
|
|
140
|
+
* construction rather than by review.
|
|
141
|
+
*
|
|
142
|
+
* Both fallbacks name what they do not know. `x verify --json` was the old answer for all 327 of
|
|
143
|
+
* them, and it is a lie for every runtime code: the gate does not raise `X_UNAUTHENTICATED`, so
|
|
144
|
+
* running it reports green and the reader is exactly where they started.
|
|
145
|
+
*/
|
|
146
|
+
function projectedFix(code: string): string {
|
|
147
|
+
const sites = codeFixes().get(code) ?? [];
|
|
148
|
+
const readable = sites.filter((site) => site.fix !== undefined);
|
|
149
|
+
const first = readable[0];
|
|
150
|
+
if (first?.fix === undefined) {
|
|
151
|
+
const site = sites[0];
|
|
152
|
+
if (site !== undefined) {
|
|
153
|
+
// The file comes FIRST and carries no verb. `open …` read as a command — `open(1)`,
|
|
154
|
+
// `xdg-open` — and an agent that executed it got `command not found`, which is the same
|
|
155
|
+
// axiom-4 failure as the `x verify --json` this replaced, one step further along. There is
|
|
156
|
+
// genuinely no command here: the fix is assembled from values only the raised error holds.
|
|
157
|
+
// "A file they can open" is the error contract's own fourth shape (`COMMAND_TOKENS`), and
|
|
158
|
+
// citing a command that does not really fix it is the mistake `fix-command.ts` warns about.
|
|
159
|
+
// `x errors explain --json` carries the same site as DATA, so nothing has to parse this.
|
|
160
|
+
return `${site.at}:${site.line} — the fix is built there out of values only the raised error carries, so reproduce the error and read its own fix line`;
|
|
161
|
+
}
|
|
162
|
+
if (codeFixScan() === 'unread') {
|
|
163
|
+
// Not "nothing raises it": nothing LOOKED. `cmd-docs.ts` answers the same broken install
|
|
164
|
+
// with the same line, because it is the same condition seen from a second command.
|
|
165
|
+
return `bun install && x doctor --json # the installed @ultimat3 packages could not be read, so no throw site could be quoted for ${code}`;
|
|
166
|
+
}
|
|
167
|
+
return `x errors list --json # nothing in the installed framework raises ${code}, so the package that registered it owns its fix`;
|
|
168
|
+
}
|
|
169
|
+
// Both notes name a thing this answer does NOT know, because an answer that hides either is one
|
|
170
|
+
// an agent acts on without noticing: which of several throw sites it is quoting, and which words
|
|
171
|
+
// in it were an interpolation at the throw site and are a placeholder here.
|
|
172
|
+
const notes: string[] = [];
|
|
173
|
+
if (readable.length > 1) {
|
|
174
|
+
notes.push(
|
|
175
|
+
`${code} is raised at ${readable.length} sites; this one is ${first.at}:${first.line}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (first.fix.includes('<value>')) {
|
|
179
|
+
notes.push('each <value> is filled in by the error that raises it');
|
|
180
|
+
}
|
|
181
|
+
return notes.length === 0 ? first.fix : `${first.fix} # ${notes.join('; ')}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
76
184
|
/**
|
|
77
185
|
* `undefined` for a code nobody registered — the tool then answers "unknown error code", which
|
|
78
186
|
* beats an invented explanation. The framework-wide registry holds a title and a docs URL but no
|
|
79
|
-
* fix
|
|
187
|
+
* fix, so the fix comes from `error-fixes.ts`'s read of the throw sites; a caller that has not
|
|
188
|
+
* awaited `loadCodeFixes()` gets the honest fallback rather than a stale answer.
|
|
80
189
|
*/
|
|
81
190
|
export function explainErrorCode(code: string): ErrorExplanation | undefined {
|
|
82
191
|
const cli = isCliCode(code);
|
|
@@ -85,7 +194,7 @@ export function explainErrorCode(code: string): ErrorExplanation | undefined {
|
|
|
85
194
|
return {
|
|
86
195
|
code,
|
|
87
196
|
cause: described.title,
|
|
88
|
-
fix: cli ? CLI_FIXES[code] :
|
|
197
|
+
fix: cli ? CLI_FIXES[code] : projectedFix(code),
|
|
89
198
|
docs: cli ? docsFor(code) : described.docs,
|
|
90
199
|
};
|
|
91
200
|
}
|
package/src/mcp-host.ts
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
// the gate. The description half is the framework's own `frameworkIntrospection`, so nothing here
|
|
4
4
|
// is a second catalog of routes, entities, actions, queries or jobs.
|
|
5
5
|
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
6
|
import { join } from 'node:path';
|
|
8
|
-
import { agentActor, UltimateError } from '@ultimat3/core';
|
|
7
|
+
import { agentActor, isUltimateError, UltimateError } from '@ultimat3/core';
|
|
9
8
|
import type { DbClient } from '@ultimat3/db';
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ensureReadOnlyRole,
|
|
11
|
+
isLedgerMissing,
|
|
12
|
+
migrate,
|
|
13
|
+
pendingMigrations,
|
|
14
|
+
readLedger,
|
|
15
|
+
readOnlyQuery,
|
|
16
|
+
} from '@ultimat3/db';
|
|
11
17
|
import { inspectJobList, inspectQueues } from '@ultimat3/jobs';
|
|
12
18
|
import { MANIFEST_FILENAME } from '@ultimat3/manifest';
|
|
13
19
|
import type {
|
|
@@ -28,13 +34,14 @@ import type { RunningServices } from './dev-runtime';
|
|
|
28
34
|
import { startServices } from './dev-runtime';
|
|
29
35
|
import type { DevServices, Env } from './dev-services';
|
|
30
36
|
import { resolveServices } from './dev-services';
|
|
31
|
-
import {
|
|
37
|
+
import { loadCodeFixes } from './error-fixes';
|
|
32
38
|
import { CliNotImplementedError } from './errors';
|
|
33
39
|
import type { Runner } from './exec';
|
|
34
40
|
import { execOutput } from './exec';
|
|
35
41
|
import { databaseTarget } from './mcp-db-target';
|
|
36
42
|
import { explainErrorCode } from './mcp-errors';
|
|
37
43
|
import { parseBunTest } from './mcp-test-output';
|
|
44
|
+
import { readMigrations } from './migrations';
|
|
38
45
|
|
|
39
46
|
export interface DevHostInput {
|
|
40
47
|
readonly root: string;
|
|
@@ -106,20 +113,25 @@ export function lazyServices(input: DevHostInput): LazyServices {
|
|
|
106
113
|
};
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Migration ids on disk that the ledger does not record. Both halves are the framework's own —
|
|
118
|
+
* `readMigrations` is the list `ROLE=migrate` applies and `pendingMigrations` is the filter
|
|
119
|
+
* `migrate()` applies it through, so this tool can never report a pending set the migrator would
|
|
120
|
+
* disagree with.
|
|
121
|
+
*/
|
|
122
|
+
async function pendingIds(root: string, lazy: LazyServices): Promise<readonly string[]> {
|
|
123
|
+
const migrations = await readMigrations(root);
|
|
124
|
+
if (migrations.length === 0) return [];
|
|
117
125
|
const { db } = await lazy.running();
|
|
118
126
|
// No ledger table means nothing has been applied. `ensureLedger` would create it, and a dry run
|
|
119
|
-
// is not allowed to write.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
// is not allowed to write. Only that condition: a permission denied or an unreachable server is
|
|
128
|
+
// a ledger nobody read, and answering it with `[]` reports every migration as pending against a
|
|
129
|
+
// database whose state this tool never saw.
|
|
130
|
+
const ledger = await readLedger(db).catch((error: unknown) => {
|
|
131
|
+
if (!isLedgerMissing(error)) throw error;
|
|
132
|
+
return [];
|
|
133
|
+
});
|
|
134
|
+
return pendingMigrations(ledger, migrations).map((migration) => migration.id);
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
// ── the capabilities ─────────────────────────────────────────────────────────
|
|
@@ -164,7 +176,7 @@ function capabilities(input: DevHostInput, lazy: LazyServices): DevCapabilities
|
|
|
164
176
|
let readOnlyRole: Promise<string | null> | undefined;
|
|
165
177
|
|
|
166
178
|
return {
|
|
167
|
-
database: databaseTarget(lazy.services),
|
|
179
|
+
database: databaseTarget(lazy.services, input.env),
|
|
168
180
|
|
|
169
181
|
async runQuery(sql: string, limits: QueryLimits): Promise<QueryRows> {
|
|
170
182
|
const { db } = await lazy.running();
|
|
@@ -175,20 +187,24 @@ function capabilities(input: DevHostInput, lazy: LazyServices): DevCapabilities
|
|
|
175
187
|
},
|
|
176
188
|
|
|
177
189
|
async runMigrations(branch: string, dryRun: boolean) {
|
|
178
|
-
const before = await
|
|
190
|
+
const before = await pendingIds(root, lazy);
|
|
179
191
|
if (dryRun) return { branch, applied: [], pending: before };
|
|
180
|
-
const
|
|
181
|
-
|
|
192
|
+
const { db } = await lazy.running();
|
|
193
|
+
try {
|
|
194
|
+
await migrate({ migrations: await readMigrations(root), client: db });
|
|
195
|
+
} catch (error) {
|
|
182
196
|
// Thrown, not returned: `server.ts` renders any X_* error as the three-line
|
|
183
|
-
// code/cause/fix result, which is what an agent needs to act without a round trip.
|
|
197
|
+
// code/cause/fix result, which is what an agent needs to act without a round trip. The
|
|
198
|
+
// engine's own errors already carry that shape and pass through untouched.
|
|
199
|
+
if (isUltimateError(error)) throw error;
|
|
184
200
|
throw new UltimateError({
|
|
185
201
|
code: 'X_DB_MIGRATE_FAILED',
|
|
186
|
-
cause:
|
|
202
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
187
203
|
fix: 'x db reset',
|
|
188
204
|
});
|
|
189
205
|
}
|
|
190
|
-
// The ledger is the evidence for "applied" — never the migrator's own
|
|
191
|
-
const pending = await
|
|
206
|
+
// The ledger is the evidence for "applied" — never the migrator's own return value.
|
|
207
|
+
const pending = await pendingIds(root, lazy);
|
|
192
208
|
return { branch, applied: before.filter((id) => !pending.includes(id)), pending };
|
|
193
209
|
},
|
|
194
210
|
|
|
@@ -270,7 +286,10 @@ function capabilities(input: DevHostInput, lazy: LazyServices): DevCapabilities
|
|
|
270
286
|
* introspection tool then answers from the framework's own registries, not from a scan.
|
|
271
287
|
*/
|
|
272
288
|
export async function createDevMcpServer(input: DevHostInput): Promise<CliMcpServer> {
|
|
273
|
-
|
|
289
|
+
// `explainError` is synchronous by `DevCapabilities`' own signature, so the walk that reads the
|
|
290
|
+
// framework's `fix:` lines happens here, once, or `errors.explain` answers with the fallback for
|
|
291
|
+
// every code it could have quoted.
|
|
292
|
+
await Promise.all([loadApp(input.root), loadCodeFixes()]);
|
|
274
293
|
const lazy = lazyServices(input);
|
|
275
294
|
const introspection = frameworkIntrospection({
|
|
276
295
|
routes: () => describeRoutes(),
|
package/src/messages.ts
CHANGED
|
@@ -10,6 +10,7 @@ const CATALOG = {
|
|
|
10
10
|
'cli.flags.heading': 'flags',
|
|
11
11
|
'cli.commands.heading': 'commands',
|
|
12
12
|
'cli.build.done': 'built {target}',
|
|
13
|
+
'cli.build.failed': '{target} build failed',
|
|
13
14
|
// `describeCron`'s vocabulary. `@ultimat3/time` is tier 1 and reaches no i18n runtime, so the
|
|
14
15
|
// caller supplies the words — and the caller here is a rendered `x tasks show` line, which is
|
|
15
16
|
// exactly what this catalog holds. `msg()` leaves an un-supplied `{n}`/`{time}`/`{days}`/
|
|
@@ -24,7 +25,43 @@ const CATALOG = {
|
|
|
24
25
|
'cli.cron.inMonths': 'in {months}',
|
|
25
26
|
'cli.cron.onDaysOfMonth': 'on day {days} of the month',
|
|
26
27
|
'cli.cron.onWeekdays': 'on {days}',
|
|
28
|
+
'cli.db.backfill.empty': 'no backfill has run against this database yet',
|
|
29
|
+
'cli.db.backfill.listed': '{count} backfill pass(es)',
|
|
30
|
+
/** The empty cell in a `x db backfill --list` column — a value, not a column key. */
|
|
31
|
+
'cli.db.backfill.none': '-',
|
|
32
|
+
'cli.db.backfill.pending': '{count} of {declared} declared backfill(s) never completed',
|
|
33
|
+
'cli.db.backfill.swept': 'every one of {declared} declared backfill(s) has completed',
|
|
34
|
+
// Every action counted, never derived: `count - enqueued` folded a deduped pass into "blocked",
|
|
35
|
+
// so the summary said blocked while `--json` said deduped for the same row — two renderers
|
|
36
|
+
// stating different facts about one run, which is the thing `--json` exists to make impossible.
|
|
37
|
+
'cli.db.backfill.planned':
|
|
38
|
+
'{count} backfill(s): {enqueued} enqueued, {deduped} already live, {blocked} blocked',
|
|
39
|
+
'cli.db.backfill.dryRun': '{count} backfill(s) would run — nothing written without --write',
|
|
27
40
|
'cli.db.branch.ready': 'branch {name} ready',
|
|
41
|
+
'cli.db.branch.dropped': 'branch {name} dropped',
|
|
42
|
+
'cli.db.branch.failed': 'branch command failed',
|
|
43
|
+
'cli.db.branch.listed': '{count} branch(es) of this database',
|
|
44
|
+
'cli.db.branch.none': 'this database has no branch',
|
|
45
|
+
/** The empty cell in an `x db branch ls` column — a value, not a column key. */
|
|
46
|
+
'cli.db.branch.unknown': '-',
|
|
47
|
+
'cli.db.gen.failed': 'migration not generated',
|
|
48
|
+
'cli.db.gen.unchanged': 'entities and migrations agree — nothing to generate',
|
|
49
|
+
// A THIRD outcome, and it is neither of the other two: nothing to generate, but the sidecar the
|
|
50
|
+
// `drift` step reads did move — an edit under `packages/db/src` that implies no DDL. Rendering it
|
|
51
|
+
// as `written` would name a migration nobody can apply; as `unchanged`, it would hide a file this
|
|
52
|
+
// command wrote. `GeneratedFiles.outcome` is what `--json` carries the same distinction on.
|
|
53
|
+
'cli.db.gen.recorded': 'no migration needed — schema hash re-recorded in {file}',
|
|
54
|
+
'cli.db.gen.written': 'migration {id} generated',
|
|
55
|
+
'cli.db.migrate.applied': 'migrations applied',
|
|
56
|
+
'cli.db.migrate.failed': 'migration failed',
|
|
57
|
+
'cli.db.reset.done': 'database reset and migrated',
|
|
58
|
+
// Every seed counted per outcome, exactly as the backfill summary is: a replayed seed writes
|
|
59
|
+
// nothing and skips everything, and a total that hid that would make the second run look idle.
|
|
60
|
+
'cli.db.seed.done':
|
|
61
|
+
'{count} seed(s): {inserted} inserted, {updated} updated, {skipped} already stored',
|
|
62
|
+
'cli.db.seed.dryRun': '{count} seed(s) would run — nothing written while --dry-run is set',
|
|
63
|
+
'cli.db.seed.failed': '{failed} of {count} seed(s) failed',
|
|
64
|
+
'cli.db.seed.none': 'no seed matched — nothing to run',
|
|
28
65
|
'cli.dev.ready': 'dev ready on {url} — /_x mounted ({panels} panels), {services}',
|
|
29
66
|
// The mail and CDN halves of that boot line. Rendered text, so it lives here — while
|
|
30
67
|
// `describeMail`/`describeCdn` keep the same wording as the fixed vocabulary `x dev --json`
|
|
@@ -33,6 +70,7 @@ const CATALOG = {
|
|
|
33
70
|
'cli.dev.cdn.none': 'cdn=none',
|
|
34
71
|
'cli.dev.mail.embedded': 'mail=embedded',
|
|
35
72
|
'cli.dev.mail.external': 'mail=external({driver} via {detail})',
|
|
73
|
+
'cli.dev.mail.refused': 'mail=refused({detail})',
|
|
36
74
|
'cli.dev.hmr': 'reloaded {file} in {ms}ms',
|
|
37
75
|
'cli.dev.roles': ' roles {roles}',
|
|
38
76
|
'cli.dev.panels': ' panels {panels}',
|
|
@@ -41,17 +79,42 @@ const CATALOG = {
|
|
|
41
79
|
'cli.deploy.plan': 'containers only: {images} image, roles {roles}',
|
|
42
80
|
'cli.doctor.clean': 'no findings — environment is shippable',
|
|
43
81
|
'cli.doctor.findings': '{count} finding(s)',
|
|
82
|
+
'cli.docs.code': '{code} is an error code — x errors explain answers it',
|
|
83
|
+
'cli.docs.exports': 'exports: {list}',
|
|
84
|
+
'cli.docs.installed': 'installed: {list}',
|
|
85
|
+
'cli.docs.tryErrors': 'every X_* code, with its fix',
|
|
86
|
+
'cli.docs.tryActions': "this app's own primitives, not the framework's",
|
|
87
|
+
'cli.docs.found': '{count} doc(s) for "{query}"',
|
|
88
|
+
'cli.docs.none': 'no framework doc matches "{query}"',
|
|
89
|
+
'cli.docs.unresolved': 'the installed framework packages could not be located',
|
|
44
90
|
'cli.errors.count': '{count} registered error code(s)',
|
|
45
91
|
'cli.errors.explained': '{code} — {title}',
|
|
92
|
+
// One rendering of "this file was written", for every command that writes files — `x g`, its
|
|
93
|
+
// own `--dry-run`, and `x new`. Three copies of the same two characters is how a fourth writer
|
|
94
|
+
// arrives with a fifth marker; `--json` carries the paths themselves in `data.files`.
|
|
95
|
+
'cli.file.added': ' + {path}',
|
|
46
96
|
'cli.fix.clean': 'no boundary violation involves {file}',
|
|
47
|
-
|
|
97
|
+
// "nothing written", the same admission `cli.generate.planned` makes and for the same reason: a
|
|
98
|
+
// command called `fix` that only ever REPORTS teaches an agent to expect a repair and act as
|
|
99
|
+
// though one happened. There is no `--write` and there is not going to be one
|
|
100
|
+
// (`docs/architecture/02-boundaries.md`), so the line that runs says so every time.
|
|
101
|
+
'cli.fix.plan':
|
|
102
|
+
'{count} boundary violation(s) involve {file} — {edits} edit(s) to make, nothing written',
|
|
48
103
|
'cli.generate.wrote': 'wrote {count} file(s) for {kind} {name}',
|
|
104
|
+
// A distinct key, not the same sentence with a flag beside it: `--dry-run` reported "wrote 4
|
|
105
|
+
// file(s)" while `data.dryRun` said nothing had landed, so an agent branching on `summary`
|
|
106
|
+
// believed the files were on disk.
|
|
107
|
+
'cli.generate.planned': 'would write {count} file(s) for {kind} {name} — nothing written',
|
|
49
108
|
'cli.i18n.added': 'added {locale} — {keys} key(s) seeded from {from}',
|
|
50
109
|
'cli.i18n.dynamic': '{count} dynamic t() call(s) the extractor cannot verify:',
|
|
51
110
|
'cli.i18n.gaps': '{missing} missing key(s) across {locales} locale(s)',
|
|
52
111
|
'cli.i18n.ok': '{locales} locale(s), {keys} key(s) used — no gaps',
|
|
53
112
|
'cli.i18n.synced': 'synced {locale} from {from} — {added} key(s) added, {total} total',
|
|
54
113
|
'cli.i18n.unused': '{count} key(s) defined in {locale} and never used:',
|
|
114
|
+
'cli.jobs.backfillNoCursor': 'no cursor yet',
|
|
115
|
+
'cli.jobs.cancelled': 'job {id} cancelled — {state}',
|
|
116
|
+
'cli.jobs.backfillRow': '{name} — {rows} row(s) so far, cursor {cursor}',
|
|
117
|
+
'cli.jobs.backfills': '{count} backfill(s) in flight:',
|
|
55
118
|
'cli.jobs.deadLetters': '{count} dead letter(s):',
|
|
56
119
|
'cli.jobs.depth':
|
|
57
120
|
'{ready} ready · {running} running · {delayed} delayed · {dead} dead across {queues} queue(s)',
|
|
@@ -69,7 +132,10 @@ const CATALOG = {
|
|
|
69
132
|
'cli.manifest.wrote': 'manifest written to {path} ({routes} routes, {actions} actions)',
|
|
70
133
|
'cli.mcp.serving': 'mcp {transport} serving {tools} tools',
|
|
71
134
|
'cli.mcp.scopes': ' scopes {scopes}',
|
|
72
|
-
|
|
135
|
+
// `x db gen "initial"` is a first step, not an optional one: the scaffold writes no migration, so
|
|
136
|
+
// the app has a schema no migration records and `x verify`'s drift step says so until it runs.
|
|
137
|
+
'cli.new.done':
|
|
138
|
+
'created {name} — next: cd {name} && bun install && x db gen "initial" && x db migrate && x dev',
|
|
73
139
|
'cli.policy.count':
|
|
74
140
|
'{permissions} permission(s), {roles} role(s), {enforced} enforced by a declaration',
|
|
75
141
|
// One row per (declaration, actor) pair, never per role: a permission two declarations enforce
|
|
@@ -97,6 +163,31 @@ const CATALOG = {
|
|
|
97
163
|
'cli.test.type.pass': '{type} — {files} test file(s) on {workers} worker(s) passed in {ms}ms',
|
|
98
164
|
'cli.verify.pass': 'all {count} steps passed in {ms}ms',
|
|
99
165
|
'cli.verify.fail': '{failed} of {count} steps failed',
|
|
166
|
+
// A skipped step is not a passed one, so the two counts never share a sentence — and the skipped
|
|
167
|
+
// ones are named, because "which suite has nothing to run here?" is the question a green gate
|
|
168
|
+
// over a missing suite has to answer on its own line. Whole sentences per case rather than a
|
|
169
|
+
// clause the caller glues on, the same shape `cli.jobs.drained`/`drainedPartial` already uses.
|
|
170
|
+
'cli.verify.passSkipped':
|
|
171
|
+
'{passed} of {count} steps passed in {ms}ms — {skipped} skipped: {names}',
|
|
172
|
+
'cli.verify.failSkipped': '{failed} of {count} steps failed — {skipped} skipped: {names}',
|
|
173
|
+
'cli.verify.serial': 'serial',
|
|
174
|
+
'cli.verify.workers': '{workers} workers',
|
|
175
|
+
'cli.env.checked': '{count} declared variable(s), all present and valid',
|
|
176
|
+
'cli.env.invalid': '{count} of {total} declared variable(s) missing or malformed',
|
|
177
|
+
'cli.env.wrote': 'wrote {path} — {count} declared variable(s)',
|
|
178
|
+
'cli.env.fresh': '{path} already matches the declaration',
|
|
179
|
+
'cli.secrets.init': 'sealed {path} — master key {kid}, and .gitignore now covers the key file',
|
|
180
|
+
'cli.secrets.deploy': ' carry the key into a deploy with {env}="$(cat {keyPath})"',
|
|
181
|
+
'cli.secrets.redeploy': ' set {env}="$(cat {keyPath})" in every deploy before the next release',
|
|
182
|
+
'cli.secrets.shown': '{count} secret(s) in {path}, sealed with master key {kid}',
|
|
183
|
+
'cli.secrets.empty': '{path} holds no secrets yet',
|
|
184
|
+
'cli.secrets.undeclared':
|
|
185
|
+
'{count} secret(s) no envSchema declares, so nothing reads them: {names}',
|
|
186
|
+
'cli.secrets.edited': '{path} resealed — {added} added, {updated} changed, {removed} removed',
|
|
187
|
+
'cli.secrets.unchanged': '{path} unchanged — nothing was written',
|
|
188
|
+
'cli.secrets.set': 'sealed {name} into {path} — {count} secret(s)',
|
|
189
|
+
'cli.secrets.rotated':
|
|
190
|
+
'rotated {path} from master key {from} to {to} — {count} secret(s) resealed',
|
|
100
191
|
} as const;
|
|
101
192
|
|
|
102
193
|
export type MessageKey = keyof typeof CATALOG;
|