@ultimat3/cli 22.15.0 → 24.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 +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
package/src/cmd-db.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// directory tree. `node:path` for `join` — Bun exposes no path joiner.
|
|
12
12
|
import { rm } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import { resolveEnvironment } from '@ultimat3/core';
|
|
14
|
+
import { NotImplementedError, resolveEnvironment } from '@ultimat3/core';
|
|
15
15
|
import { type DriftReport, driftError, withTransaction } from '@ultimat3/db';
|
|
16
16
|
import { postgresDriver } from '@ultimat3/entity';
|
|
17
17
|
import { requireAppRoot } from './app-root';
|
|
@@ -22,6 +22,13 @@ import { plannedSubcommand } from './cmd-planned';
|
|
|
22
22
|
import type { CliCommand, CommandContext } from './command';
|
|
23
23
|
import { stepFinding } from './db-finding';
|
|
24
24
|
import { generateAppMigration, unrenderedJson, unrenderedLines } from './db-generate';
|
|
25
|
+
import { liveObjectDrift } from './db-object-drift';
|
|
26
|
+
import {
|
|
27
|
+
afterWrittenMigration,
|
|
28
|
+
refreshSchemaDump,
|
|
29
|
+
type SchemaDumpRefresh,
|
|
30
|
+
schemaDumpJson,
|
|
31
|
+
} from './db-schema-refresh';
|
|
25
32
|
import type { SeedPassRow } from './db-seed';
|
|
26
33
|
import {
|
|
27
34
|
discoverSeeds,
|
|
@@ -33,7 +40,7 @@ import {
|
|
|
33
40
|
selectSeeds,
|
|
34
41
|
} from './db-seed';
|
|
35
42
|
import { DevAlreadyRunningError, liveDevLock } from './dev-lock';
|
|
36
|
-
import {
|
|
43
|
+
import { MissingSubcommandError, UnknownCommandError } from './errors';
|
|
37
44
|
import { withJobDriver } from './jobs-driver';
|
|
38
45
|
import { msg } from './messages';
|
|
39
46
|
import type { CommandResult, Finding } from './output';
|
|
@@ -109,34 +116,52 @@ async function runGen(ctx: CommandContext, root: string, name: string): Promise<
|
|
|
109
116
|
// `x i18n add fr` failure, repeated. The count is the verdict; the gate's own `drift` step is
|
|
110
117
|
// where a red belongs, and it reads the same list to decide that `x db gen` is not the fix.
|
|
111
118
|
const lost = unrenderedLines(generated.unrendered);
|
|
119
|
+
// After the migration, from the migrations alone: the dump is what replaying them produces, so
|
|
120
|
+
// it is rewritten on all three outcomes — `unchanged` included, which is what makes this
|
|
121
|
+
// command the `fix:` on `X_SCHEMA_DUMP_DRIFT` when a framework table or a hand edit moved it.
|
|
122
|
+
const dump = await refreshSchemaDump(root, ctx.env);
|
|
123
|
+
const dumpFindings = dump.finding === undefined ? [] : [dump.finding];
|
|
112
124
|
const migration = generated.migration;
|
|
113
125
|
if (migration === undefined) {
|
|
114
126
|
return {
|
|
115
|
-
ok: generated.findings.length === 0,
|
|
127
|
+
ok: generated.findings.length === 0 && dumpFindings.length === 0,
|
|
116
128
|
command: 'db',
|
|
117
129
|
// The sidecar path, never a bare id: `hash-recorded` writes exactly one, and it is the file
|
|
118
130
|
// the `drift` step reads back.
|
|
131
|
+
// `blocked` FIRST: the app did not load, so nothing was diffed, and every sentence below
|
|
132
|
+
// it is a claim about a comparison that never ran. It read "entities and migrations agree"
|
|
133
|
+
// over an exit 1.
|
|
119
134
|
summary:
|
|
120
|
-
generated.outcome === '
|
|
121
|
-
? msg('cli.db.gen.
|
|
122
|
-
:
|
|
123
|
-
|
|
135
|
+
generated.outcome === 'blocked'
|
|
136
|
+
? msg('cli.db.gen.blocked', { count: generated.findings.length })
|
|
137
|
+
: generated.outcome === 'hash-recorded'
|
|
138
|
+
? msg('cli.db.gen.recorded', { file: generated.files[0] ?? '' })
|
|
139
|
+
: dump.status === 'written'
|
|
140
|
+
? msg('cli.db.gen.dumped', { count: dump.written.length + dump.removed.length })
|
|
141
|
+
: msg('cli.db.gen.unchanged'),
|
|
142
|
+
findings: [...generated.findings, ...dumpFindings],
|
|
124
143
|
// `files` is what this command WROTE, so the empty array here was a false claim.
|
|
125
|
-
lines: [...generated.files.map((file) => ` ${file}`), ...lost],
|
|
144
|
+
lines: [...generated.files.map((file) => ` ${file}`), ...dumpLines(dump), ...lost],
|
|
126
145
|
data: {
|
|
127
146
|
outcome: generated.outcome,
|
|
128
147
|
migration: null,
|
|
129
148
|
files: [...generated.files],
|
|
130
149
|
schemaHash: generated.schemaHash ?? null,
|
|
131
150
|
unrendered: unrenderedJson(generated.unrendered),
|
|
151
|
+
schemaDump: schemaDumpJson(dump),
|
|
132
152
|
},
|
|
133
153
|
};
|
|
134
154
|
}
|
|
135
155
|
return {
|
|
136
|
-
ok:
|
|
156
|
+
ok: dumpFindings.length === 0,
|
|
137
157
|
command: 'db',
|
|
138
|
-
summary:
|
|
139
|
-
|
|
158
|
+
summary:
|
|
159
|
+
dumpFindings.length === 0
|
|
160
|
+
? msg('cli.db.gen.written', { id: migration.id })
|
|
161
|
+
: msg('cli.db.gen.writtenDumpFailed', { id: migration.id }),
|
|
162
|
+
// Exit 1 with a migration on disk: the finding has to say so, or the next move is a duplicate.
|
|
163
|
+
findings: dumpFindings.map((finding) => afterWrittenMigration(finding, migration.id)),
|
|
164
|
+
lines: [...generated.files.map((file) => ` ${file}`), ...dumpLines(dump), ...lost],
|
|
140
165
|
data: {
|
|
141
166
|
outcome: generated.outcome,
|
|
142
167
|
migration: migration.id,
|
|
@@ -144,10 +169,20 @@ async function runGen(ctx: CommandContext, root: string, name: string): Promise<
|
|
|
144
169
|
files: [...generated.files],
|
|
145
170
|
schemaHash: generated.schemaHash ?? null,
|
|
146
171
|
unrendered: unrenderedJson(generated.unrendered),
|
|
172
|
+
schemaDump: schemaDumpJson(dump),
|
|
147
173
|
},
|
|
148
174
|
};
|
|
149
175
|
}
|
|
150
176
|
|
|
177
|
+
/**
|
|
178
|
+
* What the dump refresh did, one path per line — the same list `data.schemaDump` carries, so the
|
|
179
|
+
* human render and `--json` report one fact. A refresh that changed nothing prints nothing.
|
|
180
|
+
*/
|
|
181
|
+
const dumpLines = (dump: SchemaDumpRefresh): readonly string[] => [
|
|
182
|
+
...dump.written.map((file) => ` ${file}`),
|
|
183
|
+
...dump.removed.map((file) => ` - ${file}`),
|
|
184
|
+
];
|
|
185
|
+
|
|
151
186
|
/**
|
|
152
187
|
* The post-migrate report, rendered. Through `driftError` rather than a second literal: the
|
|
153
188
|
* three-line `X_DB_DRIFT` output is pinned by the framework contract, and this command must not be
|
|
@@ -175,16 +210,30 @@ async function runMigrate(
|
|
|
175
210
|
try {
|
|
176
211
|
const migrated = await runMigrations({ root, env: ctx.env });
|
|
177
212
|
const report = migrated.report;
|
|
213
|
+
// A hand-written or pulled migration reaches the dump here, with no `x db gen` in between.
|
|
214
|
+
const dump = await refreshSchemaDump(root, ctx.env);
|
|
215
|
+
// The half a snapshot cannot hold: a trigger, function, view, type or sequence this database
|
|
216
|
+
// carries and no migration creates. Asked only when the replay produced a catalog to ask
|
|
217
|
+
// against — without one there is nothing that says what the migrations create.
|
|
218
|
+
const objects =
|
|
219
|
+
dump.catalog === undefined ? [] : await liveObjectDrift(root, ctx.env, dump.catalog);
|
|
220
|
+
const findings = [
|
|
221
|
+
...driftFindings(migrated.drift),
|
|
222
|
+
...objects.map((difference) => findingFrom(driftError(difference))),
|
|
223
|
+
...(dump.finding === undefined ? [] : [dump.finding]),
|
|
224
|
+
];
|
|
178
225
|
return {
|
|
179
|
-
ok:
|
|
226
|
+
ok: findings.length === 0,
|
|
180
227
|
command: 'db',
|
|
181
228
|
summary,
|
|
182
|
-
findings
|
|
229
|
+
findings,
|
|
230
|
+
lines: dumpLines(dump),
|
|
183
231
|
data: {
|
|
184
232
|
applied: report.applied.map((entry) => entry.id),
|
|
185
233
|
skipped: report.skipped.length,
|
|
186
234
|
appVersion: report.appVersion,
|
|
187
235
|
durationMs: report.durationMs,
|
|
236
|
+
schemaDump: schemaDumpJson(dump),
|
|
188
237
|
},
|
|
189
238
|
};
|
|
190
239
|
} catch (error) {
|
|
@@ -205,8 +254,8 @@ async function runMigrate(
|
|
|
205
254
|
async function runReset(ctx: CommandContext, root: string): Promise<CommandResult> {
|
|
206
255
|
const services = resolveServices(root, ctx.env);
|
|
207
256
|
if (services.db.mode === 'external') {
|
|
208
|
-
throw new
|
|
209
|
-
|
|
257
|
+
throw new NotImplementedError({
|
|
258
|
+
cause: 'x db reset against an external Postgres is not implemented in this build',
|
|
210
259
|
fix: 'drop and recreate the database yourself, then run: x db migrate',
|
|
211
260
|
});
|
|
212
261
|
}
|
|
@@ -252,7 +301,8 @@ async function runSeed(ctx: CommandContext, root: string): Promise<CommandResult
|
|
|
252
301
|
});
|
|
253
302
|
if (chosen.length === 0) {
|
|
254
303
|
return {
|
|
255
|
-
|
|
304
|
+
// A seed module that would not import may hold the seed that was asked for.
|
|
305
|
+
ok: discovery.findings.length === 0,
|
|
256
306
|
command: 'db',
|
|
257
307
|
summary: msg('cli.db.seed.none'),
|
|
258
308
|
findings: discovery.findings,
|
package/src/cmd-deploy-helm.ts
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// why: Bun exposes no path-join primitive; the chart path is handed to helm as one joined string.
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { UltimateError } from '@ultimat3/core';
|
|
11
|
-
import {
|
|
12
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
11
|
+
import { loadAppConfig } from './app-config-load';
|
|
13
12
|
import { BadFlagError } from './errors';
|
|
14
13
|
|
|
15
14
|
/** Long enough for a migration a pre-upgrade hook must finish; helm's own 5m default is not. */
|
|
@@ -67,16 +66,18 @@ export function readLabel(flag: 'namespace' | 'release', raw: string): string {
|
|
|
67
66
|
*/
|
|
68
67
|
export async function readReleaseName(root: string, flag: string | undefined): Promise<string> {
|
|
69
68
|
if (flag !== undefined) return readLabel('release', flag);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const name =
|
|
69
|
+
const config = await loadAppConfig(root);
|
|
70
|
+
// `unknown`, not the declared `string`: core reads an absent name as `"undefined"` and lets it by.
|
|
71
|
+
const name: unknown = config?.name;
|
|
73
72
|
if (typeof name === 'string' && name.length <= RELEASE_MAX && LABEL.test(name)) return name;
|
|
74
73
|
throw new UltimateError({
|
|
75
74
|
code: 'X_CONFIG_INVALID',
|
|
76
75
|
cause:
|
|
77
76
|
typeof name === 'string'
|
|
78
77
|
? `app.config.ts names the app "${name.slice(0, 80)}", which is not a helm release name (a DNS-1123 label of at most ${RELEASE_MAX} characters)`
|
|
79
|
-
:
|
|
78
|
+
: config === undefined
|
|
79
|
+
? `${root} has no app.config.ts, and x deploy --method helm names the release after its config.name`
|
|
80
|
+
: 'app.config.ts exports no config.name, and x deploy --method helm names the release after it',
|
|
80
81
|
fix: 'name the release explicitly: x deploy --method helm --release my-app --json',
|
|
81
82
|
});
|
|
82
83
|
}
|
package/src/cmd-deploy.ts
CHANGED
|
@@ -176,10 +176,10 @@ export function planDeploy(
|
|
|
176
176
|
command: [
|
|
177
177
|
'docker',
|
|
178
178
|
'compose',
|
|
179
|
-
// Compose interpolates `${SYNC_URL:?…}` and `${POSTGRES_PASSWORD:?…}` from
|
|
180
|
-
// `--env-file` only — never from a service's `env_file:`. Without this an
|
|
181
|
-
// them in `.env.production`, the one file the compose file tells them to
|
|
182
|
-
// step die on a parse error. Global flag, so it precedes `-f`; the shell still wins over it.
|
|
179
|
+
// Compose interpolates `${SYNC_URL:?…}`, `${APP_URL:?…}` and `${POSTGRES_PASSWORD:?…}` from
|
|
180
|
+
// the shell and `--env-file` only — never from a service's `env_file:`. Without this an
|
|
181
|
+
// operator who put them in `.env.production`, the one file the compose file tells them to
|
|
182
|
+
// fill, had every step die on a parse error. Global flag, so it precedes `-f`; the shell still wins over it.
|
|
183
183
|
'--env-file',
|
|
184
184
|
join(root, PROD_ENV_FILE),
|
|
185
185
|
'-f',
|
package/src/cmd-dev.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type { OverlayNotice, RequestContext } from '@ultimat3/http';
|
|
|
13
13
|
import { asCtx } from '@ultimat3/http';
|
|
14
14
|
import type { Manifest } from '@ultimat3/manifest';
|
|
15
15
|
import { MANIFEST_FILENAME } from '@ultimat3/manifest';
|
|
16
|
-
import { createIsrController } from '@ultimat3/render/server';
|
|
17
16
|
import { loadSignInPath } from './app-auth';
|
|
18
17
|
import { appManifest } from './app-manifest';
|
|
19
18
|
import type { StalePin } from './app-reload-graph';
|
|
@@ -50,6 +49,7 @@ import {
|
|
|
50
49
|
resolveServices,
|
|
51
50
|
withRealtimeEvents,
|
|
52
51
|
} from './runtime-bindings';
|
|
52
|
+
import { attachedIsr } from './runtime-isr';
|
|
53
53
|
import { liveFeedLabel } from './runtime-live-feed';
|
|
54
54
|
import type { RuntimeOverrides } from './runtime-overrides';
|
|
55
55
|
import { replicaOverrides } from './runtime-replica';
|
|
@@ -239,9 +239,10 @@ async function bootDev(
|
|
|
239
239
|
};
|
|
240
240
|
const panels = devPanels(dashboard).map((panel) => panel.key);
|
|
241
241
|
|
|
242
|
-
// `x dev`'s own, so a reload can empty it
|
|
243
|
-
//
|
|
244
|
-
const isr =
|
|
242
|
+
// `x dev`'s own, so a reload can empty it (a save otherwise kept a page's first render for its
|
|
243
|
+
// ttl); attached, so a tag bust reaches it too. Released with the roles.
|
|
244
|
+
const { isr, release: releaseIsr } = attachedIsr({ buildId });
|
|
245
|
+
acquired.push(releaseIsr);
|
|
245
246
|
const { routes, theme, speculation, errorStyles, mcpPath, mcpPaths } = await devRouteTable({
|
|
246
247
|
isr,
|
|
247
248
|
root: options.root,
|
|
@@ -350,16 +351,16 @@ async function bootDev(
|
|
|
350
351
|
async stop() {
|
|
351
352
|
watcher.close();
|
|
352
353
|
await running.stop();
|
|
354
|
+
releaseIsr();
|
|
353
355
|
await runtime.stop();
|
|
354
356
|
// Released after the roles: a span opened by an in-flight request still has an exporter to
|
|
355
357
|
// end into. `configureTelemetry` merges, so handing back the noop is how it is uninstalled —
|
|
356
358
|
// leaving it in place would keep every span of the next `startDev` in this process's buffer.
|
|
357
359
|
configureTelemetry({ exporter: noopExporter });
|
|
358
360
|
traces.reset();
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
// and, worse, keep instrumentation on in a process that is no longer a dev server.
|
|
361
|
+
// With the exporter, for the same reason: an in-flight statement is observed by the ledger
|
|
362
|
+
// that counted its request. Left installed, the next `startDev`'s statements land in these
|
|
363
|
+
// counts — and instrumentation stays on in a process that is no longer a dev server.
|
|
363
364
|
setStatementObserver(undefined);
|
|
364
365
|
statements.reset();
|
|
365
366
|
},
|
package/src/cmd-doctor-spec.ts
CHANGED
|
@@ -3,19 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CommandSpec } from './parse';
|
|
5
5
|
|
|
6
|
-
/** The port `x dev` binds by default, so the probe answers about the port the developer will use. */
|
|
7
|
-
export const DEFAULT_DOCTOR_PORT = 3000;
|
|
8
|
-
|
|
9
6
|
export const doctorSpec: CommandSpec = {
|
|
10
7
|
name: 'doctor',
|
|
11
8
|
summary: 'environment, versions, drift, ports, PWA prerequisites — each with a fix command',
|
|
12
9
|
usage: 'x doctor [--port 3000] [--json]',
|
|
13
10
|
flags: [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type: 'string',
|
|
17
|
-
summary: 'port to test',
|
|
18
|
-
default: String(DEFAULT_DOCTOR_PORT),
|
|
19
|
-
},
|
|
11
|
+
// No `default`: it would be indistinguishable from a typed value, and `PORT` could never win.
|
|
12
|
+
{ name: 'port', type: 'string', summary: 'port to test (default: PORT, then 3000)' },
|
|
20
13
|
],
|
|
21
14
|
};
|
package/src/cmd-doctor.ts
CHANGED
|
@@ -19,12 +19,18 @@ import {
|
|
|
19
19
|
} from '@ultimat3/db';
|
|
20
20
|
import { STORAGE_SIGNING_SECRET_KEY, usesDevStorageSecret } from '@ultimat3/storage';
|
|
21
21
|
import { findAppRoot, REQUIRED_BUN, versionAtLeast } from './app-root';
|
|
22
|
-
import {
|
|
22
|
+
import { doctorSpec } from './cmd-doctor-spec';
|
|
23
23
|
import type { CliCommand, CommandContext } from './command';
|
|
24
24
|
import { checkMigrationSnapshots } from './db-snapshot';
|
|
25
|
+
import { appUrlFindings } from './doctor-app-url';
|
|
26
|
+
import { type AuthStorageFact, authStorageFindings, authStorageProbe } from './doctor-auth';
|
|
27
|
+
import { coverageExcludesProbe, withCoverageExcludes } from './doctor-coverage';
|
|
28
|
+
import { shippedGuardsProbe, withGuardListing } from './doctor-guards';
|
|
25
29
|
import type { OfflineFallbackFact } from './doctor-offline';
|
|
26
30
|
import { offlineFallbackFinding, offlineFallbackProbe } from './doctor-offline';
|
|
27
|
-
import {
|
|
31
|
+
import type { SealedKeysFact } from './doctor-sealed';
|
|
32
|
+
import { sealedKeyFindings, sealedKeysProbe } from './doctor-sealed';
|
|
33
|
+
import { PORT_RANGE, portPairAfter, readIntFlag } from './flag-number';
|
|
28
34
|
import { ICON_SOURCE } from './icon-assets';
|
|
29
35
|
import { msg } from './messages';
|
|
30
36
|
import type { CommandResult, Finding } from './output';
|
|
@@ -33,6 +39,7 @@ import type { ParsedArgs } from './parse';
|
|
|
33
39
|
import { portFree } from './port-probe';
|
|
34
40
|
import { syncPortFor } from './role-sync';
|
|
35
41
|
import { checkMigrationDrift } from './schema-drift';
|
|
42
|
+
import { portFromEnv } from './serve-env';
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
45
|
* The injection seam `runDoctor` reads instead of the environment. Not a semver surface —
|
|
@@ -45,6 +52,8 @@ export interface DoctorProbe {
|
|
|
45
52
|
/** App root, or undefined when the command runs outside an app. */
|
|
46
53
|
readonly root: string | undefined;
|
|
47
54
|
readonly port: number;
|
|
55
|
+
/** `APP_URL` as this environment declares it; `undefined` when unset. */
|
|
56
|
+
readonly appUrl: string | undefined;
|
|
48
57
|
/** True while cursors are signed with the key shipped in the published package. */
|
|
49
58
|
readonly devCursorSecret: boolean;
|
|
50
59
|
/**
|
|
@@ -91,6 +100,17 @@ export interface DoctorProbe {
|
|
|
91
100
|
* reaching the disk — the same split `drift()` makes one question over.
|
|
92
101
|
*/
|
|
93
102
|
offlineFallback(): Promise<OfflineFallbackFact>;
|
|
103
|
+
/**
|
|
104
|
+
* The app's `.sealed()` columns and the master keys this environment declares. A FACT, for the
|
|
105
|
+
* reason `offlineFallback` is one: loading the app and opening the secrets file is IO, and what
|
|
106
|
+
* a retired key beside a sealed column means is `sealedKeyFindings`' pure rule.
|
|
107
|
+
*/
|
|
108
|
+
sealedKeys(): Promise<SealedKeysFact>;
|
|
109
|
+
/**
|
|
110
|
+
* What an upgrade left in the auth tables for an operator to finish: unsealed second-factor
|
|
111
|
+
* secrets, and tables no release reads. Asked of an external database only.
|
|
112
|
+
*/
|
|
113
|
+
authStorage(): Promise<AuthStorageFact>;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
/** What `x doctor` reads about the embedded database, without opening it. */
|
|
@@ -247,6 +267,7 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
|
|
|
247
267
|
);
|
|
248
268
|
}
|
|
249
269
|
findings.push(...(await portFindings(probe)));
|
|
270
|
+
findings.push(...appUrlFindings(probe.appUrl, probe.port));
|
|
250
271
|
// `@ultimat3/pwa`'s own codes, not CLI twins of them. `X_PWA_NO_ICON_SOURCE` and
|
|
251
272
|
// `X_PWA_NO_FALLBACK` used to be declared here for the same two conditions the package already
|
|
252
273
|
// names — two codes for one condition, one of them registered by nobody, so `x errors explain`
|
|
@@ -281,20 +302,30 @@ export async function runDoctor(probe: DoctorProbe): Promise<readonly Finding[]>
|
|
|
281
302
|
// author reaches it from: `x db gen` throwing `X_MIGRATION_SNAPSHOT_MISSING` used to be a
|
|
282
303
|
// condition this diagnostic could not see at all, so the fix line ran clean over a broken app.
|
|
283
304
|
findings.push(...(await probe.snapshots()));
|
|
305
|
+
// Sealed columns against the key ring: no key at all, or a rotation whose re-seal is pending.
|
|
306
|
+
findings.push(...sealedKeyFindings(await probe.sealedKeys()));
|
|
307
|
+
findings.push(...authStorageFindings(await probe.authStorage()));
|
|
284
308
|
return findings;
|
|
285
309
|
}
|
|
286
310
|
|
|
287
311
|
/**
|
|
288
|
-
* The port to TEST, and the one place `x doctor` reads it
|
|
289
|
-
*
|
|
290
|
-
*
|
|
312
|
+
* The port to TEST, and the one place `x doctor` reads it: `devPortFor`'s order — `--port`, then
|
|
313
|
+
* `PORT` (`portFromEnv`, the one reader of it), then 3000 — so the probe answers about the port
|
|
314
|
+
* `x dev` will bind. It probed 3000 under `PORT=4000` while dev bound 4000 and 4001. `PORT_RANGE.min`
|
|
315
|
+
* is 0 because `x dev --port 0` means "let the kernel pick"; here 0 means nothing, and
|
|
316
|
+
* `Bun.serve({ port: 0 })` always succeeds — so the port check could not fail.
|
|
291
317
|
*/
|
|
292
|
-
export const doctorPort = (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
318
|
+
export const doctorPort = (
|
|
319
|
+
args: ParsedArgs,
|
|
320
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
321
|
+
): number =>
|
|
322
|
+
readIntFlag(args, {
|
|
323
|
+
name: 'port',
|
|
324
|
+
command: 'doctor',
|
|
325
|
+
...PORT_RANGE,
|
|
326
|
+
min: 1,
|
|
327
|
+
example: 'x doctor --port 3000',
|
|
328
|
+
}) ?? portFromEnv(env);
|
|
298
329
|
|
|
299
330
|
/**
|
|
300
331
|
* `DATABASE_URL` as BOTH halves of the database question read it: unset and blank are one case.
|
|
@@ -363,12 +394,19 @@ async function probeDatabase(url: string | undefined): Promise<Finding | null> {
|
|
|
363
394
|
}
|
|
364
395
|
}
|
|
365
396
|
|
|
366
|
-
export function probeFor(
|
|
397
|
+
export function probeFor(
|
|
398
|
+
cwd: string,
|
|
399
|
+
bunVersion: string,
|
|
400
|
+
port: number,
|
|
401
|
+
// The environment `doctorPort` read `PORT` from, so `APP_URL` is judged against the same one.
|
|
402
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
403
|
+
): DoctorProbe {
|
|
367
404
|
const root = findAppRoot(cwd)?.dir;
|
|
368
405
|
return {
|
|
369
406
|
bunVersion,
|
|
370
407
|
root,
|
|
371
408
|
port,
|
|
409
|
+
appUrl: env['APP_URL'],
|
|
372
410
|
devCursorSecret: usesDevCursorSecret(),
|
|
373
411
|
devStorageSecret: usesDevStorageSecret(),
|
|
374
412
|
// `ULTIMATE_ENV`, through core — the one key that says which deploy this is, with `NODE_ENV`
|
|
@@ -394,23 +432,36 @@ export function probeFor(cwd: string, bunVersion: string, port: number): DoctorP
|
|
|
394
432
|
// `runDoctor` returns on `X_NOT_IN_APP` before this can be asked.
|
|
395
433
|
offlineFallback: async () =>
|
|
396
434
|
root === undefined ? { fallback: null, routes: undefined } : offlineFallbackProbe(root),
|
|
435
|
+
sealedKeys: async () =>
|
|
436
|
+
root === undefined ? { columns: undefined, keys: undefined } : sealedKeysProbe(root),
|
|
437
|
+
authStorage: () => authStorageProbe(process.env['DATABASE_URL']),
|
|
397
438
|
};
|
|
398
439
|
}
|
|
399
440
|
|
|
400
441
|
export const doctorCommand: CliCommand = {
|
|
401
442
|
spec: doctorSpec,
|
|
402
443
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
403
|
-
const port = doctorPort(ctx.args);
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
444
|
+
const port = doctorPort(ctx.args, ctx.env);
|
|
445
|
+
const probe = probeFor(ctx.cwd, ctx.bunVersion, port, ctx.env);
|
|
446
|
+
const findings = await runDoctor(probe);
|
|
447
|
+
const listed = withGuardListing(
|
|
448
|
+
{
|
|
449
|
+
ok: findings.length === 0,
|
|
450
|
+
command: 'doctor',
|
|
451
|
+
summary:
|
|
452
|
+
findings.length === 0
|
|
453
|
+
? msg('cli.doctor.clean')
|
|
454
|
+
: msg('cli.doctor.findings', { count: findings.length }),
|
|
455
|
+
findings,
|
|
456
|
+
data: { count: findings.length, codes: findings.map((entry) => entry.code) },
|
|
457
|
+
},
|
|
458
|
+
// A listing beside the verdict, never part of it (`doctor-guards.ts` says why).
|
|
459
|
+
probe.root === undefined ? [] : await shippedGuardsProbe(probe.root),
|
|
460
|
+
);
|
|
461
|
+
// The second listing: what the coverage floor does not see (`doctor-coverage.ts`).
|
|
462
|
+
return withCoverageExcludes(
|
|
463
|
+
listed,
|
|
464
|
+
probe.root === undefined ? [] : await coverageExcludesProbe(probe.root),
|
|
465
|
+
);
|
|
415
466
|
},
|
|
416
467
|
};
|
package/src/cmd-env.ts
CHANGED
|
@@ -4,15 +4,39 @@
|
|
|
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 {
|
|
7
|
+
import {
|
|
8
|
+
checkEnv,
|
|
9
|
+
ENV_EXAMPLE_PATH,
|
|
10
|
+
ERROR_DOCS_URL,
|
|
11
|
+
maskedEnvValues,
|
|
12
|
+
UltimateError,
|
|
13
|
+
} from '@ultimat3/core';
|
|
8
14
|
import { envExampleFor, loadEnvSchema } from './app-env';
|
|
9
15
|
import { requireAppRoot } from './app-root';
|
|
10
16
|
import { envSpec } from './cmd-env-spec';
|
|
11
17
|
import type { CliCommand, CommandContext } from './command';
|
|
12
|
-
import { EnvSchemaMissingError } from './errors';
|
|
13
18
|
import { msg } from './messages';
|
|
14
19
|
import type { CommandResult, Finding, JsonValue } from './output';
|
|
15
20
|
|
|
21
|
+
/**
|
|
22
|
+
* `x env` was run in an app whose `app.config.ts` exports no `envSchema`. Not a silent success:
|
|
23
|
+
* writing a `.env.example` with no variables in it, or reporting "0 declared variables, all
|
|
24
|
+
* present", both read as a working environment declaration to whoever runs the command next.
|
|
25
|
+
*
|
|
26
|
+
* `X_CONFIG_INVALID` is core's code for "a configuration this process cannot boot on — env or
|
|
27
|
+
* `app.config.ts`", which is exactly this; the CLI names it in `CLI_BORROWED_ERROR_CODES` rather
|
|
28
|
+
* than minting a synonym.
|
|
29
|
+
*/
|
|
30
|
+
export class EnvSchemaMissingError extends UltimateError {
|
|
31
|
+
constructor(input: { subcommand: string }) {
|
|
32
|
+
super({
|
|
33
|
+
code: 'X_CONFIG_INVALID',
|
|
34
|
+
cause: `x env ${input.subcommand} needs the env declaration, and app.config.ts exports no "envSchema"`,
|
|
35
|
+
fix: "add to app.config.ts: export const envSchema = { DATABASE_URL: { type: 'url', description: 'Postgres connection URL' } } satisfies EnvSchema; export const env = defineEnv(envSchema);",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
/**
|
|
17
41
|
* Every subcommand needs the declaration, and an app without one is a usage error rather than an
|
|
18
42
|
* empty success: `x env example` writing a two-comment file would look like it worked.
|