@ultimat3/cli 8.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +9 -3
- package/package.json +26 -25
- package/src/affected.ts +0 -3
- package/src/app-boundaries.ts +4 -5
- package/src/app-env.ts +7 -2
- package/src/app-load.ts +7 -0
- package/src/browser-launcher.ts +0 -2
- package/src/budgets.ts +4 -3
- package/src/cmd-build.ts +2 -2
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-deploy.ts +20 -6
- package/src/cmd-docs.ts +2 -1
- package/src/cmd-doctor.ts +3 -5
- package/src/cmd-env.ts +2 -2
- package/src/cmd-fix.ts +2 -4
- package/src/cmd-new.ts +2 -2
- package/src/cmd-shot.ts +22 -2
- package/src/db-finding.ts +2 -2
- package/src/db-seed.ts +0 -3
- package/src/dev-assets.ts +4 -7
- package/src/dev-cache.ts +140 -36
- package/src/dev-lock.ts +8 -7
- package/src/dev-purge.ts +120 -0
- package/src/dev-queue.ts +31 -6
- package/src/dev-render.ts +11 -14
- package/src/dev-runtime.ts +62 -15
- package/src/dev-storage.ts +8 -2
- package/src/dev-sync.ts +37 -2
- package/src/document-styles.ts +4 -2
- package/src/drift.ts +3 -2
- package/src/error-codes.ts +9 -2
- package/src/error-contract.ts +5 -5
- package/src/errors.ts +1 -29
- package/src/flag-reads.ts +2 -2
- package/src/generate-write.ts +3 -2
- package/src/guards.ts +4 -4
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +34 -10
- package/src/island-routes.ts +7 -1
- package/src/island-styles.ts +1 -1
- package/src/mcp-errors.ts +2 -2
- package/src/metrics-endpoint.ts +0 -2
- package/src/output.ts +2 -2
- package/src/prerender.ts +34 -20
- package/src/runtime-overrides.ts +1 -1
- package/src/serve.ts +1 -1
- package/src/solid-loader.ts +1 -1
- package/src/static-report.ts +41 -3
- package/src/style-csp.ts +2 -1
- package/src/templates/scaffold-container.ts +12 -0
- package/src/templates/scaffold-docs.ts +11 -4
- package/src/templates/scaffold-domain-package.ts +3 -1
- package/src/templates/scaffold-repo.ts +19 -9
- package/src/templates/slice-foundation.ts +3 -5
- package/src/test-shards.ts +2 -2
- package/src/tsconfig-references.ts +2 -2
- package/src/verify-checks.ts +3 -2
- package/src/verify-floor.ts +8 -6
- package/src/verify-run.ts +2 -2
- package/src/verify-step.ts +2 -1
- package/src/verify-test-run.ts +2 -2
- package/src/workspace-checks.ts +10 -12
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +7 -1
package/CLAUDE.md
CHANGED
|
@@ -535,6 +535,7 @@ hand-written layout and `readMigrations` skips it — read as a migration it sor
|
|
|
535
535
|
| `dev-queue.ts` | the db + queue pair alone, and the one place that takes every ambient accessor back |
|
|
536
536
|
| `dev-runtime.ts` | start the rest on top of it and install the remaining accessors (storage, mail, transport) |
|
|
537
537
|
| `dev-cache.ts` | which cache tiers this process reads through, and the cross-instance invalidation hop |
|
|
538
|
+
| `dev-purge.ts` | the hourly retention sweep: which framework tables this boot owns, the `purge()` job over them and the `task` that fires it |
|
|
538
539
|
| `dev-sync.ts` | the `sync` role: its live-query registry, who is dialling it, and the socket it owns |
|
|
539
540
|
| `runtime-overrides.ts` | the one field a host hands the framework a driver through |
|
|
540
541
|
| `sync-authenticator.ts` | the app's HTTP authenticator, seen as the sync node's |
|
|
@@ -602,6 +603,8 @@ relay draining it.
|
|
|
602
603
|
| the durable scheduler | `pgSchedulerState` + `createPgLeaseLeader` in `startRoles` | a watermark forgotten on restart, and every replica its own leader |
|
|
603
604
|
| the Postgres event bus | `dev-queue.ts` | `step.waitForEvent` forgot every correlation on restart |
|
|
604
605
|
| the shared idempotency store | `dev-queue.ts` | a retry on another replica charged the card twice |
|
|
606
|
+
| the shared auth limiter | `configureAuthLimiters` in `startServices` | account lockouts counted per POD, so N replicas granted `maxAttempts × N` guesses |
|
|
607
|
+
| the retention sweep | `dev-purge.ts`, declared in `startServices` | three `purgeExpired()` with no caller — every row `x_idempotency`, `x_rate_limit` and `x_auth_*` ever took was kept |
|
|
605
608
|
| the cache tiers | `dev-cache.ts` | only the CDN tier was registered; memo, LRU and Redis had zero callers |
|
|
606
609
|
| WebSocket authentication | `dev-sync.ts` | `actorId: null` on every socket — realtime was single-tenant by wiring |
|
|
607
610
|
| OTLP export | `otlp-export.ts` | the chart set the variable and no code read it |
|
|
@@ -769,9 +772,12 @@ Promoting it to `x verify`'s `boundaries` host check is one line in `scripts/ver
|
|
|
769
772
|
rule over names. Two stronger rules were measured and rejected: "the read must not be a property
|
|
770
773
|
initializer" reports six flags, five of which work (`x db --allow-destructive`, `x jobs --queue`);
|
|
771
774
|
"the summary must match the behaviour" is undecidable. So the flag's summary now says what it does,
|
|
772
|
-
and forcing a reload
|
|
773
|
-
|
|
774
|
-
|
|
775
|
+
and forcing a reload is **not a thing this framework does**, `As of 2026-08`. `updateSignal`
|
|
776
|
+
had no runtime caller for four majors and 9.0.0 deleted it rather than wiring it: `pwa` is tier 4
|
|
777
|
+
and the two runtimes holding both build ids — `http` (2) and `sync` (3) — are below it, so no
|
|
778
|
+
legal import could ever have reached the function. A deploy command has no channel to a running
|
|
779
|
+
client regardless; the plan is `docker compose up` or `helm upgrade`. What ships is notification:
|
|
780
|
+
`useConnection().updateAvailable` from `@ultimat3/realtime`.
|
|
775
781
|
|
|
776
782
|
## Planned commands are commands
|
|
777
783
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.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",
|
|
@@ -37,30 +37,31 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "
|
|
41
|
-
"@ultimat3/admin": "
|
|
42
|
-
"@ultimat3/ai": "
|
|
43
|
-
"@ultimat3/
|
|
44
|
-
"@ultimat3/
|
|
45
|
-
"@ultimat3/
|
|
46
|
-
"@ultimat3/
|
|
47
|
-
"@ultimat3/
|
|
48
|
-
"@ultimat3/
|
|
49
|
-
"@ultimat3/
|
|
50
|
-
"@ultimat3/
|
|
51
|
-
"@ultimat3/
|
|
52
|
-
"@ultimat3/
|
|
53
|
-
"@ultimat3/
|
|
54
|
-
"@ultimat3/
|
|
55
|
-
"@ultimat3/
|
|
56
|
-
"@ultimat3/
|
|
57
|
-
"@ultimat3/
|
|
58
|
-
"@ultimat3/
|
|
59
|
-
"@ultimat3/
|
|
60
|
-
"@ultimat3/
|
|
61
|
-
"@ultimat3/
|
|
62
|
-
"@ultimat3/
|
|
63
|
-
"@ultimat3/
|
|
40
|
+
"@ultimat3/action": "10.0.0",
|
|
41
|
+
"@ultimat3/admin": "10.0.0",
|
|
42
|
+
"@ultimat3/ai": "10.0.0",
|
|
43
|
+
"@ultimat3/auth": "10.0.0",
|
|
44
|
+
"@ultimat3/cache": "10.0.0",
|
|
45
|
+
"@ultimat3/core": "10.0.0",
|
|
46
|
+
"@ultimat3/db": "10.0.0",
|
|
47
|
+
"@ultimat3/entity": "10.0.0",
|
|
48
|
+
"@ultimat3/http": "10.0.0",
|
|
49
|
+
"@ultimat3/i18n": "10.0.0",
|
|
50
|
+
"@ultimat3/jobs": "10.0.0",
|
|
51
|
+
"@ultimat3/mail": "10.0.0",
|
|
52
|
+
"@ultimat3/manifest": "10.0.0",
|
|
53
|
+
"@ultimat3/mcp": "10.0.0",
|
|
54
|
+
"@ultimat3/policy": "10.0.0",
|
|
55
|
+
"@ultimat3/pwa": "10.0.0",
|
|
56
|
+
"@ultimat3/query": "10.0.0",
|
|
57
|
+
"@ultimat3/realtime": "10.0.0",
|
|
58
|
+
"@ultimat3/render": "10.0.0",
|
|
59
|
+
"@ultimat3/schema": "10.0.0",
|
|
60
|
+
"@ultimat3/scraping": "10.0.0",
|
|
61
|
+
"@ultimat3/seo": "10.0.0",
|
|
62
|
+
"@ultimat3/storage": "10.0.0",
|
|
63
|
+
"@ultimat3/testing": "10.0.0",
|
|
64
|
+
"@ultimat3/time": "10.0.0",
|
|
64
65
|
"babel-preset-solid": "^1.9.15"
|
|
65
66
|
}
|
|
66
67
|
}
|
package/src/affected.ts
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// directory is checkout-relative while a caller's scan yields paths relative to its own root.
|
|
14
14
|
import { join, relative } from 'node:path';
|
|
15
15
|
import { singleLine, UltimateError } from '@ultimat3/core';
|
|
16
|
-
import { docsFor } from './error-codes';
|
|
17
16
|
import { BadFlagError } from './errors';
|
|
18
17
|
import type { ExecResult, Runner } from './exec';
|
|
19
18
|
import { execOutput } from './exec';
|
|
@@ -193,7 +192,6 @@ export async function gitRoot(runner: Runner, cwd: string, command: string): Pro
|
|
|
193
192
|
code: 'X_CLI_UNEXPECTED',
|
|
194
193
|
cause: `x ${command} reads its diff from git and "git rev-parse --show-toplevel" exited ${result.code} in ${cwd}: ${singleLine(execOutput(result))}`,
|
|
195
194
|
fix: `run x ${command} from inside a git checkout — confirm with: git rev-parse --show-toplevel`,
|
|
196
|
-
docs: docsFor('X_CLI_UNEXPECTED'),
|
|
197
195
|
});
|
|
198
196
|
}
|
|
199
197
|
|
|
@@ -245,7 +243,6 @@ export async function changedFiles(
|
|
|
245
243
|
code: 'X_CLI_UNEXPECTED',
|
|
246
244
|
cause: `"${failed.command.join(' ')}" exited ${failed.code} in ${options.cwd}: ${singleLine(execOutput(failed))}`,
|
|
247
245
|
fix: `run it yourself to see why: ${failed.command.join(' ')}`,
|
|
248
|
-
docs: docsFor('X_CLI_UNEXPECTED'),
|
|
249
246
|
});
|
|
250
247
|
}
|
|
251
248
|
return [...new Set(runs.flatMap((run) => paths(run.stdout)))].sort();
|
package/src/app-boundaries.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join as joinPath } from 'node:path';
|
|
15
15
|
// The POSIX variants resolve specifiers against import-graph keys, which are POSIX on every host.
|
|
16
16
|
import { dirname, join, normalize, relative } from 'node:path/posix';
|
|
17
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
17
18
|
import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
|
|
18
19
|
import { checkSurfaceBoundary, importGraph, SURFACES } from '@ultimat3/render';
|
|
19
20
|
import type { Finding } from './output';
|
|
@@ -48,8 +49,6 @@ const CODE_OF: Readonly<Record<BoundaryRule, BoundaryCode>> = {
|
|
|
48
49
|
*/
|
|
49
50
|
export const boundaryCodeOf = (rule: BoundaryRule): BoundaryCode => CODE_OF[rule];
|
|
50
51
|
|
|
51
|
-
const docs = (code: BoundaryCode): string => `https://ultimate.dev/errors/${code}`;
|
|
52
|
-
|
|
53
52
|
const isRoute = (path: string): boolean => /\/(page|layout|route)\.[cm]?tsx?$/.test(path);
|
|
54
53
|
const isService = (path: string): boolean => /\/service\.[cm]?ts$/.test(path);
|
|
55
54
|
const isDbSpecifier = (specifier: string): boolean =>
|
|
@@ -128,7 +127,7 @@ const surfaceFindings = (graph: ImportGraph): readonly Finding[] =>
|
|
|
128
127
|
code,
|
|
129
128
|
cause: violation.cause,
|
|
130
129
|
fix: violation.fix,
|
|
131
|
-
docs:
|
|
130
|
+
docs: ERROR_DOCS_URL,
|
|
132
131
|
at: violation.importer,
|
|
133
132
|
};
|
|
134
133
|
});
|
|
@@ -198,7 +197,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
|
|
|
198
197
|
code: 'X_BOUNDARY_ROUTE_TO_DB',
|
|
199
198
|
cause: `route imports the database ("${specifier}") — routes call actions and queries`,
|
|
200
199
|
fix: generate('query', file.path, 'then call it from'),
|
|
201
|
-
docs:
|
|
200
|
+
docs: ERROR_DOCS_URL,
|
|
202
201
|
at: file.path,
|
|
203
202
|
});
|
|
204
203
|
}
|
|
@@ -207,7 +206,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
|
|
|
207
206
|
code: 'X_BOUNDARY_SERVICE_TO_HTTP',
|
|
208
207
|
cause: `service imports HTTP ("${specifier}") — a service that knows about requests cannot be reused by a job`,
|
|
209
208
|
fix: generate('action', file.path, 'read the request there and pass plain values to'),
|
|
210
|
-
docs:
|
|
209
|
+
docs: ERROR_DOCS_URL,
|
|
211
210
|
at: file.path,
|
|
212
211
|
});
|
|
213
212
|
}
|
package/src/app-env.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { EnvSchema, EnvVarDecl } from '@ultimat3/core';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
checkEnvExample,
|
|
12
|
+
ENV_EXAMPLE_PATH,
|
|
13
|
+
ERROR_DOCS_URL,
|
|
14
|
+
renderEnvExample,
|
|
15
|
+
} from '@ultimat3/core';
|
|
11
16
|
import { APP_CONFIG_FILE } from './app-root';
|
|
12
17
|
import type { Finding } from './output';
|
|
13
18
|
import { findingFrom } from './output';
|
|
@@ -63,7 +68,7 @@ const driftFinding = (cause: string): Finding => ({
|
|
|
63
68
|
// The generator, not the assertion: `assertEnvExample`'s own fix is a `Bun.write(…)` call for
|
|
64
69
|
// an app that has a schema object in scope, and a gate reader has a shell.
|
|
65
70
|
fix: 'x env example',
|
|
66
|
-
docs:
|
|
71
|
+
docs: ERROR_DOCS_URL,
|
|
67
72
|
at: ENV_EXAMPLE_PATH,
|
|
68
73
|
});
|
|
69
74
|
|
package/src/app-load.ts
CHANGED
|
@@ -11,6 +11,13 @@ import { localeConfig } from '@ultimat3/i18n';
|
|
|
11
11
|
import type { ErrorCodeFact } from '@ultimat3/manifest';
|
|
12
12
|
import { registerQueries } from '@ultimat3/query';
|
|
13
13
|
import { isRouteConfig, pageComponentOf, registerRoute } from '@ultimat3/render';
|
|
14
|
+
// For the SIDE EFFECT, and it is this module's to hold: importing `@ultimat3/render/server`
|
|
15
|
+
// installs the `.tsx`/`.scss` Bun plugin, a plugin only transforms modules loaded AFTER it, and
|
|
16
|
+
// every app module below is loaded by the dynamic `import()` in this file. Before the render
|
|
17
|
+
// barrel split it came free with the line above; after it, the only other path to `/server` from
|
|
18
|
+
// here is six hops through `error-contract` → `fix-command` → the command registry, which is an
|
|
19
|
+
// accident one refactor away from compiling every app's `.tsx` to `React.createElement`.
|
|
20
|
+
import '@ultimat3/render/server';
|
|
14
21
|
import { collectDeclaredCodes } from './error-contract';
|
|
15
22
|
import type { Finding } from './output';
|
|
16
23
|
import { findingFrom } from './output';
|
package/src/browser-launcher.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { UltimateError } from '@ultimat3/core';
|
|
8
8
|
import type { CdpLauncherLike, ScrapeDriver } from '@ultimat3/scraping';
|
|
9
9
|
import { localBrowser } from '@ultimat3/scraping';
|
|
10
|
-
import { docsFor } from './error-codes';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* The one library this works against. Playwright is not an alternative and is not a flag:
|
|
@@ -33,7 +32,6 @@ export class ShotBrowserMissingError extends UltimateError {
|
|
|
33
32
|
// one literal, and a fix line the gate cannot read is a fix line nothing holds to the
|
|
34
33
|
// contract. `browser-launcher.test.ts` pins it against the constant instead.
|
|
35
34
|
fix: 'bun add -d puppeteer-core',
|
|
36
|
-
docs: docsFor('X_SHOT_BROWSER_MISSING'),
|
|
37
35
|
meta: { root: input.root, package: BROWSER_PACKAGE },
|
|
38
36
|
});
|
|
39
37
|
}
|
package/src/budgets.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
11
12
|
import type { Manifest, RouteFact } from '@ultimat3/manifest';
|
|
12
13
|
import { formatBytes, parseByteBudget } from '@ultimat3/render';
|
|
13
14
|
import type { Finding } from './output';
|
|
@@ -75,7 +76,7 @@ function unmeasuredFinding(url: string, declared: string, built: boolean): Findi
|
|
|
75
76
|
fix: built
|
|
76
77
|
? `x build --target static --json # its "unmeasured" list says why ${url} could not be weighed`
|
|
77
78
|
: 'x build --target static --json && x verify --json',
|
|
78
|
-
docs:
|
|
79
|
+
docs: ERROR_DOCS_URL,
|
|
79
80
|
at: url,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -106,7 +107,7 @@ export function checkBudgets(
|
|
|
106
107
|
code: 'X_BUDGET_EXCEEDED',
|
|
107
108
|
cause: `${route.url} ships ${formatBytes(measured.jsBytes)} of JS over a ${formatBytes(js)} budget via ${chainOf(measured)}`,
|
|
108
109
|
fix: `x routes --json to see the chain, then move the heavy import behind hydrate: 'interaction'`,
|
|
109
|
-
docs:
|
|
110
|
+
docs: ERROR_DOCS_URL,
|
|
110
111
|
at: route.url,
|
|
111
112
|
});
|
|
112
113
|
}
|
|
@@ -115,7 +116,7 @@ export function checkBudgets(
|
|
|
115
116
|
code: 'X_BUDGET_EXCEEDED',
|
|
116
117
|
cause: `${route.url} LCP ${measured.lcpMs}ms over the ${lcp}ms budget`,
|
|
117
118
|
fix: `raise the budget in defineRoute, or switch render to 'isr' to serve it prebuilt`,
|
|
118
|
-
docs:
|
|
119
|
+
docs: ERROR_DOCS_URL,
|
|
119
120
|
at: route.url,
|
|
120
121
|
});
|
|
121
122
|
}
|
package/src/cmd-build.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, frameworkVersion, VERSION_DEFINE } from '@ultimat3/core';
|
|
7
7
|
import { requireAppRoot } from './app-root';
|
|
8
8
|
import { runVerify } from './cmd-verify';
|
|
9
9
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -143,7 +143,7 @@ export function buildResult(input: {
|
|
|
143
143
|
code: 'X_BUILD_FAILED',
|
|
144
144
|
cause: `${input.command.join(' ')} exited ${result.code}`,
|
|
145
145
|
fix: target === 'docker' ? 'x doctor --json && docker info' : 'x verify --json',
|
|
146
|
-
docs:
|
|
146
|
+
docs: ERROR_DOCS_URL,
|
|
147
147
|
},
|
|
148
148
|
],
|
|
149
149
|
data: {
|
package/src/cmd-db-branch.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `x db branch ls` used to clone a database called `ls`, because the argument was the name.
|
|
4
4
|
// The facts (what a branch is, per mode) are `db-branch.ts`; the client lifetime is here.
|
|
5
5
|
|
|
6
|
-
import { nearestName } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
|
|
7
7
|
import { createPostgresClient, type DbClient } from '@ultimat3/db';
|
|
8
8
|
import type { CommandContext } from './command';
|
|
9
9
|
import type { BranchRow } from './db-branch';
|
|
@@ -215,6 +215,6 @@ function notABranch(services: DevServices, name: string): CommandResult {
|
|
|
215
215
|
code: 'X_DB_BRANCH_FAILED',
|
|
216
216
|
cause: `"${name}" is not a branch of this database, so nothing was dropped (it would be ${target})`,
|
|
217
217
|
fix: LIST_FIX,
|
|
218
|
-
docs:
|
|
218
|
+
docs: ERROR_DOCS_URL,
|
|
219
219
|
});
|
|
220
220
|
}
|
package/src/cmd-deploy.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// anything that runs containers can execute.
|
|
4
4
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
6
7
|
import { requireAppRoot } from './app-root';
|
|
7
8
|
import type { CliCommand, CommandContext } from './command';
|
|
8
9
|
import { BadFlagError, UnknownCommandError } from './errors';
|
|
@@ -31,6 +32,13 @@ import { quoteArg } from './shell-quote';
|
|
|
31
32
|
* honours. Both compose definitions carry it — `docker/docker-compose.prod.yml`'s `backfill` and
|
|
32
33
|
* the one `templates/scaffold-container.ts` scaffolds, which also gates on `migrate` completing.
|
|
33
34
|
* This paragraph said they "still owe" both for as long as they have had them.
|
|
35
|
+
*
|
|
36
|
+
* COMPOSE-ONLY, and `backfill` is why that has to be written down. `docker/helm`'s `roles:` map is
|
|
37
|
+
* `web|sync|worker|scheduler|replicator` plus a `migrate` Job — there is no `backfill` object in
|
|
38
|
+
* the chart at all — and the helm plan is one `helm upgrade --install`, so this list describes
|
|
39
|
+
* neither the objects a chart deploy creates nor the steps it runs. Every reader of it is therefore
|
|
40
|
+
* inside the compose branch; the SUMMARY reads `plan.steps` instead, or `x deploy --method helm`
|
|
41
|
+
* reports a post-deploy sweep to an operator that nothing will ever run.
|
|
34
42
|
*/
|
|
35
43
|
export const DEPLOY_ROLES = ['migrate', 'web', 'sync', 'worker', 'scheduler', 'backfill'] as const;
|
|
36
44
|
|
|
@@ -175,9 +183,12 @@ export const deployCommand: CliCommand = {
|
|
|
175
183
|
// `critical: <bool>`, and no file in `packages/` read that field — so the flag changed
|
|
176
184
|
// nothing about what `x deploy` did, on either method. `flag-reads.ts`'s
|
|
177
185
|
// `X_CLI_FLAG_UNREAD` passed it, because that gate proves a flag is READ and this one was:
|
|
178
|
-
// into a field with no reader.
|
|
179
|
-
// `updateSignal({ reason: 'security' })`,
|
|
180
|
-
//
|
|
186
|
+
// into a field with no reader. It is not coming back: `@ultimat3/pwa`'s
|
|
187
|
+
// `updateSignal({ reason: 'security' })`, the call it was to have triggered, is **deleted**
|
|
188
|
+
// as of 9.0.0 for having had no runtime caller of its own, and nothing in the framework
|
|
189
|
+
// force-navigates a client. A deploy also has no channel to one — the plan is
|
|
190
|
+
// `docker compose up` / `helm upgrade`, and the client's build id is read by `http` (tier 2)
|
|
191
|
+
// and `sync` (tier 3), neither of which may import a tier-4 package to act on it.
|
|
181
192
|
],
|
|
182
193
|
},
|
|
183
194
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
@@ -199,11 +210,14 @@ export const deployCommand: CliCommand = {
|
|
|
199
210
|
env: { ...plan.env },
|
|
200
211
|
steps: plan.steps.map((step) => ({ role: step.role, command: step.command.join(' ') })),
|
|
201
212
|
};
|
|
213
|
+
// The roles THIS plan has, never the compose list: on `--method helm` there is one step,
|
|
214
|
+
// `all`, and naming `backfill` there promises an operator a sweep the chart cannot run.
|
|
215
|
+
const roles = plan.steps.map((step) => step.role).join(',');
|
|
202
216
|
if (flagBool(ctx.args, 'dry-run')) {
|
|
203
217
|
return {
|
|
204
218
|
ok: true,
|
|
205
219
|
command: 'deploy',
|
|
206
|
-
summary: msg('cli.deploy.plan', { images: 1, roles
|
|
220
|
+
summary: msg('cli.deploy.plan', { images: 1, roles }),
|
|
207
221
|
data: planJson,
|
|
208
222
|
lines: plan.steps.map(
|
|
209
223
|
(step) => ` ${step.role.padEnd(10)} ${stepLine(plan.env, step.command)}`,
|
|
@@ -222,7 +236,7 @@ export const deployCommand: CliCommand = {
|
|
|
222
236
|
code: 'X_DEPLOY_FAILED',
|
|
223
237
|
cause: `role "${step.role}" step exited ${result.code}`,
|
|
224
238
|
fix: `${stepLine(plan.env, step.command)} # run it directly to see the full output`,
|
|
225
|
-
docs:
|
|
239
|
+
docs: ERROR_DOCS_URL,
|
|
226
240
|
},
|
|
227
241
|
],
|
|
228
242
|
data: planJson,
|
|
@@ -232,7 +246,7 @@ export const deployCommand: CliCommand = {
|
|
|
232
246
|
return {
|
|
233
247
|
ok: true,
|
|
234
248
|
command: 'deploy',
|
|
235
|
-
summary: msg('cli.deploy.plan', { images: 1, roles
|
|
249
|
+
summary: msg('cli.deploy.plan', { images: 1, roles }),
|
|
236
250
|
data: planJson,
|
|
237
251
|
};
|
|
238
252
|
},
|
package/src/cmd-docs.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// should not have to guess which of 29 packages holds the answer, and it must never be handed a
|
|
5
5
|
// URL: `node_modules` already contains every doc, because the published artifact IS the source.
|
|
6
6
|
|
|
7
|
+
import { ERROR_DOCS_URL } from '@ultimat3/core';
|
|
7
8
|
import type { DocEntry, DocHit } from '@ultimat3/manifest';
|
|
8
9
|
import { nearestTopics, scanInstalledDocs, searchDocs } from '@ultimat3/manifest';
|
|
9
10
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -22,7 +23,7 @@ const unresolvedFinding = (): Finding => ({
|
|
|
22
23
|
code: 'X_CLI_UNEXPECTED',
|
|
23
24
|
cause: '@ultimat3/core does not resolve from the installed CLI, so no docs could be read',
|
|
24
25
|
fix: 'bun install && x doctor --json',
|
|
25
|
-
docs:
|
|
26
|
+
docs: ERROR_DOCS_URL,
|
|
26
27
|
at: import.meta.dir,
|
|
27
28
|
});
|
|
28
29
|
|
package/src/cmd-doctor.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
|
|
7
|
+
import { ERROR_DOCS_URL, tryResolveEnvironment, usesDevCursorSecret } from '@ultimat3/core';
|
|
8
8
|
import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
|
|
9
9
|
import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
|
|
10
10
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -49,12 +49,10 @@ export interface DoctorProbe {
|
|
|
49
49
|
snapshots(): Promise<readonly Finding[]>;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const docs = (code: string): string => `https://ultimate.dev/errors/${code}`;
|
|
53
|
-
|
|
54
52
|
const finding = (code: string, cause: string, fix: string, at?: string): Finding =>
|
|
55
53
|
at === undefined
|
|
56
|
-
? { code, cause, fix, docs:
|
|
57
|
-
: { code, cause, fix, docs:
|
|
54
|
+
? { code, cause, fix, docs: ERROR_DOCS_URL }
|
|
55
|
+
: { code, cause, fix, docs: ERROR_DOCS_URL, at };
|
|
58
56
|
|
|
59
57
|
export const OFFLINE_FALLBACK = 'apps/web/app/offline.tsx';
|
|
60
58
|
|
package/src/cmd-env.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// Bun ships no path-join primitive, and `.env.example` is written app-root-relative.
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { checkEnv, ENV_EXAMPLE_PATH, maskedEnvValues } from '@ultimat3/core';
|
|
7
|
+
import { checkEnv, ENV_EXAMPLE_PATH, ERROR_DOCS_URL, maskedEnvValues } from '@ultimat3/core';
|
|
8
8
|
import { ENV_SCHEMA_EXPORT, envExampleFor, loadEnvSchema } from './app-env';
|
|
9
9
|
import { APP_CONFIG_FILE, requireAppRoot } from './app-root';
|
|
10
10
|
import type { CliCommand, CommandContext } from './command';
|
|
@@ -54,7 +54,7 @@ async function checkProcessEnv(ctx: CommandContext): Promise<CommandResult> {
|
|
|
54
54
|
code: 'X_ENV_MISSING',
|
|
55
55
|
cause: `${issue.key} is ${issue.reason} (expected ${issue.expected})`,
|
|
56
56
|
fix: issue.fix,
|
|
57
|
-
docs:
|
|
57
|
+
docs: ERROR_DOCS_URL,
|
|
58
58
|
at: ENV_EXAMPLE_PATH,
|
|
59
59
|
}));
|
|
60
60
|
return {
|
package/src/cmd-fix.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// (`docs/architecture/02-boundaries.md`) — a caller runs the printed edit, or the generated
|
|
4
4
|
// `git mv`, itself.
|
|
5
5
|
|
|
6
|
-
import { nearestName } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, nearestName } from '@ultimat3/core';
|
|
7
7
|
import { appImportGraph, readAppSources } from './app-boundaries';
|
|
8
8
|
import { requireAppRoot } from './app-root';
|
|
9
9
|
import type { BoundaryCut } from './boundary-cuts';
|
|
@@ -18,8 +18,6 @@ export { planBoundaryCuts };
|
|
|
18
18
|
|
|
19
19
|
export const FIX_SUBCOMMANDS = ['boundary'] as const;
|
|
20
20
|
|
|
21
|
-
const docsUrl = (code: string): string => `https://ultimate.dev/errors/${code}`;
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Accept either an app-root-relative path or a suffix that matches exactly one scanned file —
|
|
25
23
|
* an agent copying the path out of a `fix:` line has the short form
|
|
@@ -81,7 +79,7 @@ const findingForCut = (cut: BoundaryCut): Finding => ({
|
|
|
81
79
|
code: cut.code,
|
|
82
80
|
cause: cut.cause,
|
|
83
81
|
fix: cut.edit,
|
|
84
|
-
docs:
|
|
82
|
+
docs: ERROR_DOCS_URL,
|
|
85
83
|
at: cut.at,
|
|
86
84
|
});
|
|
87
85
|
|
package/src/cmd-new.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { chmod } from 'node:fs/promises';
|
|
7
7
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
8
|
-
import { renderThrowable } from '@ultimat3/core';
|
|
8
|
+
import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
|
|
9
9
|
import { dedupe } from './cmd-generate';
|
|
10
10
|
import type { CliCommand, CommandContext } from './command';
|
|
11
11
|
import { MissingPositionalError } from './errors';
|
|
@@ -195,7 +195,7 @@ export const newCommand: CliCommand = {
|
|
|
195
195
|
code: 'X_GENERATE_CONFLICT',
|
|
196
196
|
cause: `${target} already exists`,
|
|
197
197
|
fix: `x new ${app.kebab} --force, or choose another name`,
|
|
198
|
-
docs:
|
|
198
|
+
docs: ERROR_DOCS_URL,
|
|
199
199
|
at: target,
|
|
200
200
|
},
|
|
201
201
|
],
|
package/src/cmd-shot.ts
CHANGED
|
@@ -164,6 +164,17 @@ const intFlag = (
|
|
|
164
164
|
fallback,
|
|
165
165
|
);
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* How `devServerFor` starts a scratch server. A parameter with a default rather than a direct
|
|
169
|
+
* call, for the reason every `Runner` in this package is one: the failure path below — a boot that
|
|
170
|
+
* throws, and the lock it has to hand back — is otherwise only reachable by breaking a real app.
|
|
171
|
+
*/
|
|
172
|
+
export type BootDevServer = (input: {
|
|
173
|
+
readonly root: string;
|
|
174
|
+
readonly port: number;
|
|
175
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
176
|
+
}) => Promise<{ readonly url: string; stop(): Promise<void> }>;
|
|
177
|
+
|
|
167
178
|
export interface ShotServer {
|
|
168
179
|
readonly url: string;
|
|
169
180
|
/** Which server the picture is of. Reported, because the two have different failure modes. */
|
|
@@ -272,6 +283,7 @@ export async function devServerFor(
|
|
|
272
283
|
root: string,
|
|
273
284
|
env: Readonly<Record<string, string | undefined>>,
|
|
274
285
|
port: number,
|
|
286
|
+
boot: BootDevServer = (input) => startDev(input),
|
|
275
287
|
): Promise<ShotServer> {
|
|
276
288
|
const services = resolveServices(root, env);
|
|
277
289
|
const file = Bun.file(lockPath(services.stateDir));
|
|
@@ -281,13 +293,21 @@ export async function devServerFor(
|
|
|
281
293
|
return { url: lock.url, origin: 'reused', stop: () => Promise.resolve() };
|
|
282
294
|
}
|
|
283
295
|
}
|
|
284
|
-
await preflight({
|
|
296
|
+
const { release } = await preflight({
|
|
285
297
|
stateDir: services.stateDir,
|
|
286
298
|
port,
|
|
287
299
|
hostname: DEV_BINDING.hostname,
|
|
288
300
|
embeddedDb: services.db.mode === 'embedded',
|
|
289
301
|
});
|
|
290
|
-
|
|
302
|
+
// The directory is CLAIMED from here down — `preflight` returns holding it, never having merely
|
|
303
|
+
// looked — so a boot that throws has to give it back. `cmd-dev.ts` states the same rule at the
|
|
304
|
+
// same seam. Without it one failed `x shot` refused every later `x dev` and `x shot` on this
|
|
305
|
+
// checkout, naming a pid that had already exited. The original error is re-thrown untouched: a
|
|
306
|
+
// teardown must never replace the failure it is cleaning up after.
|
|
307
|
+
const dev = await boot({ root, port, env }).catch((error: unknown) => {
|
|
308
|
+
release();
|
|
309
|
+
throw error;
|
|
310
|
+
});
|
|
291
311
|
await writeLock(services.stateDir, {
|
|
292
312
|
pid: process.pid,
|
|
293
313
|
port,
|
package/src/db-finding.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// step that failed. Its own module because `cmd-db.ts` and `cmd-db-branch.ts` both need it and
|
|
4
4
|
// neither may import the other.
|
|
5
5
|
|
|
6
|
-
import { renderThrowable } from '@ultimat3/core';
|
|
6
|
+
import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
|
|
7
7
|
import type { Finding } from './output';
|
|
8
8
|
import { findingFrom, isUltimateErrorShape } from './output';
|
|
9
9
|
|
|
@@ -24,5 +24,5 @@ export const stepFinding = (error: unknown, code: string): Finding =>
|
|
|
24
24
|
// a TypeError raised while reporting it.
|
|
25
25
|
cause: renderThrowable(error),
|
|
26
26
|
fix: 'x doctor --json',
|
|
27
|
-
docs:
|
|
27
|
+
docs: ERROR_DOCS_URL,
|
|
28
28
|
};
|
package/src/db-seed.ts
CHANGED
|
@@ -12,7 +12,6 @@ import type { Environment } from '@ultimat3/core';
|
|
|
12
12
|
import { UltimateError } from '@ultimat3/core';
|
|
13
13
|
import type { Driver, Seed, SeedTier } from '@ultimat3/entity';
|
|
14
14
|
import { isSeed, SEED_TIERS, seedTiersFor } from '@ultimat3/entity';
|
|
15
|
-
import { docsFor } from './error-codes';
|
|
16
15
|
import { BadFlagError } from './errors';
|
|
17
16
|
import type { Finding, JsonValue } from './output';
|
|
18
17
|
import { findingFrom } from './output';
|
|
@@ -49,7 +48,6 @@ export class SeedUnknownError extends UltimateError {
|
|
|
49
48
|
// A dry run, never a bare `x db seed`: the command that answers "which seeds are there" must
|
|
50
49
|
// not be the command that writes them.
|
|
51
50
|
fix: 'x db seed --dry-run --json',
|
|
52
|
-
docs: docsFor('X_DECLARATION_UNKNOWN'),
|
|
53
51
|
});
|
|
54
52
|
}
|
|
55
53
|
}
|
|
@@ -75,7 +73,6 @@ export class SeedEnvironmentError extends UltimateError {
|
|
|
75
73
|
code: 'X_SEED_ENVIRONMENT',
|
|
76
74
|
cause: `seed "${input.seed}" is tier ${input.tier} and ULTIMATE_ENV resolved ${input.environment}, where x db seed runs ${input.tiers.join(', ')} — ULTIMATE_SEED_TIER=${input.tier} says this deploy takes it anyway`,
|
|
77
75
|
fix: `x db seed ${input.seed} --tier ${input.tier} --json`,
|
|
78
|
-
docs: docsFor('X_SEED_ENVIRONMENT'),
|
|
79
76
|
});
|
|
80
77
|
}
|
|
81
78
|
}
|
package/src/dev-assets.ts
CHANGED
|
@@ -15,8 +15,8 @@ import type { IconPlan } from '@ultimat3/pwa';
|
|
|
15
15
|
import { BuiltinImagePipeline, PwaIconMissingError, planIcons } from '@ultimat3/pwa';
|
|
16
16
|
import type { ImageQuery, ImageTransformDriver } from '@ultimat3/seo';
|
|
17
17
|
import { builtinImageDriver, DEFAULT_WIDTHS, parseImageQuery } from '@ultimat3/seo';
|
|
18
|
-
import type {
|
|
19
|
-
import {
|
|
18
|
+
import type { ImageTransform, Storage, VariantFormat } from '@ultimat3/storage';
|
|
19
|
+
import { isTenantScoped, isVariantFormat, variantKey } from '@ultimat3/storage';
|
|
20
20
|
import {
|
|
21
21
|
AUTHORIZED_OBJECT_CACHE,
|
|
22
22
|
assertReadableKey,
|
|
@@ -87,14 +87,11 @@ const imageResponse = (bytes: Uint8Array, contentType: string, cache: CacheHint)
|
|
|
87
87
|
const mediaCache = (key: string): CacheHint =>
|
|
88
88
|
isTenantScoped(key) ? AUTHORIZED_OBJECT_CACHE : IMMUTABLE_IMAGE;
|
|
89
89
|
|
|
90
|
-
const isImageFormat = (value: string): value is ImageFormat =>
|
|
91
|
-
(IMAGE_FORMATS as readonly string[]).includes(value);
|
|
92
|
-
|
|
93
90
|
/**
|
|
94
91
|
* `exactOptionalPropertyTypes` makes an explicit `undefined` a different answer from an absent
|
|
95
92
|
* key, and `variantKey` reads presence — so a spread, never an assignment.
|
|
96
93
|
*/
|
|
97
|
-
function storageTransform(query: ImageQuery, format:
|
|
94
|
+
function storageTransform(query: ImageQuery, format: VariantFormat | undefined): ImageTransform {
|
|
98
95
|
return {
|
|
99
96
|
...(query.width === undefined ? {} : { width: query.width }),
|
|
100
97
|
...(format === undefined ? {} : { format }),
|
|
@@ -117,7 +114,7 @@ async function transformedVariant(
|
|
|
117
114
|
// A format storage cannot name has no variant key, so it cannot be cached. The driver refuses
|
|
118
115
|
// it with core's `X_IMAGE_UNSUPPORTED`; refusing it here too would give one bad URL two codes.
|
|
119
116
|
const format =
|
|
120
|
-
query.format !== undefined &&
|
|
117
|
+
query.format !== undefined && isVariantFormat(query.format) ? query.format : undefined;
|
|
121
118
|
const cacheable = query.format === undefined || format !== undefined;
|
|
122
119
|
const cached = cacheable ? variantKey(key, storageTransform(query, format)) : undefined;
|
|
123
120
|
// The SOURCE key decides the posture, not the variant's: `variantKey` keeps the source's prefix,
|