@volter/world-runtime 3.0.143 → 3.0.145
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/dist/src/cli-commands.js +13 -2
- package/dist/src/covers.d.ts +21 -1
- package/dist/src/covers.js +46 -4
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/infra.js +42 -8
- package/dist/src/init.d.ts +4 -0
- package/dist/src/init.js +20 -2
- package/dist/src/native-postgres-backing.d.ts +15 -1
- package/dist/src/native-postgres-backing.js +178 -7
- package/dist/src/package-manifest.d.ts +7 -0
- package/dist/src/package-manifest.js +29 -0
- package/dist/src/pglite-backing.d.ts +85 -0
- package/dist/src/pglite-backing.js +249 -33
- package/dist/src/process-groups.mjs +25 -4
- package/dist/src/process-signal.mjs +63 -14
- package/dist/src/project-inspect.d.ts +9 -0
- package/dist/src/project-inspect.js +31 -8
- package/dist/src/runtime.js +23 -2
- package/dist/src/schema.d.ts +68 -0
- package/dist/src/schema.js +70 -4
- package/dist/src/up-cli.js +21 -1
- package/package.json +4 -4
- package/src/cli-commands.ts +10 -2
- package/src/covers.ts +53 -5
- package/src/index.ts +4 -0
- package/src/infra.ts +40 -8
- package/src/init.ts +22 -1
- package/src/native-postgres-backing.ts +175 -8
- package/src/package-manifest.ts +31 -0
- package/src/pglite-backing.ts +227 -22
- package/src/process-groups.mjs +25 -4
- package/src/process-signal.mjs +63 -14
- package/src/project-inspect.ts +38 -1
- package/src/runtime.ts +15 -1
- package/src/schema.ts +99 -5
- package/src/up-cli.ts +16 -1
package/dist/src/cli-commands.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
|
-
import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, writeWorldClock, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
|
|
10
|
+
import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, writeWorldClock, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, recordCoverage, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
|
|
11
11
|
import { formatApplication, formatChangeset, formatRebaseReport, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, parseVerifierExpression } from '@volter/world-core/runtime';
|
|
12
12
|
import { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer } from "./serve.js";
|
|
13
13
|
import { clockFile, instanceDir, pruneWorlds, withoutVaultSignIn } from "./runtime.js";
|
|
@@ -57,7 +57,7 @@ function printHelp() {
|
|
|
57
57
|
# emission. Exit 0 = ready to \`up\`; exit 1 = the honest worklist.
|
|
58
58
|
# Deterministic: same repo + same pack catalog => identical bytes.
|
|
59
59
|
# init DIAGNOSES; it never boots — it prints the exact next commands
|
|
60
|
-
volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--app-url <url>] [--json] [--root <repo>]
|
|
60
|
+
volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--app-url <url>] [--in-process] [--json] [--root <repo>]
|
|
61
61
|
volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
|
|
62
62
|
volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
|
|
63
63
|
volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
|
|
@@ -516,6 +516,17 @@ export async function main() {
|
|
|
516
516
|
throw new Error('volter-world covers: --repo <path> is required (the application repo to prove coverage for)');
|
|
517
517
|
const report = coverWorld(subject, repo, { root, allowUnknown: rest.includes('--allow-unknown'), acknowledge: acknowledgeOptions(rest, 'covers') });
|
|
518
518
|
process.stdout.write(rest.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatCoverageReport(report));
|
|
519
|
+
// `covers` is a reading. It keeps the proof it printed only as the plain verb on a World's own config: with --json
|
|
520
|
+
// it prints and writes nothing, and it never rewrites the config a running instance booted from. Whether it kept
|
|
521
|
+
// the proof is said on stderr, beside the report and outside its JSON.
|
|
522
|
+
if (!rest.includes('--json')) {
|
|
523
|
+
if (report.worldSource !== 'config')
|
|
524
|
+
process.stderr.write(`coverage not recorded: ${report.world} was read from its instance (${report.worldPath}); the config it booted from is not rewritten\n`);
|
|
525
|
+
else if (recordCoverage(report, root))
|
|
526
|
+
process.stderr.write(`coverage recorded in ${report.configPath}\n`);
|
|
527
|
+
else
|
|
528
|
+
process.stderr.write(`coverage not recorded: ${report.configPath} is a format-1 document, which has no discovery section, or is no longer on disk\n`);
|
|
529
|
+
}
|
|
519
530
|
if (!report.ok)
|
|
520
531
|
process.exit(1);
|
|
521
532
|
return;
|
package/dist/src/covers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.js';
|
|
1
|
+
import { type WorldCoverage, type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.js';
|
|
2
2
|
export type CoverageStatus = 'covered' | 'missing' | 'uninterceptable' | 'unknown-sdk' | 'acknowledged' | 'excluded';
|
|
3
3
|
export type CoverageRow = {
|
|
4
4
|
/** vendor key for mapped vendors; the raw dep/env name for unknown-sdk rows */
|
|
@@ -16,6 +16,9 @@ export type CoverageReport = {
|
|
|
16
16
|
/** 'instance' when a booted/recorded instance was read; 'config' for the stable config */
|
|
17
17
|
worldSource: 'instance' | 'config';
|
|
18
18
|
worldPath: string;
|
|
19
|
+
/** The World's config, where the proof is kept (`recordCoverage`): `worldPath` itself for a config, the config
|
|
20
|
+
* an instance booted from otherwise. */
|
|
21
|
+
configPath: string;
|
|
19
22
|
repo: string;
|
|
20
23
|
twinsInWorld: string[];
|
|
21
24
|
rows: CoverageRow[];
|
|
@@ -121,6 +124,8 @@ type WorldTwinInventory = {
|
|
|
121
124
|
services: Array<WorldServiceConfig | WorldServiceInstance>;
|
|
122
125
|
source: 'instance' | 'config';
|
|
123
126
|
path: string;
|
|
127
|
+
/** The config the World is declared in: the one read, or the one its instance booted from. */
|
|
128
|
+
configPath: string;
|
|
124
129
|
/** normalized identifier → id record (e.g. 'stripe' → { label: 'stripe (service)', … }) */
|
|
125
130
|
ids: Map<string, WorldTwinId>;
|
|
126
131
|
connections: Map<string, {
|
|
@@ -180,5 +185,20 @@ export type RepoVendorSignals = {
|
|
|
180
185
|
* and source lists, so callers that preserve it are deterministic. */
|
|
181
186
|
export declare function detectRepoVendors(repoPath: string, services?: Array<WorldServiceConfig | WorldServiceInstance>): RepoVendorSignals;
|
|
182
187
|
export declare function coverWorld(world: string, repoPath: string, options?: CoverageOptions): CoverageReport;
|
|
188
|
+
/**
|
|
189
|
+
* The report as the data `init` keeps in the config (`discovery.coverage`): the same rows `formatCoverageReport`
|
|
190
|
+
* prints, grouped by the status each verdict line counts. Every push to `covered`, `missing`, `uninterceptable`,
|
|
191
|
+
* `unknown`, `acknowledged` and `excluded` in `coverWorld` adds the row of that status, so the lists here name what
|
|
192
|
+
* those lines name. Paths (`worldPath`, `configPath`, `repo`) stay out: the config is portable and init's bytes are deterministic.
|
|
193
|
+
*/
|
|
194
|
+
export declare function coverageRecord(report: CoverageReport): WorldCoverage;
|
|
195
|
+
/**
|
|
196
|
+
* Keep a proof just made in the World's config, replacing the record of an earlier one. It answers true when it
|
|
197
|
+
* wrote the record and false when it wrote nothing: a format-1 document has no `discovery` section and a World whose
|
|
198
|
+
* config is no longer on disk has nowhere to keep it. The caller decides whether to record at all and says the
|
|
199
|
+
* answer (`init` writes its own record into the config it makes): `covers` records only as the plain verb on a
|
|
200
|
+
* World's own config, never with --json and never into the config a running instance booted from (cli-commands.ts).
|
|
201
|
+
*/
|
|
202
|
+
export declare function recordCoverage(report: CoverageReport, root?: string): boolean;
|
|
183
203
|
export declare function formatCoverageReport(report: CoverageReport): string;
|
|
184
204
|
export {};
|
package/dist/src/covers.js
CHANGED
|
@@ -25,8 +25,9 @@ import { errorMessage } from "./error-message.js";
|
|
|
25
25
|
import { existsSync, readFileSync } from 'node:fs';
|
|
26
26
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
|
-
import { installedPackageDir, packVendor, vendorOfPackModule } from '@volter/world-core/runtime';
|
|
29
|
-
import { loadWorldConfig } from "./configs.js";
|
|
28
|
+
import { getActiveWorldStore, installedPackageDir, packVendor, vendorOfPackModule } from '@volter/world-core/runtime';
|
|
29
|
+
import { loadWorldConfig, writeWorldConfig } from "./configs.js";
|
|
30
|
+
import { runtimeManifest } from "./package-manifest.js";
|
|
30
31
|
import { VENDOR_SERVER_BACKINGS } from "./backings.js";
|
|
31
32
|
import { injectableVendorKeys, loadInject, twinUrlVendorFor } from "./inject-map.js";
|
|
32
33
|
import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
|
|
@@ -472,7 +473,7 @@ export function worldTwinInventory(world, root) {
|
|
|
472
473
|
if (stem)
|
|
473
474
|
addId(ids, normalizeId(stem[1]), `env ${key}`, twinUrlVendorFor(key) !== null);
|
|
474
475
|
}
|
|
475
|
-
return { services: Object.values(status.services), source: 'instance', path: `${status.dirs.instance}/instance.json`, ids, connections: serviceConnections(Object.values(status.services), status.env, root), selection: status.selection, declared: declaredTwin(Object.values(status.services), status.env, root) };
|
|
476
|
+
return { services: Object.values(status.services), source: 'instance', path: `${status.dirs.instance}/instance.json`, configPath: status.configPath, ids, connections: serviceConnections(Object.values(status.services), status.env, root), selection: status.selection, declared: declaredTwin(Object.values(status.services), status.env, root) };
|
|
476
477
|
}
|
|
477
478
|
catch (error) {
|
|
478
479
|
instanceError = errorMessage(error);
|
|
@@ -491,7 +492,7 @@ export function worldTwinInventory(world, root) {
|
|
|
491
492
|
if (value.startsWith(ISSUE_ENV_PREFIX))
|
|
492
493
|
connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
|
|
493
494
|
}
|
|
494
|
-
return { services: config.services, source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services, undefined, root) };
|
|
495
|
+
return { services: config.services, source: 'config', path, configPath: path, ids, connections, selection: config.selection, declared: declaredTwin(config.services, undefined, root) };
|
|
495
496
|
}
|
|
496
497
|
catch (error) {
|
|
497
498
|
const configError = errorMessage(error);
|
|
@@ -895,6 +896,7 @@ export function coverWorld(world, repoPath, options = {}) {
|
|
|
895
896
|
world,
|
|
896
897
|
worldSource: inventory.source,
|
|
897
898
|
worldPath: inventory.path,
|
|
899
|
+
configPath: inventory.configPath,
|
|
898
900
|
repo,
|
|
899
901
|
twinsInWorld: [...inventory.ids.values()].map((id) => id.label).sort(),
|
|
900
902
|
rows,
|
|
@@ -908,6 +910,46 @@ export function coverWorld(world, repoPath, options = {}) {
|
|
|
908
910
|
ok: missing.length === 0 && uninterceptable.length === 0 && (allowUnknown || unknown.length === 0),
|
|
909
911
|
};
|
|
910
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* The report as the data `init` keeps in the config (`discovery.coverage`): the same rows `formatCoverageReport`
|
|
915
|
+
* prints, grouped by the status each verdict line counts. Every push to `covered`, `missing`, `uninterceptable`,
|
|
916
|
+
* `unknown`, `acknowledged` and `excluded` in `coverWorld` adds the row of that status, so the lists here name what
|
|
917
|
+
* those lines name. Paths (`worldPath`, `configPath`, `repo`) stay out: the config is portable and init's bytes are deterministic.
|
|
918
|
+
*/
|
|
919
|
+
export function coverageRecord(report) {
|
|
920
|
+
const entry = (row) => ({
|
|
921
|
+
evidence: row.detectedVia,
|
|
922
|
+
...(row.connection === undefined ? {} : { connection: row.connection }),
|
|
923
|
+
...(row.twinInWorld === null ? {} : { twinInWorld: row.twinInWorld }),
|
|
924
|
+
});
|
|
925
|
+
const of = (status) => report.rows.filter((row) => row.status === status).map((row) => ({ vendor: row.vendor, ...entry(row) }));
|
|
926
|
+
return {
|
|
927
|
+
producer: { package: '@volter/world-runtime', version: runtimeManifest().version ?? 'unversioned' },
|
|
928
|
+
ok: report.ok,
|
|
929
|
+
covered: of('covered'),
|
|
930
|
+
notCovered: of('missing'),
|
|
931
|
+
uninterceptable: of('uninterceptable'),
|
|
932
|
+
unknownSdk: report.rows.filter((row) => row.status === 'unknown-sdk').map((row) => ({ signal: row.vendor, ...entry(row) })),
|
|
933
|
+
acknowledged: of('acknowledged').map((row) => ({ ...row, reason: report.acknowledged.find((held) => held.vendor === row.vendor).reason })),
|
|
934
|
+
excludedBySelection: of('excluded'),
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Keep a proof just made in the World's config, replacing the record of an earlier one. It answers true when it
|
|
939
|
+
* wrote the record and false when it wrote nothing: a format-1 document has no `discovery` section and a World whose
|
|
940
|
+
* config is no longer on disk has nowhere to keep it. The caller decides whether to record at all and says the
|
|
941
|
+
* answer (`init` writes its own record into the config it makes): `covers` records only as the plain verb on a
|
|
942
|
+
* World's own config, never with --json and never into the config a running instance booted from (cli-commands.ts).
|
|
943
|
+
*/
|
|
944
|
+
export function recordCoverage(report, root = process.cwd()) {
|
|
945
|
+
if (!getActiveWorldStore().exists(report.configPath))
|
|
946
|
+
return false;
|
|
947
|
+
const { path, config } = loadWorldConfig(report.configPath, root);
|
|
948
|
+
if (config.manifestVersion !== 2)
|
|
949
|
+
return false;
|
|
950
|
+
writeWorldConfig(path, { ...config, coverage: coverageRecord(report) });
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
911
953
|
export function formatCoverageReport(report) {
|
|
912
954
|
const lines = [];
|
|
913
955
|
lines.push(`World ${report.world} (${report.worldSource}: ${report.worldPath})`);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { WorldConfig, WorldInstance, WorldIsolation, WorldMode, WorldServiceConfig, WorldServiceType, WorldExternalServiceConfig, WorldExternalDiscover, WorldExternalDiscoverSource, WorldExternalReadyWhen, WorldRunOutcome, WorldRunRecord, WorldResourceRequirements, WorldManifestV2, WorldSelection, WorldSelectionSelector, WorldUsage, } from './schema.js';
|
|
1
|
+
export type { WorldConfig, WorldInstance, WorldIsolation, WorldMode, WorldServiceConfig, WorldServiceType, WorldExternalServiceConfig, WorldExternalDiscover, WorldExternalDiscoverSource, WorldExternalReadyWhen, WorldRunOutcome, WorldRunRecord, WorldResourceRequirements, WorldManifestV2, WorldCoverage, WorldCoverageEntry, WorldSelection, WorldSelectionSelector, WorldUsage, } from './schema.js';
|
|
2
2
|
export { DEFAULT_WORLD_SELECTION, LEGACY_WORLD_SELECTION, WORLD_SCHEMA_VERSION, WORLD_USAGES, worldConfigDocument, worldSelectionIncludes } from './schema.js';
|
|
3
3
|
export type { WorldChangesetRules } from './schema.js';
|
|
4
4
|
export { loadWorldConfig, migrateWorldConfig, resolveConfigPath, worldChangesetRules, writeWorldConfig } from './configs.js';
|
|
@@ -17,7 +17,7 @@ export { inspectProject, formatProjectInspection, projectDependencies, projectEn
|
|
|
17
17
|
export type { ProjectInspection } from './project-inspect.js';
|
|
18
18
|
export { fakeEnvValue, fakeGoogleOAuthClientJson, fakeGoogleServiceAccountJson, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from './fixture-env.js';
|
|
19
19
|
export type { FakeServiceAccountOptions } from './fixture-env.js';
|
|
20
|
-
export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, worldTwinInventory, } from './covers.js';
|
|
20
|
+
export { coverWorld, coverageRecord, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, recordCoverage, worldTwinInventory, } from './covers.js';
|
|
21
21
|
export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.js';
|
|
22
22
|
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from './init.js';
|
|
23
23
|
export { STATE_GITIGNORE } from './init.js';
|
package/dist/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { inspectProject, formatProjectInspection, projectDependencies, projectEn
|
|
|
14
14
|
// Structurally valid fake credentials for world configs (fixture env) — see fixture-env.ts
|
|
15
15
|
// for the pattern (a fake a client SDK parses/signs with must be structurally real).
|
|
16
16
|
export { fakeEnvValue, fakeGoogleOAuthClientJson, fakeGoogleServiceAccountJson, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
|
|
17
|
-
export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, worldTwinInventory, } from "./covers.js";
|
|
17
|
+
export { coverWorld, coverageRecord, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, recordCoverage, worldTwinInventory, } from "./covers.js";
|
|
18
18
|
// `volter-world init` — the deterministic front door: detect the repo's vendors (covers), emit a
|
|
19
19
|
// world config + env file OUTSIDE the repo (fixture-env), and prove the emission with `covers`.
|
|
20
20
|
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from "./init.js";
|
package/dist/src/infra.js
CHANGED
|
@@ -12,7 +12,7 @@ import { delimiter, dirname, join } from 'node:path';
|
|
|
12
12
|
import { signalChild } from './process-signal.mjs';
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
|
|
15
|
+
import { containerlessDataDirectory, infraConnections, liveDatabaseBehind, liveDatabaseMigration, developmentMigratesAtRuntime, DEVELOPMENT_MIGRATION_NOTICE, parseInfraDefinition } from "./pglite-backing.js";
|
|
16
16
|
import { errorMessage } from "./error-message.js";
|
|
17
17
|
/** Whether a program is on PATH, read from the file system without starting a process: where a missing program
|
|
18
18
|
* still costs a process round trip (a browser tab's kernel), the container probe is not spawned at all. */
|
|
@@ -98,6 +98,10 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
98
98
|
// prepare: the image build's step that makes the World's PGlite cluster once, on its host, so the World's first up
|
|
99
99
|
// opens it instead of running initdb (pglite-backing.ts, pglitePrepare; ADR 0018). It needs the World's
|
|
100
100
|
// definition, not an instance.
|
|
101
|
+
// A backing the World's environment forces is said where the backing is said, and in the status fact: forcing one
|
|
102
|
+
// is a choice someone made for this World, and it is never silent.
|
|
103
|
+
const forcedBy = (backing) => env.VOLTER_WORLD_INFRA_BACKING === backing ? ' (forced by VOLTER_WORLD_INFRA_BACKING)' : '';
|
|
104
|
+
const backingFact = (backing) => ({ backing: { is: backing, by: env.VOLTER_WORLD_INFRA_BACKING === backing ? 'forced' : 'chosen' } });
|
|
101
105
|
if (phase === 'prepare') {
|
|
102
106
|
const config = env.VOLTER_WORLD_CONFIG;
|
|
103
107
|
const declaredDefinition = config ? join(dirname(config), 'world.infrastructure.yml') : undefined;
|
|
@@ -111,12 +115,24 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
111
115
|
return 0;
|
|
112
116
|
}
|
|
113
117
|
const { pglitePrepare } = await import("./pglite-backing.js");
|
|
118
|
+
const { nativePrepare, nativePostgresTools } = await import("./native-postgres-backing.js");
|
|
119
|
+
// The backing up would choose for this PostgreSQL: native where the PostgreSQL programs are (containerless rule
|
|
120
|
+
// 2; in a tab image, the tab's own build), PGlite otherwise or when forced.
|
|
121
|
+
const native = env.VOLTER_WORLD_INFRA_BACKING !== 'pglite' && nativePostgresTools(env) !== undefined;
|
|
122
|
+
// The database prepare step's own output (prisma's "N migrations found … No pending migrations") is the
|
|
123
|
+
// preparation's record, said where its caller reads it.
|
|
124
|
+
const launch = { env, ...(signal ? { signal } : {}), note: (what) => io.out(`${what.trimEnd()}\n`) };
|
|
114
125
|
try {
|
|
115
|
-
|
|
126
|
+
const prepared = native
|
|
127
|
+
? await nativePrepare(postgres[0], readFileSync(declaredDefinition, 'utf8'), config, launch)
|
|
128
|
+
: await pglitePrepare(postgres[0], config, launch);
|
|
129
|
+
io.out(`managed infrastructure prepared ${prepared} (backing: ${native ? 'native PostgreSQL' : 'PGlite'}${forcedBy(native ? 'native' : 'pglite')})\n`);
|
|
116
130
|
return 0;
|
|
117
131
|
}
|
|
118
132
|
catch (error) {
|
|
119
133
|
io.err(`managed infrastructure prepare failed: ${errorMessage(error)}\n`);
|
|
134
|
+
// Said where the person or agent reading the failure stands: the fix is here, in the preparation.
|
|
135
|
+
io.err('The fix is in this build-time preparation (the step above, or what it runs against). Migrating at run time is not the remedy for a failed preparation.\n');
|
|
120
136
|
return 1;
|
|
121
137
|
}
|
|
122
138
|
}
|
|
@@ -131,6 +147,19 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
131
147
|
io.err('managed infrastructure: declared definition is missing\n');
|
|
132
148
|
return 2;
|
|
133
149
|
}
|
|
150
|
+
// A development World says so every time it comes up: its use is never silent.
|
|
151
|
+
if (phase === 'up' && developmentMigratesAtRuntime(worldConfig))
|
|
152
|
+
io.out(`${DEVELOPMENT_MIGRATION_NOTICE}\n`);
|
|
153
|
+
// What the app's commands are told of the World's database (architecture, managed infrastructure): the declared
|
|
154
|
+
// step's command while the live data carries the migration the image expects, the same command when the World
|
|
155
|
+
// declares the step and the live data does not carry it, and whether this is a development World whose app migrates when it runs. `postgresData` is the serving backing's data directory, set where a status knows it.
|
|
156
|
+
let postgresData;
|
|
157
|
+
const databaseFacts = () => {
|
|
158
|
+
const migrated = postgresData ? liveDatabaseMigration(worldConfig, env, postgresData) : undefined;
|
|
159
|
+
const behind = postgresData ? liveDatabaseBehind(worldConfig, env, postgresData) : undefined;
|
|
160
|
+
return { ...(migrated || behind ? { database: { ...(migrated ? { migrated } : {}), ...(behind ? { behind } : {}) } } : {}),
|
|
161
|
+
...(developmentMigratesAtRuntime(worldConfig) ? { development: { migrateDatabaseAtRuntime: true } } : {}) };
|
|
162
|
+
};
|
|
134
163
|
const base = ['compose', '-f', definition];
|
|
135
164
|
const run = async (args) => {
|
|
136
165
|
const ran = await runCommand('docker', [...base, ...args], { env: { ...env, VOLTER_WORLD_DATA: worldData }, timeout: 120_000, ...(signal ? { signal } : {}) });
|
|
@@ -245,7 +274,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
245
274
|
return 1;
|
|
246
275
|
}
|
|
247
276
|
if (phase === 'up') {
|
|
248
|
-
io.out(
|
|
277
|
+
io.out(`managed infrastructure backing: containerless${forcedBy('pglite') || ' (no container runtime)'}\n`);
|
|
249
278
|
if (services.some((s) => s.kind === 'postgres')) {
|
|
250
279
|
io.out('managed infrastructure: PostgreSQL is PGlite, one session every connection takes in turn; a read through '
|
|
251
280
|
+ 'the pool inside a transaction waits on itself until that transaction ends. A postgres server on PATH, or '
|
|
@@ -263,11 +292,13 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
263
292
|
}
|
|
264
293
|
if (phase === 'status') {
|
|
265
294
|
const status = await pgliteStatus(services, worldData);
|
|
295
|
+
if (services.some((s) => s.kind === 'postgres'))
|
|
296
|
+
postgresData = join(worldData, containerlessDataDirectory('postgres'));
|
|
266
297
|
if (!status.ok) {
|
|
267
298
|
io.err(`managed infrastructure status failed: ${status.ready}/${services.length} declared services are ready\n`);
|
|
268
299
|
return 1;
|
|
269
300
|
}
|
|
270
|
-
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
|
|
301
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('pglite'), ...databaseFacts() })}\n`);
|
|
271
302
|
return 0;
|
|
272
303
|
}
|
|
273
304
|
await pgliteDown(services, worldData);
|
|
@@ -336,7 +367,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
336
367
|
if (phase === 'up')
|
|
337
368
|
io.out('managed infrastructure ready\n');
|
|
338
369
|
else if (phase === 'status')
|
|
339
|
-
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
|
|
370
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() })}\n`);
|
|
340
371
|
else
|
|
341
372
|
io.out('managed infrastructure stopped\n');
|
|
342
373
|
return 0;
|
|
@@ -357,7 +388,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
357
388
|
}
|
|
358
389
|
try {
|
|
359
390
|
if (phase === 'up') {
|
|
360
|
-
io.out(
|
|
391
|
+
io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')}\n`);
|
|
361
392
|
await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
|
|
362
393
|
try {
|
|
363
394
|
await pgliteUp(others, worldData, launch);
|
|
@@ -377,7 +408,8 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
377
408
|
const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
|
|
378
409
|
if (!ready || !await nativePostgresStatus(worldData))
|
|
379
410
|
return 1;
|
|
380
|
-
|
|
411
|
+
postgresData = join(worldData, 'native-postgres-data');
|
|
412
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`);
|
|
381
413
|
}
|
|
382
414
|
else {
|
|
383
415
|
await pgliteDown(others, worldData);
|
|
@@ -393,6 +425,8 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
393
425
|
}
|
|
394
426
|
const only = composed ?? [];
|
|
395
427
|
if (phase === 'up') {
|
|
428
|
+
if (forcedBy('docker'))
|
|
429
|
+
io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
|
|
396
430
|
const result = await run(['up', '-d', '--wait', ...only]);
|
|
397
431
|
if (result.status !== 0)
|
|
398
432
|
return fail(result);
|
|
@@ -411,7 +445,7 @@ export async function runInfrastructure(phase, env, io, signal) {
|
|
|
411
445
|
io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length + native.length}/${expectedNames.length + twinned.length + native.length} declared services are ready\n`);
|
|
412
446
|
return 1;
|
|
413
447
|
}
|
|
414
|
-
io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env) })}\n`);
|
|
448
|
+
io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env), ...backingFact('docker'), ...databaseFacts() })}\n`);
|
|
415
449
|
}
|
|
416
450
|
else {
|
|
417
451
|
const result = await run(['down', '--remove-orphans']);
|
package/dist/src/init.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CoverageReport } from './covers.js';
|
|
2
|
+
import { type ProjectMigrationTool } from './project-inspect.js';
|
|
2
3
|
import { type WorldConfig, type WorldServiceConfig } from './schema.js';
|
|
3
4
|
/** How a twin service in the emitted world is reached by the app's traffic. */
|
|
4
5
|
export type InitWiring =
|
|
@@ -111,6 +112,9 @@ export type InitPlan = {
|
|
|
111
112
|
envSources: string[];
|
|
112
113
|
/** empty example values read by production source — prominent warning, not vendor-coverage fact */
|
|
113
114
|
bootRisks: InitBootRisk[];
|
|
115
|
+
/** migration tools found in the repository while the World manages a database and declares no step
|
|
116
|
+
* (runtime.database.prepare): reported, never written, and no part of the vendor-coverage verdict */
|
|
117
|
+
undeclaredMigrations: ProjectMigrationTool[];
|
|
114
118
|
infra: InitInfraStub[];
|
|
115
119
|
/** the services emitted into the private managed-infrastructure definition */
|
|
116
120
|
compose: InitComposeService[];
|
package/dist/src/init.js
CHANGED
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
37
37
|
import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core/runtime';
|
|
38
38
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
39
|
-
import { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorEnvNameForKey, injectorVendorKeysFor, isCredentialShapedEnvName, registryAcknowledgedReason, } from "./covers.js";
|
|
39
|
+
import { coverWorld, detectRepoVendors, envNameVendor, coverageRecord, formatCoverageReport, injectorEnvNameForKey, injectorVendorKeysFor, isCredentialShapedEnvName, registryAcknowledgedReason, } from "./covers.js";
|
|
40
40
|
import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
|
|
41
41
|
import { findInstalledPackage, packCli, packEntry, resolveCatalog } from "./catalog.js";
|
|
42
42
|
import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-index.js";
|
|
43
|
-
import { projectEnvReads, projectManifestDirs } from "./project-inspect.js";
|
|
43
|
+
import { projectEnvReads, projectManifestDirs, projectMigrationTools } from "./project-inspect.js";
|
|
44
44
|
import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
|
|
45
45
|
import { VENDOR_SERVER_BACKINGS } from "./backings.js";
|
|
46
46
|
import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
|
|
@@ -1047,6 +1047,9 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
1047
1047
|
envRows,
|
|
1048
1048
|
envSources,
|
|
1049
1049
|
bootRisks: [...bootRisks].sort((a, b) => a.name.localeCompare(b.name)),
|
|
1050
|
+
// A migration belongs in the World's step, run once where the image is built. init can see the tool, not the step.
|
|
1051
|
+
undeclaredMigrations: existing?.databasePrepare !== undefined || !(compose.some((service) => service.kind === 'postgres' || service.kind === 'mysql') || existingIds.has('infrastructure'))
|
|
1052
|
+
? [] : projectMigrationTools(repo),
|
|
1050
1053
|
infra,
|
|
1051
1054
|
compose,
|
|
1052
1055
|
composePath: compose.length > 0 ? resolve(out, 'world.infrastructure.yml') : null,
|
|
@@ -1248,6 +1251,10 @@ export function initWorld(name, repoPath, options = {}) {
|
|
|
1248
1251
|
// The proof was addressed BY PATH (so it reads the bytes just written, not the in-memory plan);
|
|
1249
1252
|
// report it under the world's id, which is what every other verb takes.
|
|
1250
1253
|
const coverage = { ...proof, world: plan.name };
|
|
1254
|
+
// The proof is kept in the config it was made against, beside the selection: the report printed below, as data.
|
|
1255
|
+
// Written after the proof because the proof reads the config's bytes; it reads nothing of `discovery.coverage`.
|
|
1256
|
+
plan.config.coverage = coverageRecord(coverage);
|
|
1257
|
+
writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
|
|
1251
1258
|
const next = plan.inRepo && resolve(options.root ?? plan.repo) === plan.repo
|
|
1252
1259
|
? [
|
|
1253
1260
|
'volter world up # starts the twins and loads the default data; the live env lands in .volter/world.env',
|
|
@@ -1329,6 +1336,17 @@ export function formatInitReport(result) {
|
|
|
1329
1336
|
}
|
|
1330
1337
|
lines.push(' Resolve these before expecting the app to listen. Vendor coverage can be green while the app rejects its own env.');
|
|
1331
1338
|
}
|
|
1339
|
+
if (plan.undeclaredMigrations.length > 0) {
|
|
1340
|
+
lines.push('');
|
|
1341
|
+
lines.push(`MIGRATION TOOL FOUND, NO STEP DECLARED (${plan.undeclaredMigrations.length}):`);
|
|
1342
|
+
for (const found of plan.undeclaredMigrations)
|
|
1343
|
+
lines.push(` ${found.tool} — migrations at ${found.migrations} (schema ${found.schema})`);
|
|
1344
|
+
lines.push(' The World manages this app\'s database and declares no runtime.database.prepare, so the app would migrate it at');
|
|
1345
|
+
lines.push(' every start. Declare the step in the World: `command` (the migration as the app\'s image runs it, an argv), `cwd`');
|
|
1346
|
+
lines.push(' (the image path it runs in) and `inputs` (the schema and migrations, as paths under it). These are the IMAGE\'s');
|
|
1347
|
+
lines.push(' paths, which its Dockerfile decides and this repository does not state; an image build that reads the image');
|
|
1348
|
+
lines.push(' prints the step.');
|
|
1349
|
+
}
|
|
1332
1350
|
if (plan.compose.length > 0) {
|
|
1333
1351
|
lines.push('');
|
|
1334
1352
|
lines.push('Infrastructure (owned by the World lifecycle):');
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type HostLaunch, type InfraService } from './pglite-backing.js';
|
|
2
2
|
/** The PostgreSQL tool directory this machine offers, or undefined: `VOLTER_WORLD_POSTGRES_BIN` as named, else the
|
|
3
3
|
* directory of a `postgres` server on PATH, resolved through its links, holding the server's own tools beside it
|
|
4
4
|
* (libpq's client tools on PATH name no server). Read from executable files; nothing is launched or installed
|
|
5
5
|
* (architecture, managed infrastructure). */
|
|
6
6
|
export declare function nativePostgresTools(env: NodeJS.ProcessEnv): string | undefined;
|
|
7
|
+
/** Where a World's prepared native cluster lives, beside its world.json, and its stamp (ADR 0018's layout). */
|
|
8
|
+
export declare function nativePreparedCluster(worldConfig: string): {
|
|
9
|
+
directory: string;
|
|
10
|
+
stamp: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* `volter-world-infra prepare` for a World whose PostgreSQL is native (architecture, managed infrastructure; ADR
|
|
14
|
+
* 0018's phase): this lifecycle's own steps once, into `<World dir>/.volter-prepared/native-postgres-data`. initdb with
|
|
15
|
+
* up's arguments, the postmaster on the World's declared loopback port, the declared database, and a clean SIGINT stop (its
|
|
16
|
+
* shutdown checkpoint; no postmaster.pid left), then the stamp. Where the World declares runtime.database.prepare, the
|
|
17
|
+
* step runs against that postmaster before its stop, and a step that fails leaves no cluster. The image build runs it with the programs the tab
|
|
18
|
+
* runs, under the tab's kernel, so the cluster is the one a first boot there would write.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nativePrepare(service: InfraService, definition: string, worldConfig: string, launch?: HostLaunch): Promise<string>;
|
|
7
21
|
export declare function nativePostgresUp(services: InfraService[], root: string, definition: string, launch: HostLaunch): Promise<void>;
|
|
8
22
|
export declare function nativePostgresStatus(root: string): Promise<boolean>;
|
|
9
23
|
export declare function nativePostgresDown(root: string): Promise<void>;
|