@ultimat3/cli 25.2.0 → 26.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/README.md +3 -3
- package/package.json +28 -28
- 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-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/db-branch.ts +6 -6
- package/src/db-finding.ts +1 -1
- package/src/dev-restart-retire.ts +3 -1
- 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/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 +2 -2
- package/src/parse.ts +23 -2
- package/src/role-replicator.ts +1 -1
- package/src/runtime-jobs.ts +1 -1
- package/src/serve-boot.ts +8 -2
- package/src/serve-drain.ts +5 -1
- package/src/serve-retire.ts +7 -3
- package/src/site-seo.ts +1 -4
- 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/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.0.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.0.0",
|
|
43
|
+
"@ultimat3/admin": "26.0.0",
|
|
44
|
+
"@ultimat3/ai": "26.0.0",
|
|
45
|
+
"@ultimat3/auth": "26.0.0",
|
|
46
|
+
"@ultimat3/cache": "26.0.0",
|
|
47
|
+
"@ultimat3/core": "26.0.0",
|
|
48
|
+
"@ultimat3/db": "26.0.0",
|
|
49
|
+
"@ultimat3/entity": "26.0.0",
|
|
50
|
+
"@ultimat3/flags": "26.0.0",
|
|
51
|
+
"@ultimat3/http": "26.0.0",
|
|
52
|
+
"@ultimat3/i18n": "26.0.0",
|
|
53
|
+
"@ultimat3/jobs": "26.0.0",
|
|
54
|
+
"@ultimat3/mail": "26.0.0",
|
|
55
|
+
"@ultimat3/manifest": "26.0.0",
|
|
56
|
+
"@ultimat3/mcp": "26.0.0",
|
|
57
|
+
"@ultimat3/money": "26.0.0",
|
|
58
|
+
"@ultimat3/notify": "26.0.0",
|
|
59
|
+
"@ultimat3/policy": "26.0.0",
|
|
60
|
+
"@ultimat3/pwa": "26.0.0",
|
|
61
|
+
"@ultimat3/query": "26.0.0",
|
|
62
|
+
"@ultimat3/realtime": "26.0.0",
|
|
63
|
+
"@ultimat3/render": "26.0.0",
|
|
64
|
+
"@ultimat3/schema": "26.0.0",
|
|
65
|
+
"@ultimat3/seo": "26.0.0",
|
|
66
|
+
"@ultimat3/storage": "26.0.0",
|
|
67
|
+
"@ultimat3/testing": "26.0.0",
|
|
68
|
+
"@ultimat3/time": "26.0.0",
|
|
69
69
|
"babel-preset-solid": "^1.9.15"
|
|
70
70
|
}
|
|
71
71
|
}
|
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-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
|
|
package/src/db-branch.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
* `reap` is deliberately absent: a nightly sweep is a `task` (`reapBranches` from `@ultimat3/db`),
|
|
28
28
|
* and a CLI verb for it would be a second path to one job with a max-age nobody can default.
|
|
29
29
|
*/
|
|
30
|
-
export const BRANCH_SUBCOMMANDS = ['
|
|
30
|
+
export const BRANCH_SUBCOMMANDS = ['list', 'create', 'delete'] as const;
|
|
31
31
|
|
|
32
32
|
export type BranchSubcommand = (typeof BRANCH_SUBCOMMANDS)[number];
|
|
33
33
|
|
|
@@ -91,7 +91,7 @@ export function branchDatabaseName(source: string, branch: string): string {
|
|
|
91
91
|
* `db.migrate` is aimed at a private database from a URL alone, with no connection to ask
|
|
92
92
|
* `current_database()` — so "a branch of somebody" is the only question it can pose, and the
|
|
93
93
|
* answer it wants for `analytics_branch_feat` is still "not the shared database". Anything holding
|
|
94
|
-
* a client asks `branchNameIn` instead, which is the question `
|
|
94
|
+
* a client asks `branchNameIn` instead, which is the question `list` and `delete` need.
|
|
95
95
|
*/
|
|
96
96
|
export const branchNameOf = (database: string): string | null =>
|
|
97
97
|
/_branch_(.+)$/.exec(database)?.[1] ?? null;
|
|
@@ -112,7 +112,7 @@ export function branchNameIn(source: string, database: string): string | null {
|
|
|
112
112
|
/**
|
|
113
113
|
* The embedded peer: `branchPglite` copies `<dir>` to `<dir>-<name>`, so the branch name is the
|
|
114
114
|
* suffix. `pgliteBranchDir` is the forward rule and this is its inverse — written once, because
|
|
115
|
-
* `x db branch
|
|
115
|
+
* `x db branch list` and the MCP host's branch check must agree about what a branch directory is.
|
|
116
116
|
*/
|
|
117
117
|
export function pgliteBranchName(dir: string, source: string): string | null {
|
|
118
118
|
return dir.startsWith(`${source}-`) ? dir.slice(source.length + 1) : null;
|
|
@@ -213,7 +213,7 @@ async function branchesOf(client: DbClient, source: string): Promise<readonly Br
|
|
|
213
213
|
/**
|
|
214
214
|
* Only databases carrying `createBranch`'s own marker comment, and only branches of the database
|
|
215
215
|
* this session is connected to. A database this listing does not name is one `drop` may not touch,
|
|
216
|
-
* which is what makes "you may only
|
|
216
|
+
* which is what makes "you may only delete what `list` shows" a guard rather than a courtesy — and a
|
|
217
217
|
* row belonging to another app on the same server made that guard answer for a database it had
|
|
218
218
|
* never seen.
|
|
219
219
|
*/
|
|
@@ -224,7 +224,7 @@ export async function listExternalBranches(client: DbClient): Promise<readonly B
|
|
|
224
224
|
/**
|
|
225
225
|
* Through `createBranch`, never a hand-written `CREATE DATABASE`: it validates the name, refuses a
|
|
226
226
|
* database that already exists with `X_BRANCH_EXISTS`, and writes the marker comment that makes
|
|
227
|
-
* the clone visible to `
|
|
227
|
+
* the clone visible to `list`. The CLI shelled out to `psql` until now and wrote no marker at all,
|
|
228
228
|
* so every branch it made was invisible to the only lister the framework has.
|
|
229
229
|
*/
|
|
230
230
|
export async function createExternalBranch(client: DbClient, branch: string): Promise<BranchRow> {
|
|
@@ -232,7 +232,7 @@ export async function createExternalBranch(client: DbClient, branch: string): Pr
|
|
|
232
232
|
const database = branchDatabaseName(source, branch);
|
|
233
233
|
const info = await createBranch(database, { client, base: source });
|
|
234
234
|
return {
|
|
235
|
-
// The name `
|
|
235
|
+
// The name `list` will show for it, derived the way `list` derives one — a create that reported a
|
|
236
236
|
// name the listing then spells differently is a `drop` the caller has to guess at.
|
|
237
237
|
name: branchNameIn(source, database) ?? database,
|
|
238
238
|
location: database,
|
package/src/db-finding.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { findingFrom, isUltimateErrorShape } from './output';
|
|
|
10
10
|
/**
|
|
11
11
|
* The engine names its own failures — `X_MIGRATION_CONFLICT` carries the ledger row that disagrees,
|
|
12
12
|
* `X_MIGRATION_IRREVERSIBLE` carries the exact `--allow-destructive` line to rerun, and
|
|
13
|
-
* `X_BRANCH_EXISTS` carries the `x db branch
|
|
13
|
+
* `X_BRANCH_EXISTS` carries the `x db branch delete` that clears it — so those reach the caller
|
|
14
14
|
* verbatim. `X_DB_GEN_FAILED` / `X_DB_MIGRATE_FAILED` / `X_DB_BRANCH_FAILED` are what is left: the
|
|
15
15
|
* step failed for a reason no framework error claimed, and the raw message is all there is.
|
|
16
16
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// login) ran twice. The worker is retired first — `stop()` claims nothing and aborts nothing — and
|
|
5
5
|
// only then is the drain asked to release the port, the lock and the embedded database.
|
|
6
6
|
|
|
7
|
-
import { drain, logger, renderThrowable } from '@ultimat3/core';
|
|
7
|
+
import { drain, logger, markRetiring, renderThrowable } from '@ultimat3/core';
|
|
8
8
|
import type { Worker } from '@ultimat3/jobs';
|
|
9
9
|
import { msg } from './messages';
|
|
10
10
|
import { writeErrorLine } from './write-line';
|
|
@@ -32,6 +32,8 @@ export async function retireThenDrain(
|
|
|
32
32
|
worker: RetiringWorker | null,
|
|
33
33
|
deps: RetireDeps = DEFAULT_DEPS,
|
|
34
34
|
): Promise<void> {
|
|
35
|
+
// The same retire the production SIGUSR2 is (`serve-retire.ts`), so the app reads it the same way.
|
|
36
|
+
markRetiring();
|
|
35
37
|
try {
|
|
36
38
|
if (worker !== null) {
|
|
37
39
|
const { inFlight } = await worker.stats();
|
package/src/document-styles.ts
CHANGED
|
@@ -30,14 +30,12 @@ export interface SurfaceDocument {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Every surface that renders a document, with the CSS one would carry
|
|
34
|
-
*
|
|
35
|
-
* tokens. Read from render's own registry, filled when the CLI loaded the app: a second walk of
|
|
33
|
+
* Every surface that renders a document, with the CSS one would carry — a route lives on `site/`
|
|
34
|
+
* or `app/` only, so `api/` (no document) never appears. Read from render's own registry, filled when the CLI loaded the app: a second walk of
|
|
36
35
|
* the app's stylesheets here would be a second answer to "what does this document contain".
|
|
37
36
|
*/
|
|
38
37
|
export function documentSurfaces(): readonly SurfaceDocument[] {
|
|
39
38
|
const surfaces = new Set(routeEntries().map((entry) => entry.surface));
|
|
40
|
-
surfaces.delete('api');
|
|
41
39
|
return [...surfaces]
|
|
42
40
|
.sort()
|
|
43
41
|
.map((surface) => ({ surface, css: stylesFor(surface) }) satisfies SurfaceDocument);
|
package/src/error-contract.ts
CHANGED
|
@@ -32,7 +32,7 @@ export const BANNED_PHRASES: readonly RegExp[] = [
|
|
|
32
32
|
/**
|
|
33
33
|
* What makes a fix actionable as written: the `x` CLI, a tool the machine already has, a call the
|
|
34
34
|
* reader can paste, or a file they can open. A banned phrase is only a failure without one of
|
|
35
|
-
* these — "check the gateway, then: x actions
|
|
35
|
+
* these — "check the gateway, then: x actions show posts.publish --json" names the observation
|
|
36
36
|
* *and* the command, which is the shape the contract wants.
|
|
37
37
|
*/
|
|
38
38
|
export const COMMAND_TOKENS: readonly RegExp[] = [
|
package/src/errors.ts
CHANGED
|
@@ -248,7 +248,7 @@ export class ErrorCodeUnknownError extends UltimateError {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
|
-
* `x actions|queries|entities
|
|
251
|
+
* `x actions|queries|entities|tasks show <name>` named a declaration the registries do not hold —
|
|
252
252
|
* a typo, or a module that never imported. `known` is the count, not the list: a 200-action app
|
|
253
253
|
* would bury the fix line under names nobody asked for, and `list` is one command away.
|
|
254
254
|
*/
|
|
@@ -259,7 +259,7 @@ export class DeclarationUnknownError extends UltimateError {
|
|
|
259
259
|
name: string;
|
|
260
260
|
known: readonly string[];
|
|
261
261
|
suggestion?: string;
|
|
262
|
-
/** The subcommand that takes one name
|
|
262
|
+
/** The subcommand that takes one name: `show` (#709), or `explain` for `x policy` — the "why". */
|
|
263
263
|
verb?: string;
|
|
264
264
|
}) {
|
|
265
265
|
super({
|
|
@@ -268,7 +268,7 @@ export class DeclarationUnknownError extends UltimateError {
|
|
|
268
268
|
fix:
|
|
269
269
|
input.suggestion === undefined
|
|
270
270
|
? `x ${input.kind} list --json`
|
|
271
|
-
: `x ${input.kind} ${input.verb ?? '
|
|
271
|
+
: `x ${input.kind} ${input.verb ?? 'show'} ${input.suggestion}`,
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
274
|
}
|
|
@@ -279,7 +279,7 @@ export class JobUnknownError extends UltimateError {
|
|
|
279
279
|
super({
|
|
280
280
|
code: 'X_JOB_UNKNOWN',
|
|
281
281
|
cause: `the "${input.driver}" queue holds no job with id "${input.id}"`,
|
|
282
|
-
fix: 'x jobs
|
|
282
|
+
fix: 'x jobs list --json',
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
285
|
}
|
package/src/fix-command.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { GLOBAL_FLAGS } from './parse';
|
|
|
27
27
|
// slot where the reader has nothing to substitute, so `x db branch <name>` — two shipped fix lines
|
|
28
28
|
// in `@ultimat3/mcp` — is `X_CLI_UNKNOWN_COMMAND` when run and resolved clean while a placeholder
|
|
29
29
|
// was invisible to the reader. Second and fourth slots are open positionals (`x new my-app`,
|
|
30
|
-
// `x db branch
|
|
30
|
+
// `x db branch delete <name>`), where a placeholder is exactly right.
|
|
31
31
|
// A `:` is part of a word only when a letter follows it, which is what separates the shipped
|
|
32
32
|
// positional `admin:page` from prose that ends a citation with a colon (`x verify: the gate`).
|
|
33
33
|
// Read without it, `x g admin:page` cites `x g admin` — a positional the CLI does not ship —
|
package/src/jobs-report.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Pure, driver-injected job operations behind `x jobs`: flag parsing, plus
|
|
1
|
+
// Pure, driver-injected job operations behind `x jobs`: flag parsing, plus list / show / retry. No
|
|
2
2
|
// CLI parsing, no process I/O, no rendering — a test drives every path with `memoryJobDriver()`
|
|
3
3
|
// alone. The `--json` shapes are `jobs-json.ts`, the table `jobs-table.ts`.
|
|
4
4
|
|
|
@@ -61,7 +61,7 @@ export function parseLimitFlag(value: string | undefined, command = 'jobs'): num
|
|
|
61
61
|
return limit;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// ──
|
|
64
|
+
// ── list ──────────────────────────────────────────────────────────────────
|
|
65
65
|
|
|
66
66
|
export interface JobsListFilter {
|
|
67
67
|
readonly queue?: string | undefined;
|
|
@@ -105,7 +105,7 @@ async function jobPage(driver: JobDriver, filter: JobFilter): Promise<Page<JobRe
|
|
|
105
105
|
* `--state ready` filter (or the default 100-row cap) pushes out of view is the exact failure
|
|
106
106
|
* mode this command exists to prevent.
|
|
107
107
|
*
|
|
108
|
-
* Backfills are read `running` only. `x jobs
|
|
108
|
+
* Backfills are read `running` only. `x jobs list` is a LIVE view of the queue — a pass that
|
|
109
109
|
* finished last week is history, and `x db backfill --list` is where that question is asked and
|
|
110
110
|
* answered with the whole ledger. A driver with no ledger answers `[]` rather than throwing, so
|
|
111
111
|
* the queue view never fails over a fact nobody asked about.
|
package/src/jobs-table.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The `x jobs
|
|
1
|
+
// The `x jobs list` terminal table, and nothing else. Split out of `jobs-report.ts` so that
|
|
2
2
|
// deciding how a value LOOKS lives apart from deciding what the queue DOES: a rendering rule
|
|
3
3
|
// (column order, padding, the run-at unit) is reviewed here, once, with no queue logic around it.
|
|
4
4
|
|
package/src/live-routes.ts
CHANGED
|
@@ -183,7 +183,6 @@ export async function liveRouteGaps(
|
|
|
183
183
|
const reported = new Set<string>();
|
|
184
184
|
const gaps: LiveRouteGap[] = [];
|
|
185
185
|
for (const entry of entries) {
|
|
186
|
-
if (entry.surface === 'api') continue;
|
|
187
186
|
const never = entry.config.hydrate === 'never';
|
|
188
187
|
for (const read of await browserOnlyReads(root, entry.file)) {
|
|
189
188
|
if (reported.has(read.at) || (!never && inBrowser.has(read.at))) continue;
|
package/src/mcp-db-target.ts
CHANGED
|
@@ -56,7 +56,7 @@ const isProduction = (env: Env): boolean => {
|
|
|
56
56
|
/**
|
|
57
57
|
* `x db branch create <name>` names an external clone `<source>_branch_<name>`. Both readings come
|
|
58
58
|
* from `db-branch.ts` — the module that also WRITES those names — because a target that disagrees
|
|
59
|
-
* with `x db branch
|
|
59
|
+
* with `x db branch list` about what a branch is would let `db.migrate` run against a shared
|
|
60
60
|
* database on the strength of a naming rule one of the two had drifted away from.
|
|
61
61
|
*/
|
|
62
62
|
function postgresBranch(url: string): string | null {
|
package/src/mcp-errors.ts
CHANGED
|
@@ -60,7 +60,7 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
|
60
60
|
X_APP_PACKAGE_INVALID: 'bun pm pkg set name=my-app version=0.1.0',
|
|
61
61
|
X_ERROR_CODE_UNKNOWN: 'x errors list --json',
|
|
62
62
|
X_DECLARATION_UNKNOWN: 'x actions list --json',
|
|
63
|
-
X_JOB_UNKNOWN: 'x jobs
|
|
63
|
+
X_JOB_UNKNOWN: 'x jobs list --json',
|
|
64
64
|
X_FIX_TARGET_UNKNOWN: 'x fix boundary apps/web/site/page.tsx --json',
|
|
65
65
|
X_ERROR_FIX_INVALID: 'x verify --json # the finding names the file, the line and the fix text',
|
|
66
66
|
X_ERROR_FIX_PATH_MISSING:
|
|
@@ -213,7 +213,7 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
|
213
213
|
X_QUERY_SUBSCRIBES_UNKNOWN:
|
|
214
214
|
'x db gen "retry after fixing subscribes" --json # first edit subscribes: on the query the cause names',
|
|
215
215
|
X_DB_MIGRATE_FAILED: 'x doctor --json # cause carries the Postgres error verbatim',
|
|
216
|
-
X_DB_BRANCH_FAILED: 'x db branch
|
|
216
|
+
X_DB_BRANCH_FAILED: 'x db branch list --json',
|
|
217
217
|
X_DB_STUDIO_FAILED: 'x doctor --json',
|
|
218
218
|
// Runnable first, the narrowing behind a `#`, exactly as X_CLI_UNKNOWN_COMMAND above: naming
|
|
219
219
|
// the tier IS the consent, and which seed to consent to is the one thing this table cannot
|
package/src/messages.ts
CHANGED
|
@@ -46,7 +46,7 @@ const CATALOG = {
|
|
|
46
46
|
'cli.db.branch.failed': 'branch command failed',
|
|
47
47
|
'cli.db.branch.listed': '{count} branch(es) of this database',
|
|
48
48
|
'cli.db.branch.none': 'this database has no branch',
|
|
49
|
-
/** The empty cell in an `x db branch
|
|
49
|
+
/** The empty cell in an `x db branch list` column — a value, not a column key. */
|
|
50
50
|
'cli.db.branch.unknown': '-',
|
|
51
51
|
'cli.db.gen.failed': 'migration not generated',
|
|
52
52
|
'cli.db.gen.unchanged': 'entities and migrations agree — nothing to generate',
|
|
@@ -149,7 +149,7 @@ const CATALOG = {
|
|
|
149
149
|
'cli.jobs.depth':
|
|
150
150
|
'{ready} ready · {running} running · {delayed} delayed · {dead} dead across {queues} queue(s)',
|
|
151
151
|
'cli.jobs.listed': '{count} job(s)',
|
|
152
|
-
'cli.jobs.nextPage': 'more — next page: x jobs
|
|
152
|
+
'cli.jobs.nextPage': 'more — next page: x jobs list --after {cursor}',
|
|
153
153
|
'cli.jobs.noError': 'no error recorded',
|
|
154
154
|
'cli.jobs.paused': 'queue {queue} paused — no worker claims from it; enqueues still land',
|
|
155
155
|
'cli.jobs.pausedQueues': 'paused queue(s): {queues}',
|
package/src/parse.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface CommandSpec {
|
|
|
44
44
|
* than a misspelt one. Declared per command, never inferred, because only some commands can say
|
|
45
45
|
* it truthfully: `x errors X_PERMISSION_UNKNOWN` can only be a code, and `x jobs 4f2a` is
|
|
46
46
|
* genuinely ambiguous with `show`, so an unconditional fallback would turn `x jobs <id>` into a
|
|
47
|
-
* silent `x jobs
|
|
47
|
+
* silent `x jobs list` that ignores the id.
|
|
48
48
|
*
|
|
49
49
|
* `x errors X_PERMISSION_UNKNOWN --json` answered `X_CLI_UNKNOWN_COMMAND … fix: x help`, and
|
|
50
50
|
* `x help` prints `errors an X_* code, explained` — which reads as exactly the form that was
|
|
@@ -304,6 +304,27 @@ function splitInline(raw: string): [string, string | undefined] {
|
|
|
304
304
|
return [raw.slice(0, eq), raw.slice(eq + 1)];
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The verbs 26.0.0 retired (#709): one verb each across every `x` registry — `list`, `show <one>`,
|
|
309
|
+
* `delete` — and no alias, because two spellings of one verb is the ambiguity axiom 1 forbids. A
|
|
310
|
+
* retired verb is REFUSED, never accepted; this table only lets the refusal name its replacement.
|
|
311
|
+
*/
|
|
312
|
+
export const RETIRED_VERBS = Object.freeze<Record<string, string>>({
|
|
313
|
+
ls: 'list',
|
|
314
|
+
rm: 'delete',
|
|
315
|
+
describe: 'show',
|
|
316
|
+
drop: 'delete',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/** The verb that replaced `word`, when `word` was retired and its replacement is in `allowed`. */
|
|
320
|
+
export function retiredVerbReplacement(
|
|
321
|
+
word: string,
|
|
322
|
+
allowed: readonly string[],
|
|
323
|
+
): string | undefined {
|
|
324
|
+
const replacement = Object.hasOwn(RETIRED_VERBS, word) ? RETIRED_VERBS[word] : undefined;
|
|
325
|
+
return replacement !== undefined && allowed.includes(replacement) ? replacement : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
/** Which subcommand ran, and whether the caller's first positional is what named it. */
|
|
308
329
|
interface ResolvedSubcommand {
|
|
309
330
|
readonly name: string | undefined;
|
|
@@ -323,7 +344,7 @@ function readSubcommand(spec: CommandSpec, positionals: readonly string[]): Reso
|
|
|
323
344
|
throw new MissingSubcommandError({ command: spec.name, known: allowed });
|
|
324
345
|
}
|
|
325
346
|
if (allowed.includes(token)) return { name: token, consumed: true };
|
|
326
|
-
const suggestion = nearestName(token, allowed);
|
|
347
|
+
const suggestion = retiredVerbReplacement(token, allowed) ?? nearestName(token, allowed);
|
|
327
348
|
// The declared fallback, and only past the near-miss guard: a word within `nearestName`'s edit
|
|
328
349
|
// budget of a real subcommand is a typo, and reading it as the default subcommand's argument
|
|
329
350
|
// would answer a question nobody asked.
|
package/src/role-replicator.ts
CHANGED
|
@@ -73,7 +73,7 @@ const embeddedRefusal = (dev: boolean): BadFlagError | ConfigInvalidError =>
|
|
|
73
73
|
* Both readers of this list are catalog readers. `PgReplicationStream` keeps a change only when
|
|
74
74
|
* `#entities.has(relation.name)`, and a pgoutput Relation message names the table; `warnPartialIdentity`
|
|
75
75
|
* matches the same list against `pg_class.relname`. An entity NAME is the framework's own registry
|
|
76
|
-
* key — what a cache tag, a policy and `x entities
|
|
76
|
+
* key — what a cache tag, a policy and `x entities show` are keyed by — and `entity('user',
|
|
77
77
|
* { table: 'users' })` makes the two different strings. Passing the name meant a renamed table
|
|
78
78
|
* matched nothing on either side: every change SKIPPED, and a replica-identity warning that could
|
|
79
79
|
* never fire. Latent wherever the two agree, which is every entity in `examples/dummy`.
|
package/src/runtime-jobs.ts
CHANGED
|
@@ -93,7 +93,7 @@ function reportUnserved(served: readonly string[], registered: readonly string[]
|
|
|
93
93
|
logger.warn('jobs.worker.queue-unserved', {
|
|
94
94
|
queues,
|
|
95
95
|
served,
|
|
96
|
-
fix: `x jobs
|
|
96
|
+
fix: `x jobs list --json — every queue listed with no worker needs one: WORKER_QUEUES=<queue>,<queue> on another worker Deployment, or add it to this one's`,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
package/src/serve-boot.ts
CHANGED
|
@@ -54,13 +54,19 @@ const LISTENING_ROLES: readonly Role[] = ['web', 'sync'];
|
|
|
54
54
|
* — it only went on claiming jobs for those seconds, then aborted them at the drain (s1-con #7).
|
|
55
55
|
*
|
|
56
56
|
* The budget (`deadlineMs`) is every role's: it is the time a worker's running job has to finish
|
|
57
|
-
* on a deploy, so it is the one key here a non-listening role needs MOST.
|
|
57
|
+
* on a deploy, so it is the one key here a non-listening role needs MOST. The worker's alone is
|
|
58
|
+
* `workerDeadlineMs` when declared — the hour `deadlineMs` is capped at is too short for a job
|
|
59
|
+
* that must not run twice, and a web role has no business waiting two hours on a request.
|
|
58
60
|
*/
|
|
59
61
|
export function lifecycleForRole(
|
|
60
62
|
role: Role,
|
|
61
63
|
drain: Partial<DrainConfig> | undefined,
|
|
62
64
|
): LifecycleOptions {
|
|
63
|
-
const
|
|
65
|
+
const declared =
|
|
66
|
+
role === 'worker' && drain?.workerDeadlineMs !== undefined
|
|
67
|
+
? drain.workerDeadlineMs
|
|
68
|
+
: drain?.deadlineMs;
|
|
69
|
+
const budget = declared === undefined ? {} : { deadlineMs: declared };
|
|
64
70
|
if (!LISTENING_ROLES.includes(role)) return { readinessGraceMs: 0, ...budget };
|
|
65
71
|
return drain?.readinessGraceMs === undefined
|
|
66
72
|
? budget
|
package/src/serve-drain.ts
CHANGED
|
@@ -9,7 +9,11 @@ export async function loadDrainConfig(root: string): Promise<Partial<DrainConfig
|
|
|
9
9
|
const drain = (await loadAppConfig(root))?.drain;
|
|
10
10
|
return drain === undefined
|
|
11
11
|
? undefined
|
|
12
|
-
: {
|
|
12
|
+
: {
|
|
13
|
+
readinessGraceMs: drain.readinessGraceMs,
|
|
14
|
+
deadlineMs: drain.deadlineMs,
|
|
15
|
+
workerDeadlineMs: drain.workerDeadlineMs,
|
|
16
|
+
};
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/** `health.readiness`, or `undefined` for a root with no config file (core keeps its default). */
|
package/src/serve-retire.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// `ROLE=worker`'s retire, on SIGUSR2 (issue #669): stop claiming, finish every held job however
|
|
2
2
|
// long it takes, abort nothing — then the ordinary drain, so the process exits 0. It is what a
|
|
3
3
|
// Kubernetes `preStop` sends before the SIGTERM a rollout ends in, for a job whose side effect may
|
|
4
|
-
// happen at most once and may outlast
|
|
4
|
+
// happen at most once and may outlast the drain budget. Framework-owned: no port, no app code.
|
|
5
5
|
|
|
6
|
-
import { drain, logger, renderThrowable } from '@ultimat3/core';
|
|
6
|
+
import { drain, logger, markRetiring, renderThrowable } from '@ultimat3/core';
|
|
7
7
|
import type { Worker } from '@ultimat3/jobs';
|
|
8
8
|
|
|
9
9
|
/** The signal a worker retires on. Free in Bun and Node (SIGUSR1 is the inspector's). */
|
|
@@ -43,7 +43,8 @@ const NOTHING: WorkerRetire = Object.freeze({ adopt: () => undefined, disarm: ()
|
|
|
43
43
|
* Exits rather than holds, decided 2026-10-07: the `preStop` has no channel back but PID 1 itself,
|
|
44
44
|
* so "retired" has to be observable as "PID 1 is gone" — and a pod that has stopped claiming serves
|
|
45
45
|
* nothing it could keep holding for. A SIGTERM landing mid-retire is the ordinary drain: it binds
|
|
46
|
-
* the retire to `drain.deadlineMs` and cuts
|
|
46
|
+
* the retire to the worker's budget — `drain.workerDeadlineMs`, else `drain.deadlineMs` — and cuts
|
|
47
|
+
* off at the margin (`packages/jobs/src/worker.ts`). `isRetiring()` is true from the signal on.
|
|
47
48
|
*/
|
|
48
49
|
export function armWorkerRetire(options: WorkerRetireOptions = {}): WorkerRetire {
|
|
49
50
|
if ((options.platform ?? process.platform) === 'win32') return NOTHING;
|
|
@@ -84,6 +85,9 @@ export function armWorkerRetire(options: WorkerRetireOptions = {}): WorkerRetire
|
|
|
84
85
|
const listener = (): void => {
|
|
85
86
|
if (retiring) return;
|
|
86
87
|
retiring = true;
|
|
88
|
+
// At the signal, before the worker is awaited or stopped: the app's `isRetiring()` is how its
|
|
89
|
+
// own code (a heartbeat, a status row) sees a retire that `isDraining()` only shows at the end.
|
|
90
|
+
markRetiring();
|
|
87
91
|
// Never rejects: `retire` catches its own failure and always reaches the drain.
|
|
88
92
|
void retire();
|
|
89
93
|
};
|
package/src/site-seo.ts
CHANGED
|
@@ -110,7 +110,7 @@ type Stamp = (file: string) => { readonly lastmod?: string };
|
|
|
110
110
|
/**
|
|
111
111
|
* The app's `seo.sitemap.extra` paths as records, each resolved to the route that answers it — the
|
|
112
112
|
* most specific match, the router's own tie-break. REFUSED, never skipped, when it names no route,
|
|
113
|
-
*
|
|
113
|
+
* a gated one or a `site/` page (listed already): a sitemap missing a page its
|
|
114
114
|
* author listed is the silent drop, and a gated page in it is a URL a crawler cannot open.
|
|
115
115
|
*/
|
|
116
116
|
function extraRoutes(extra: readonly string[], stamp: Stamp): readonly RouteRecord[] {
|
|
@@ -121,9 +121,6 @@ function extraRoutes(extra: readonly string[], stamp: Stamp): readonly RouteReco
|
|
|
121
121
|
.sort((a, b) => b.pattern.specificity - a.pattern.specificity)[0];
|
|
122
122
|
if (entry === undefined)
|
|
123
123
|
throw new SitemapExtraInvalidError(path, 'no registered route answers');
|
|
124
|
-
if (entry.surface === 'api') {
|
|
125
|
-
throw new SitemapExtraInvalidError(path, `is the api/ route ${entry.file}, not a page`);
|
|
126
|
-
}
|
|
127
124
|
if (entry.surface === 'site') {
|
|
128
125
|
throw new SitemapExtraInvalidError(
|
|
129
126
|
path,
|
|
@@ -40,7 +40,7 @@ stop; that answer is worth more than a plan built on a shape the app cannot hold
|
|
|
40
40
|
|---|---|
|
|
41
41
|
| \`apps/web/site/<path>/page.tsx\` | public, SEO-critical, 0kb JS |
|
|
42
42
|
| \`apps/web/app/<slice>/\` | the feature slice: entity, repo, policy, actions, queries, jobs, UI |
|
|
43
|
-
| \`apps/web/api
|
|
43
|
+
| \`apps/web/api/index.ts\` | the one \`defineApi()\` collecting every action and query — no route file lives under \`api/\` |
|
|
44
44
|
| \`apps/web/shared/\` | tokens, primitives, the actor type — a leaf, imports nothing of yours |
|
|
45
45
|
| \`packages/db/\` | schema, migrations, seed |
|
|
46
46
|
| \`packages/ui/\` | components with no feature knowledge |
|
|
@@ -79,7 +79,7 @@ feature slice. Nothing else. A change outside that set is a collision — report
|
|
|
79
79
|
| Money | \`{ minor, currency }\` — integer minor units and an ISO code, both, always. Never a float |
|
|
80
80
|
| Time | store UTC; a formatted date always names an explicit IANA time zone |
|
|
81
81
|
|
|
82
|
-
Inspect before you change: \`x entities list\`, \`x entities
|
|
82
|
+
Inspect before you change: \`x entities list\`, \`x entities show <name>\`, both with \`--json\`.
|
|
83
83
|
|
|
84
84
|
Checks: \`bun test <path>/entity.test.ts\`, \`bunx biome check --write <paths>\`, and \`bun run typecheck\`
|
|
85
85
|
once when you are otherwise done. Never \`x verify\` — that belongs to whoever coordinates you.
|
|
@@ -105,7 +105,7 @@ renders. A change outside that set is a collision — report it, do not make it.
|
|
|
105
105
|
| Tasks name a time zone | a cron schedule with an ambient zone is a different time twice a year |
|
|
106
106
|
| No \`any\` | \`unknown\` plus a schema parse |
|
|
107
107
|
|
|
108
|
-
Inspect before you change: \`x actions
|
|
108
|
+
Inspect before you change: \`x actions show <name>\`, \`x queries show <name>\`, \`x jobs list\`,
|
|
109
109
|
\`x tasks list\`, \`x policy explain <subject>\` — every one takes \`--json\`.
|
|
110
110
|
|
|
111
111
|
Checks: \`bun test <path>/<file>.test.ts\`, \`bunx biome check --write <paths>\`, and \`bun run typecheck\`
|
|
@@ -126,7 +126,7 @@ calls. A change outside that set is a collision — report it, do not make it.
|
|
|
126
126
|
|---|---|
|
|
127
127
|
| \`site/\` is 0kb JS | and may not import from \`app/\`. One interactive control on a static page is an island: \`x g island <name> --at <dir>\` |
|
|
128
128
|
| \`shared/\` is a leaf | tokens, primitives, the actor type. It imports nothing of yours |
|
|
129
|
-
| The directory is the URL | \`page.tsx\` under \`site/\`/\`app
|
|
129
|
+
| The directory is the URL | \`page.tsx\` under \`site/\`/\`app/\`; \`api/\` holds no route file, only \`defineApi()\`'s actions and queries. The filename is never the path |
|
|
130
130
|
| Every string through \`t()\` | a literal in a component is a string no locale can ever translate |
|
|
131
131
|
| Semantic tokens only | never a raw hex, in a component or a stylesheet |
|
|
132
132
|
| Dates name a zone | explicit IANA time zone at every call site, no ambient default |
|
|
@@ -66,12 +66,12 @@ SQL into \`packages/db/migrations/\`.
|
|
|
66
66
|
|---|---|---|
|
|
67
67
|
| \`apps/web/site/\` | 0kb JS, may not import \`apps/web/app/\` | the static path pays the app's bundle |
|
|
68
68
|
| \`apps/web/app/\` | authed, streaming, hydrated | — |
|
|
69
|
-
| \`apps/web/api/\` | actions only, \`
|
|
69
|
+
| \`apps/web/api/\` | actions and queries only, collected by \`defineApi()\` — no route file | a second HTTP surface |
|
|
70
70
|
| \`apps/web/shared/\` | a leaf: imports nothing of yours | an import cycle across surfaces |
|
|
71
71
|
| \`repo.ts\` | the only file that touches the database | authz bypassed by a raw read |
|
|
72
72
|
| routes | call actions and queries, never a repo | policy skipped |
|
|
73
73
|
|
|
74
|
-
Route files: \`page.tsx\` under \`site/\`/\`app/\`,
|
|
74
|
+
Route files: \`page.tsx\` under \`site/\`/\`app/\`, and none under \`api/\`. **The directory is the URL** —
|
|
75
75
|
the filename never is. One interactive control on a 0kb page is an island: \`x g island <name> --at <dir>\`.
|
|
76
76
|
|
|
77
77
|
## 4. Gate it
|
|
@@ -195,14 +195,18 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
195
195
|
{{/*
|
|
196
196
|
A role's terminationGracePeriodSeconds, in the order the kubelet's budget is spent: the preStop
|
|
197
197
|
sleep (web and sync, 1.30+), the worker's opt-in retire (roles.worker.retireSeconds), the
|
|
198
|
-
readiness grace (web and sync — every other role drains with none), the drain budget (app.config.ts drain.deadlineMs, every role
|
|
198
|
+
readiness grace (web and sync — every other role drains with none), the drain budget (app.config.ts drain.deadlineMs, every role;
|
|
199
|
+
the worker's is drain.workerDeadlineSeconds instead when > 0, from drain.workerDeadlineMs) and the teardown margin.
|
|
199
200
|
Defaults: 45s web/sync (40s below 1.30), 35s the rest. x deploy --method helm sets
|
|
200
|
-
drain.deadlineSeconds and drain.readinessGraceSeconds from app.config.ts.
|
|
201
|
+
drain.deadlineSeconds, drain.workerDeadlineSeconds and drain.readinessGraceSeconds from app.config.ts.
|
|
201
202
|
*/}}
|
|
202
203
|
{{- define "${app.kebab}.terminationGracePeriodSeconds" -}}
|
|
203
204
|
{{- $drain := .root.Values.drain -}}
|
|
204
205
|
{{- $total := add (int $drain.deadlineSeconds) (int $drain.teardownMarginSeconds) -}}
|
|
205
206
|
{{- if eq .role "worker" -}}
|
|
207
|
+
{{- if gt (int $drain.workerDeadlineSeconds) 0 -}}
|
|
208
|
+
{{- $total = add (int $drain.workerDeadlineSeconds) (int $drain.teardownMarginSeconds) -}}
|
|
209
|
+
{{- end -}}
|
|
206
210
|
{{- $total = add $total (int .cfg.retireSeconds) -}}
|
|
207
211
|
{{- end -}}
|
|
208
212
|
{{- if .cfg.port -}}
|
|
@@ -99,11 +99,14 @@ securityContext:
|
|
|
99
99
|
# readinessGraceSeconds app.config.ts drain.readinessGraceMs / 1000 (web and sync only)
|
|
100
100
|
# deadlineSeconds app.config.ts drain.deadlineMs / 1000 — x deploy --method helm sets
|
|
101
101
|
# both from app.config.ts, so a raised budget raises the grace with it
|
|
102
|
+
# workerDeadlineSeconds app.config.ts drain.workerDeadlineMs / 1000 — the worker's budget in place
|
|
103
|
+
# of deadlineSeconds, up to a day; 0 means the worker uses deadlineSeconds
|
|
102
104
|
# teardownMarginSeconds the release after the drain, the telemetry flush, the pool close
|
|
103
105
|
drain:
|
|
104
106
|
preStopSleepSeconds: 5
|
|
105
107
|
readinessGraceSeconds: 5
|
|
106
108
|
deadlineSeconds: 25
|
|
109
|
+
workerDeadlineSeconds: 0
|
|
107
110
|
teardownMarginSeconds: 10
|
|
108
111
|
|
|
109
112
|
# How long a new pod must stay Ready before a rollout counts it and stops an old one.
|
|
@@ -166,7 +169,7 @@ roles:
|
|
|
166
169
|
replicas: 2
|
|
167
170
|
# The retire, OFF at 0. Set to the longest a held job may run, in seconds, and the worker's
|
|
168
171
|
# preStop sends its PID 1 SIGUSR2: stop claiming, finish every held job, abort nothing, exit 0
|
|
169
|
-
# — for a job whose side effect must happen at most once and may outlast drain.
|
|
172
|
+
# — for a job whose side effect must happen at most once and may outlast its drain budget.
|
|
170
173
|
# Added to this role's terminationGracePeriodSeconds, which the kubelet counts the preStop against.
|
|
171
174
|
retireSeconds: 0
|
|
172
175
|
resources:
|