@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/schema.d.ts
CHANGED
|
@@ -26,6 +26,38 @@ export type WorldSelection = {
|
|
|
26
26
|
include: WorldSelectionSelector[];
|
|
27
27
|
exclude: WorldSelectionSelector[];
|
|
28
28
|
};
|
|
29
|
+
/** One row of the coverage proof as it is kept: `evidence` is the proof table's detected-via column, `connection`
|
|
30
|
+
* its transport column and `twinInWorld` its twin-in-world column (a display label, not a service id). */
|
|
31
|
+
export type WorldCoverageEntry = {
|
|
32
|
+
vendor: string;
|
|
33
|
+
evidence: string[];
|
|
34
|
+
connection?: string;
|
|
35
|
+
twinInWorld?: string;
|
|
36
|
+
};
|
|
37
|
+
/** The last coverage proof `init` or `covers` printed, kept beside the selection it applied (covers.ts
|
|
38
|
+
* `coverageRecord`): each list is the proof's rows of one status. A record; nothing reads it to route, select or boot. */
|
|
39
|
+
export type WorldCoverage = {
|
|
40
|
+
/** The package that made the proof. */
|
|
41
|
+
producer: {
|
|
42
|
+
package: string;
|
|
43
|
+
version: string;
|
|
44
|
+
};
|
|
45
|
+
/** The proof's verdict, which was the command's exit code. */
|
|
46
|
+
ok: boolean;
|
|
47
|
+
covered: WorldCoverageEntry[];
|
|
48
|
+
notCovered: WorldCoverageEntry[];
|
|
49
|
+
uninterceptable: WorldCoverageEntry[];
|
|
50
|
+
unknownSdk: {
|
|
51
|
+
signal: string;
|
|
52
|
+
evidence: string[];
|
|
53
|
+
connection?: string;
|
|
54
|
+
twinInWorld?: string;
|
|
55
|
+
}[];
|
|
56
|
+
acknowledged: (WorldCoverageEntry & {
|
|
57
|
+
reason: string;
|
|
58
|
+
})[];
|
|
59
|
+
excludedBySelection: WorldCoverageEntry[];
|
|
60
|
+
};
|
|
29
61
|
/** The format-2 document is normalized to WorldConfig before runtime code sees it. */
|
|
30
62
|
export type WorldManifestV2 = {
|
|
31
63
|
schemaVersion: 2;
|
|
@@ -35,6 +67,7 @@ export type WorldManifestV2 = {
|
|
|
35
67
|
};
|
|
36
68
|
discovery?: {
|
|
37
69
|
selection?: WorldSelection;
|
|
70
|
+
coverage?: WorldCoverage;
|
|
38
71
|
};
|
|
39
72
|
services: unknown[];
|
|
40
73
|
runtime?: {
|
|
@@ -44,6 +77,10 @@ export type WorldManifestV2 = {
|
|
|
44
77
|
strip?: string[];
|
|
45
78
|
};
|
|
46
79
|
network?: WorldNetwork;
|
|
80
|
+
database?: {
|
|
81
|
+
prepare?: WorldDatabasePrepare;
|
|
82
|
+
};
|
|
83
|
+
development?: WorldDevelopment;
|
|
47
84
|
};
|
|
48
85
|
serving?: {
|
|
49
86
|
mode?: 'app' | 'bare';
|
|
@@ -290,8 +327,36 @@ export type WorldChangesetRules = {
|
|
|
290
327
|
approvals?: number;
|
|
291
328
|
summary?: 'required' | 'generated';
|
|
292
329
|
};
|
|
330
|
+
/**
|
|
331
|
+
* The app's own migration of its database, run by `volter-world-infra prepare` against the prepared cluster
|
|
332
|
+
* (architecture, managed infrastructure): its command as the app's image runs it (an argv), the image path it runs in,
|
|
333
|
+
* and the paths under it whose bytes decide what it writes (the prepared cluster's stamp names their digests).
|
|
334
|
+
*/
|
|
335
|
+
export type WorldDatabasePrepare = {
|
|
336
|
+
command: string[];
|
|
337
|
+
cwd: string;
|
|
338
|
+
inputs: string[];
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* SETTINGS FOR THE PERSON DEVELOPING THE APP AGAINST THIS WORLD (runtime.development). A World that sets any of
|
|
342
|
+
* them is a development World: `up` and an image's build say so each time, and an image built from it is not a
|
|
343
|
+
* prepared one. None of them is a way to make an application boot or to get past a failed preparation; an
|
|
344
|
+
* application's World does not set them.
|
|
345
|
+
*
|
|
346
|
+
* migrateDatabaseAtRuntime: the app may run its own migration when it starts, for a schema its developer is
|
|
347
|
+
* changing right now. Omitted, no: a migration is the World's `runtime.database.prepare`, run when the image is
|
|
348
|
+
* built.
|
|
349
|
+
*/
|
|
350
|
+
export type WorldDevelopment = {
|
|
351
|
+
migrateDatabaseAtRuntime?: boolean;
|
|
352
|
+
};
|
|
293
353
|
export type WorldConfig = {
|
|
294
354
|
id: string;
|
|
355
|
+
/** The app's own migration step for the World's prepared database cluster (runtime.database.prepare). */
|
|
356
|
+
databasePrepare?: WorldDatabasePrepare;
|
|
357
|
+
/** The World's development settings (runtime.development; WorldDevelopment says whose they are). Its
|
|
358
|
+
* migrateDatabaseAtRuntime is said to the app's commands as VOLTER_WORLD_DEVELOPMENT_MIGRATE_AT_RUNTIME. */
|
|
359
|
+
development?: WorldDevelopment;
|
|
295
360
|
/** How changesets are made and readied here; omitted: one approval, a summary required. */
|
|
296
361
|
changesets?: WorldChangesetRules;
|
|
297
362
|
/** HTTPS origin/exact-path/subtree grants with declared phase and explicit build passthrough; [] closes real egress. */
|
|
@@ -299,6 +364,8 @@ export type WorldConfig = {
|
|
|
299
364
|
/** Internal normalized source format and saved discovery selection. */
|
|
300
365
|
manifestVersion?: 1 | 2;
|
|
301
366
|
selection?: WorldSelection;
|
|
367
|
+
/** The last coverage proof made for this config (`discovery.coverage`); carried so a rewrite keeps it. */
|
|
368
|
+
coverage?: WorldCoverage;
|
|
302
369
|
description?: string;
|
|
303
370
|
/** A world with no app in front of it (`init --bare <org>/<world>`): a shared world for a team, or a
|
|
304
371
|
* world standing in for a vendor. `name` is how it is served: `/<org>/<world>/…`. */
|
|
@@ -497,6 +564,7 @@ export declare function isEnvironmentName(value: unknown): value is string;
|
|
|
497
564
|
export declare const DEFAULT_WORLD_SELECTION: WorldSelection;
|
|
498
565
|
export declare const LEGACY_WORLD_SELECTION: WorldSelection;
|
|
499
566
|
export declare function assertWorldSelection(value: unknown, path: string): asserts value is WorldSelection;
|
|
567
|
+
export declare function assertWorldCoverage(value: unknown, path: string): asserts value is WorldCoverage;
|
|
500
568
|
export declare function assertWorldConfig(value: unknown, path: string): WorldConfig;
|
|
501
569
|
export declare function worldSelectionIncludes(selection: WorldSelection, vendor: string, usage: WorldUsage): boolean;
|
|
502
570
|
/** Serialize normalized intent as the committed format-2 manifest. */
|
package/dist/src/schema.js
CHANGED
|
@@ -489,8 +489,32 @@ function normalizeV2(value, path) {
|
|
|
489
489
|
const serving = (value.serving ?? {});
|
|
490
490
|
const scenario = (value.scenario ?? {});
|
|
491
491
|
const provenance = (value.provenance ?? {});
|
|
492
|
-
assertKeys(discovery, ['selection'], 'discovery', path);
|
|
493
|
-
assertKeys(runtime, ['isolation', 'environment', 'network'], 'runtime', path);
|
|
492
|
+
assertKeys(discovery, ['selection', 'coverage'], 'discovery', path);
|
|
493
|
+
assertKeys(runtime, ['isolation', 'environment', 'network', 'database', 'development'], 'runtime', path);
|
|
494
|
+
const database = runtime.database ?? {};
|
|
495
|
+
if (!record(database))
|
|
496
|
+
throw new Error(`runtime.database must be an object in ${path}`);
|
|
497
|
+
assertKeys(database, ['prepare'], 'runtime.database', path);
|
|
498
|
+
const development = runtime.development ?? {};
|
|
499
|
+
if (!record(development))
|
|
500
|
+
throw new Error(`runtime.development must be an object in ${path}`);
|
|
501
|
+
assertKeys(development, ['migrateDatabaseAtRuntime'], 'runtime.development', path);
|
|
502
|
+
if (development.migrateDatabaseAtRuntime !== undefined && typeof development.migrateDatabaseAtRuntime !== 'boolean')
|
|
503
|
+
throw new Error(`runtime.development.migrateDatabaseAtRuntime must be a boolean in ${path}`);
|
|
504
|
+
const databasePrepare = database.prepare;
|
|
505
|
+
if (databasePrepare !== undefined) {
|
|
506
|
+
if (!record(databasePrepare))
|
|
507
|
+
throw new Error(`runtime.database.prepare must be an object in ${path}`);
|
|
508
|
+
assertKeys(databasePrepare, ['command', 'cwd', 'inputs'], 'runtime.database.prepare', path);
|
|
509
|
+
const strings = (value) => Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string' && item !== '');
|
|
510
|
+
if (!strings(databasePrepare.command))
|
|
511
|
+
throw new Error(`runtime.database.prepare.command must be a non-empty argv in ${path}`);
|
|
512
|
+
if (typeof databasePrepare.cwd !== 'string' || !databasePrepare.cwd.startsWith('/'))
|
|
513
|
+
throw new Error(`runtime.database.prepare.cwd must be an absolute image path in ${path}`);
|
|
514
|
+
if (!strings(databasePrepare.inputs) || databasePrepare.inputs.some((input) => input.startsWith('/') || input.split('/').includes('..'))) {
|
|
515
|
+
throw new Error(`runtime.database.prepare.inputs must name paths under its cwd in ${path}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
494
518
|
assertKeys(serving, ['mode', 'name', 'share'], 'serving', path);
|
|
495
519
|
assertKeys(scenario, ['actors', 'fixtures'], 'scenario', path);
|
|
496
520
|
assertKeys(provenance, ['catalog'], 'provenance', path);
|
|
@@ -513,6 +537,8 @@ function normalizeV2(value, path) {
|
|
|
513
537
|
throw new Error(`App serving must omit serving.name in ${path}`);
|
|
514
538
|
const selection = discovery.selection ?? DEFAULT_WORLD_SELECTION;
|
|
515
539
|
assertWorldSelection(selection, path);
|
|
540
|
+
if (discovery.coverage !== undefined)
|
|
541
|
+
assertWorldCoverage(discovery.coverage, path);
|
|
516
542
|
if (!Array.isArray(value.services))
|
|
517
543
|
throw new Error(`World manifest must define services[] in ${path}`);
|
|
518
544
|
const normalized = {
|
|
@@ -521,9 +547,12 @@ function normalizeV2(value, path) {
|
|
|
521
547
|
...(metadata.description === undefined ? {} : { description: metadata.description }),
|
|
522
548
|
manifestVersion: 2,
|
|
523
549
|
selection,
|
|
550
|
+
...(discovery.coverage === undefined ? {} : { coverage: discovery.coverage }),
|
|
524
551
|
services: value.services.map((service) => serviceV2ToLegacy(service, path)),
|
|
525
552
|
...(runtime.isolation === undefined ? {} : { isolation: runtime.isolation }),
|
|
526
553
|
...(runtime.network === undefined ? {} : { network: normalizeWorldNetwork(runtime.network) }),
|
|
554
|
+
...(databasePrepare === undefined ? {} : { databasePrepare: databasePrepare }),
|
|
555
|
+
...(development.migrateDatabaseAtRuntime === true ? { development: { migrateDatabaseAtRuntime: true } } : {}),
|
|
527
556
|
...(environment.values === undefined ? {} : { env: environment.values }),
|
|
528
557
|
...(environment.strip === undefined ? {} : { stripEnv: environment.strip }),
|
|
529
558
|
...(mode === 'bare' ? { bare: { name: serving.name } } : {}),
|
|
@@ -536,6 +565,41 @@ function normalizeV2(value, path) {
|
|
|
536
565
|
};
|
|
537
566
|
return { ...assertLegacyWorldConfig(normalized, path), manifestVersion: 2, selection };
|
|
538
567
|
}
|
|
568
|
+
const WORLD_COVERAGE_LISTS = ['covered', 'notCovered', 'uninterceptable', 'unknownSdk', 'acknowledged', 'excludedBySelection'];
|
|
569
|
+
export function assertWorldCoverage(value, path) {
|
|
570
|
+
if (!record(value))
|
|
571
|
+
throw new Error(`discovery.coverage must be an object in ${path}`);
|
|
572
|
+
assertKeys(value, ['producer', 'ok', ...WORLD_COVERAGE_LISTS], 'discovery.coverage', path);
|
|
573
|
+
const producer = value.producer;
|
|
574
|
+
if (!record(producer))
|
|
575
|
+
throw new Error(`discovery.coverage.producer must be an object in ${path}`);
|
|
576
|
+
assertKeys(producer, ['package', 'version'], 'discovery.coverage.producer', path);
|
|
577
|
+
for (const key of ['package', 'version'])
|
|
578
|
+
if (typeof producer[key] !== 'string' || producer[key] === '')
|
|
579
|
+
throw new Error(`discovery.coverage.producer.${key} must be a non-empty string in ${path}`);
|
|
580
|
+
if (typeof value.ok !== 'boolean')
|
|
581
|
+
throw new Error(`discovery.coverage.ok must be a boolean in ${path}`);
|
|
582
|
+
for (const list of WORLD_COVERAGE_LISTS) {
|
|
583
|
+
if (!Array.isArray(value[list]))
|
|
584
|
+
throw new Error(`discovery.coverage.${list} must be an array in ${path}`);
|
|
585
|
+
const name = list === 'unknownSdk' ? 'signal' : 'vendor';
|
|
586
|
+
const strings = [name, 'connection', 'twinInWorld', ...(list === 'acknowledged' ? ['reason'] : [])];
|
|
587
|
+
const required = [name, ...(list === 'acknowledged' ? ['reason'] : [])];
|
|
588
|
+
for (const entry of value[list]) {
|
|
589
|
+
if (!record(entry))
|
|
590
|
+
throw new Error(`discovery.coverage.${list} entries must be objects in ${path}`);
|
|
591
|
+
assertKeys(entry, [...strings, 'evidence'], `discovery.coverage.${list} entry`, path);
|
|
592
|
+
for (const key of strings) {
|
|
593
|
+
if (entry[key] === undefined && !required.includes(key))
|
|
594
|
+
continue;
|
|
595
|
+
if (typeof entry[key] !== 'string' || entry[key] === '')
|
|
596
|
+
throw new Error(`discovery.coverage.${list} ${key} must be a non-empty string in ${path}`);
|
|
597
|
+
}
|
|
598
|
+
if (!Array.isArray(entry.evidence) || entry.evidence.some((item) => typeof item !== 'string'))
|
|
599
|
+
throw new Error(`discovery.coverage.${list} evidence must be an array of strings in ${path}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
539
603
|
export function assertWorldConfig(value, path) {
|
|
540
604
|
if (record(value) && value.schemaVersion !== undefined) {
|
|
541
605
|
if (value.schemaVersion !== WORLD_SCHEMA_VERSION)
|
|
@@ -604,9 +668,11 @@ export function worldConfigDocument(config, selection = config.selection ?? DEFA
|
|
|
604
668
|
return {
|
|
605
669
|
schemaVersion: WORLD_SCHEMA_VERSION,
|
|
606
670
|
metadata: { id: config.id, ...(config.description === undefined ? {} : { description: config.description }) },
|
|
607
|
-
discovery: { selection },
|
|
608
|
-
...(config.isolation === undefined && config.env === undefined && config.stripEnv === undefined && config.network === undefined ? {} : { runtime: {
|
|
671
|
+
discovery: { selection, ...(config.coverage === undefined ? {} : { coverage: config.coverage }) },
|
|
672
|
+
...(config.isolation === undefined && config.env === undefined && config.stripEnv === undefined && config.network === undefined && config.databasePrepare === undefined && !config.development?.migrateDatabaseAtRuntime ? {} : { runtime: {
|
|
609
673
|
...(config.isolation === undefined ? {} : { isolation: config.isolation }),
|
|
674
|
+
...(config.databasePrepare === undefined ? {} : { database: { prepare: config.databasePrepare } }),
|
|
675
|
+
...(config.development?.migrateDatabaseAtRuntime ? { development: { migrateDatabaseAtRuntime: true } } : {}),
|
|
610
676
|
...(config.network === undefined ? {} : { network: normalizeWorldNetwork(config.network) }),
|
|
611
677
|
...(config.env === undefined && config.stripEnv === undefined ? {} : { environment: {
|
|
612
678
|
...(config.env === undefined ? {} : { values: config.env }),
|
package/dist/src/up-cli.js
CHANGED
|
@@ -29,7 +29,13 @@ export async function upCommand(subject, rest, root) {
|
|
|
29
29
|
const appUrl = optionValue(rest, '--app-url') || undefined;
|
|
30
30
|
if (appUrl !== undefined && !(URL.canParse(appUrl) && /^https?:$/.test(new URL(appUrl).protocol)))
|
|
31
31
|
throw new Error(`volter-world up: --app-url "${appUrl}" must be an http(s) URL`);
|
|
32
|
-
const
|
|
32
|
+
const options = { name, root, mode, envFile, forceEnvOutput: rest.includes('--force'), owner: ownerOption(rest), ...(isolation ? { isolation } : {}), ...(keepState ? { keepState: true } : {}) };
|
|
33
|
+
// --in-process: the boot runs in this process, for a caller with no terminal to lose (a browser tab's page, whose
|
|
34
|
+
// loss ends every process it runs). The owner process exists so a closed terminal does not leave a boot half-done;
|
|
35
|
+
// where there is none, it costs a whole Node start before the boot and its own end after it (Rallly run 35: 611 ms
|
|
36
|
+
// from the owner's "returning" to its reply reaching this process). The caller's SIGINT or SIGTERM still cancels the
|
|
37
|
+
// boot, which rolls back.
|
|
38
|
+
const instance = rest.includes('--in-process') ? await upInProcess(subject, options) : await superviseWorldUp(subject, options);
|
|
33
39
|
if (appUrl !== undefined) {
|
|
34
40
|
const { setAppUrl } = await import("./app-url.js");
|
|
35
41
|
setAppUrl(instance.name, appUrl, { root });
|
|
@@ -40,6 +46,20 @@ export async function upCommand(subject, rest, root) {
|
|
|
40
46
|
else
|
|
41
47
|
printReady(instance);
|
|
42
48
|
}
|
|
49
|
+
async function upInProcess(configId, options) {
|
|
50
|
+
const { upWorld } = await import("./runtime.js");
|
|
51
|
+
const abort = new AbortController();
|
|
52
|
+
const cancel = (signal) => abort.abort(signal);
|
|
53
|
+
process.on('SIGINT', cancel);
|
|
54
|
+
process.on('SIGTERM', cancel);
|
|
55
|
+
try {
|
|
56
|
+
return await upWorld(configId, options, abort.signal);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
process.off('SIGINT', cancel);
|
|
60
|
+
process.off('SIGTERM', cancel);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
export function printReady(instance) {
|
|
44
64
|
process.stdout.write(`World ${instance.name} is ready (${instance.config})
|
|
45
65
|
Instance: ${instance.instance}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.145",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
"node": ">=22.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@volter/world-core": "3.0.
|
|
74
|
-
"@volter/world-console": "3.0.
|
|
73
|
+
"@volter/world-core": "3.0.145",
|
|
74
|
+
"@volter/world-console": "3.0.140"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@electric-sql/pglite": "0.5.8",
|
|
78
78
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
79
79
|
"@volter/world-access": "3.0.1",
|
|
80
|
-
"@volter/world-core": "3.0.
|
|
80
|
+
"@volter/world-core": "3.0.145",
|
|
81
81
|
"mongodb": "~6.20.0",
|
|
82
82
|
"pg-gateway": "0.3.0-beta.4",
|
|
83
83
|
"smol-toml": "^1.8.0",
|
package/src/cli-commands.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { dirname, join, resolve, resolve as resolvePath } 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, upWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from './index.ts';
|
|
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, upWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from './index.ts';
|
|
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.ts';
|
|
13
13
|
import type { WorldManifest } from './attach.ts';
|
|
@@ -61,7 +61,7 @@ function printHelp(): void {
|
|
|
61
61
|
# emission. Exit 0 = ready to \`up\`; exit 1 = the honest worklist.
|
|
62
62
|
# Deterministic: same repo + same pack catalog => identical bytes.
|
|
63
63
|
# init DIAGNOSES; it never boots — it prints the exact next commands
|
|
64
|
-
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>]
|
|
64
|
+
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>]
|
|
65
65
|
volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
|
|
66
66
|
volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
|
|
67
67
|
volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
|
|
@@ -468,6 +468,14 @@ export async function main(): Promise<void> {
|
|
|
468
468
|
if (!repo) throw new Error('volter-world covers: --repo <path> is required (the application repo to prove coverage for)');
|
|
469
469
|
const report = coverWorld(subject, repo, { root, allowUnknown: rest.includes('--allow-unknown'), acknowledge: acknowledgeOptions(rest, 'covers') });
|
|
470
470
|
process.stdout.write(rest.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatCoverageReport(report));
|
|
471
|
+
// `covers` is a reading. It keeps the proof it printed only as the plain verb on a World's own config: with --json
|
|
472
|
+
// it prints and writes nothing, and it never rewrites the config a running instance booted from. Whether it kept
|
|
473
|
+
// the proof is said on stderr, beside the report and outside its JSON.
|
|
474
|
+
if (!rest.includes('--json')) {
|
|
475
|
+
if (report.worldSource !== 'config') process.stderr.write(`coverage not recorded: ${report.world} was read from its instance (${report.worldPath}); the config it booted from is not rewritten\n`);
|
|
476
|
+
else if (recordCoverage(report, root)) process.stderr.write(`coverage recorded in ${report.configPath}\n`);
|
|
477
|
+
else 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`);
|
|
478
|
+
}
|
|
471
479
|
if (!report.ok) process.exit(1);
|
|
472
480
|
return;
|
|
473
481
|
}
|
package/src/covers.ts
CHANGED
|
@@ -26,14 +26,15 @@ import { errorMessage } from './error-message.ts';
|
|
|
26
26
|
import { existsSync, readFileSync } from 'node:fs';
|
|
27
27
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
-
import { installedPackageDir, packVendor, vendorOfPackModule } from '@volter/world-core/runtime';
|
|
30
|
-
import { loadWorldConfig } from './configs.ts';
|
|
29
|
+
import { getActiveWorldStore, installedPackageDir, packVendor, vendorOfPackModule } from '@volter/world-core/runtime';
|
|
30
|
+
import { loadWorldConfig, writeWorldConfig } from './configs.ts';
|
|
31
|
+
import { runtimeManifest } from './package-manifest.ts';
|
|
31
32
|
import { VENDOR_SERVER_BACKINGS } from './backings.ts';
|
|
32
33
|
import { injectableVendorKeys, loadInject, twinUrlVendorFor } from './inject-map.ts';
|
|
33
34
|
import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from './project-inspect.ts';
|
|
34
35
|
import { overlayCoversMaps, packFacts } from './pack-facts.ts';
|
|
35
36
|
import { statusWorld } from './runtime.ts';
|
|
36
|
-
import { ISSUE_ENV_PREFIX, worldSelectionIncludes, type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.ts';
|
|
37
|
+
import { ISSUE_ENV_PREFIX, worldSelectionIncludes, type WorldCoverage, type WorldCoverageEntry, type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.ts';
|
|
37
38
|
|
|
38
39
|
export type CoverageStatus = 'covered' | 'missing' | 'uninterceptable' | 'unknown-sdk' | 'acknowledged' | 'excluded';
|
|
39
40
|
|
|
@@ -54,6 +55,9 @@ export type CoverageReport = {
|
|
|
54
55
|
/** 'instance' when a booted/recorded instance was read; 'config' for the stable config */
|
|
55
56
|
worldSource: 'instance' | 'config';
|
|
56
57
|
worldPath: string;
|
|
58
|
+
/** The World's config, where the proof is kept (`recordCoverage`): `worldPath` itself for a config, the config
|
|
59
|
+
* an instance booted from otherwise. */
|
|
60
|
+
configPath: string;
|
|
57
61
|
repo: string;
|
|
58
62
|
twinsInWorld: string[];
|
|
59
63
|
rows: CoverageRow[];
|
|
@@ -348,6 +352,8 @@ type WorldTwinInventory = {
|
|
|
348
352
|
services: Array<WorldServiceConfig | WorldServiceInstance>;
|
|
349
353
|
source: 'instance' | 'config';
|
|
350
354
|
path: string;
|
|
355
|
+
/** The config the World is declared in: the one read, or the one its instance booted from. */
|
|
356
|
+
configPath: string;
|
|
351
357
|
/** normalized identifier → id record (e.g. 'stripe' → { label: 'stripe (service)', … }) */
|
|
352
358
|
ids: Map<string, WorldTwinId>;
|
|
353
359
|
connections: Map<string, { protocol: string | null; label: string }>;
|
|
@@ -593,7 +599,7 @@ export function worldTwinInventory(world: string, root: string): WorldTwinInvent
|
|
|
593
599
|
// a bare env `*_TWIN_URL` var is only a REACHABLE twin when the injector reads it
|
|
594
600
|
if (stem) addId(ids, normalizeId(stem[1]!), `env ${key}`, twinUrlVendorFor(key) !== null);
|
|
595
601
|
}
|
|
596
|
-
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) };
|
|
602
|
+
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) };
|
|
597
603
|
} catch (error) {
|
|
598
604
|
instanceError = errorMessage(error);
|
|
599
605
|
}
|
|
@@ -608,7 +614,7 @@ export function worldTwinInventory(world: string, root: string): WorldTwinInvent
|
|
|
608
614
|
for (const [name, value] of Object.entries(config.env ?? {})) {
|
|
609
615
|
if (value.startsWith(ISSUE_ENV_PREFIX)) connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
|
|
610
616
|
}
|
|
611
|
-
return { services: config.services, source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services, undefined, root) };
|
|
617
|
+
return { services: config.services, source: 'config', path, configPath: path, ids, connections, selection: config.selection, declared: declaredTwin(config.services, undefined, root) };
|
|
612
618
|
} catch (error) {
|
|
613
619
|
const configError = errorMessage(error);
|
|
614
620
|
throw new Error(
|
|
@@ -1023,6 +1029,7 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
|
|
|
1023
1029
|
world,
|
|
1024
1030
|
worldSource: inventory.source,
|
|
1025
1031
|
worldPath: inventory.path,
|
|
1032
|
+
configPath: inventory.configPath,
|
|
1026
1033
|
repo,
|
|
1027
1034
|
twinsInWorld: [...inventory.ids.values()].map((id) => id.label).sort(),
|
|
1028
1035
|
rows,
|
|
@@ -1037,6 +1044,47 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
|
|
|
1037
1044
|
};
|
|
1038
1045
|
}
|
|
1039
1046
|
|
|
1047
|
+
/**
|
|
1048
|
+
* The report as the data `init` keeps in the config (`discovery.coverage`): the same rows `formatCoverageReport`
|
|
1049
|
+
* prints, grouped by the status each verdict line counts. Every push to `covered`, `missing`, `uninterceptable`,
|
|
1050
|
+
* `unknown`, `acknowledged` and `excluded` in `coverWorld` adds the row of that status, so the lists here name what
|
|
1051
|
+
* those lines name. Paths (`worldPath`, `configPath`, `repo`) stay out: the config is portable and init's bytes are deterministic.
|
|
1052
|
+
*/
|
|
1053
|
+
export function coverageRecord(report: CoverageReport): WorldCoverage {
|
|
1054
|
+
const entry = (row: CoverageRow): Omit<WorldCoverageEntry, 'vendor'> => ({
|
|
1055
|
+
evidence: row.detectedVia,
|
|
1056
|
+
...(row.connection === undefined ? {} : { connection: row.connection }),
|
|
1057
|
+
...(row.twinInWorld === null ? {} : { twinInWorld: row.twinInWorld }),
|
|
1058
|
+
});
|
|
1059
|
+
const of = (status: CoverageStatus): WorldCoverageEntry[] =>
|
|
1060
|
+
report.rows.filter((row) => row.status === status).map((row) => ({ vendor: row.vendor, ...entry(row) }));
|
|
1061
|
+
return {
|
|
1062
|
+
producer: { package: '@volter/world-runtime', version: runtimeManifest().version ?? 'unversioned' },
|
|
1063
|
+
ok: report.ok,
|
|
1064
|
+
covered: of('covered'),
|
|
1065
|
+
notCovered: of('missing'),
|
|
1066
|
+
uninterceptable: of('uninterceptable'),
|
|
1067
|
+
unknownSdk: report.rows.filter((row) => row.status === 'unknown-sdk').map((row) => ({ signal: row.vendor, ...entry(row) })),
|
|
1068
|
+
acknowledged: of('acknowledged').map((row) => ({ ...row, reason: report.acknowledged.find((held) => held.vendor === row.vendor)!.reason })),
|
|
1069
|
+
excludedBySelection: of('excluded'),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Keep a proof just made in the World's config, replacing the record of an earlier one. It answers true when it
|
|
1075
|
+
* wrote the record and false when it wrote nothing: a format-1 document has no `discovery` section and a World whose
|
|
1076
|
+
* config is no longer on disk has nowhere to keep it. The caller decides whether to record at all and says the
|
|
1077
|
+
* answer (`init` writes its own record into the config it makes): `covers` records only as the plain verb on a
|
|
1078
|
+
* World's own config, never with --json and never into the config a running instance booted from (cli-commands.ts).
|
|
1079
|
+
*/
|
|
1080
|
+
export function recordCoverage(report: CoverageReport, root = process.cwd()): boolean {
|
|
1081
|
+
if (!getActiveWorldStore().exists(report.configPath)) return false;
|
|
1082
|
+
const { path, config } = loadWorldConfig(report.configPath, root);
|
|
1083
|
+
if (config.manifestVersion !== 2) return false;
|
|
1084
|
+
writeWorldConfig(path, { ...config, coverage: coverageRecord(report) });
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1040
1088
|
export function formatCoverageReport(report: CoverageReport): string {
|
|
1041
1089
|
const lines: string[] = [];
|
|
1042
1090
|
lines.push(`World ${report.world} (${report.worldSource}: ${report.worldPath})`);
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type {
|
|
|
13
13
|
WorldRunRecord,
|
|
14
14
|
WorldResourceRequirements,
|
|
15
15
|
WorldManifestV2,
|
|
16
|
+
WorldCoverage,
|
|
17
|
+
WorldCoverageEntry,
|
|
16
18
|
WorldSelection,
|
|
17
19
|
WorldSelectionSelector,
|
|
18
20
|
WorldUsage,
|
|
@@ -87,11 +89,13 @@ export { fakeEnvValue, fakeGoogleOAuthClientJson, fakeGoogleServiceAccountJson,
|
|
|
87
89
|
export type { FakeServiceAccountOptions } from './fixture-env.ts';
|
|
88
90
|
export {
|
|
89
91
|
coverWorld,
|
|
92
|
+
coverageRecord,
|
|
90
93
|
detectRepoVendors,
|
|
91
94
|
envNameVendor,
|
|
92
95
|
formatCoverageReport,
|
|
93
96
|
injectorVendorKeysFor,
|
|
94
97
|
isCredentialShapedEnvName,
|
|
98
|
+
recordCoverage,
|
|
95
99
|
worldTwinInventory,
|
|
96
100
|
} from './covers.ts';
|
|
97
101
|
export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.ts';
|
package/src/infra.ts
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.ts';
|
|
15
|
+
import { containerlessDataDirectory, infraConnections, liveDatabaseBehind, liveDatabaseMigration, developmentMigratesAtRuntime, DEVELOPMENT_MIGRATION_NOTICE, parseInfraDefinition } from './pglite-backing.ts';
|
|
16
16
|
import { errorMessage } from './error-message.ts';
|
|
17
17
|
|
|
18
18
|
/** Whether a program is on PATH, read from the file system without starting a process: where a missing program
|
|
@@ -77,6 +77,11 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
77
77
|
// prepare: the image build's step that makes the World's PGlite cluster once, on its host, so the World's first up
|
|
78
78
|
// opens it instead of running initdb (pglite-backing.ts, pglitePrepare; ADR 0018). It needs the World's
|
|
79
79
|
// definition, not an instance.
|
|
80
|
+
// A backing the World's environment forces is said where the backing is said, and in the status fact: forcing one
|
|
81
|
+
// is a choice someone made for this World, and it is never silent.
|
|
82
|
+
const forcedBy = (backing: 'docker' | 'pglite' | 'native'): string => env.VOLTER_WORLD_INFRA_BACKING === backing ? ' (forced by VOLTER_WORLD_INFRA_BACKING)' : '';
|
|
83
|
+
const backingFact = (backing: 'docker' | 'pglite' | 'native'): { backing: { is: string; by: 'forced' | 'chosen' } } =>
|
|
84
|
+
({ backing: { is: backing, by: env.VOLTER_WORLD_INFRA_BACKING === backing ? 'forced' : 'chosen' } });
|
|
80
85
|
if (phase === 'prepare') {
|
|
81
86
|
const config = env.VOLTER_WORLD_CONFIG;
|
|
82
87
|
const declaredDefinition = config ? join(dirname(config), 'world.infrastructure.yml') : undefined;
|
|
@@ -90,11 +95,23 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
90
95
|
return 0;
|
|
91
96
|
}
|
|
92
97
|
const { pglitePrepare } = await import('./pglite-backing.ts');
|
|
98
|
+
const { nativePrepare, nativePostgresTools } = await import('./native-postgres-backing.ts');
|
|
99
|
+
// The backing up would choose for this PostgreSQL: native where the PostgreSQL programs are (containerless rule
|
|
100
|
+
// 2; in a tab image, the tab's own build), PGlite otherwise or when forced.
|
|
101
|
+
const native = env.VOLTER_WORLD_INFRA_BACKING !== 'pglite' && nativePostgresTools(env) !== undefined;
|
|
102
|
+
// The database prepare step's own output (prisma's "N migrations found … No pending migrations") is the
|
|
103
|
+
// preparation's record, said where its caller reads it.
|
|
104
|
+
const launch = { env, ...(signal ? { signal } : {}), note: (what: string) => io.out(`${what.trimEnd()}\n`) };
|
|
93
105
|
try {
|
|
94
|
-
|
|
106
|
+
const prepared = native
|
|
107
|
+
? await nativePrepare(postgres[0]!, readFileSync(declaredDefinition, 'utf8'), config, launch)
|
|
108
|
+
: await pglitePrepare(postgres[0]!, config, launch);
|
|
109
|
+
io.out(`managed infrastructure prepared ${prepared} (backing: ${native ? 'native PostgreSQL' : 'PGlite'}${forcedBy(native ? 'native' : 'pglite')})\n`);
|
|
95
110
|
return 0;
|
|
96
111
|
} catch (error) {
|
|
97
112
|
io.err(`managed infrastructure prepare failed: ${errorMessage(error)}\n`);
|
|
113
|
+
// Said where the person or agent reading the failure stands: the fix is here, in the preparation.
|
|
114
|
+
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');
|
|
98
115
|
return 1;
|
|
99
116
|
}
|
|
100
117
|
}
|
|
@@ -110,7 +127,19 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
110
127
|
io.err('managed infrastructure: declared definition is missing\n');
|
|
111
128
|
return 2;
|
|
112
129
|
}
|
|
130
|
+
// A development World says so every time it comes up: its use is never silent.
|
|
131
|
+
if (phase === 'up' && developmentMigratesAtRuntime(worldConfig)) io.out(`${DEVELOPMENT_MIGRATION_NOTICE}\n`);
|
|
113
132
|
|
|
133
|
+
// What the app's commands are told of the World's database (architecture, managed infrastructure): the declared
|
|
134
|
+
// step's command while the live data carries the migration the image expects, the same command when the World
|
|
135
|
+
// 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.
|
|
136
|
+
let postgresData: string | undefined;
|
|
137
|
+
const databaseFacts = (): { database?: { migrated?: string[]; behind?: string[] }; development?: { migrateDatabaseAtRuntime: true } } => {
|
|
138
|
+
const migrated = postgresData ? liveDatabaseMigration(worldConfig, env, postgresData) : undefined;
|
|
139
|
+
const behind = postgresData ? liveDatabaseBehind(worldConfig, env, postgresData) : undefined;
|
|
140
|
+
return { ...(migrated || behind ? { database: { ...(migrated ? { migrated } : {}), ...(behind ? { behind } : {}) } } : {}),
|
|
141
|
+
...(developmentMigratesAtRuntime(worldConfig) ? { development: { migrateDatabaseAtRuntime: true as const } } : {}) };
|
|
142
|
+
};
|
|
114
143
|
const base = ['compose', '-f', definition];
|
|
115
144
|
const run = async (args: string[]): Promise<Ran> => {
|
|
116
145
|
const ran = await runCommand('docker', [...base, ...args], { env: { ...env, VOLTER_WORLD_DATA: worldData }, timeout: 120_000, ...(signal ? { signal } : {}) });
|
|
@@ -216,7 +245,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
216
245
|
return 1;
|
|
217
246
|
}
|
|
218
247
|
if (phase === 'up') {
|
|
219
|
-
io.out(
|
|
248
|
+
io.out(`managed infrastructure backing: containerless${forcedBy('pglite') || ' (no container runtime)'}\n`);
|
|
220
249
|
if (services.some((s) => s.kind === 'postgres')) {
|
|
221
250
|
io.out('managed infrastructure: PostgreSQL is PGlite, one session every connection takes in turn; a read through '
|
|
222
251
|
+ 'the pool inside a transaction waits on itself until that transaction ends. A postgres server on PATH, or '
|
|
@@ -233,11 +262,12 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
233
262
|
}
|
|
234
263
|
if (phase === 'status') {
|
|
235
264
|
const status = await pgliteStatus(services, worldData!);
|
|
265
|
+
if (services.some((s) => s.kind === 'postgres')) postgresData = join(worldData!, containerlessDataDirectory('postgres')!);
|
|
236
266
|
if (!status.ok) {
|
|
237
267
|
io.err(`managed infrastructure status failed: ${status.ready}/${services.length} declared services are ready\n`);
|
|
238
268
|
return 1;
|
|
239
269
|
}
|
|
240
|
-
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
|
|
270
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('pglite'), ...databaseFacts() })}\n`);
|
|
241
271
|
return 0;
|
|
242
272
|
}
|
|
243
273
|
await pgliteDown(services, worldData!);
|
|
@@ -298,7 +328,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
298
328
|
}
|
|
299
329
|
if (composed?.length === 0) {
|
|
300
330
|
if (phase === 'up') io.out('managed infrastructure ready\n');
|
|
301
|
-
else if (phase === 'status') io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
|
|
331
|
+
else if (phase === 'status') io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() })}\n`);
|
|
302
332
|
else io.out('managed infrastructure stopped\n');
|
|
303
333
|
return 0;
|
|
304
334
|
}
|
|
@@ -317,7 +347,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
317
347
|
}
|
|
318
348
|
try {
|
|
319
349
|
if (phase === 'up') {
|
|
320
|
-
io.out(
|
|
350
|
+
io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')}\n`);
|
|
321
351
|
await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
|
|
322
352
|
try { await pgliteUp(others, worldData, launch); }
|
|
323
353
|
catch (error) {
|
|
@@ -329,7 +359,8 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
329
359
|
} else if (phase === 'status') {
|
|
330
360
|
const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
|
|
331
361
|
if (!ready || !await nativePostgresStatus(worldData)) return 1;
|
|
332
|
-
|
|
362
|
+
postgresData = join(worldData, 'native-postgres-data');
|
|
363
|
+
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`);
|
|
333
364
|
} else {
|
|
334
365
|
await pgliteDown(others, worldData);
|
|
335
366
|
await nativePostgresDown(worldData);
|
|
@@ -345,6 +376,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
345
376
|
|
|
346
377
|
const only = composed ?? [];
|
|
347
378
|
if (phase === 'up') {
|
|
379
|
+
if (forcedBy('docker')) io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
|
|
348
380
|
const result = await run(['up', '-d', '--wait', ...only]);
|
|
349
381
|
if (result.status !== 0) return fail(result);
|
|
350
382
|
io.out('managed infrastructure ready\n');
|
|
@@ -359,7 +391,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
359
391
|
io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length + native.length}/${expectedNames.length + twinned.length + native.length} declared services are ready\n`);
|
|
360
392
|
return 1;
|
|
361
393
|
}
|
|
362
|
-
io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env) })}\n`);
|
|
394
|
+
io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env), ...backingFact('docker'), ...databaseFacts() })}\n`);
|
|
363
395
|
} else {
|
|
364
396
|
const result = await run(['down', '--remove-orphans']);
|
|
365
397
|
if (result.status !== 0) return fail(result);
|