@ultimat3/cli 21.0.0 → 22.1.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 -1650
- package/README.md +40 -2
- package/package.json +30 -30
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- 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 +11 -1
- 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 +111 -43
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +55 -36
- 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 +3 -3
- 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 +28 -30
- package/src/error-pages.ts +19 -7
- package/src/errors.ts +14 -0
- 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 -348
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +6 -1
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +15 -8
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +1 -1
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +28 -27
- 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 +23 -17
- 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/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/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} +22 -8
- 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} +39 -16
- 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} +18 -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-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 -347
- 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/sync-url.ts +21 -1
- 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 +49 -12
- package/src/templates/resource.ts +10 -4
- 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 +24 -159
- 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 +10 -7
- package/src/verify-run.ts +11 -1
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +1 -26
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -94
- package/src/cdp-connection.ts +0 -247
- package/src/cdp-e2e-page.ts +0 -180
- package/src/cdp-e2e-session.ts +0 -199
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -193
- package/src/cdp-offline-script.ts +0 -73
- package/src/cdp-pipe.ts +0 -77
- package/src/e2e-app.ts +0 -103
- package/src/e2e-browser-handle.ts +0 -55
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -117
- package/src/e2e-errors.ts +0 -117
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -153
- package/src/e2e-preload.ts +0 -64
- package/src/e2e-probe.ts +0 -23
- package/src/e2e-selection.ts +0 -182
- package/src/e2e-spawn.ts +0 -169
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
- /package/src/{dev-render.ts → runtime-render.ts} +0 -0
package/src/cmd-deploy.ts
CHANGED
|
@@ -5,13 +5,22 @@
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
7
|
import { requireAppRoot } from './app-root';
|
|
8
|
+
import {
|
|
9
|
+
type HelmTarget,
|
|
10
|
+
helmUpgradeArgs,
|
|
11
|
+
readHelmTimeout,
|
|
12
|
+
readLabel,
|
|
13
|
+
readReleaseName,
|
|
14
|
+
readRollout,
|
|
15
|
+
} from './cmd-deploy-helm';
|
|
16
|
+
import { deploySpec } from './cmd-deploy-spec';
|
|
8
17
|
import type { CliCommand, CommandContext } from './command';
|
|
9
18
|
import { BadFlagError, UnknownCommandError } from './errors';
|
|
10
19
|
import { msg } from './messages';
|
|
11
20
|
import type { CommandResult, JsonValue } from './output';
|
|
12
21
|
import { flagBool, flagString } from './parse';
|
|
13
22
|
import { quoteArg } from './shell-quote';
|
|
14
|
-
import { PROD_ENV_FILE } from './templates/scaffold-container';
|
|
23
|
+
import { PROD_ENV_FILE } from './templates/scaffold-container-compose';
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Ordered, and the order is the design. `migrate` GATES — it runs to completion before anything
|
|
@@ -105,8 +114,40 @@ export function helmImageOverrides(image: string): readonly string[] {
|
|
|
105
114
|
: ['--set', `image.repository=${repository}`, '--set', `image.tag=${tag}`];
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
/** What was asked for: the method, and for helm the release it is aimed at. */
|
|
118
|
+
export type DeployRequest =
|
|
119
|
+
| { readonly method: 'compose' }
|
|
120
|
+
| ({ readonly method: 'helm' } & HelmTarget);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The plan for one method. A helm plan takes its target — release, namespace, timeout — because
|
|
124
|
+
* the release is read off `app.config.ts`, which only the command can import; the compose plan
|
|
125
|
+
* needs none, so its three-argument form is unchanged.
|
|
126
|
+
*/
|
|
127
|
+
export function planDeploy(image: string, method: 'compose', root: string): DeployPlan;
|
|
128
|
+
export function planDeploy(
|
|
129
|
+
image: string,
|
|
130
|
+
method: 'helm',
|
|
131
|
+
root: string,
|
|
132
|
+
target: HelmTarget,
|
|
133
|
+
): DeployPlan;
|
|
134
|
+
export function planDeploy(
|
|
135
|
+
image: string,
|
|
136
|
+
method: DeployMethod,
|
|
137
|
+
root: string,
|
|
138
|
+
target?: HelmTarget,
|
|
139
|
+
): DeployPlan {
|
|
109
140
|
if (method === 'helm') {
|
|
141
|
+
// A caller from plain JS can still reach this without one; `app` for every app is the
|
|
142
|
+
// defect the target exists to end, so it is refused rather than defaulted back.
|
|
143
|
+
if (target === undefined) {
|
|
144
|
+
throw new BadFlagError({
|
|
145
|
+
flag: 'release',
|
|
146
|
+
command: 'deploy',
|
|
147
|
+
reason: 'a helm plan names its release, and planDeploy was called with no target',
|
|
148
|
+
fix: 'x deploy --method helm --release my-app --json',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
110
151
|
// `repo@sha256:…` is a reference this chart cannot express: it renders `repository:tag` and
|
|
111
152
|
// has no digest branch, so passing one through would deploy `repo@sha256:…:<appVersion>` —
|
|
112
153
|
// a tag no registry has. Refused here rather than by a `helm upgrade` failing halfway.
|
|
@@ -121,19 +162,7 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
|
|
|
121
162
|
return {
|
|
122
163
|
image,
|
|
123
164
|
env: {},
|
|
124
|
-
steps: [
|
|
125
|
-
{
|
|
126
|
-
role: 'all',
|
|
127
|
-
command: [
|
|
128
|
-
'helm',
|
|
129
|
-
'upgrade',
|
|
130
|
-
'--install',
|
|
131
|
-
'app',
|
|
132
|
-
join(root, 'docker', 'helm'),
|
|
133
|
-
...helmImageOverrides(image),
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
|
-
],
|
|
165
|
+
steps: [{ role: 'all', command: helmUpgradeArgs(root, target, helmImageOverrides(image)) }],
|
|
137
166
|
};
|
|
138
167
|
}
|
|
139
168
|
return {
|
|
@@ -171,33 +200,50 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
|
|
|
171
200
|
* renderers and the failure `fix:` go through here, so the plan `--json` reports, the plan the
|
|
172
201
|
* terminal shows and the line the refusal hands back can never name three different deployments.
|
|
173
202
|
*/
|
|
203
|
+
/** The flags only a helm deploy reads. Named once: the spec, the refusal and the reader agree. */
|
|
204
|
+
const HELM_ONLY_FLAGS = ['release', 'namespace', 'timeout'] as const;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The method and its target. A helm-only flag on the compose method is REFUSED, never dropped:
|
|
208
|
+
* `--namespace staging` on a compose deploy would otherwise run against the one box there is and
|
|
209
|
+
* report success, which is the silent direction every declared-and-ignored knob in this repo took.
|
|
210
|
+
*/
|
|
211
|
+
export async function readDeployRequest(
|
|
212
|
+
ctx: CommandContext,
|
|
213
|
+
method: DeployMethod,
|
|
214
|
+
root: string,
|
|
215
|
+
): Promise<DeployRequest> {
|
|
216
|
+
if (method === 'compose') {
|
|
217
|
+
const stray = HELM_ONLY_FLAGS.find((flag) => flagString(ctx.args, flag) !== undefined);
|
|
218
|
+
if (stray !== undefined) {
|
|
219
|
+
throw new BadFlagError({
|
|
220
|
+
flag: stray,
|
|
221
|
+
command: 'deploy',
|
|
222
|
+
reason: 'it applies to --method helm only; a compose deploy has one box and no release',
|
|
223
|
+
fix: 'x deploy --method helm --release my-app --namespace my-namespace --json',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return { method };
|
|
227
|
+
}
|
|
228
|
+
const namespace = flagString(ctx.args, 'namespace');
|
|
229
|
+
return {
|
|
230
|
+
method,
|
|
231
|
+
release: await readReleaseName(root, flagString(ctx.args, 'release')),
|
|
232
|
+
namespace: namespace === undefined ? undefined : readLabel('namespace', namespace),
|
|
233
|
+
timeout: readHelmTimeout(flagString(ctx.args, 'timeout')),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
174
237
|
const stepLine = (env: Readonly<Record<string, string>>, command: readonly string[]): string =>
|
|
175
|
-
[
|
|
176
|
-
|
|
177
|
-
|
|
238
|
+
[
|
|
239
|
+
...Object.entries(env).map(([name, value]) => `${name}=${quoteArg(value)}`),
|
|
240
|
+
// Every word quoted where it must be: the chart and compose paths are joined from the app
|
|
241
|
+
// root, and a root holding a space or a `$(` pasted back as two words, or as a second command.
|
|
242
|
+
...command.map(quoteArg),
|
|
243
|
+
].join(' ');
|
|
178
244
|
|
|
179
245
|
export const deployCommand: CliCommand = {
|
|
180
|
-
spec:
|
|
181
|
-
name: 'deploy',
|
|
182
|
-
summary: 'run the container deploy plan: migrate first, then the serving roles',
|
|
183
|
-
usage: 'x deploy --image repo/app:tag [--method compose|helm] [--dry-run] [--json]',
|
|
184
|
-
requiresApp: true,
|
|
185
|
-
flags: [
|
|
186
|
-
{ name: 'image', type: 'string', summary: 'image reference to deploy' },
|
|
187
|
-
{ name: 'method', type: 'string', summary: 'compose | helm', default: 'compose' },
|
|
188
|
-
{ name: 'dry-run', type: 'boolean', summary: 'print the plan, run nothing' },
|
|
189
|
-
// `--critical` was here and is gone. It parsed, it was echoed into the plan JSON as
|
|
190
|
-
// `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
|
|
191
|
-
// nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
|
|
192
|
-
// `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
|
|
193
|
-
// into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
|
|
194
|
-
// `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
|
|
195
|
-
// as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
|
|
196
|
-
// force-navigates a client. A deploy also has no channel to one — the plan is
|
|
197
|
-
// `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
|
|
198
|
-
// and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
|
|
199
|
-
],
|
|
200
|
-
},
|
|
246
|
+
spec: deploySpec,
|
|
201
247
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
202
248
|
const root = requireAppRoot('deploy', ctx.cwd).dir;
|
|
203
249
|
const image = flagString(ctx.args, 'image') ?? 'ultimate-app:dev';
|
|
@@ -208,10 +254,27 @@ export const deployCommand: CliCommand = {
|
|
|
208
254
|
// `docker/docker-compose.prod.yml`. An app that deleted the chart gets helm's own error through
|
|
209
255
|
// X_DEPLOY_FAILED, whose fix is the exact command to rerun.
|
|
210
256
|
const method = readMethod(flagString(ctx.args, 'method'));
|
|
211
|
-
const
|
|
212
|
-
const
|
|
257
|
+
const request = await readDeployRequest(ctx, method, root);
|
|
258
|
+
const plan =
|
|
259
|
+
request.method === 'helm'
|
|
260
|
+
? planDeploy(image, 'helm', root, request)
|
|
261
|
+
: planDeploy(image, 'compose', root);
|
|
262
|
+
// What the helm flags resolved to — the release above all, which is read off `app.config.ts`
|
|
263
|
+
// and so is not something the operator typed. Absent on compose, which has no release.
|
|
264
|
+
const target: { readonly [key: string]: JsonValue } =
|
|
265
|
+
request.method === 'helm'
|
|
266
|
+
? {
|
|
267
|
+
helm: {
|
|
268
|
+
release: request.release,
|
|
269
|
+
namespace: request.namespace ?? null,
|
|
270
|
+
timeout: request.timeout,
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
: {};
|
|
274
|
+
const planJson: { readonly [key: string]: JsonValue } = {
|
|
213
275
|
image: plan.image,
|
|
214
276
|
method,
|
|
277
|
+
...target,
|
|
215
278
|
// Reported, because it is what makes `image` above true on the compose method — a dry run
|
|
216
279
|
// that names an image the steps do not carry is the defect this field closed.
|
|
217
280
|
env: { ...plan.env },
|
|
@@ -231,8 +294,13 @@ export const deployCommand: CliCommand = {
|
|
|
231
294
|
),
|
|
232
295
|
};
|
|
233
296
|
}
|
|
297
|
+
// helm prints its release record (`--output json`), so the report carries helm's own verdict
|
|
298
|
+
// and revision rather than an exit code standing in for one. Compose has no such record.
|
|
299
|
+
let rollout: { readonly [key: string]: JsonValue } = {};
|
|
234
300
|
for (const step of plan.steps) {
|
|
235
301
|
const result = await ctx.runner(step.command, { cwd: root, env: plan.env });
|
|
302
|
+
if (request.method === 'helm')
|
|
303
|
+
rollout = { rollout: { ...readRollout(request, result.stdout) } };
|
|
236
304
|
if (!result.ok) {
|
|
237
305
|
return {
|
|
238
306
|
ok: false,
|
|
@@ -246,7 +314,7 @@ export const deployCommand: CliCommand = {
|
|
|
246
314
|
docs: ERROR_DOCS_URL,
|
|
247
315
|
},
|
|
248
316
|
],
|
|
249
|
-
data: planJson,
|
|
317
|
+
data: { ...planJson, ...rollout },
|
|
250
318
|
};
|
|
251
319
|
}
|
|
252
320
|
}
|
|
@@ -254,7 +322,7 @@ export const deployCommand: CliCommand = {
|
|
|
254
322
|
ok: true,
|
|
255
323
|
command: 'deploy',
|
|
256
324
|
summary: msg('cli.deploy.plan', { images: 1, roles }),
|
|
257
|
-
data: planJson,
|
|
325
|
+
data: { ...planJson, ...rollout },
|
|
258
326
|
};
|
|
259
327
|
},
|
|
260
328
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `x dev`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-dev.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
import { DEV_ROLES } from './role-start-types';
|
|
6
|
+
|
|
7
|
+
export const devSpec: CommandSpec = {
|
|
8
|
+
name: 'dev',
|
|
9
|
+
summary: 'all roles in one process: embedded services, sub-second reload, /_x mounted',
|
|
10
|
+
usage: 'x dev [--port 3000] [--role web,worker] [--once] [--json]',
|
|
11
|
+
requiresApp: true,
|
|
12
|
+
flags: [
|
|
13
|
+
// No `default`: a default reads exactly like a value the caller typed, and it outranked
|
|
14
|
+
// `PORT` from the app's `.env.development` — `devPortFor` supplies the 3000 last.
|
|
15
|
+
{ name: 'port', type: 'string', summary: 'HTTP port (default: PORT, else 3000)' },
|
|
16
|
+
{
|
|
17
|
+
name: 'role',
|
|
18
|
+
type: 'string',
|
|
19
|
+
// `replicator` is named because it is selectable and NOT default — it takes a replication
|
|
20
|
+
// slot on a shared database, which is not something every `x dev` should do by starting.
|
|
21
|
+
summary: `roles to run (default: all of ${DEV_ROLES.join(',')}; replicator is opt-in)`,
|
|
22
|
+
},
|
|
23
|
+
{ name: 'once', type: 'boolean', summary: 'boot, report, exit — for smoke tests and CI' },
|
|
24
|
+
],
|
|
25
|
+
};
|
package/src/cmd-dev.ts
CHANGED
|
@@ -17,25 +17,18 @@ import { loadSignInPath } from './app-auth';
|
|
|
17
17
|
import { appManifest } from './app-manifest';
|
|
18
18
|
import { requireAppRoot } from './app-root';
|
|
19
19
|
import { loadAppRuntime } from './app-runtime';
|
|
20
|
+
import { devSpec } from './cmd-dev-spec';
|
|
20
21
|
import type { CliCommand, CommandContext } from './command';
|
|
21
22
|
import type { DevDashboardInput, DevStatus } from './dev-dashboard';
|
|
22
23
|
import { devPanels } from './dev-dashboard';
|
|
23
|
-
import { declareDevEnvironment } from './dev-environment';
|
|
24
|
-
import { liveFeedLabel } from './dev-live-feed';
|
|
24
|
+
import { declareDevEnvironment, needsDevEnvironmentDeclaration } from './dev-environment';
|
|
25
25
|
import { clearLock, preflight, writeLock } from './dev-lock';
|
|
26
26
|
import { createStatementLedger } from './dev-n-plus-one';
|
|
27
|
+
import { devPortFor } from './dev-port';
|
|
27
28
|
import { coalesceReloads } from './dev-reload';
|
|
28
|
-
import { replicaOverrides } from './dev-replica';
|
|
29
|
-
import type { RunningRoles } from './dev-roles';
|
|
30
|
-
import { DEV_BINDING, DEV_ROLES, selectRoles, startRoles } from './dev-roles';
|
|
31
29
|
import { devRouteTable } from './dev-route-table';
|
|
32
|
-
import type { RunningServices } from './dev-runtime';
|
|
33
|
-
import { cdnLabel, describeCdn, describeMail, mailLabel, startServices } from './dev-runtime';
|
|
34
|
-
import type { DevServices } from './dev-services';
|
|
35
|
-
import { describeServices, reportedUrls, resolveServices } from './dev-services';
|
|
36
30
|
import { createTraceRecorder } from './dev-traces';
|
|
37
31
|
import { watchTree } from './dev-watch-tree';
|
|
38
|
-
import { intFlagOr, PORT_RANGE } from './flag-number';
|
|
39
32
|
import { holdUntilShutdown } from './hold';
|
|
40
33
|
import type { IslandBundle } from './island-bundle';
|
|
41
34
|
import { buildIslands } from './island-bundle';
|
|
@@ -44,10 +37,23 @@ import { msg } from './messages';
|
|
|
44
37
|
import type { CommandResult, Finding } from './output';
|
|
45
38
|
import { findingFrom } from './output';
|
|
46
39
|
import { flagString } from './parse';
|
|
47
|
-
import {
|
|
40
|
+
import type { RunningRoles } from './role-start';
|
|
41
|
+
import { DEV_BINDING, DEV_ROLES, selectRoles, startRoles } from './role-start';
|
|
42
|
+
import type { DevServices } from './runtime-bindings';
|
|
43
|
+
import {
|
|
44
|
+
describeServices,
|
|
45
|
+
reportedUrls,
|
|
46
|
+
resolveServices,
|
|
47
|
+
withRealtimeEvents,
|
|
48
|
+
} from './runtime-bindings';
|
|
49
|
+
import { liveFeedLabel } from './runtime-live-feed';
|
|
50
|
+
import { replicaOverrides } from './runtime-replica';
|
|
51
|
+
import type { RunningServices } from './runtime-services';
|
|
52
|
+
import { cdnLabel, describeCdn, describeMail, mailLabel, startServices } from './runtime-services';
|
|
53
|
+
import { metricsPortFor, releaseBoot } from './serve';
|
|
48
54
|
import { loopFacts, loopFinding, loopNotice } from './statement-loop';
|
|
49
55
|
|
|
50
|
-
const
|
|
56
|
+
const _DEFAULT_PORT = 3000;
|
|
51
57
|
|
|
52
58
|
export interface DevServer {
|
|
53
59
|
readonly url: string;
|
|
@@ -121,13 +127,41 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
121
127
|
// EVERY boot: a scratch server (`x shot`, `ui.shot`) boots here too, and without this a
|
|
122
128
|
// fail-closed dev actor installs nothing — the picture is of a 401. Idempotent.
|
|
123
129
|
declareDevEnvironment(options.env);
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
// The declaration lands on `process.env`; the env this boot passes on must carry it too, or every
|
|
131
|
+
// service resolved from `options.env` still reads no `ULTIMATE_ENV`.
|
|
132
|
+
const env = needsDevEnvironmentDeclaration(options.env)
|
|
133
|
+
? { ...options.env, ULTIMATE_ENV: 'development' }
|
|
134
|
+
: options.env;
|
|
135
|
+
const resolved = resolveServices(options.root, env);
|
|
136
|
+
const runtime: RunningServices = await startServices(resolved, env);
|
|
137
|
+
// The events binding the boot line reports is the bus the runtime chose, read off it.
|
|
138
|
+
const services = withRealtimeEvents(resolved, env, runtime.realtime);
|
|
139
|
+
// `serve.ts`'s `releaseBoot` shape: everything acquired from here on is released, newest first,
|
|
140
|
+
// if the boot throws — a failed `x dev` left PGlite holding `.x/pgdata` for the retry to meet.
|
|
141
|
+
const acquired: (() => void | Promise<void>)[] = [() => runtime.stop()];
|
|
142
|
+
try {
|
|
143
|
+
return await bootDev(options, services, runtime, acquired);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await releaseBoot(acquired);
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function bootDev(
|
|
151
|
+
options: StartDevOptions,
|
|
152
|
+
services: ReturnType<typeof resolveServices>,
|
|
153
|
+
runtime: RunningServices,
|
|
154
|
+
acquired: (() => void | Promise<void>)[],
|
|
155
|
+
): Promise<DevServer> {
|
|
126
156
|
// Installed before the app loads, so a span opened during registration is already recorded.
|
|
127
157
|
// Tracing is always on in the framework and free until an exporter is configured; `x dev` is
|
|
128
158
|
// what configures one, which is the whole reason `/_x/timeline` has anything to draw.
|
|
129
159
|
const traces = createTraceRecorder();
|
|
130
160
|
configureTelemetry({ exporter: traces.exporter });
|
|
161
|
+
acquired.push(() => {
|
|
162
|
+
configureTelemetry({ exporter: noopExporter });
|
|
163
|
+
traces.reset();
|
|
164
|
+
});
|
|
131
165
|
// Installed at the same moment and for the same reason: an observer is the single switch that
|
|
132
166
|
// turns statement instrumentation on at all (`@ultimat3/db`'s `observe.ts`), so the timeline's
|
|
133
167
|
// SQL rows and the repeat counts arrive together rather than through two toggles. `serve.ts`
|
|
@@ -135,6 +169,10 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
135
169
|
// uninstalled, and nothing more (axiom 6).
|
|
136
170
|
const statements = createStatementLedger();
|
|
137
171
|
setStatementObserver(statements.observer);
|
|
172
|
+
acquired.push(() => {
|
|
173
|
+
setStatementObserver(undefined);
|
|
174
|
+
statements.reset();
|
|
175
|
+
});
|
|
138
176
|
// ONE load at boot, the same call the rebuild below makes: the manifest and the findings are
|
|
139
177
|
// two projections of one scan. Until 2026-09-07 this was `loadApp` for the findings and then
|
|
140
178
|
// `appManifest` — which loads again — for the manifest, so a save landing between the two put
|
|
@@ -224,6 +262,7 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
224
262
|
// standby — so this key does not exist on a homework app's boot at all.
|
|
225
263
|
...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
|
|
226
264
|
});
|
|
265
|
+
acquired.push(() => running.stop());
|
|
227
266
|
|
|
228
267
|
// One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
|
|
229
268
|
// trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
|
|
@@ -295,32 +334,12 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
295
334
|
}
|
|
296
335
|
|
|
297
336
|
export const devCommand: CliCommand = {
|
|
298
|
-
spec:
|
|
299
|
-
name: 'dev',
|
|
300
|
-
summary: 'all roles in one process: embedded services, sub-second reload, /_x mounted',
|
|
301
|
-
usage: 'x dev [--port 3000] [--role web,worker] [--once] [--json]',
|
|
302
|
-
requiresApp: true,
|
|
303
|
-
flags: [
|
|
304
|
-
{ name: 'port', type: 'string', summary: 'HTTP port', default: String(DEFAULT_PORT) },
|
|
305
|
-
{
|
|
306
|
-
name: 'role',
|
|
307
|
-
type: 'string',
|
|
308
|
-
// `replicator` is named because it is selectable and NOT default — it takes a replication
|
|
309
|
-
// slot on a shared database, which is not something every `x dev` should do by starting.
|
|
310
|
-
summary: `roles to run (default: all of ${DEV_ROLES.join(',')}; replicator is opt-in)`,
|
|
311
|
-
},
|
|
312
|
-
{ name: 'once', type: 'boolean', summary: 'boot, report, exit — for smoke tests and CI' },
|
|
313
|
-
],
|
|
314
|
-
},
|
|
337
|
+
spec: devSpec,
|
|
315
338
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
316
339
|
const root = requireAppRoot('dev', ctx.cwd).dir;
|
|
317
340
|
// Validated, not `parseInt`'d: `x dev --port abc` handed `NaN` to `Bun.serve`, which binds an
|
|
318
341
|
// arbitrary port — a dev server reachable at an address nothing printed.
|
|
319
|
-
const port =
|
|
320
|
-
ctx.args,
|
|
321
|
-
{ name: 'port', command: 'dev', ...PORT_RANGE, example: `x dev --port ${DEFAULT_PORT}` },
|
|
322
|
-
DEFAULT_PORT,
|
|
323
|
-
);
|
|
342
|
+
const port = devPortFor(ctx.args, ctx.env);
|
|
324
343
|
const roles = selectRoles(flagString(ctx.args, 'role'));
|
|
325
344
|
// BEFORE anything boots. Both failures this catches were reachable and both reported the wrong
|
|
326
345
|
// thing: a taken port surfaced as X_CLI_UNEXPECTED wrapping "Is port 3000 in use?" with a `fix:`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `x docs`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-docs.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
/** Matches printed by default. Enough to choose between, few enough to read all of. */
|
|
7
|
+
export const DEFAULT_LIMIT = 5;
|
|
8
|
+
|
|
9
|
+
export const docsSpec: CommandSpec = {
|
|
10
|
+
name: 'docs',
|
|
11
|
+
summary: 'the framework docs, answered offline from the installed packages',
|
|
12
|
+
usage: 'x docs "<question|topic|symbol>" [--limit <n>] [--json]',
|
|
13
|
+
flags: [
|
|
14
|
+
{ name: 'limit', type: 'string', summary: `matches to return (default: ${DEFAULT_LIMIT})` },
|
|
15
|
+
],
|
|
16
|
+
};
|
package/src/cmd-docs.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
8
8
|
import type { DocEntry, DocHit } from '@ultimat3/manifest';
|
|
9
9
|
import { nearestTopics, scanInstalledDocs, searchDocs } from '@ultimat3/manifest';
|
|
10
|
+
import { DEFAULT_LIMIT, docsSpec } from './cmd-docs-spec';
|
|
10
11
|
import type { CliCommand, CommandContext } from './command';
|
|
11
12
|
import { MissingPositionalError } from './errors';
|
|
12
13
|
import { frameworkScopeDir } from './framework-scope';
|
|
@@ -15,9 +16,6 @@ import { msg } from './messages';
|
|
|
15
16
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
16
17
|
import { flagString } from './parse';
|
|
17
18
|
|
|
18
|
-
/** Matches printed by default. Enough to choose between, few enough to read all of. */
|
|
19
|
-
const DEFAULT_LIMIT = 5;
|
|
20
|
-
|
|
21
19
|
/** An install where the CLI cannot see its own dependency is broken, not merely undocumented. */
|
|
22
20
|
const unresolvedFinding = (): Finding => ({
|
|
23
21
|
code: 'X_CLI_UNEXPECTED',
|
|
@@ -115,14 +113,7 @@ function missResult(query: string, entries: readonly DocEntry[]): CommandResult
|
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
export const docsCommand: CliCommand = {
|
|
118
|
-
spec:
|
|
119
|
-
name: 'docs',
|
|
120
|
-
summary: 'the framework docs, answered offline from the installed packages',
|
|
121
|
-
usage: 'x docs "<question|topic|symbol>" [--limit <n>] [--json]',
|
|
122
|
-
flags: [
|
|
123
|
-
{ name: 'limit', type: 'string', summary: `matches to return (default: ${DEFAULT_LIMIT})` },
|
|
124
|
-
],
|
|
125
|
-
},
|
|
116
|
+
spec: docsSpec,
|
|
126
117
|
// `async` is load-bearing: a synchronous throw would escape every caller that awaits the
|
|
127
118
|
// promise this signature promises, including the dispatcher's own error path.
|
|
128
119
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
@@ -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> {
|