@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,40 @@
|
|
|
1
|
+
// `x deploy`'s declaration, apart from its body: the parser, `x help` and the `errors` step
|
|
2
|
+
// read it without loading `cmd-deploy.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const deploySpec: CommandSpec = {
|
|
7
|
+
name: 'deploy',
|
|
8
|
+
summary: 'run the container deploy plan: migrate first, then the serving roles',
|
|
9
|
+
usage:
|
|
10
|
+
'x deploy --image repo/app:tag [--method compose|helm] [--release name] [--namespace ns] [--timeout 15m] [--dry-run] [--json]',
|
|
11
|
+
requiresApp: true,
|
|
12
|
+
flags: [
|
|
13
|
+
{ name: 'image', type: 'string', summary: 'image reference to deploy' },
|
|
14
|
+
{ name: 'method', type: 'string', summary: 'compose | helm', default: 'compose' },
|
|
15
|
+
{ name: 'dry-run', type: 'boolean', summary: 'print the plan, run nothing' },
|
|
16
|
+
{
|
|
17
|
+
name: 'release',
|
|
18
|
+
type: 'string',
|
|
19
|
+
summary: 'helm release name (default: app.config.ts name)',
|
|
20
|
+
},
|
|
21
|
+
{ name: 'namespace', type: 'string', summary: 'helm namespace (default: the kube context)' },
|
|
22
|
+
{
|
|
23
|
+
name: 'timeout',
|
|
24
|
+
type: 'string',
|
|
25
|
+
// No `default:` — the parser would then fill it on every compose deploy too, and the refusal
|
|
26
|
+
// of a helm-only flag there could not tell an operator's value from its own.
|
|
27
|
+
summary: 'how long helm waits for the migrate hook and the rollout (default 15m)',
|
|
28
|
+
},
|
|
29
|
+
// `--critical` was here and is gone. It parsed, it was echoed into the plan JSON as
|
|
30
|
+
// `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
|
|
31
|
+
// nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
|
|
32
|
+
// `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
|
|
33
|
+
// into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
|
|
34
|
+
// `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
|
|
35
|
+
// as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
|
|
36
|
+
// force-navigates a client. A deploy also has no channel to one — the plan is
|
|
37
|
+
// `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
|
|
38
|
+
// and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
|
|
39
|
+
],
|
|
40
|
+
};
|
package/src/cmd-deploy.ts
CHANGED
|
@@ -5,12 +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';
|
|
23
|
+
import { PROD_ENV_FILE } from './templates/scaffold-container-compose';
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* Ordered, and the order is the design. `migrate` GATES — it runs to completion before anything
|
|
@@ -104,8 +114,40 @@ export function helmImageOverrides(image: string): readonly string[] {
|
|
|
104
114
|
: ['--set', `image.repository=${repository}`, '--set', `image.tag=${tag}`];
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
|
|
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 {
|
|
108
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
|
+
}
|
|
109
151
|
// `repo@sha256:…` is a reference this chart cannot express: it renders `repository:tag` and
|
|
110
152
|
// has no digest branch, so passing one through would deploy `repo@sha256:…:<appVersion>` —
|
|
111
153
|
// a tag no registry has. Refused here rather than by a `helm upgrade` failing halfway.
|
|
@@ -120,19 +162,7 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
|
|
|
120
162
|
return {
|
|
121
163
|
image,
|
|
122
164
|
env: {},
|
|
123
|
-
steps: [
|
|
124
|
-
{
|
|
125
|
-
role: 'all',
|
|
126
|
-
command: [
|
|
127
|
-
'helm',
|
|
128
|
-
'upgrade',
|
|
129
|
-
'--install',
|
|
130
|
-
'app',
|
|
131
|
-
join(root, 'docker', 'helm'),
|
|
132
|
-
...helmImageOverrides(image),
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
],
|
|
165
|
+
steps: [{ role: 'all', command: helmUpgradeArgs(root, target, helmImageOverrides(image)) }],
|
|
136
166
|
};
|
|
137
167
|
}
|
|
138
168
|
return {
|
|
@@ -146,6 +176,12 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
|
|
|
146
176
|
command: [
|
|
147
177
|
'docker',
|
|
148
178
|
'compose',
|
|
179
|
+
// Compose interpolates `${SYNC_URL:?…}` and `${POSTGRES_PASSWORD:?…}` from the shell and
|
|
180
|
+
// `--env-file` only — never from a service's `env_file:`. Without this an operator who put
|
|
181
|
+
// them in `.env.production`, the one file the compose file tells them to fill, had every
|
|
182
|
+
// step die on a parse error. Global flag, so it precedes `-f`; the shell still wins over it.
|
|
183
|
+
'--env-file',
|
|
184
|
+
join(root, PROD_ENV_FILE),
|
|
149
185
|
'-f',
|
|
150
186
|
join(root, 'docker', 'docker-compose.prod.yml'),
|
|
151
187
|
ONE_SHOT_ROLES.includes(role) ? 'run' : 'up',
|
|
@@ -164,33 +200,50 @@ export function planDeploy(image: string, method: DeployMethod, root: string): D
|
|
|
164
200
|
* renderers and the failure `fix:` go through here, so the plan `--json` reports, the plan the
|
|
165
201
|
* terminal shows and the line the refusal hands back can never name three different deployments.
|
|
166
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
|
+
|
|
167
237
|
const stepLine = (env: Readonly<Record<string, string>>, command: readonly string[]): string =>
|
|
168
|
-
[
|
|
169
|
-
|
|
170
|
-
|
|
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(' ');
|
|
171
244
|
|
|
172
245
|
export const deployCommand: CliCommand = {
|
|
173
|
-
spec:
|
|
174
|
-
name: 'deploy',
|
|
175
|
-
summary: 'run the container deploy plan: migrate first, then the serving roles',
|
|
176
|
-
usage: 'x deploy --image repo/app:tag [--method compose|helm] [--dry-run] [--json]',
|
|
177
|
-
requiresApp: true,
|
|
178
|
-
flags: [
|
|
179
|
-
{ name: 'image', type: 'string', summary: 'image reference to deploy' },
|
|
180
|
-
{ name: 'method', type: 'string', summary: 'compose | helm', default: 'compose' },
|
|
181
|
-
{ name: 'dry-run', type: 'boolean', summary: 'print the plan, run nothing' },
|
|
182
|
-
// `--critical` was here and is gone. It parsed, it was echoed into the plan JSON as
|
|
183
|
-
// `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
|
|
184
|
-
// nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
|
|
185
|
-
// `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
|
|
186
|
-
// into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
|
|
187
|
-
// `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
|
|
188
|
-
// as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
|
|
189
|
-
// force-navigates a client. A deploy also has no channel to one — the plan is
|
|
190
|
-
// `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
|
|
191
|
-
// and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
|
|
192
|
-
],
|
|
193
|
-
},
|
|
246
|
+
spec: deploySpec,
|
|
194
247
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
195
248
|
const root = requireAppRoot('deploy', ctx.cwd).dir;
|
|
196
249
|
const image = flagString(ctx.args, 'image') ?? 'ultimate-app:dev';
|
|
@@ -201,10 +254,27 @@ export const deployCommand: CliCommand = {
|
|
|
201
254
|
// `docker/docker-compose.prod.yml`. An app that deleted the chart gets helm's own error through
|
|
202
255
|
// X_DEPLOY_FAILED, whose fix is the exact command to rerun.
|
|
203
256
|
const method = readMethod(flagString(ctx.args, 'method'));
|
|
204
|
-
const
|
|
205
|
-
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 } = {
|
|
206
275
|
image: plan.image,
|
|
207
276
|
method,
|
|
277
|
+
...target,
|
|
208
278
|
// Reported, because it is what makes `image` above true on the compose method — a dry run
|
|
209
279
|
// that names an image the steps do not carry is the defect this field closed.
|
|
210
280
|
env: { ...plan.env },
|
|
@@ -224,8 +294,13 @@ export const deployCommand: CliCommand = {
|
|
|
224
294
|
),
|
|
225
295
|
};
|
|
226
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 } = {};
|
|
227
300
|
for (const step of plan.steps) {
|
|
228
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) } };
|
|
229
304
|
if (!result.ok) {
|
|
230
305
|
return {
|
|
231
306
|
ok: false,
|
|
@@ -239,7 +314,7 @@ export const deployCommand: CliCommand = {
|
|
|
239
314
|
docs: ERROR_DOCS_URL,
|
|
240
315
|
},
|
|
241
316
|
],
|
|
242
|
-
data: planJson,
|
|
317
|
+
data: { ...planJson, ...rollout },
|
|
243
318
|
};
|
|
244
319
|
}
|
|
245
320
|
}
|
|
@@ -247,7 +322,7 @@ export const deployCommand: CliCommand = {
|
|
|
247
322
|
ok: true,
|
|
248
323
|
command: 'deploy',
|
|
249
324
|
summary: msg('cli.deploy.plan', { images: 1, roles }),
|
|
250
|
-
data: planJson,
|
|
325
|
+
data: { ...planJson, ...rollout },
|
|
251
326
|
};
|
|
252
327
|
},
|
|
253
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
|
@@ -9,66 +9,60 @@ import { devShellStyle } from '@ultimat3/admin/dev';
|
|
|
9
9
|
import type { Role } from '@ultimat3/core';
|
|
10
10
|
import { configureTelemetry, METRICS_PATH, noopExporter } from '@ultimat3/core';
|
|
11
11
|
import { setStatementObserver } from '@ultimat3/db';
|
|
12
|
-
import type { OverlayNotice, RequestContext
|
|
12
|
+
import type { OverlayNotice, RequestContext } from '@ultimat3/http';
|
|
13
13
|
import { asCtx } from '@ultimat3/http';
|
|
14
14
|
import type { Manifest } from '@ultimat3/manifest';
|
|
15
15
|
import { MANIFEST_FILENAME } from '@ultimat3/manifest';
|
|
16
|
-
import { describeRoutes } from '@ultimat3/render';
|
|
17
|
-
import { apiRoutes } from './api-routes';
|
|
18
16
|
import { loadSignInPath } from './app-auth';
|
|
19
17
|
import { appManifest } from './app-manifest';
|
|
20
|
-
import { mountAppMcp } from './app-mcp';
|
|
21
18
|
import { requireAppRoot } from './app-root';
|
|
22
19
|
import { loadAppRuntime } from './app-runtime';
|
|
20
|
+
import { devSpec } from './cmd-dev-spec';
|
|
23
21
|
import type { CliCommand, CommandContext } from './command';
|
|
24
|
-
import { assetRoutes } from './dev-assets';
|
|
25
22
|
import type { DevDashboardInput, DevStatus } from './dev-dashboard';
|
|
26
|
-
import {
|
|
27
|
-
import { declareDevEnvironment } from './dev-environment';
|
|
28
|
-
import { liveFeedLabel } from './dev-live-feed';
|
|
23
|
+
import { devPanels } from './dev-dashboard';
|
|
24
|
+
import { declareDevEnvironment, needsDevEnvironmentDeclaration } from './dev-environment';
|
|
29
25
|
import { clearLock, preflight, writeLock } from './dev-lock';
|
|
30
26
|
import { createStatementLedger } from './dev-n-plus-one';
|
|
27
|
+
import { devPortFor } from './dev-port';
|
|
31
28
|
import { coalesceReloads } from './dev-reload';
|
|
32
|
-
import {
|
|
33
|
-
import { replicaOverrides } from './dev-replica';
|
|
34
|
-
import type { RunningRoles } from './dev-roles';
|
|
35
|
-
import { DEV_BINDING, DEV_ROLES, selectRoles, startRoles } from './dev-roles';
|
|
36
|
-
import type { RunningServices } from './dev-runtime';
|
|
37
|
-
import { cdnLabel, describeCdn, describeMail, mailLabel, startServices } from './dev-runtime';
|
|
38
|
-
import type { DevServices } from './dev-services';
|
|
39
|
-
import { describeServices, reportedUrls, resolveServices } from './dev-services';
|
|
40
|
-
import { storageRoutes } from './dev-storage';
|
|
29
|
+
import { devRouteTable } from './dev-route-table';
|
|
41
30
|
import { createTraceRecorder } from './dev-traces';
|
|
42
31
|
import { watchTree } from './dev-watch-tree';
|
|
43
|
-
import { errorPageStyleSources } from './error-page-csp';
|
|
44
|
-
import { intFlagOr, PORT_RANGE } from './flag-number';
|
|
45
32
|
import { holdUntilShutdown } from './hold';
|
|
46
33
|
import type { IslandBundle } from './island-bundle';
|
|
47
34
|
import { buildIslands } from './island-bundle';
|
|
48
35
|
import { FRAME_STYLE } from './island-harness';
|
|
49
|
-
import { islandHarnessRoutes } from './island-harness-route';
|
|
50
|
-
import { islandRoutes } from './island-routes';
|
|
51
|
-
import { loadIslandStates } from './island-states-load';
|
|
52
36
|
import { msg } from './messages';
|
|
53
37
|
import type { CommandResult, Finding } from './output';
|
|
54
38
|
import { findingFrom } from './output';
|
|
55
39
|
import { flagString } from './parse';
|
|
56
|
-
import {
|
|
57
|
-
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';
|
|
58
54
|
import { loopFacts, loopFinding, loopNotice } from './statement-loop';
|
|
59
|
-
import { styleBundle } from './style-bundle';
|
|
60
|
-
import { styleRoutes } from './style-routes';
|
|
61
|
-
import { serviceWorkerArtifacts } from './sw-artifacts';
|
|
62
|
-
import { serviceWorkerRoutes } from './sw-routes';
|
|
63
|
-
import { loadThemeMode, themeBoot } from './theme-boot';
|
|
64
55
|
|
|
65
|
-
const
|
|
56
|
+
const _DEFAULT_PORT = 3000;
|
|
66
57
|
|
|
67
58
|
export interface DevServer {
|
|
68
59
|
readonly url: string;
|
|
69
60
|
readonly services: DevServices;
|
|
70
61
|
readonly roles: readonly Role[];
|
|
71
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* `BUILD_ID` when stamped — the id every response carries. Otherwise the manifest as it stands
|
|
64
|
+
* now, so a reload that registers a new route moves it.
|
|
65
|
+
*/
|
|
72
66
|
readonly buildId: string;
|
|
73
67
|
/**
|
|
74
68
|
* Modules that would not import, primitives that would not register, reloads that would not
|
|
@@ -133,13 +127,41 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
133
127
|
// EVERY boot: a scratch server (`x shot`, `ui.shot`) boots here too, and without this a
|
|
134
128
|
// fail-closed dev actor installs nothing — the picture is of a 401. Idempotent.
|
|
135
129
|
declareDevEnvironment(options.env);
|
|
136
|
-
|
|
137
|
-
|
|
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> {
|
|
138
156
|
// Installed before the app loads, so a span opened during registration is already recorded.
|
|
139
157
|
// Tracing is always on in the framework and free until an exporter is configured; `x dev` is
|
|
140
158
|
// what configures one, which is the whole reason `/_x/timeline` has anything to draw.
|
|
141
159
|
const traces = createTraceRecorder();
|
|
142
160
|
configureTelemetry({ exporter: traces.exporter });
|
|
161
|
+
acquired.push(() => {
|
|
162
|
+
configureTelemetry({ exporter: noopExporter });
|
|
163
|
+
traces.reset();
|
|
164
|
+
});
|
|
143
165
|
// Installed at the same moment and for the same reason: an observer is the single switch that
|
|
144
166
|
// turns statement instrumentation on at all (`@ultimat3/db`'s `observe.ts`), so the timeline's
|
|
145
167
|
// SQL rows and the repeat counts arrive together rather than through two toggles. `serve.ts`
|
|
@@ -147,6 +169,10 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
147
169
|
// uninstalled, and nothing more (axiom 6).
|
|
148
170
|
const statements = createStatementLedger();
|
|
149
171
|
setStatementObserver(statements.observer);
|
|
172
|
+
acquired.push(() => {
|
|
173
|
+
setStatementObserver(undefined);
|
|
174
|
+
statements.reset();
|
|
175
|
+
});
|
|
150
176
|
// ONE load at boot, the same call the rebuild below makes: the manifest and the findings are
|
|
151
177
|
// two projections of one scan. Until 2026-09-07 this was `loadApp` for the findings and then
|
|
152
178
|
// `appManifest` — which loads again — for the manifest, so a save landing between the two put
|
|
@@ -164,7 +190,12 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
164
190
|
// boot on purpose: the header is handed to the HTTP config and the render modes once, and a
|
|
165
191
|
// reload cannot re-pin it — `state.manifest.buildId` is what `/_x` and `--json` report, so a
|
|
166
192
|
// divergence between the two is visible rather than silent, and a restart closes it.
|
|
167
|
-
|
|
193
|
+
// `BUILD_ID` wins when set — `serve.ts`'s rule, so an e2e `deploy.newBuild()` can restart `x dev`
|
|
194
|
+
// as a new build with the same sources.
|
|
195
|
+
// Stamped, it is ALSO what `server.buildId` answers below: the process serves no other build.
|
|
196
|
+
const rawStamp = options.env['BUILD_ID'];
|
|
197
|
+
const stamped = rawStamp !== undefined && rawStamp !== '' ? rawStamp : undefined;
|
|
198
|
+
const buildId = stamped ?? state.manifest.buildId;
|
|
168
199
|
|
|
169
200
|
let server: DevServer;
|
|
170
201
|
// Read at request time, never captured at boot: `/_x/services` must report the reload counter
|
|
@@ -185,70 +216,14 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
185
216
|
};
|
|
186
217
|
const panels = devPanels(dashboard).map((panel) => panel.key);
|
|
187
218
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// worker that changes under a page it already controls is the update path, and re-emitting one
|
|
197
|
-
// per keystroke would exercise it on every save.
|
|
198
|
-
const serviceWorker =
|
|
199
|
-
pwa === undefined
|
|
200
|
-
? undefined
|
|
201
|
-
: serviceWorkerArtifacts({
|
|
202
|
-
pwa,
|
|
203
|
-
buildId,
|
|
204
|
-
routes: describeRoutes(),
|
|
205
|
-
islands: state.islands,
|
|
206
|
-
styles: styleBundle(),
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// The app's own MCP endpoint, discovered from `apps/<app>/mcp.ts` and mounted through the SAME
|
|
210
|
-
// call `runRole` makes — `POST /mcp` answered 404 in every process the framework booted until
|
|
211
|
-
// one of them asked. Warned once here when `expose` is true and nothing can be mounted.
|
|
212
|
-
const mcpMount = await mountAppMcp(options.root);
|
|
213
|
-
const routes: readonly Route[] = [
|
|
214
|
-
...devDashboardRoutes(dashboard),
|
|
215
|
-
// The same API table the container serves: a read that answers here and 404s in production
|
|
216
|
-
// is exactly the drift one composition exists to prevent.
|
|
217
|
-
...apiRoutes(),
|
|
218
|
-
...mcpMount.routes,
|
|
219
|
-
// The image pipeline's only HTTP surface: the icons the web manifest declares, and the
|
|
220
|
-
// variants every `srcset` promises. Mounted before the app's own routes so a page route can
|
|
221
|
-
// never shadow `/icons` or `/media`.
|
|
222
|
-
...assetRoutes({
|
|
223
|
-
root: options.root,
|
|
224
|
-
storage: runtime.storage,
|
|
225
|
-
...(pwa === undefined ? {} : { pwa }),
|
|
226
|
-
}),
|
|
227
|
-
...storageRoutes({ storage: runtime.storage }),
|
|
228
|
-
// The chunks the documents below name. Mounted before the app's routes for the reason
|
|
229
|
-
// `/icons` and `/media` are: a page route must not be able to shadow an asset URL.
|
|
230
|
-
...islandRoutes(() => state.islands),
|
|
231
|
-
// And the stylesheet every one of those documents links. Read through the getter for the
|
|
232
|
-
// reason the islands are: a rebuilt island registers CSS, which mints a new URL, and a table
|
|
233
|
-
// captured at boot would answer 404 for the href the document now carries.
|
|
234
|
-
...styleRoutes(() => styleBundle()),
|
|
235
|
-
// `x shot --island`'s harness, in the `/_x` dev namespace so no app route can shadow it. It
|
|
236
|
-
// lives here rather than in a second server because everything it needs is in THIS process:
|
|
237
|
-
// the built chunks, the app's stylesheet registry, and the one embedded Postgres a checkout
|
|
238
|
-
// may have. The states are read per REQUEST — an author editing a state and re-running the
|
|
239
|
-
// command must not need a restart to see it.
|
|
240
|
-
...islandHarnessRoutes({
|
|
241
|
-
islands: () => state.islands,
|
|
242
|
-
states: () => loadIslandStates(options.root),
|
|
243
|
-
}),
|
|
244
|
-
...(serviceWorker === undefined ? [] : serviceWorkerRoutes(serviceWorker)),
|
|
245
|
-
...appRoutes({
|
|
246
|
-
buildId,
|
|
247
|
-
resolveIsland: (file) => state.islands.resolverFor(file),
|
|
248
|
-
themeHead: theme.head,
|
|
249
|
-
...(pwa === undefined ? {} : { pwaHead: pwa.head + (serviceWorker?.head ?? '') }),
|
|
250
|
-
}),
|
|
251
|
-
];
|
|
219
|
+
const { routes, theme, errorStyles, mcpPath } = await devRouteTable({
|
|
220
|
+
root: options.root,
|
|
221
|
+
env: options.env,
|
|
222
|
+
buildId,
|
|
223
|
+
storage: runtime.storage,
|
|
224
|
+
dashboard,
|
|
225
|
+
islands: () => state.islands,
|
|
226
|
+
});
|
|
252
227
|
|
|
253
228
|
// The app's `apps/<app>/runtime.ts`, composed exactly as `runRole` composes a caller's
|
|
254
229
|
// `runtime`: the replica scope in front, the app's own middleware behind it. Before this the
|
|
@@ -287,6 +262,7 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
287
262
|
// standby — so this key does not exist on a homework app's boot at all.
|
|
288
263
|
...(replicaOverride === undefined ? {} : { overrides: replicaOverride }),
|
|
289
264
|
});
|
|
265
|
+
acquired.push(() => running.stop());
|
|
290
266
|
|
|
291
267
|
// One rebuild at a time, and the last save wins: a tick arriving mid-build coalesces into ONE
|
|
292
268
|
// trailing rebuild instead of racing the one in flight for `state.manifest` and `state.islands`.
|
|
@@ -320,9 +296,9 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
320
296
|
url: running.url ?? `http://localhost:${options.port}`,
|
|
321
297
|
services,
|
|
322
298
|
roles: running.roles,
|
|
323
|
-
mcp:
|
|
299
|
+
mcp: mcpPath,
|
|
324
300
|
get buildId(): string {
|
|
325
|
-
return state.manifest.buildId;
|
|
301
|
+
return stamped ?? state.manifest.buildId;
|
|
326
302
|
},
|
|
327
303
|
// A getter, not a snapshot: `/_x` and `--json` must show the reload that just failed and the
|
|
328
304
|
// loop the last request tripped, not the findings as they were when the route table was built.
|
|
@@ -358,32 +334,12 @@ export async function startDev(options: StartDevOptions): Promise<DevServer> {
|
|
|
358
334
|
}
|
|
359
335
|
|
|
360
336
|
export const devCommand: CliCommand = {
|
|
361
|
-
spec:
|
|
362
|
-
name: 'dev',
|
|
363
|
-
summary: 'all roles in one process: embedded services, sub-second reload, /_x mounted',
|
|
364
|
-
usage: 'x dev [--port 3000] [--role web,worker] [--once] [--json]',
|
|
365
|
-
requiresApp: true,
|
|
366
|
-
flags: [
|
|
367
|
-
{ name: 'port', type: 'string', summary: 'HTTP port', default: String(DEFAULT_PORT) },
|
|
368
|
-
{
|
|
369
|
-
name: 'role',
|
|
370
|
-
type: 'string',
|
|
371
|
-
// `replicator` is named because it is selectable and NOT default — it takes a replication
|
|
372
|
-
// slot on a shared database, which is not something every `x dev` should do by starting.
|
|
373
|
-
summary: `roles to run (default: all of ${DEV_ROLES.join(',')}; replicator is opt-in)`,
|
|
374
|
-
},
|
|
375
|
-
{ name: 'once', type: 'boolean', summary: 'boot, report, exit — for smoke tests and CI' },
|
|
376
|
-
],
|
|
377
|
-
},
|
|
337
|
+
spec: devSpec,
|
|
378
338
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
379
339
|
const root = requireAppRoot('dev', ctx.cwd).dir;
|
|
380
340
|
// Validated, not `parseInt`'d: `x dev --port abc` handed `NaN` to `Bun.serve`, which binds an
|
|
381
341
|
// arbitrary port — a dev server reachable at an address nothing printed.
|
|
382
|
-
const port =
|
|
383
|
-
ctx.args,
|
|
384
|
-
{ name: 'port', command: 'dev', ...PORT_RANGE, example: `x dev --port ${DEFAULT_PORT}` },
|
|
385
|
-
DEFAULT_PORT,
|
|
386
|
-
);
|
|
342
|
+
const port = devPortFor(ctx.args, ctx.env);
|
|
387
343
|
const roles = selectRoles(flagString(ctx.args, 'role'));
|
|
388
344
|
// BEFORE anything boots. Both failures this catches were reachable and both reported the wrong
|
|
389
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> {
|