@ultimat3/cli 25.2.0 → 26.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/README.md +3 -3
- package/package.json +28 -28
- package/src/app-env.ts +23 -8
- package/src/budgets.ts +2 -1
- package/src/cmd-db-branch.ts +15 -11
- package/src/cmd-db-spec.ts +2 -2
- package/src/cmd-deploy-drain.ts +6 -1
- package/src/cmd-docs.ts +1 -1
- package/src/cmd-env.ts +4 -4
- package/src/cmd-jobs-spec.ts +11 -7
- package/src/cmd-jobs.ts +10 -10
- package/src/cmd-planned.ts +3 -3
- package/src/cmd-registries-spec.ts +6 -6
- package/src/cmd-registries.ts +6 -6
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-vapid-spec.ts +17 -0
- package/src/cmd-vapid.ts +135 -0
- package/src/db-branch.ts +6 -6
- package/src/db-finding.ts +1 -1
- package/src/dev-restart-retire.ts +3 -1
- package/src/dev-route-table.ts +4 -0
- package/src/document-styles.ts +2 -4
- package/src/error-contract.ts +1 -1
- package/src/errors.ts +4 -4
- package/src/fix-command.ts +1 -1
- package/src/framework-env.ts +92 -15
- package/src/framework-schema.ts +12 -0
- package/src/jobs-report.ts +3 -3
- package/src/jobs-table.ts +1 -1
- package/src/live-routes.ts +0 -1
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +2 -2
- package/src/messages.ts +11 -3
- package/src/page-navigation.ts +6 -1
- package/src/parse.ts +23 -2
- package/src/pwa-artifacts.ts +2 -2
- package/src/registry.ts +2 -0
- package/src/role-replicator.ts +1 -1
- package/src/runtime-jobs.ts +1 -1
- package/src/runtime-push.ts +80 -0
- package/src/runtime-services.ts +7 -0
- package/src/serve-boot.ts +8 -2
- package/src/serve-drain.ts +5 -1
- package/src/serve-retire.ts +7 -3
- package/src/serve-web.ts +4 -0
- package/src/site-seo.ts +1 -4
- package/src/sw-artifacts.ts +30 -13
- package/src/sync-url.ts +9 -0
- package/src/templates/scaffold-claude-agents.ts +4 -4
- package/src/templates/scaffold-claude-commands.ts +2 -2
- package/src/templates/scaffold-claude.ts +1 -1
- package/src/templates/scaffold-helm-templates.ts +6 -2
- package/src/templates/scaffold-helm.ts +4 -1
- package/src/worker-bundle.ts +21 -8
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ Commands and the `x verify` step count, `As of 2026-08`:
|
|
|
19
19
|
| `x doctor` | environment, ports, drift, PWA prerequisites, `APP_URL` | every finding carries a fix command; probes the port `x dev` binds ([which one](../../wiki/CLI-Reference.md#x-doctor)); the framework's deploy secrets only where the machine names `staging`/`production` |
|
|
20
20
|
| `x deploy` | container deploy plan | compose or helm; zero platform primitives |
|
|
21
21
|
| `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
|
|
22
|
-
| `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `
|
|
22
|
+
| `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `show <name>`, straight off the registries |
|
|
23
23
|
| `x tasks list\|show` | cron tasks | timezone and next run, off `registeredTasks()` |
|
|
24
|
-
| `x jobs
|
|
24
|
+
| `x jobs list\|show\|retry\|cancel\|delete\|promote\|pause\|resume` | the queue | depth, dead letters, step traces, `retry --from-step`, `cancel --reason`; `list --json` pages as `rows`/`nextCursor`/`hasMore`; `drain` is planned (`X_NOT_IMPLEMENTED`) |
|
|
25
25
|
| `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
|
|
26
26
|
| `x env check\|example` | the typed environment `envSchema` declares | and the `.env.example` rendered from it, plus the framework's deploy-required secrets (`ULTIMATE_CURSOR_SECRET`, `STORAGE_SIGNING_SECRET` unless `S3_ENDPOINT` is set), which `check` asks for wherever the boot refuses them — outside `development`/`test`, or with no environment named |
|
|
27
27
|
| `x auth seal-mfa` | one-shot auth maintenance | seals every `x_users.mfa_secret` still in the clear; idempotent, `{ sealed, alreadySealed, skipped }` |
|
|
@@ -171,7 +171,7 @@ that name where there is one (`x new` writes all seventeen), the blank template
|
|
|
171
171
|
| `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
|
|
172
172
|
| `app-transport.ts` / `browser-transport.ts` | one browser transport, on `boundaries` in every app: a raw `fetch(`, `new WebSocket(` or `new XMLHttpRequest(` in an island's closure is `X_BROWSER_TRANSPORT_BYPASS`; a server barrel there is `X_BROWSER_SERVER_BARREL` |
|
|
173
173
|
| `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
|
|
174
|
-
| `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each. `ROLE=worker` also retires on SIGUSR2 (`serve-retire.ts`, 25.0.0): stop claiming, finish every held job, abort nothing, drain, exit 0 — what the chart's opt-in `roles.worker.retireSeconds` `preStop` sends; a no-op on Windows ([Deployment](../../wiki/Deployment.md#retiring-a-worker-before-sigterm)) |
|
|
174
|
+
| `serve.ts` | **what a container starts** — `runRole(options)`, the same boot `x dev` runs minus the watcher, `/_x` and `dev: true`. `x new`'s `apps/web/server.ts` is three lines that call it. `ROLE`, `PORT` and `HOST` are read from `env`; `role`, `port` and `hostname` on `ServeOptions` override each. `ROLE=worker` also retires on SIGUSR2 (`serve-retire.ts`, 25.0.0): stop claiming, finish every held job, abort nothing, drain, exit 0 — what the chart's opt-in `roles.worker.retireSeconds` `preStop` sends; core's `isRetiring()` is true from the signal on, and a SIGTERM mid-retire drains on `drain.workerDeadlineMs` when declared (`lifecycleForRole`); a no-op on Windows ([Deployment](../../wiki/Deployment.md#retiring-a-worker-before-sigterm)) |
|
|
175
175
|
| `prerender.ts` | `x build --target static`: which `site/` routes qualify, and where the bytes land |
|
|
176
176
|
| `metrics-endpoint.ts` | the `METRICS_PATH` scrape listener every role opens, on `METRICS_PORT`; opening it starts the `process_*` series, labelled by `startMetricsEndpoint({ role })` |
|
|
177
177
|
| `role-load.ts` / `document-graph.ts` / `serve-web.ts` | what each container role imports: `web`, `sync` and `replicator` every app module; `worker` and `scheduler` the API index plus every module that reaches no component or stylesheet |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.1.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,33 +39,33 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@babel/core": "^8.0.1",
|
|
42
|
-
"@ultimat3/action": "
|
|
43
|
-
"@ultimat3/admin": "
|
|
44
|
-
"@ultimat3/ai": "
|
|
45
|
-
"@ultimat3/auth": "
|
|
46
|
-
"@ultimat3/cache": "
|
|
47
|
-
"@ultimat3/core": "
|
|
48
|
-
"@ultimat3/db": "
|
|
49
|
-
"@ultimat3/entity": "
|
|
50
|
-
"@ultimat3/flags": "
|
|
51
|
-
"@ultimat3/http": "
|
|
52
|
-
"@ultimat3/i18n": "
|
|
53
|
-
"@ultimat3/jobs": "
|
|
54
|
-
"@ultimat3/mail": "
|
|
55
|
-
"@ultimat3/manifest": "
|
|
56
|
-
"@ultimat3/mcp": "
|
|
57
|
-
"@ultimat3/money": "
|
|
58
|
-
"@ultimat3/notify": "
|
|
59
|
-
"@ultimat3/policy": "
|
|
60
|
-
"@ultimat3/pwa": "
|
|
61
|
-
"@ultimat3/query": "
|
|
62
|
-
"@ultimat3/realtime": "
|
|
63
|
-
"@ultimat3/render": "
|
|
64
|
-
"@ultimat3/schema": "
|
|
65
|
-
"@ultimat3/seo": "
|
|
66
|
-
"@ultimat3/storage": "
|
|
67
|
-
"@ultimat3/testing": "
|
|
68
|
-
"@ultimat3/time": "
|
|
42
|
+
"@ultimat3/action": "26.1.0",
|
|
43
|
+
"@ultimat3/admin": "26.1.0",
|
|
44
|
+
"@ultimat3/ai": "26.1.0",
|
|
45
|
+
"@ultimat3/auth": "26.1.0",
|
|
46
|
+
"@ultimat3/cache": "26.1.0",
|
|
47
|
+
"@ultimat3/core": "26.1.0",
|
|
48
|
+
"@ultimat3/db": "26.1.0",
|
|
49
|
+
"@ultimat3/entity": "26.1.0",
|
|
50
|
+
"@ultimat3/flags": "26.1.0",
|
|
51
|
+
"@ultimat3/http": "26.1.0",
|
|
52
|
+
"@ultimat3/i18n": "26.1.0",
|
|
53
|
+
"@ultimat3/jobs": "26.1.0",
|
|
54
|
+
"@ultimat3/mail": "26.1.0",
|
|
55
|
+
"@ultimat3/manifest": "26.1.0",
|
|
56
|
+
"@ultimat3/mcp": "26.1.0",
|
|
57
|
+
"@ultimat3/money": "26.1.0",
|
|
58
|
+
"@ultimat3/notify": "26.1.0",
|
|
59
|
+
"@ultimat3/policy": "26.1.0",
|
|
60
|
+
"@ultimat3/pwa": "26.1.0",
|
|
61
|
+
"@ultimat3/query": "26.1.0",
|
|
62
|
+
"@ultimat3/realtime": "26.1.0",
|
|
63
|
+
"@ultimat3/render": "26.1.0",
|
|
64
|
+
"@ultimat3/schema": "26.1.0",
|
|
65
|
+
"@ultimat3/seo": "26.1.0",
|
|
66
|
+
"@ultimat3/storage": "26.1.0",
|
|
67
|
+
"@ultimat3/testing": "26.1.0",
|
|
68
|
+
"@ultimat3/time": "26.1.0",
|
|
69
69
|
"babel-preset-solid": "^1.9.15"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/app-env.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// why: Bun exposes no path API — the two files this module reads are joined to the app root.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
8
|
-
import { ENV_EXAMPLE_PATH, ERROR_DOCS_URL, parseEnvKeys } from '@ultimat3/core';
|
|
9
|
-
import { appConfigExport } from './app-config-load';
|
|
8
|
+
import { ENV_EXAMPLE_PATH, ERROR_DOCS_URL, parseEnvKeys, pushWired } from '@ultimat3/core';
|
|
9
|
+
import { appConfigExport, loadAppConfig } from './app-config-load';
|
|
10
10
|
import { APP_CONFIG_FILE } from './app-root';
|
|
11
|
-
import {
|
|
11
|
+
import type { AppSecretFacts } from './framework-env';
|
|
12
|
+
import { appEnvExample, configOwes, FRAMEWORK_SECRETS } from './framework-env';
|
|
12
13
|
import type { Finding } from './output';
|
|
13
14
|
import { findingFrom } from './output';
|
|
14
15
|
|
|
@@ -53,7 +54,17 @@ export async function loadEnvSchema(root: string): Promise<EnvSchema | undefined
|
|
|
53
54
|
* The bytes `.env.example` must hold: the app's declaration, then the framework's deploy-required
|
|
54
55
|
* keys (`framework-env.ts`). Deterministic, so a rewrite that changes nothing diffs to nothing.
|
|
55
56
|
*/
|
|
56
|
-
export const envExampleFor = (schema: EnvSchema): string =>
|
|
57
|
+
export const envExampleFor = (schema: EnvSchema, app: AppSecretFacts): string =>
|
|
58
|
+
appEnvExample(schema, app);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What `app.config.ts` decides about the framework keys this app owes — `pwa.push` today, off the
|
|
62
|
+
* one loader, so the example, the drift gate and `x env check` read one answer.
|
|
63
|
+
*/
|
|
64
|
+
export async function appSecretFacts(root: string): Promise<AppSecretFacts> {
|
|
65
|
+
const pwa = (await loadAppConfig(root))?.pwa;
|
|
66
|
+
return { push: pwa !== undefined && pushWired(pwa) };
|
|
67
|
+
}
|
|
57
68
|
|
|
58
69
|
const driftFinding = (cause: string): Finding => ({
|
|
59
70
|
code: 'X_ENV_EXAMPLE_DRIFT',
|
|
@@ -73,13 +84,17 @@ const driftFinding = (cause: string): Finding => ({
|
|
|
73
84
|
*/
|
|
74
85
|
export async function envExampleFindings(root: string): Promise<readonly Finding[]> {
|
|
75
86
|
let schema: EnvSchema | undefined;
|
|
87
|
+
let app: AppSecretFacts;
|
|
76
88
|
try {
|
|
77
89
|
schema = await loadEnvSchema(root);
|
|
90
|
+
// Inside the `try`: this read runs `defineConfig`, which `loadEnvSchema` does not — a config
|
|
91
|
+
// it refuses (`pwa.push` with no `vapid.subject`) is a finding here, never a crash of the step.
|
|
92
|
+
app = await appSecretFacts(root);
|
|
78
93
|
} catch (error) {
|
|
79
94
|
return [{ ...findingFrom(error), at: APP_CONFIG_FILE }];
|
|
80
95
|
}
|
|
81
96
|
if (schema === undefined) return [];
|
|
82
|
-
const expected = envExampleFor(schema);
|
|
97
|
+
const expected = envExampleFor(schema, app);
|
|
83
98
|
const file = Bun.file(join(root, ENV_EXAMPLE_PATH));
|
|
84
99
|
if (!(await file.exists())) {
|
|
85
100
|
return [
|
|
@@ -94,9 +109,9 @@ export async function envExampleFindings(root: string): Promise<readonly Finding
|
|
|
94
109
|
const missing = Object.keys(schema).filter((key) => !present.has(key));
|
|
95
110
|
// The framework's keys are owed by every deployed app, whatever its schema says (#679), and an
|
|
96
111
|
// example written before they were part of the contract is missing exactly these.
|
|
97
|
-
const owed = FRAMEWORK_SECRETS.
|
|
98
|
-
(
|
|
99
|
-
|
|
112
|
+
const owed = FRAMEWORK_SECRETS.filter((secret) => configOwes(secret, app))
|
|
113
|
+
.map((secret) => secret.key)
|
|
114
|
+
.filter((key) => !Object.hasOwn(schema, key) && !present.has(key));
|
|
100
115
|
return [
|
|
101
116
|
driftFinding(
|
|
102
117
|
missing.length > 0
|
package/src/budgets.ts
CHANGED
|
@@ -212,7 +212,8 @@ export async function readBuildStats(root: string): Promise<BuildStats | undefin
|
|
|
212
212
|
export interface MeasureOptions {
|
|
213
213
|
/**
|
|
214
214
|
* Scripts a document names that the PROCESS serves and the static artifact does not carry, by
|
|
215
|
-
* URL: the page boot (`/_x/page-boot/<id>.js`). Charged like any `<script src>` —
|
|
215
|
+
* URL: the page boot (`/_x/assets/page-boot/<id>.js`). Charged like any `<script src>` —
|
|
216
|
+
* ledger #28.
|
|
216
217
|
*/
|
|
217
218
|
readonly served?: ReadonlyMap<string, string>;
|
|
218
219
|
/**
|
package/src/cmd-db-branch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// `x db branch
|
|
1
|
+
// `x db branch list|create|delete` — the wiring alone: which verb, which database, which refusal.
|
|
2
2
|
// A VERB is required and comes from a closed set, so a branch name can never be read as one:
|
|
3
|
-
// `x db branch ls` used to clone a database called `ls`, because the argument was the name.
|
|
3
|
+
// `x db branch ls` used to clone a database called `ls`, because the argument was the name. One
|
|
4
|
+
// verb each across every `x` registry (#709): `list` and `delete`, no `ls`/`drop` alias.
|
|
4
5
|
// The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
|
|
5
6
|
|
|
6
7
|
import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
|
|
@@ -26,7 +27,7 @@ import { stepFinding } from './db-finding';
|
|
|
26
27
|
import { MissingPositionalError, UnknownCommandError } from './errors';
|
|
27
28
|
import { msg } from './messages';
|
|
28
29
|
import type { CommandResult, Finding } from './output';
|
|
29
|
-
import { flagString } from './parse';
|
|
30
|
+
import { flagString, retiredVerbReplacement } from './parse';
|
|
30
31
|
import type { DevServices } from './runtime-bindings';
|
|
31
32
|
import { resolveServices } from './runtime-bindings';
|
|
32
33
|
import { portFromEnv } from './serve';
|
|
@@ -35,9 +36,9 @@ import { renderTable } from './table';
|
|
|
35
36
|
/**
|
|
36
37
|
* Always runnable, always the next thing a caller needs: what branches there are. Spelled twice
|
|
37
38
|
* because `UnknownCommandError` prefixes its `suggestion` with `x ` and `MissingPositionalError`
|
|
38
|
-
* takes a whole invocation — one of them handed back `x x db branch
|
|
39
|
+
* takes a whole invocation — one of them handed back `x x db branch list --json`.
|
|
39
40
|
*/
|
|
40
|
-
const LIST_ARGV = 'db branch
|
|
41
|
+
const LIST_ARGV = 'db branch list --json';
|
|
41
42
|
const LIST_FIX = `x ${LIST_ARGV}`;
|
|
42
43
|
|
|
43
44
|
/**
|
|
@@ -47,7 +48,10 @@ const LIST_FIX = `x ${LIST_ARGV}`;
|
|
|
47
48
|
* `x` excluded — the error class adds it.
|
|
48
49
|
*/
|
|
49
50
|
function branchRetry(word: string, name: string | undefined): string {
|
|
50
|
-
|
|
51
|
+
// A retired verb (`ls`, `drop`) is a legal branch NAME too, so without this `drop feat` would be
|
|
52
|
+
// answered with `create drop` — the exact confusion the closed verb set exists to end.
|
|
53
|
+
const near =
|
|
54
|
+
retiredVerbReplacement(word, BRANCH_SUBCOMMANDS) ?? nearestName(word, [...BRANCH_SUBCOMMANDS]);
|
|
51
55
|
if (near !== undefined) return name === undefined ? LIST_ARGV : `db branch ${near} ${name}`;
|
|
52
56
|
return isBranchName(word) ? `db branch create ${word}` : LIST_ARGV;
|
|
53
57
|
}
|
|
@@ -101,7 +105,7 @@ export async function runBranchCommand(ctx: CommandContext, root: string): Promi
|
|
|
101
105
|
});
|
|
102
106
|
}
|
|
103
107
|
const services = resolveServices(root, ctx.env);
|
|
104
|
-
if (verb === '
|
|
108
|
+
if (verb === 'list') return runList(services);
|
|
105
109
|
if (name === undefined) {
|
|
106
110
|
throw new MissingPositionalError({
|
|
107
111
|
command: `db branch ${verb}`,
|
|
@@ -109,7 +113,7 @@ export async function runBranchCommand(ctx: CommandContext, root: string): Promi
|
|
|
109
113
|
example: `x db branch ${verb} feature-x`,
|
|
110
114
|
});
|
|
111
115
|
}
|
|
112
|
-
return verb === 'create' ? runCreate(ctx, services, name) :
|
|
116
|
+
return verb === 'create' ? runCreate(ctx, services, name) : runDelete(services, name);
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
const row = (branch: BranchRow): readonly string[] => [
|
|
@@ -174,12 +178,12 @@ async function runCreate(
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
/**
|
|
177
|
-
* You may only
|
|
181
|
+
* You may only delete what `list` shows, and that is the whole guard — stronger than a confirmation
|
|
178
182
|
* flag, because it is the typo that is impossible rather than the keystroke that is tedious. An
|
|
179
183
|
* external branch is a database carrying `createBranch`'s marker comment AND this database's own
|
|
180
184
|
* prefix, so neither the shared database this session is connected to nor another app's clone on
|
|
181
185
|
* the same server is in the set; an embedded one is a `pgdata-<name>` directory, so `pgdata` itself
|
|
182
|
-
* is not either. `@ultimat3/db`'s own `X_BRANCH_EXISTS` fix line is `x db branch
|
|
186
|
+
* is not either. `@ultimat3/db`'s own `X_BRANCH_EXISTS` fix line is `x db branch delete <name>` with
|
|
183
187
|
* no flag on it, so a flag here would break a shipped instruction.
|
|
184
188
|
*
|
|
185
189
|
* The check is not made here, and that is the point: `false` from either drop means "there was no
|
|
@@ -187,7 +191,7 @@ async function runCreate(
|
|
|
187
191
|
* earlier. A listing taken here and acted on below is two connections and a window wide enough to
|
|
188
192
|
* hold a whole `create` — and the wiring layer is exactly where a guard must not live.
|
|
189
193
|
*/
|
|
190
|
-
async function
|
|
194
|
+
async function runDelete(services: DevServices, name: string): Promise<CommandResult> {
|
|
191
195
|
try {
|
|
192
196
|
const dropped =
|
|
193
197
|
services.db.mode === 'embedded'
|
package/src/cmd-db-spec.ts
CHANGED
|
@@ -18,11 +18,11 @@ export const dbSpec: CommandSpec = {
|
|
|
18
18
|
name: 'db',
|
|
19
19
|
summary: 'gen, migrate, reset, seed, studio, branch, backfill',
|
|
20
20
|
usage:
|
|
21
|
-
'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch
|
|
21
|
+
'x db gen "add publish_at" | migrate | reset | seed [<name>] [--tier reference|dev] [--dry-run] | studio | branch list | branch create <name> | branch delete <name> | backfill [<name>|--all] [--write] [--force] | backfill --pending | backfill --list [--name n] [--status s] [--limit n]',
|
|
22
22
|
requiresApp: true,
|
|
23
23
|
subcommands: DB_SUBCOMMANDS,
|
|
24
24
|
// Declared from the constant `runBranchCommand` validates against, never a second literal: it
|
|
25
|
-
// is what lets the `errors` step resolve `x db branch
|
|
25
|
+
// is what lets the `errors` step resolve `x db branch list` — a fix line three shipped errors
|
|
26
26
|
// hand out, which read `ls` as a branch name and cloned a database until 1.2.x.
|
|
27
27
|
subcommandPositionals: { branch: BRANCH_SUBCOMMANDS },
|
|
28
28
|
// Each flag whose summary begins `<subcommand>:` declares that scope, and the parser refuses
|
package/src/cmd-deploy-drain.ts
CHANGED
|
@@ -12,7 +12,8 @@ const seconds = (ms: number): number => Math.ceil(ms / 1000);
|
|
|
12
12
|
/**
|
|
13
13
|
* The `--set` pairs, or none when the app declares no budget (the chart's defaults are core's).
|
|
14
14
|
* The chart's `drain.deadlineSeconds` is ONE budget for every role, and so is `drain.deadlineMs`:
|
|
15
|
-
* the web role drains on it too.
|
|
15
|
+
* the web role drains on it too — the worker on `drain.workerDeadlineSeconds` instead, when the app
|
|
16
|
+
* declares `drain.workerDeadlineMs`. The margins stay the chart's.
|
|
16
17
|
*
|
|
17
18
|
* The grace is budgeted at no less than core's production default: the value read here is resolved
|
|
18
19
|
* in THIS process's environment, and an `x deploy` run under `NODE_ENV=development` resolves the
|
|
@@ -24,6 +25,10 @@ export function helmDrainOverrides(drain: Partial<DrainConfig> | undefined): rea
|
|
|
24
25
|
if (drain?.deadlineMs !== undefined) {
|
|
25
26
|
out.push('--set', `drain.deadlineSeconds=${seconds(drain.deadlineMs)}`);
|
|
26
27
|
}
|
|
28
|
+
// The worker role's own budget: the chart sizes only the worker's grace period from it.
|
|
29
|
+
if (drain?.workerDeadlineMs !== undefined) {
|
|
30
|
+
out.push('--set', `drain.workerDeadlineSeconds=${seconds(drain.workerDeadlineMs)}`);
|
|
31
|
+
}
|
|
27
32
|
if (drain === undefined) return out;
|
|
28
33
|
const grace = Math.max(drain.readinessGraceMs ?? 0, READINESS_GRACE_DEFAULT_MS);
|
|
29
34
|
out.push('--set', `drain.readinessGraceSeconds=${seconds(grace)}`);
|
package/src/cmd-docs.ts
CHANGED
|
@@ -144,7 +144,7 @@ export const docsCommand: CliCommand = {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const entries = await scanInstalledDocs(scope);
|
|
147
|
-
// `x jobs
|
|
147
|
+
// `x jobs list --limit`'s reader, and its `command` parameter exists for exactly this second
|
|
148
148
|
// caller. A local `Number.parseInt` accepted `--limit 1e9` as 1 and answered with one match,
|
|
149
149
|
// and fell silently through to the default for `abc`, `0` and `-3` — a bound other than the one
|
|
150
150
|
// typed, from the same binary that refuses all four one command over.
|
package/src/cmd-env.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
maskedEnvValues,
|
|
12
12
|
UltimateError,
|
|
13
13
|
} from '@ultimat3/core';
|
|
14
|
-
import { envExampleFor, loadEnvSchema } from './app-env';
|
|
14
|
+
import { appSecretFacts, envExampleFor, loadEnvSchema } from './app-env';
|
|
15
15
|
import { requireAppRoot } from './app-root';
|
|
16
16
|
import { envSpec } from './cmd-env-spec';
|
|
17
17
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -51,7 +51,7 @@ async function requireSchema(cwd: string, subcommand: string) {
|
|
|
51
51
|
|
|
52
52
|
async function writeExample(ctx: CommandContext): Promise<CommandResult> {
|
|
53
53
|
const { root, schema } = await requireSchema(ctx.cwd, 'example');
|
|
54
|
-
const contents = envExampleFor(schema);
|
|
54
|
+
const contents = envExampleFor(schema, await appSecretFacts(root));
|
|
55
55
|
const path = join(root, ENV_EXAMPLE_PATH);
|
|
56
56
|
const file = Bun.file(path);
|
|
57
57
|
const fresh = (await file.exists()) && (await file.text()) === contents;
|
|
@@ -78,14 +78,14 @@ async function writeExample(ctx: CommandContext): Promise<CommandResult> {
|
|
|
78
78
|
* the first deployed boot would, instead of each role crash-looping on one key at a time (#679).
|
|
79
79
|
*/
|
|
80
80
|
async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
|
|
81
|
-
const { schema } = await requireSchema(ctx.cwd, 'check');
|
|
81
|
+
const { root, schema } = await requireSchema(ctx.cwd, 'check');
|
|
82
82
|
const report = checkEnv(schema, { env: ctx.env });
|
|
83
83
|
const total = Object.keys(schema).length;
|
|
84
84
|
// A key the app's own declaration already reported is one finding, not two.
|
|
85
85
|
const reported = new Set(report.issues.map((issue) => issue.key));
|
|
86
86
|
// Gated by `deployed()` inside — the boot's own rule, so a table naming no environment is
|
|
87
87
|
// checked as the production process it boots as.
|
|
88
|
-
const framework = frameworkSecretFindings(ctx.env, reported);
|
|
88
|
+
const framework = frameworkSecretFindings(ctx.env, reported, await appSecretFacts(root));
|
|
89
89
|
const findings: readonly Finding[] = [
|
|
90
90
|
...report.issues.map((issue) => ({
|
|
91
91
|
code: 'X_ENV_MISSING',
|
package/src/cmd-jobs-spec.ts
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CommandSpec } from './parse';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* One verb each, the same across every `x` registry (#709): `list`, `show <one>`, `delete`. No
|
|
8
|
+
* `ls`/`rm` aliases — two spellings of one verb is the ambiguity axiom 1 forbids.
|
|
9
|
+
*/
|
|
6
10
|
export const JOBS_SUBCOMMANDS = [
|
|
7
|
-
'
|
|
11
|
+
'list',
|
|
8
12
|
'show',
|
|
9
13
|
'retry',
|
|
10
14
|
'cancel',
|
|
11
|
-
'
|
|
15
|
+
'delete',
|
|
12
16
|
'promote',
|
|
13
17
|
'pause',
|
|
14
18
|
'resume',
|
|
@@ -20,13 +24,13 @@ export const jobsSpec: CommandSpec = {
|
|
|
20
24
|
// Never ENDING in "(planned)": that suffix is how `x help` and the planned-table tests tell a
|
|
21
25
|
// planned COMMAND from a shipped one, and `x jobs` ships — only its `drain` does not.
|
|
22
26
|
summary:
|
|
23
|
-
'list, show, retry, cancel,
|
|
27
|
+
'list, show, retry, cancel, delete and promote jobs; pause and resume a queue (drain is planned)',
|
|
24
28
|
usage:
|
|
25
|
-
'x jobs [
|
|
29
|
+
'x jobs [list|show <id>|retry <id>|cancel <id>|delete <id>|promote <id>|pause <queue>|resume <queue>|drain] [--queue q] [--state s] [--name n] [--limit n] [--after cursor] [--from-step name] [--reason text] [--json]',
|
|
26
30
|
requiresApp: true,
|
|
27
31
|
subcommands: JOBS_SUBCOMMANDS,
|
|
28
32
|
// The bare `x jobs` lists; it never retries, cancels or drains anything.
|
|
29
|
-
defaultSubcommand: '
|
|
33
|
+
defaultSubcommand: 'list',
|
|
30
34
|
flags: [
|
|
31
35
|
{ name: 'queue', type: 'string', summary: 'filter by queue name' },
|
|
32
36
|
{ name: 'state', type: 'string', summary: 'filter by job state' },
|
|
@@ -35,8 +39,8 @@ export const jobsSpec: CommandSpec = {
|
|
|
35
39
|
{
|
|
36
40
|
name: 'after',
|
|
37
41
|
type: 'string',
|
|
38
|
-
summary: '
|
|
39
|
-
subcommands: ['
|
|
42
|
+
summary: 'list: the next page — the cursor the previous page printed',
|
|
43
|
+
subcommands: ['list'],
|
|
40
44
|
},
|
|
41
45
|
// Each of these is read by ONE subcommand — `retryJob`, `cancelJob` — and says so in its own
|
|
42
46
|
// summary. The scope is what makes the parser refuse it anywhere else instead
|
package/src/cmd-jobs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `x jobs
|
|
1
|
+
// `x jobs list|show|retry|cancel|delete|promote|pause|resume` — introspect and recover the job queue,
|
|
2
2
|
// bound to `@ultimat3/jobs`'s own introspection so the CLI, `/_x` and MCP report identically;
|
|
3
3
|
// `drain` is planned (`cmd-planned.ts`): Postgres is the one durable driver. CLI wiring only:
|
|
4
4
|
// the driver-injected logic is `jobs-report.ts`, the `--json` shapes `jobs-json.ts`, the table
|
|
@@ -56,7 +56,7 @@ function requireIdPositional(ctx: CommandContext, sub: string, driver: JobDriver
|
|
|
56
56
|
throw new MissingPositionalError({
|
|
57
57
|
command: `jobs ${sub}`,
|
|
58
58
|
positional: 'id',
|
|
59
|
-
example: 'x jobs
|
|
59
|
+
example: 'x jobs list --json',
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
if (!JOB_ID.test(id)) throw new JobUnknownError({ id, driver: driver.name });
|
|
@@ -86,11 +86,11 @@ function refuseOversizedPage(limit: string | undefined): void {
|
|
|
86
86
|
command: 'jobs',
|
|
87
87
|
reason: `one page holds at most ${MAX_JOB_PAGE} jobs`,
|
|
88
88
|
// The bound is the queue's own constant, screened like any value spliced into a command.
|
|
89
|
-
fix: `x jobs
|
|
89
|
+
fix: `x jobs list --limit ${renderFixShellArg(String(MAX_JOB_PAGE), '200')} --json # then pass its data.nextCursor as --after while data.hasMore is true`,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async function
|
|
93
|
+
async function runList(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
|
|
94
94
|
const limit = flagString(ctx.args, 'limit');
|
|
95
95
|
refuseOversizedPage(limit);
|
|
96
96
|
const result = await listJobs(driver, {
|
|
@@ -208,8 +208,8 @@ async function runCancel(driver: JobDriver, ctx: CommandContext): Promise<Comman
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
/** Exit 0 means the row is gone. An id nobody queued is `X_JOB_UNKNOWN`; a running one refuses. */
|
|
211
|
-
async function
|
|
212
|
-
const id = requireIdPositional(ctx, '
|
|
211
|
+
async function runDelete(driver: JobDriver, ctx: CommandContext): Promise<CommandResult> {
|
|
212
|
+
const id = requireIdPositional(ctx, 'delete', driver);
|
|
213
213
|
const removed = await removeJob(driver, id);
|
|
214
214
|
if (removed === undefined) throw new JobUnknownError({ id, driver: driver.name });
|
|
215
215
|
return {
|
|
@@ -249,14 +249,14 @@ async function runPause(
|
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
/** The subcommands that answer a `JobTrace`. `
|
|
252
|
+
/** The subcommands that answer a `JobTrace`. `list`, `delete`, `promote` and the rest read rows only. */
|
|
253
253
|
const TRACE_SUBCOMMANDS: ReadonlySet<string> = new Set(['show', 'retry', 'cancel']);
|
|
254
254
|
|
|
255
255
|
export const jobsCommand: CliCommand = {
|
|
256
256
|
spec: jobsSpec,
|
|
257
257
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
258
258
|
const root = requireAppRoot('jobs', ctx.cwd).dir;
|
|
259
|
-
const sub = ctx.args.subcommand ?? '
|
|
259
|
+
const sub = ctx.args.subcommand ?? 'list';
|
|
260
260
|
// BEFORE `withJobDriver`, which boots the source queue: the answer needs no server, so a box
|
|
261
261
|
// whose database is down gets it rather than the boot failure of a queue never to be used —
|
|
262
262
|
// and nothing is leased. 24.x's `--to`/`--dry-run` are deleted, so the parser refuses them
|
|
@@ -272,10 +272,10 @@ export const jobsCommand: CliCommand = {
|
|
|
272
272
|
if (sub === 'show') return runShow(driver, ctx);
|
|
273
273
|
if (sub === 'retry') return runRetry(driver, ctx);
|
|
274
274
|
if (sub === 'cancel') return runCancel(driver, ctx);
|
|
275
|
-
if (sub === '
|
|
275
|
+
if (sub === 'delete') return runDelete(driver, ctx);
|
|
276
276
|
if (sub === 'promote') return runPromote(driver, ctx);
|
|
277
277
|
if (sub === 'pause' || sub === 'resume') return runPause(driver, ctx, sub);
|
|
278
|
-
return
|
|
278
|
+
return runList(driver, ctx);
|
|
279
279
|
});
|
|
280
280
|
},
|
|
281
281
|
};
|
package/src/cmd-planned.ts
CHANGED
|
@@ -33,8 +33,8 @@ export const PLANNED_COMMANDS: readonly PlannedCommand[] = [
|
|
|
33
33
|
{
|
|
34
34
|
name: 'branch',
|
|
35
35
|
summary: 'copy-on-write branch environments with a preview URL',
|
|
36
|
-
usage: 'x branch [<name>|
|
|
37
|
-
fix: 'x db branch
|
|
36
|
+
usage: 'x branch [<name>|delete <name>] [--json]',
|
|
37
|
+
fix: 'x db branch list --json # the database half: list, create <name>, delete <name>',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
name: 'status',
|
|
@@ -134,7 +134,7 @@ export const PLANNED_SUBCOMMANDS: readonly PlannedSubcommand[] = [
|
|
|
134
134
|
{
|
|
135
135
|
command: 'jobs',
|
|
136
136
|
subcommand: 'drain',
|
|
137
|
-
fix: 'x jobs
|
|
137
|
+
fix: 'x jobs list --json # the queue as it stands; Postgres is the only durable driver, so there is nowhere to drain to',
|
|
138
138
|
},
|
|
139
139
|
];
|
|
140
140
|
|
|
@@ -7,8 +7,8 @@ import type { CommandSpec } from './parse';
|
|
|
7
7
|
export const actionsSpec: CommandSpec = {
|
|
8
8
|
name: 'actions',
|
|
9
9
|
summary: 'the action registry: input/output schema, policy, tags, MCP exposure',
|
|
10
|
-
usage: 'x actions [list|
|
|
11
|
-
subcommands: ['list', '
|
|
10
|
+
usage: 'x actions [list|show <name>] [--json]',
|
|
11
|
+
subcommands: ['list', 'show'],
|
|
12
12
|
defaultSubcommand: 'list',
|
|
13
13
|
requiresApp: true,
|
|
14
14
|
};
|
|
@@ -16,8 +16,8 @@ export const actionsSpec: CommandSpec = {
|
|
|
16
16
|
export const queriesSpec: CommandSpec = {
|
|
17
17
|
name: 'queries',
|
|
18
18
|
summary: 'the query registry: schema, policy, live, cache tags',
|
|
19
|
-
usage: 'x queries [list|
|
|
20
|
-
subcommands: ['list', '
|
|
19
|
+
usage: 'x queries [list|show <name>] [--json]',
|
|
20
|
+
subcommands: ['list', 'show'],
|
|
21
21
|
defaultSubcommand: 'list',
|
|
22
22
|
requiresApp: true,
|
|
23
23
|
};
|
|
@@ -25,8 +25,8 @@ export const queriesSpec: CommandSpec = {
|
|
|
25
25
|
export const entitiesSpec: CommandSpec = {
|
|
26
26
|
name: 'entities',
|
|
27
27
|
summary: 'the entity registry: columns, invariants, indexes, tenancy',
|
|
28
|
-
usage: 'x entities [list|
|
|
29
|
-
subcommands: ['list', '
|
|
28
|
+
usage: 'x entities [list|show <name>] [--json]',
|
|
29
|
+
subcommands: ['list', 'show'],
|
|
30
30
|
defaultSubcommand: 'list',
|
|
31
31
|
requiresApp: true,
|
|
32
32
|
};
|
package/src/cmd-registries.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Replaces grepping a source tree, which is what an agent does when the framework has no answer
|
|
3
3
|
// to "what actions/queries/entities exist". One table, one generic command body: the three
|
|
4
4
|
// commands differ only in which registry they read, a row's columns, and (for a query only) the
|
|
5
|
-
// one extra field `
|
|
5
|
+
// one extra field `show` surfaces that its descriptor does not already carry.
|
|
6
6
|
|
|
7
7
|
import type { ActionDescriptor, AnyAction } from '@ultimat3/action';
|
|
8
8
|
import { describeActions, getAction, jsonSchemaOf } from '@ultimat3/action';
|
|
@@ -35,7 +35,7 @@ const detailLines = (payload: Readonly<Record<string, JsonValue>>): readonly str
|
|
|
35
35
|
Object.entries(payload).map(([key, value]) => ` ${key}: ${formatValue(value)}`);
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* One config per registry: how `list` renders a row, and what `
|
|
38
|
+
* One config per registry: how `list` renders a row, and what `show` adds beyond the
|
|
39
39
|
* descriptor itself — only a query's input schema does (`QUERIES.extra`); `ActionDescriptor`
|
|
40
40
|
* already carries `input`/`output`, and an entity has no invocation shape to surface.
|
|
41
41
|
*/
|
|
@@ -116,7 +116,7 @@ function listResult<D extends { readonly name: string }, Raw extends { describe(
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
function
|
|
119
|
+
function showResult<D extends { readonly name: string }, Raw extends { describe(): D }>(
|
|
120
120
|
kind: RegistryKind<D, Raw>,
|
|
121
121
|
ctx: CommandContext,
|
|
122
122
|
findings: readonly Finding[],
|
|
@@ -126,7 +126,7 @@ function describeResult<D extends { readonly name: string }, Raw extends { descr
|
|
|
126
126
|
// A positional, so `MissingPositionalError` — `--name on "x actions"` named a flag no registry
|
|
127
127
|
// command declares, and reading it as one is a second refusal for the first one's advice.
|
|
128
128
|
throw new MissingPositionalError({
|
|
129
|
-
command: `${kind.kind}
|
|
129
|
+
command: `${kind.kind} show`,
|
|
130
130
|
positional: 'name',
|
|
131
131
|
example: `x ${kind.kind} list --json`,
|
|
132
132
|
});
|
|
@@ -158,8 +158,8 @@ async function runRegistryCommand<
|
|
|
158
158
|
>(kind: RegistryKind<D, Raw>, ctx: CommandContext): Promise<CommandResult> {
|
|
159
159
|
const root = requireAppRoot(kind.kind, ctx.cwd).dir;
|
|
160
160
|
const { findings } = await loadApp(root);
|
|
161
|
-
return ctx.args.subcommand === '
|
|
162
|
-
?
|
|
161
|
+
return ctx.args.subcommand === 'show'
|
|
162
|
+
? showResult(kind, ctx, findings)
|
|
163
163
|
: listResult(kind, findings);
|
|
164
164
|
}
|
|
165
165
|
|
package/src/cmd-tasks.ts
CHANGED
|
@@ -141,8 +141,8 @@ function requireHandle(ctx: CommandContext): TaskHandle {
|
|
|
141
141
|
const suggestion = nearestName(name, known);
|
|
142
142
|
throw new DeclarationUnknownError(
|
|
143
143
|
suggestion === undefined
|
|
144
|
-
? { kind: 'tasks', singular: 'task', name, known
|
|
145
|
-
: { kind: 'tasks', singular: 'task', name, known, suggestion
|
|
144
|
+
? { kind: 'tasks', singular: 'task', name, known }
|
|
145
|
+
: { kind: 'tasks', singular: 'task', name, known, suggestion },
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// `x vapid`'s declaration, apart from its body: the parser, `x help` and the `errors` step read it
|
|
2
|
+
// without loading `cmd-vapid.ts`, which `registry.ts` imports only when the command runs.
|
|
3
|
+
|
|
4
|
+
import type { CommandSpec } from './parse';
|
|
5
|
+
|
|
6
|
+
export const VAPID_SUBCOMMANDS = ['show', 'create'] as const;
|
|
7
|
+
|
|
8
|
+
export const vapidSpec: CommandSpec = {
|
|
9
|
+
name: 'vapid',
|
|
10
|
+
summary: 'the Web Push key pair: show which one this app signs with, or create and seal one',
|
|
11
|
+
usage: 'x vapid [show|create] [--json]',
|
|
12
|
+
requiresApp: true,
|
|
13
|
+
subcommands: [...VAPID_SUBCOMMANDS],
|
|
14
|
+
// The bare form reads; `create` writes the committed secrets file, so it is never the default.
|
|
15
|
+
defaultSubcommand: 'show',
|
|
16
|
+
flags: [],
|
|
17
|
+
};
|