@volter/world-runtime 2.0.15 → 2.0.17

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.
@@ -1,185 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Private implementation for infrastructure emitted by `volter-world init`. World configs invoke
3
- // this declared service; application agents never operate or diagnose its backing mechanism.
4
- import { existsSync, readFileSync } from 'node:fs';
5
- import { dirname, join } from 'node:path';
6
- import { spawnSync } from 'node:child_process';
7
- import { fileURLToPath } from 'node:url';
8
- import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
9
- const phase = process.argv[2];
10
- if (phase !== 'up' && phase !== 'status' && phase !== 'down') {
11
- process.stderr.write('managed infrastructure: expected up, status, or down\n');
12
- process.exit(2);
13
- }
14
- const worldConfig = process.env.VOLTER_WORLD_CONFIG;
15
- const worldData = process.env.VOLTER_WORLD_DATA;
16
- if (!worldConfig || !worldData) {
17
- process.stderr.write('managed infrastructure: missing World lifecycle context\n');
18
- process.exit(2);
19
- }
20
- const definition = join(dirname(worldConfig), 'world.infrastructure.yml');
21
- if (!existsSync(definition)) {
22
- process.stderr.write('managed infrastructure: declared definition is missing\n');
23
- process.exit(2);
24
- }
25
- const base = ['compose', '-f', definition];
26
- const run = (args) => spawnSync('docker', [...base, ...args], { windowsHide: true,
27
- encoding: 'utf8',
28
- env: { ...process.env, VOLTER_WORLD_DATA: worldData },
29
- timeout: 120_000,
2
+ // `volter-world-infra up|status|down`: the World-managed infrastructure's declared lifecycle commands (infra.ts), as
3
+ // a process. World configs invoke this declared service; application agents never operate or diagnose its backing.
4
+ import { runInfrastructure } from "./infra.js";
5
+ const code = await runInfrastructure(process.argv[2] ?? '', process.env, {
6
+ out: (text) => { process.stdout.write(text); },
7
+ err: (text) => { process.stderr.write(text); },
30
8
  });
31
- const classify = (raw) => /no space left on device|enospc/iu.test(raw)
32
- ? 'insufficient writable storage'
33
- : /out of memory|cannot allocate memory|killed/iu.test(raw)
34
- ? 'insufficient memory'
35
- : /cannot connect|not running|daemon/iu.test(raw)
36
- ? 'local execution capacity is unavailable'
37
- : 'managed infrastructure operation failed';
38
- const fail = (result) => {
39
- const raw = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
40
- process.stderr.write(`managed infrastructure ${phase} failed: ${classify(raw)}\n`);
41
- process.exit(result.status && result.status > 0 ? result.status : 1);
42
- };
43
- // ---- backing selection ------------------------------------------------------
44
- // The declared service contract (up/status/down) is the boundary; WHICH
45
- // runtime answers it is private and chosen here, per machine, at each phase:
46
- // 1. VOLTER_WORLD_INFRA_BACKING=docker|pglite — explicit, for tests/operators;
47
- // 2. a working container runtime — the compose path, byte-identical to before;
48
- // 3. no container runtime + a definition of postgres and/or mongodb services —
49
- // the containerless backing (pglite-backing.ts: PGlite for postgres, the
50
- // MongoDB twin for mongodb), announced loudly;
51
- // 4. otherwise the honest refusal naming what this machine cannot serve.
52
- function selectBacking() {
53
- const forced = process.env.VOLTER_WORLD_INFRA_BACKING;
54
- if (forced === 'docker' || forced === 'pglite')
55
- return forced;
56
- if (forced !== undefined) {
57
- process.stderr.write(`managed infrastructure: VOLTER_WORLD_INFRA_BACKING must be docker or pglite (got ${JSON.stringify(forced)})\n`);
58
- process.exit(2);
59
- }
60
- const probe = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], { windowsHide: true, encoding: 'utf8', timeout: 10_000 });
61
- if (probe.status === 0)
62
- return 'docker';
63
- // Absence of a container runtime (no binary, no daemon) is a capability
64
- // difference: swap backings. A PRESENT runtime failing on resources stays
65
- // on the docker path so the real capacity problem is classified and
66
- // surfaced — a full disk starves every backing equally.
67
- if (probe.error !== undefined)
68
- return 'pglite';
69
- const reason = classify(`${probe.stdout ?? ''}\n${probe.stderr ?? ''}`);
70
- return reason === 'local execution capacity is unavailable' ? 'pglite' : 'docker';
71
- }
72
- async function runPglite() {
73
- const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
74
- const services = parseInfraDefinition(readFileSync(definition, 'utf8')).filter((s) => composed === null || composed.includes(s.kind));
75
- if (services.length === 0) {
76
- process.stderr.write('managed infrastructure: the declared definition names no services\n');
77
- process.exit(1);
78
- }
79
- const unsupported = unsupportedKinds(services);
80
- if (unsupported.length > 0) {
81
- process.stderr.write(`managed infrastructure ${phase} failed: local execution capacity is unavailable (no container runtime, and the containerless backing cannot serve: ${unsupported.join(', ')})\n`);
82
- process.exit(1);
83
- }
84
- if (phase === 'up') {
85
- process.stdout.write('managed infrastructure backing: containerless (no container runtime)\n');
86
- try {
87
- await pgliteUp(services, worldData);
88
- }
89
- catch (error) {
90
- process.stderr.write(`managed infrastructure up failed: ${String(error.message ?? error)}\n`);
91
- process.exit(1);
92
- }
93
- process.stdout.write('managed infrastructure ready\n');
94
- process.exit(0);
95
- }
96
- if (phase === 'status') {
97
- const status = await pgliteStatus(services, worldData);
98
- if (!status.ok) {
99
- process.stderr.write(`managed infrastructure status failed: ${status.ready}/${services.length} declared services are ready\n`);
100
- process.exit(1);
101
- }
102
- process.stdout.write(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, process.env) })}\n`);
103
- process.exit(0);
104
- }
105
- await pgliteDown(services, worldData);
106
- process.stdout.write('managed infrastructure stopped\n');
107
- process.exit(0);
108
- }
109
- // ---- redis: the twin, containerless ----------------------------------------
110
- // A redis service is served by the redis twin (redis-backing.ts) on every machine, container runtime or not,
111
- // unless the operator forces the container backing; the other services take the backing chosen above. Where the
112
- // twin is not installed (@volter/twin-redis is not this runtime's dependency), redis goes to that backing too:
113
- // the container where there is one, else the backing's refusal naming redis.
114
- const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
115
- const { redisTwinCli } = await import("./redis-backing.js");
116
- const twinCli = redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)]);
117
- const twinned = process.env.VOLTER_WORLD_INFRA_BACKING === 'docker' || twinCli === undefined ? [] : declared.filter((s) => s.kind === 'redis');
118
- /** The services the backing below answers for: every one, or those the twin does not serve (by compose service name). */
119
- const composed = twinned.length === 0 ? null : declared.filter((s) => s.kind !== 'redis').map((s) => s.kind);
120
- if (twinned.length > 0)
121
- await runRedisTwin();
122
- async function runRedisTwin() {
123
- const { redisTwinDown, redisTwinStatus, redisTwinUp } = await import("./redis-backing.js");
124
- const cli = twinCli;
125
- if (phase === 'up') {
126
- process.stdout.write('managed infrastructure backing: redis twin (containerless)\n');
127
- try {
128
- await redisTwinUp(twinned, worldData, cli);
129
- }
130
- catch (error) {
131
- process.stderr.write(`managed infrastructure up failed: ${String(error.message ?? error)}\n`);
132
- process.exit(1);
133
- }
134
- }
135
- else if (phase === 'status') {
136
- const ready = await redisTwinStatus(twinned, worldData);
137
- if (ready !== twinned.length) {
138
- process.stderr.write(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
139
- process.exit(1);
140
- }
141
- }
142
- else {
143
- await redisTwinDown(twinned, worldData);
144
- }
145
- if (composed.length === 0) {
146
- if (phase === 'up')
147
- process.stdout.write('managed infrastructure ready\n');
148
- else if (phase === 'status')
149
- process.stdout.write(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, process.env) })}\n`);
150
- else
151
- process.stdout.write('managed infrastructure stopped\n');
152
- process.exit(0);
153
- }
154
- }
155
- if (selectBacking() === 'pglite') {
156
- await runPglite();
157
- }
158
- const only = composed ?? [];
159
- if (phase === 'up') {
160
- const result = run(['up', '-d', '--wait', ...only]);
161
- if (result.status !== 0)
162
- fail(result);
163
- process.stdout.write('managed infrastructure ready\n');
164
- }
165
- else if (phase === 'status') {
166
- const expected = run(['config', '--services']);
167
- if (expected.status !== 0)
168
- fail(expected);
169
- const running = run(['ps', '--status', 'running', '--services']);
170
- if (running.status !== 0)
171
- fail(running);
172
- const expectedNames = expected.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
173
- const runningNames = running.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
174
- if (expectedNames.length === 0 || expectedNames.join('\0') !== runningNames.join('\0')) {
175
- process.stderr.write(`managed infrastructure status failed: ${runningNames.length + twinned.length}/${expectedNames.length + twinned.length} declared services are ready\n`);
176
- process.exit(1);
177
- }
178
- process.stdout.write(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length, connections: infraConnections(declared, process.env) })}\n`);
179
- }
180
- else {
181
- const result = run(['down', '--remove-orphans']);
182
- if (result.status !== 0)
183
- fail(result);
184
- process.stdout.write('managed infrastructure stopped\n');
185
- }
9
+ process.exit(code);
@@ -0,0 +1,7 @@
1
+ export interface InfrastructureIo {
2
+ out: (text: string) => void;
3
+ err: (text: string) => void;
4
+ }
5
+ /** One phase of the World-managed infrastructure. `env` is the infrastructure service's environment, which the
6
+ * services this starts run in; `signal` is the boot's abort (the World's own up passes it; the tool has none). */
7
+ export declare function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, io: InfrastructureIo, signal?: AbortSignal): Promise<number>;
@@ -0,0 +1,237 @@
1
+ // Private implementation for infrastructure emitted by `volter-world init`. World configs invoke
2
+ // this declared service; application agents never operate or diagnose its backing mechanism.
3
+ //
4
+ // One phase (up, status, down) per call, answering its exit code, with its output written through `io`. The
5
+ // `volter-world-infra` bin (infra-cli.ts) runs it as a process; the World's own `up` runs the up phase in its own
6
+ // process (runtime.ts, `ownInfrastructureUp`), so the containerless services it starts are children of the process
7
+ // that owns the World's lifetime, as every other service's recorder is, and not of a tool that exits once they
8
+ // serve. A process layer where a child does not outlive its parent (a browser tab's) otherwise ended the PGlite host
9
+ // the moment `volter-world-infra up` returned, and the World's readiness probe never saw it.
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { dirname, join } from 'node:path';
12
+ import { spawn } from 'node:child_process';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
15
+ /** A command run to its end without blocking this process: in the World's own `up` the phase shares the event loop
16
+ * with the boot's signal handling, where the tool's spawnSync blocked only the tool. Ended by `timeout` (SIGTERM,
17
+ * as spawnSync's timeout) or by the boot's abort. */
18
+ function runCommand(command, args, options) {
19
+ return new Promise((resolveRan) => {
20
+ let stdout = '';
21
+ let stderr = '';
22
+ let settled = false;
23
+ let grace;
24
+ const child = spawn(command, args, { windowsHide: true, env: options.env, stdio: ['ignore', 'pipe', 'pipe'], ...(options.signal ? { signal: options.signal } : {}) });
25
+ const settle = (ran) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ clearTimeout(timer);
30
+ if (grace)
31
+ clearTimeout(grace);
32
+ // a grandchild that still holds the pipes does not hold this answer
33
+ child.stdout?.destroy();
34
+ child.stderr?.destroy();
35
+ resolveRan(ran);
36
+ };
37
+ // the timeout is a hard limit and answers as spawnSync's did: the command is killed and the result carries an
38
+ // ETIMEDOUT error (a hung `docker info`, a daemon that never answers, is then no container runtime, as before)
39
+ const timer = setTimeout(() => {
40
+ child.kill('SIGTERM');
41
+ settle({ status: null, stdout, stderr, error: Object.assign(new Error(`spawn ${command} ETIMEDOUT`), { code: 'ETIMEDOUT' }) });
42
+ }, options.timeout);
43
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
44
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
45
+ // a spawn that fails (no such binary, an abort) answers as spawnSync did: no status, the error
46
+ child.on('error', (error) => settle({ status: null, stdout, stderr, error }));
47
+ // the output is whole at `close`; once the command has exited, a descendant holding its pipes open delays the
48
+ // answer by at most a moment, never to the timeout
49
+ child.on('exit', (code) => { grace = setTimeout(() => settle({ status: code, stdout, stderr }), 500); });
50
+ child.on('close', (code) => settle({ status: code, stdout, stderr }));
51
+ });
52
+ }
53
+ /** One phase of the World-managed infrastructure. `env` is the infrastructure service's environment, which the
54
+ * services this starts run in; `signal` is the boot's abort (the World's own up passes it; the tool has none). */
55
+ export async function runInfrastructure(phase, env, io, signal) {
56
+ if (phase !== 'up' && phase !== 'status' && phase !== 'down') {
57
+ io.err('managed infrastructure: expected up, status, or down\n');
58
+ return 2;
59
+ }
60
+ const worldConfig = env.VOLTER_WORLD_CONFIG;
61
+ const worldData = env.VOLTER_WORLD_DATA;
62
+ if (!worldConfig || !worldData) {
63
+ io.err('managed infrastructure: missing World lifecycle context\n');
64
+ return 2;
65
+ }
66
+ const definition = join(dirname(worldConfig), 'world.infrastructure.yml');
67
+ if (!existsSync(definition)) {
68
+ io.err('managed infrastructure: declared definition is missing\n');
69
+ return 2;
70
+ }
71
+ const base = ['compose', '-f', definition];
72
+ const run = async (args) => {
73
+ const ran = await runCommand('docker', [...base, ...args], { env: { ...env, VOLTER_WORLD_DATA: worldData }, timeout: 120_000, ...(signal ? { signal } : {}) });
74
+ signal?.throwIfAborted();
75
+ return ran;
76
+ };
77
+ const launch = { env, ...(signal ? { signal } : {}) };
78
+ const classify = (raw) => /no space left on device|enospc/iu.test(raw)
79
+ ? 'insufficient writable storage'
80
+ : /out of memory|cannot allocate memory|killed/iu.test(raw)
81
+ ? 'insufficient memory'
82
+ : /cannot connect|not running|daemon/iu.test(raw)
83
+ ? 'local execution capacity is unavailable'
84
+ : 'managed infrastructure operation failed';
85
+ const fail = (result) => {
86
+ const raw = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
87
+ io.err(`managed infrastructure ${phase} failed: ${classify(raw)}\n`);
88
+ return result.status && result.status > 0 ? result.status : 1;
89
+ };
90
+ // ---- backing selection ------------------------------------------------------
91
+ // The declared service contract (up/status/down) is the boundary; WHICH
92
+ // runtime answers it is private and chosen here, per machine, at each phase:
93
+ // 1. VOLTER_WORLD_INFRA_BACKING=docker|pglite — explicit, for tests/operators;
94
+ // 2. a working container runtime — the compose path, byte-identical to before;
95
+ // 3. no container runtime + a definition of postgres and/or mongodb services —
96
+ // the containerless backing (pglite-backing.ts: PGlite for postgres, the
97
+ // MongoDB twin for mongodb), announced loudly;
98
+ // 4. otherwise the honest refusal naming what this machine cannot serve.
99
+ async function selectBacking() {
100
+ const forced = env.VOLTER_WORLD_INFRA_BACKING;
101
+ if (forced === 'docker' || forced === 'pglite')
102
+ return forced;
103
+ if (forced !== undefined) {
104
+ io.err(`managed infrastructure: VOLTER_WORLD_INFRA_BACKING must be docker or pglite (got ${JSON.stringify(forced)})\n`);
105
+ return 2;
106
+ }
107
+ const probe = await runCommand('docker', ['info', '--format', '{{.ServerVersion}}'], { env, timeout: 10_000, ...(signal ? { signal } : {}) });
108
+ signal?.throwIfAborted();
109
+ if (probe.status === 0)
110
+ return 'docker';
111
+ // Absence of a container runtime (no binary, no daemon) is a capability
112
+ // difference: swap backings. A PRESENT runtime failing on resources stays
113
+ // on the docker path so the real capacity problem is classified and
114
+ // surfaced — a full disk starves every backing equally.
115
+ // A shell's "command not found" (127) is the same absence: a process layer that runs commands by name answers
116
+ // an unknown one so, where spawnSync answered ENOSYS (a browser tab's).
117
+ if (probe.error !== undefined || probe.status === 127)
118
+ return 'pglite';
119
+ const reason = classify(`${probe.stdout ?? ''}\n${probe.stderr ?? ''}`);
120
+ return reason === 'local execution capacity is unavailable' ? 'pglite' : 'docker';
121
+ }
122
+ async function runPglite() {
123
+ const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import("./pglite-backing.js");
124
+ const services = parseInfraDefinition(readFileSync(definition, 'utf8')).filter((s) => composed === null || composed.includes(s.kind));
125
+ if (services.length === 0) {
126
+ io.err('managed infrastructure: the declared definition names no services\n');
127
+ return 1;
128
+ }
129
+ const unsupported = unsupportedKinds(services);
130
+ if (unsupported.length > 0) {
131
+ io.err(`managed infrastructure ${phase} failed: local execution capacity is unavailable (no container runtime, and the containerless backing cannot serve: ${unsupported.join(', ')})\n`);
132
+ return 1;
133
+ }
134
+ if (phase === 'up') {
135
+ io.out('managed infrastructure backing: containerless (no container runtime)\n');
136
+ try {
137
+ await pgliteUp(services, worldData, launch);
138
+ }
139
+ catch (error) {
140
+ io.err(`managed infrastructure up failed: ${String(error.message ?? error)}\n`);
141
+ return 1;
142
+ }
143
+ io.out('managed infrastructure ready\n');
144
+ return 0;
145
+ }
146
+ if (phase === 'status') {
147
+ const status = await pgliteStatus(services, worldData);
148
+ if (!status.ok) {
149
+ io.err(`managed infrastructure status failed: ${status.ready}/${services.length} declared services are ready\n`);
150
+ return 1;
151
+ }
152
+ io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
153
+ return 0;
154
+ }
155
+ await pgliteDown(services, worldData);
156
+ io.out('managed infrastructure stopped\n');
157
+ return 0;
158
+ }
159
+ // ---- redis: the twin, containerless ----------------------------------------
160
+ // A redis service is served by the redis twin (redis-backing.ts) on every machine, container runtime or not,
161
+ // unless the operator forces the container backing; the other services take the backing chosen above. Where the
162
+ // twin is not installed (@volter/twin-redis is not this runtime's dependency), redis goes to that backing too:
163
+ // the container where there is one, else the backing's refusal naming redis.
164
+ const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
165
+ const { redisTwinCli } = await import("./redis-backing.js");
166
+ const twinCli = redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)]);
167
+ const twinned = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || twinCli === undefined ? [] : declared.filter((s) => s.kind === 'redis');
168
+ /** The services the backing below answers for: every one, or those the twin does not serve (by compose service name). */
169
+ const composed = twinned.length === 0 ? null : declared.filter((s) => s.kind !== 'redis').map((s) => s.kind);
170
+ if (twinned.length > 0) {
171
+ const { redisTwinDown, redisTwinStatus, redisTwinUp } = await import("./redis-backing.js");
172
+ const cli = twinCli;
173
+ if (phase === 'up') {
174
+ io.out('managed infrastructure backing: redis twin (containerless)\n');
175
+ try {
176
+ await redisTwinUp(twinned, worldData, cli, launch);
177
+ }
178
+ catch (error) {
179
+ io.err(`managed infrastructure up failed: ${String(error.message ?? error)}\n`);
180
+ return 1;
181
+ }
182
+ }
183
+ else if (phase === 'status') {
184
+ const ready = await redisTwinStatus(twinned, worldData);
185
+ if (ready !== twinned.length) {
186
+ io.err(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
187
+ return 1;
188
+ }
189
+ }
190
+ else {
191
+ await redisTwinDown(twinned, worldData);
192
+ }
193
+ if (composed.length === 0) {
194
+ if (phase === 'up')
195
+ io.out('managed infrastructure ready\n');
196
+ else if (phase === 'status')
197
+ io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
198
+ else
199
+ io.out('managed infrastructure stopped\n');
200
+ return 0;
201
+ }
202
+ }
203
+ const backing = await selectBacking();
204
+ if (typeof backing === 'number')
205
+ return backing;
206
+ if (backing === 'pglite')
207
+ return runPglite();
208
+ const only = composed ?? [];
209
+ if (phase === 'up') {
210
+ const result = await run(['up', '-d', '--wait', ...only]);
211
+ if (result.status !== 0)
212
+ return fail(result);
213
+ io.out('managed infrastructure ready\n');
214
+ }
215
+ else if (phase === 'status') {
216
+ const expected = await run(['config', '--services']);
217
+ if (expected.status !== 0)
218
+ return fail(expected);
219
+ const running = await run(['ps', '--status', 'running', '--services']);
220
+ if (running.status !== 0)
221
+ return fail(running);
222
+ const expectedNames = expected.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
223
+ const runningNames = running.stdout.split(/\s+/u).filter(Boolean).filter((n) => composed === null || composed.includes(n)).sort();
224
+ if (expectedNames.length === 0 || expectedNames.join('\0') !== runningNames.join('\0')) {
225
+ io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length}/${expectedNames.length + twinned.length} declared services are ready\n`);
226
+ return 1;
227
+ }
228
+ io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length, connections: infraConnections(declared, env) })}\n`);
229
+ }
230
+ else {
231
+ const result = await run(['down', '--remove-orphans']);
232
+ if (result.status !== 0)
233
+ return fail(result);
234
+ io.out('managed infrastructure stopped\n');
235
+ }
236
+ return 0;
237
+ }
@@ -19,9 +19,18 @@ export declare function mongodbTwinCli(worldConfig?: string, froms?: string[]):
19
19
  * stripping, so it runs under Bun (this process's, or the one on PATH) — the repository's toolchain —
20
20
  * and without Bun it is refused by name rather than started to fail. */
21
21
  export declare function twinRunner(cli: string, bunOnPath?: () => boolean, underBun?: boolean): string;
22
+ /** How a backing starts its hosts: the environment they run in (the World's service environment, which
23
+ * the runtime computes for the infrastructure service: admitted host env, the World's env, the service's own, less
24
+ * the control plane's NODE_OPTIONS and proxies), and the boot's abort. The `volter-world-infra` process passes
25
+ * nothing and its hosts inherit its own environment, which is that same service environment. The World's `up`,
26
+ * running the up phase in its own process, passes it: its own process.env is the operator's, not the service's. */
27
+ export interface HostLaunch {
28
+ env?: NodeJS.ProcessEnv;
29
+ signal?: AbortSignal;
30
+ }
22
31
  /** The kinds this backing cannot serve, or [] when it can serve the world. */
23
32
  export declare function unsupportedKinds(services: InfraService[]): string[];
24
- export declare function pgliteUp(services: InfraService[], dataDir: string): Promise<void>;
33
+ export declare function pgliteUp(services: InfraService[], dataDir: string, launch?: HostLaunch): Promise<void>;
25
34
  export declare function pgliteStatus(services: InfraService[], dataDir: string): Promise<{
26
35
  ok: boolean;
27
36
  ready: number;
@@ -102,10 +102,11 @@ export function twinRunner(cli, bunOnPath = () => spawnSync('bun', ['--version']
102
102
  throw new Error(`a checkout's MongoDB twin runs under bun, which is not on PATH (${cli} is TypeScript that Node cannot run)`);
103
103
  }
104
104
  /** The argv that serves one service on its declared loopback port, with its bytes in `serviceData`. */
105
- function hostArgv(service, serviceData) {
105
+ function hostArgv(service, serviceData, env) {
106
106
  if (service.kind === 'mongodb') {
107
- const cli = mongodbTwinCli(process.env.VOLTER_WORLD_CONFIG);
108
- return [twinRunner(cli), cli, 'serve', '--port', String(service.hostPort), '--root', serviceData];
107
+ const cli = mongodbTwinCli(env.VOLTER_WORLD_CONFIG);
108
+ // blocking (spawnSync), as it was in the tool: reached only for a checkout's TypeScript cli under Node
109
+ return [twinRunner(cli, () => spawnSync('bun', ['--version'], { stdio: 'ignore', timeout: 10_000, windowsHide: true, env }).status === 0), cli, 'serve', '--port', String(service.hostPort), '--root', serviceData];
109
110
  }
110
111
  return ['node', hostScript(), '--port', String(service.hostPort), '--data', serviceData];
111
112
  }
@@ -146,7 +147,8 @@ function logSince(log, offset) {
146
147
  return '';
147
148
  }
148
149
  }
149
- export async function pgliteUp(services, dataDir) {
150
+ export async function pgliteUp(services, dataDir, launch = {}) {
151
+ const env = launch.env ?? process.env;
150
152
  // Every check before any start: a refused up starts nothing, so it never leaves the World half-booted.
151
153
  const plan = [];
152
154
  for (const service of services) {
@@ -159,7 +161,7 @@ export async function pgliteUp(services, dataDir) {
159
161
  if (await listening(service.hostPort)) {
160
162
  throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
161
163
  }
162
- plan.push({ service, name, serviceData, log, pidFile, argv: hostArgv(service, serviceData) });
164
+ plan.push({ service, name, serviceData, log, pidFile, argv: hostArgv(service, serviceData, env) });
163
165
  }
164
166
  const started = [];
165
167
  try {
@@ -172,6 +174,7 @@ export async function pgliteUp(services, dataDir) {
172
174
  const logFd = openSync(log, 'a');
173
175
  const child = spawn(command, args, {
174
176
  windowsHide: true,
177
+ env,
175
178
  detached: true,
176
179
  stdio: ['ignore', logFd, logFd],
177
180
  });
@@ -188,6 +191,8 @@ export async function pgliteUp(services, dataDir) {
188
191
  throw new Error(`${name} exited before serving — see ${log}`);
189
192
  if (Date.now() > deadline)
190
193
  throw new Error(`${name} did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
194
+ // an aborted boot (Ctrl-C during up) stops waiting; the catch below ends what this up started
195
+ launch.signal?.throwIfAborted();
191
196
  await sleep(200);
192
197
  }
193
198
  }
@@ -1,8 +1,8 @@
1
- import type { InfraService } from './pglite-backing.js';
1
+ import type { HostLaunch, InfraService } from './pglite-backing.js';
2
2
  /** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
3
3
  export declare function redisTwinCli(from: string[]): string | undefined;
4
4
  /** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
5
5
  export declare const redisTwinRoot: (dataDir: string) => string;
6
- export declare function redisTwinUp(services: InfraService[], dataDir: string, cli: string): Promise<void>;
6
+ export declare function redisTwinUp(services: InfraService[], dataDir: string, cli: string, launch?: HostLaunch): Promise<void>;
7
7
  export declare function redisTwinStatus(services: InfraService[], dataDir: string): Promise<number>;
8
8
  export declare function redisTwinDown(services: InfraService[], dataDir: string): Promise<void>;
@@ -48,7 +48,7 @@ function listening(port) {
48
48
  });
49
49
  }
50
50
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
51
- export async function redisTwinUp(services, dataDir, cli) {
51
+ export async function redisTwinUp(services, dataDir, cli, launch = {}) {
52
52
  for (const service of services) {
53
53
  const pidFile = pidPath(dataDir, service);
54
54
  if (existsSync(pidFile) && alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort))
@@ -67,6 +67,7 @@ export async function redisTwinUp(services, dataDir, cli) {
67
67
  // type transformation, not only stripping (the kernel declares constructor parameter properties)
68
68
  const runtime = typeof globalThis.Bun !== 'undefined' || !cli.endsWith('.ts') ? [] : ['--experimental-transform-types', '--no-warnings'];
69
69
  const child = spawn(process.execPath, [...runtime, cli, 'serve', '--port', String(service.hostPort), '--root', root], {
70
+ env: launch.env ?? process.env,
70
71
  detached: true,
71
72
  stdio: ['ignore', logFd, logFd],
72
73
  });
@@ -77,6 +78,7 @@ export async function redisTwinUp(services, dataDir, cli) {
77
78
  while (!(await listening(service.hostPort)) || !alive(child.pid)) {
78
79
  if (!alive(child.pid))
79
80
  throw new Error(`redis exited before serving — see ${log}`);
81
+ launch.signal?.throwIfAborted();
80
82
  if (Date.now() > deadline)
81
83
  throw new Error(`redis did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
82
84
  await sleep(200);
@@ -12,7 +12,7 @@ import { createRequire } from 'node:module';
12
12
  import { isIP } from 'node:net';
13
13
  import { allocatePort } from "./tcp-port.js";
14
14
  import { constants as osConstants, hostname as osHostname } from 'node:os';
15
- import { appendFileSync, chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, existsSync } from 'node:fs';
15
+ import { accessSync, appendFileSync, chmodSync, closeSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync, existsSync } from 'node:fs';
16
16
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
17
17
  import { packFacts } from "./pack-facts.js";
18
18
  import { describeServiceEnd, readServiceEnd, serviceOutput } from "./service-exit.js";
@@ -803,6 +803,8 @@ async function runExternalCommand(command, cwd, serviceId, phase, env = process.
803
803
  * (instead of buffering through spawnSync's 1MB cap, which a large DB restore would overflow). Returns
804
804
  * the combined output so `discover` can read it. Throws with a bounded tail on nonzero exit. */
805
805
  async function runExternalUp(command, cwd, serviceId, env, logPath, abort) {
806
+ if (ownInfrastructureUp(command))
807
+ return runOwnInfrastructureUp(command, serviceId, env, logPath, abort);
806
808
  const [bin, ...args] = ownCommand(command);
807
809
  if (!bin)
808
810
  throw new Error(`External service "${serviceId}": up command is empty`);
@@ -846,6 +848,40 @@ async function runExternalUp(command, cwd, serviceId, env, logPath, abort) {
846
848
  }
847
849
  return chunks.join('');
848
850
  }
851
+ /** Whether an external `up` is this runtime's own managed-infrastructure tool (`volter-world-infra up`, which `init`
852
+ * names), with nothing on PATH answering the name: the phase then runs in this process (infra.ts) instead of in a
853
+ * child. The services it starts (the PGlite host, the redis and MongoDB twins) are then children of the process that
854
+ * owns the World's lifetime, as every service recorder is, not of a tool that exits once they serve. Where a child
855
+ * does not outlive its parent (a browser tab's process layer, measured 2026-09-28: a detached, unref'd listener
856
+ * ended with the process that spawned it), the PGlite host ended the moment `volter-world-infra up` returned and
857
+ * the readiness probe timed out at "0/1 declared services are ready". status and down still run as the tool.
858
+ * The phase gets what the tool's process got: the infrastructure service's environment (`env`, which the hosts it
859
+ * starts are spawned with, never this process's own) and the boot's abort; its commands (docker info, compose)
860
+ * run without blocking this process's event loop. */
861
+ function ownInfrastructureUp(command) {
862
+ return command[0] === 'volter-world-infra' && command[1] === 'up' && ownCommand(command)[0] === process.execPath;
863
+ }
864
+ async function runOwnInfrastructureUp(command, serviceId, env, logPath, abort) {
865
+ writeFileSync(logPath, `$ ${command.join(' ')}\n`, { mode: 0o600 });
866
+ chmodSync(logPath, 0o600);
867
+ const out = openPrivateLog(logPath, 'a');
868
+ const chunks = [];
869
+ try {
870
+ abort?.throwIfAborted();
871
+ const { runInfrastructure } = await import("./infra.js");
872
+ const write = (text) => { chunks.push(text); writeSync(out, text); };
873
+ const code = await runInfrastructure(command[1], env, { out: write, err: write }, abort);
874
+ abort?.throwIfAborted();
875
+ if (code !== 0) {
876
+ const detail = chunks.join('').trim().split('\n').slice(-40).join('\n');
877
+ throw new Error(`External service "${serviceId}": up command exited ${code}\n${detail}`);
878
+ }
879
+ }
880
+ finally {
881
+ closeSync(out);
882
+ }
883
+ return chunks.join('');
884
+ }
849
885
  /** Resolve `${url}`/`${host}`/`${port}` in a readiness probe's httpUrl against an owned loopback
850
886
  * service's allocated port (process/twin readiness — the port is auto-assigned, so it can't be a
851
887
  * literal in the config). Other probe kinds (command/stdoutMatch) are returned unchanged. */
@@ -2236,7 +2272,21 @@ export function ownCommand(command) {
2236
2272
  return command;
2237
2273
  return [process.execPath, siblingScript(import.meta.url, stem), ...args];
2238
2274
  }
2275
+ /** A command answers when it names an executable: a path is checked as a file (what `which` does with a path),
2276
+ * never by spawning `which`. `ownCommand` hands this the running executable's own path (process.execPath), and a
2277
+ * process layer without synchronous spawn (a browser tab's, where spawnSync is ENOSYS) answered "not found on PATH"
2278
+ * for the very node running the World: the managed infrastructure never started there. A bare name is still looked
2279
+ * up on PATH. */
2239
2280
  function onPath(command) {
2281
+ if (command.includes(sep) || command.includes('/')) {
2282
+ try {
2283
+ accessSync(command, fsConstants.X_OK);
2284
+ return statSync(command).isFile();
2285
+ }
2286
+ catch {
2287
+ return false;
2288
+ }
2289
+ }
2240
2290
  return spawnSync('which', [command], { windowsHide: true, stdio: 'ignore', env: process.env }).status === 0;
2241
2291
  }
2242
2292
  function resolveWithPublicDns(hostname) {