@volter/world-runtime 2.0.0 → 2.0.2
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/known-external-services.json +0 -8
- package/dist/src/app-url.js +1 -1
- package/dist/src/branch.js +79 -2
- package/dist/src/catalog.js +1 -1
- package/dist/src/cli.js +50 -22
- package/dist/src/console-apart.d.ts +1 -0
- package/dist/src/console-apart.js +7 -0
- package/dist/src/covers.js +4 -2
- package/dist/src/fixture-env.d.ts +3 -0
- package/dist/src/fixture-env.js +24 -0
- package/dist/src/host-worker.js +2 -9
- package/dist/src/host.js +2 -9
- package/dist/src/import-module.d.ts +1 -0
- package/dist/src/import-module.js +16 -0
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.js +4 -1
- package/dist/src/infra-cli.js +61 -12
- package/dist/src/init.d.ts +1 -1
- package/dist/src/init.js +34 -5
- package/dist/src/local-branches.d.ts +37 -0
- package/dist/src/local-branches.js +193 -0
- package/dist/src/pglite-backing.d.ts +8 -0
- package/dist/src/pglite-backing.js +121 -35
- package/dist/src/pglite-host.mjs +520 -14
- package/dist/src/prerequisites.js +1 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/redirect-proxy.d.ts +1 -1
- package/dist/src/redirect-proxy.js +4 -4
- package/dist/src/redis-backing.d.ts +8 -0
- package/dist/src/redis-backing.js +120 -0
- package/dist/src/root.d.ts +23 -0
- package/dist/src/root.js +22 -10
- package/dist/src/run-task.js +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +37 -20
- package/dist/src/schema.d.ts +5 -0
- package/dist/src/schema.js +10 -1
- package/dist/src/served-world.d.ts +196 -9
- package/dist/src/served-world.js +847 -103
- package/dist/src/service-recorder.js +1 -1
- package/dist/src/storage-capacity.js +2 -2
- package/dist/src/up-task.js +1 -1
- package/dist/src/world-origins.d.ts +13 -0
- package/dist/src/world-origins.js +37 -0
- package/dist/src/world-view.d.ts +25 -0
- package/dist/src/world-view.js +110 -0
- package/known-external-services.json +0 -8
- package/package.json +10 -4
- package/src/app-url.ts +1 -1
- package/src/branch.ts +66 -2
- package/src/catalog.ts +1 -1
- package/src/cli.ts +43 -21
- package/src/console-apart.ts +7 -1
- package/src/covers.ts +4 -2
- package/src/fixture-env.ts +25 -0
- package/src/host-worker.ts +2 -1
- package/src/host.ts +2 -1
- package/src/import-module.ts +9 -0
- package/src/index.ts +7 -2
- package/src/infra-cli.ts +56 -12
- package/src/init.ts +34 -5
- package/src/local-branches.ts +173 -0
- package/src/pglite-backing.ts +112 -36
- package/src/pglite-host.mjs +520 -14
- package/src/prerequisites.ts +1 -1
- package/src/process-groups.ts +1 -1
- package/src/redirect-proxy.ts +4 -4
- package/src/redis-backing.ts +107 -0
- package/src/root.ts +27 -2
- package/src/run-task.ts +1 -1
- package/src/runtime.ts +38 -20
- package/src/schema.ts +11 -1
- package/src/served-world.ts +762 -85
- package/src/service-recorder.ts +1 -1
- package/src/storage-capacity.ts +2 -2
- package/src/up-task.ts +1 -1
- package/src/world-origins.ts +38 -0
- package/src/world-view.ts +100 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The containerless backing for a World's managed REDIS: the redis twin (@volter/twin-redis) serving Redis's
|
|
2
|
+
// own protocol on the loopback port the definition declares, its keys held in the World's tree under
|
|
3
|
+
// VOLTER_WORLD_DATA. Chosen by `volter-world-infra` for every redis service unless the operator forces the
|
|
4
|
+
// container backing (VOLTER_WORLD_INFRA_BACKING=docker); the definition, the injected REDIS_URL and the declared
|
|
5
|
+
// service contract are the same either way, as the boundary comment in infra-cli.ts promises.
|
|
6
|
+
//
|
|
7
|
+
// Why the twin and not a container: a World's state branches, checkpoints and rolls back as one tree, and a Redis
|
|
8
|
+
// in a container is state outside it (a branch of the World would share the container's queue and cache). The
|
|
9
|
+
// twin keeps each key a subject of the World (packages/twin/redis/src/redis-storage.ts). It needs nothing
|
|
10
|
+
// installed: no container runtime, no redis-server binary.
|
|
11
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { connect } from 'node:net';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { findInstalledPackage, packCli } from './catalog.ts';
|
|
16
|
+
import type { InfraService } from './pglite-backing.ts';
|
|
17
|
+
|
|
18
|
+
const PACKAGE = '@volter/twin-redis';
|
|
19
|
+
const READY_TIMEOUT_MS = 60_000;
|
|
20
|
+
|
|
21
|
+
/** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
|
|
22
|
+
export function redisTwinCli(from: string[]): string | undefined {
|
|
23
|
+
for (const dir of from) {
|
|
24
|
+
const found = findInstalledPackage(dir, PACKAGE);
|
|
25
|
+
const cli = found === undefined ? undefined : packCli(found);
|
|
26
|
+
if (cli !== undefined && existsSync(cli)) return cli;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pidPath = (dataDir: string, service: InfraService): string => join(dataDir, `redis-twin-${service.hostPort}.pid`);
|
|
32
|
+
/** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
|
|
33
|
+
export const redisTwinRoot = (dataDir: string): string => join(dataDir, 'redis-twin');
|
|
34
|
+
|
|
35
|
+
function alive(pid: number): boolean {
|
|
36
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function listening(port: number): Promise<boolean> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const socket = connect({ host: '127.0.0.1', port, timeout: 1000 });
|
|
42
|
+
// a port that answers PING with PONG is this service; anything else is not ready (or not ours)
|
|
43
|
+
socket.once('connect', () => socket.write('PING\r\n'));
|
|
44
|
+
socket.once('data', (chunk) => { socket.destroy(); resolve(chunk.toString('latin1').startsWith('+PONG')); });
|
|
45
|
+
socket.on('error', () => resolve(false));
|
|
46
|
+
socket.on('timeout', () => { socket.destroy(); resolve(false); });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
|
|
52
|
+
export async function redisTwinUp(services: InfraService[], dataDir: string, cli: string): Promise<void> {
|
|
53
|
+
for (const service of services) {
|
|
54
|
+
const pidFile = pidPath(dataDir, service);
|
|
55
|
+
if (existsSync(pidFile) && alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort)) continue;
|
|
56
|
+
// a port that already answers is another process's (another World's twin, a redis-server): serving there would
|
|
57
|
+
// hand this World's app someone else's keys, so it is refused, never taken as ready
|
|
58
|
+
if (await listening(service.hostPort)) throw new Error(`port ${service.hostPort} already serves Redis for another process`);
|
|
59
|
+
const root = redisTwinRoot(dataDir);
|
|
60
|
+
mkdirSync(root, { recursive: true });
|
|
61
|
+
const log = join(dataDir, `redis-twin-${service.hostPort}.log`);
|
|
62
|
+
mkdirSync(dirname(log), { recursive: true });
|
|
63
|
+
// a real fd, not a pipe: the server outlives this process untethered
|
|
64
|
+
const logFd = openSync(log, 'a');
|
|
65
|
+
// the runtime this runs on: Bun runs the pack's TypeScript as it is; Node (volter-world-infra's shebang) needs
|
|
66
|
+
// type transformation, not only stripping (the kernel declares constructor parameter properties)
|
|
67
|
+
const runtime = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' || !cli.endsWith('.ts') ? [] : ['--experimental-transform-types', '--no-warnings'];
|
|
68
|
+
const child = spawn(process.execPath, [...runtime, cli, 'serve', '--port', String(service.hostPort), '--root', root], {
|
|
69
|
+
detached: true,
|
|
70
|
+
stdio: ['ignore', logFd, logFd],
|
|
71
|
+
});
|
|
72
|
+
child.unref();
|
|
73
|
+
closeSync(logFd);
|
|
74
|
+
writeFileSync(pidFile, `${child.pid}\n`);
|
|
75
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
76
|
+
while (!(await listening(service.hostPort)) || !alive(child.pid!)) {
|
|
77
|
+
if (!alive(child.pid!)) throw new Error(`redis exited before serving — see ${log}`);
|
|
78
|
+
if (Date.now() > deadline) throw new Error(`redis did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
|
|
79
|
+
await sleep(200);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function redisTwinStatus(services: InfraService[], dataDir: string): Promise<number> {
|
|
85
|
+
let ready = 0;
|
|
86
|
+
for (const service of services) {
|
|
87
|
+
const pidFile = pidPath(dataDir, service);
|
|
88
|
+
if (!existsSync(pidFile)) continue;
|
|
89
|
+
if (alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort)) ready += 1;
|
|
90
|
+
}
|
|
91
|
+
return ready;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function redisTwinDown(services: InfraService[], dataDir: string): Promise<void> {
|
|
95
|
+
for (const service of services) {
|
|
96
|
+
const pidFile = pidPath(dataDir, service);
|
|
97
|
+
if (!existsSync(pidFile)) continue;
|
|
98
|
+
const pid = Number(readFileSync(pidFile, 'utf8').trim());
|
|
99
|
+
if (alive(pid)) {
|
|
100
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
|
|
101
|
+
const deadline = Date.now() + 5000;
|
|
102
|
+
while (alive(pid) && Date.now() < deadline) await sleep(100);
|
|
103
|
+
if (alive(pid)) { try { process.kill(pid, 'SIGKILL'); } catch { /* raced */ } }
|
|
104
|
+
}
|
|
105
|
+
rmSync(pidFile, { force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/root.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { loadWorldConfig, writeWorldConfig } from './configs.ts';
|
|
17
17
|
import { changesetReadiness, collectObservations, type Observation, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, protocolStanding, userKek, type DeployReport, type ObserveReport } from '@volter/world-core';
|
|
18
18
|
import { findWorldChangeset, listWorldChangesets } from './changeset.ts';
|
|
19
|
+
import { importModule } from './import-module.ts';
|
|
19
20
|
import { statusWorld } from './runtime.ts';
|
|
20
21
|
import { findInstalledPackage, twinPackageName } from './catalog.ts';
|
|
21
22
|
import type { WorldConfig } from './schema.ts';
|
|
@@ -120,7 +121,7 @@ async function packModule(vendor: string, from: string): Promise<Record<string,
|
|
|
120
121
|
const registered = getPack(vendor);
|
|
121
122
|
if (registered) return { pack: registered };
|
|
122
123
|
const entry = packEntryPath(vendor, from);
|
|
123
|
-
return entry ?
|
|
124
|
+
return entry ? await importModule(entry) : null;
|
|
124
125
|
}
|
|
125
126
|
/** The pack's module itself, installed beside the World or in this checkout: its every export (a
|
|
126
127
|
* mirror's shell and client), where the registry holds only the descriptor. */
|
|
@@ -137,8 +138,32 @@ export async function packMirrorExports(vendor: string, from: string): Promise<R
|
|
|
137
138
|
if (!entry) return null;
|
|
138
139
|
const dir = dirname(entry);
|
|
139
140
|
const file = existsSync(dir) ? readdirSync(dir).find((f) => /-mirror-ui\.(ts|js)$/.test(f) && !f.includes('.test.')) : undefined;
|
|
140
|
-
return (
|
|
141
|
+
return importModule(file ? join(dir, file) : entry);
|
|
141
142
|
}
|
|
143
|
+
/** A pack's mirror as the pack builds it (`<vendor>MirrorHtml`, `build<Vendor>MirrorClient`,
|
|
144
|
+
* `<vendor>MirrorStyles`), found the way a World finds the pack; null when the pack has none. The
|
|
145
|
+
* client builds at serve under Bun and is served prebuilt under Node, by the pack's own builder. */
|
|
146
|
+
export type PackMirror = { html: () => string; client: () => Promise<string>; styles?: () => Promise<string>; /** the account and a credential its screens sign in with (`<vendor>MirrorSignIn`): the account found in the twin's
|
|
147
|
+
* state at `root`, the credential minted for it through the twin's own doors (`twin`), never a stored one read back */
|
|
148
|
+
signIn?: (as: string, ctx: MirrorSignInContext) => Promise<{ account: string; token: string } | null> | { account: string; token: string } | null };
|
|
149
|
+
/** What a pack's `<vendor>MirrorSignIn` is given: the twin's state root (to find the account) and the twin's own doors
|
|
150
|
+
* (to mint a credential for it, as the World's seed does through `/_twin/…`). */
|
|
151
|
+
export type MirrorSignInContext = { root: string; twin: (path: string, init?: RequestInit) => Promise<Response> };
|
|
152
|
+
export async function packMirror(vendor: string, from: string): Promise<PackMirror | null> {
|
|
153
|
+
const installed = findInstalledPackage(from, twinPackageName(vendor));
|
|
154
|
+
const dir = installed ?? resolve(import.meta.dir, '..', '..', 'twin', vendor);
|
|
155
|
+
// the pack's mirror module, as the hosted build reads it (its index need not re-export every builder)
|
|
156
|
+
const src = join(dir, 'src');
|
|
157
|
+
const file = existsSync(src) ? readdirSync(src).find((f) => f.endsWith('-mirror-ui.ts')) : undefined;
|
|
158
|
+
const entry = file ? join(src, file) : installed ? packEntry(installed) : join(src, 'index.ts');
|
|
159
|
+
if (!existsSync(entry)) return null;
|
|
160
|
+
const mod = await importModule(entry);
|
|
161
|
+
const pick = (re: RegExp): ((...a: unknown[]) => unknown) | undefined => { const k = Object.keys(mod).find((key) => re.test(key) && typeof mod[key] === 'function'); return k ? mod[k] as (...a: unknown[]) => unknown : undefined; };
|
|
162
|
+
const html = pick(/^\w+MirrorHtml$/); const client = pick(/^build\w+MirrorClient$/); const styles = pick(/^\w+MirrorStyles$/); const signIn = pick(/^\w+MirrorSignIn$/);
|
|
163
|
+
if (!html || !client) return null;
|
|
164
|
+
return { html: html as () => string, client: client as () => Promise<string>, ...(styles ? { styles: styles as () => Promise<string> } : {}), ...(signIn ? { signIn: signIn as PackMirror['signIn'] } : {}) };
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
/** The real state system's adapters for a vendor: registered with the pack (protocol 2), else the
|
|
143
168
|
* pack's anchored exports (protocol 1: `perform<Name>Action`, `sync<Name>FromRemote`). */
|
|
144
169
|
export type IngestAdapter = (request: Request, ctx: { root?: string; secret?: string }) => Response | Promise<Response>;
|
package/src/run-task.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type RunTaskReply = { result: RunTaskResult } | { error: string };
|
|
|
13
13
|
* The task process is outside the caller's POSIX process group and owns boot through teardown. */
|
|
14
14
|
export function superviseWorldRun(configId: string, command: string[], options: RunWorldOptions): Promise<RunTaskResult> {
|
|
15
15
|
return new Promise((done, reject) => {
|
|
16
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'run-task-worker')], {
|
|
16
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'run-task-worker')], { windowsHide: true,
|
|
17
17
|
cwd: process.cwd(), env: process.env, detached: process.platform !== 'win32',
|
|
18
18
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'], serialization: 'json',
|
|
19
19
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
17
17
|
import { packFacts } from './pack-facts.ts';
|
|
18
18
|
import { describeServiceEnd, readServiceEnd, serviceOutput } from './service-exit.ts';
|
|
19
19
|
import { Socket } from 'node:net';
|
|
20
|
-
import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, WORLD_BOOT_PATH, WORLD_ENV_NAMES_ENV } from '@volter/world-core';
|
|
20
|
+
import { getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core';
|
|
21
21
|
import type { PlanReviewRecord } from '@volter/world-core';
|
|
22
22
|
import { loadWorldConfig } from './configs.ts';
|
|
23
23
|
import { createWorldNetworkPolicy, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
|
|
@@ -63,7 +63,7 @@ function spawnRecorded(serviceId: string, command: string, args: string[], cwd:
|
|
|
63
63
|
const recorderEnv: NodeJS.ProcessEnv = { ...env, VOLTER_WORLD_SERVICE_ID: serviceId, VOLTER_WORLD_SERVICE_LOG: log,
|
|
64
64
|
VOLTER_WORLD_SERVICE_LOG_DIR: logDir, VOLTER_WORLD_EVENTS: worldEventsPath(dirname(log)) };
|
|
65
65
|
if (recorderEnv.NODE_OPTIONS !== undefined) { recorderEnv.VOLTER_WORLD_SERVICE_NODE_OPTIONS = recorderEnv.NODE_OPTIONS; delete recorderEnv.NODE_OPTIONS; }
|
|
66
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], {
|
|
66
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], { windowsHide: true,
|
|
67
67
|
cwd,
|
|
68
68
|
env: recorderEnv,
|
|
69
69
|
detached: true,
|
|
@@ -662,7 +662,7 @@ function processStarts(pids: number[]): Map<number, string> {
|
|
|
662
662
|
}
|
|
663
663
|
return starts;
|
|
664
664
|
}
|
|
665
|
-
const result = spawnSync('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')], { encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } });
|
|
665
|
+
const result = spawnSync('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')], { windowsHide: true, encoding: 'utf8', timeout: 1000, killSignal: 'SIGKILL', env: { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } });
|
|
666
666
|
for (const line of (result.stdout ?? '').split('\n')) {
|
|
667
667
|
const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
|
|
668
668
|
if (match) starts.set(Number(match[1]), match[2]!);
|
|
@@ -798,11 +798,11 @@ async function runExternalCommand(
|
|
|
798
798
|
command: string[], cwd: string, serviceId: string, phase: string,
|
|
799
799
|
env: NodeJS.ProcessEnv = process.env, abort?: AbortSignal, timeoutMs = 60_000,
|
|
800
800
|
): Promise<{ stdout: string; stderr: string }> {
|
|
801
|
-
const [bin, ...args] = command;
|
|
801
|
+
const [bin, ...args] = ownCommand(command);
|
|
802
802
|
if (!bin) throw new Error(`External service "${serviceId}": ${phase} command is empty`);
|
|
803
803
|
if (!commandExists(bin)) throw new Error(`External service "${serviceId}": \`${bin}\` not found on PATH (the ${phase} command's tool is not installed)`);
|
|
804
804
|
abort?.throwIfAborted();
|
|
805
|
-
const child = spawn(bin, args, { cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
805
|
+
const child = spawn(bin, args, { windowsHide: true, cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
806
806
|
const lifetime = commandLifetime(child, abort, true);
|
|
807
807
|
let stdout = ''; let stderr = ''; let failure: Error | undefined;
|
|
808
808
|
const collect = (chunk: Buffer, out: boolean) => {
|
|
@@ -832,7 +832,7 @@ async function runExternalUp(
|
|
|
832
832
|
logPath: string,
|
|
833
833
|
abort?: AbortSignal,
|
|
834
834
|
): Promise<string> {
|
|
835
|
-
const [bin, ...args] = command;
|
|
835
|
+
const [bin, ...args] = ownCommand(command);
|
|
836
836
|
if (!bin) throw new Error(`External service "${serviceId}": up command is empty`);
|
|
837
837
|
if (!commandExists(bin)) {
|
|
838
838
|
throw new Error(`External service "${serviceId}": \`${bin}\` not found on PATH (the up command's tool is not installed)`);
|
|
@@ -843,7 +843,7 @@ async function runExternalUp(
|
|
|
843
843
|
const chunks: string[] = [];
|
|
844
844
|
try {
|
|
845
845
|
abort?.throwIfAborted();
|
|
846
|
-
const child = spawn(bin, args, { cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
846
|
+
const child = spawn(bin, args, { windowsHide: true, cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
847
847
|
const lifetime = commandLifetime(child, abort, true);
|
|
848
848
|
let failure: unknown;
|
|
849
849
|
child.on('error', error => { failure = error; });
|
|
@@ -1456,7 +1456,8 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1456
1456
|
VOLTER_WORLD_MODE: mode,
|
|
1457
1457
|
VOLTER_WORLD_CONFIG: loaded.path,
|
|
1458
1458
|
VOLTER_WORLD_INSTANCE: instanceFile,
|
|
1459
|
-
|
|
1459
|
+
// every service's root is `<data>/<service id>`; the kernel finds another pack's store across them (A3, ownerStoreRoots)
|
|
1460
|
+
[WORLD_DATA_ENV]: data,
|
|
1460
1461
|
// the live env file this `up` writes: what a service that runs apps elsewhere (world-machine's guest) carries there
|
|
1461
1462
|
VOLTER_WORLD_ENV_FILE: envFile,
|
|
1462
1463
|
// THE WORLD CLOCK: every service stamps from this file via the kernel's worldNow();
|
|
@@ -2190,12 +2191,28 @@ function sleep(ms: number): Promise<void> {
|
|
|
2190
2191
|
|
|
2191
2192
|
function commandExists(command: string): boolean {
|
|
2192
2193
|
// Pass the live env so PATH reflects the current process (some runtimes snapshot env otherwise).
|
|
2193
|
-
|
|
2194
|
-
|
|
2194
|
+
return OWN_COMMANDS[command] !== undefined || onPath(command);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/** A lifecycle command naming this runtime's own bin (`volter-world-infra`, which `init` names in every managed
|
|
2198
|
+
* infrastructure's lifecycle) runs this runtime's own script with the executable running it, when nothing on PATH
|
|
2199
|
+
* answers the name: a bare `volter-world down` (no runner that put node_modules/.bin on PATH) failed "not found on
|
|
2200
|
+
* PATH" and left the infrastructure up. Only the name is mapped — no directory joins PATH — and a same-named
|
|
2201
|
+
* command on PATH still wins, as before. */
|
|
2202
|
+
const OWN_COMMANDS: Record<string, string> = { 'volter-world-infra': 'infra-cli' };
|
|
2203
|
+
export function ownCommand(command: string[]): string[] {
|
|
2204
|
+
const [bin, ...args] = command;
|
|
2205
|
+
const stem = bin === undefined ? undefined : OWN_COMMANDS[bin];
|
|
2206
|
+
if (stem === undefined || onPath(bin!)) return command;
|
|
2207
|
+
return [process.execPath, siblingScript(import.meta.url, stem), ...args];
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function onPath(command: string): boolean {
|
|
2211
|
+
return spawnSync('which', [command], { windowsHide: true, stdio: 'ignore', env: process.env }).status === 0;
|
|
2195
2212
|
}
|
|
2196
2213
|
|
|
2197
2214
|
function resolveWithPublicDns(hostname: string): string | undefined {
|
|
2198
|
-
const result = spawnSync('dig', ['+short', '@1.1.1.1', hostname, 'A'], {
|
|
2215
|
+
const result = spawnSync('dig', ['+short', '@1.1.1.1', hostname, 'A'], { windowsHide: true,
|
|
2199
2216
|
encoding: 'utf8',
|
|
2200
2217
|
});
|
|
2201
2218
|
if (result.status !== 0) return undefined;
|
|
@@ -2235,7 +2252,7 @@ async function verifyPublicUrl(
|
|
|
2235
2252
|
|| (isIP(normalizedHostname) === 4 && normalizedHostname.split('.')[0] === '127');
|
|
2236
2253
|
if (local) {
|
|
2237
2254
|
const target = publicHealthUrl(publicUrl, path);
|
|
2238
|
-
const result = spawnSync('curl', ['--noproxy', '*', '-sS', '-o', '-', '-w', '\n%{http_code}', '--max-time', String(Math.max(1, Math.ceil(timeoutMs / 1000))), target], { encoding: 'utf8' });
|
|
2255
|
+
const result = spawnSync('curl', ['--noproxy', '*', '-sS', '-o', '-', '-w', '\n%{http_code}', '--max-time', String(Math.max(1, Math.ceil(timeoutMs / 1000))), target], { windowsHide: true, encoding: 'utf8' });
|
|
2239
2256
|
const separator = result.stdout.lastIndexOf('\n');
|
|
2240
2257
|
const body = separator >= 0 ? result.stdout.slice(0, separator).trim() : result.stdout.trim();
|
|
2241
2258
|
const status = Number(separator >= 0 ? result.stdout.slice(separator + 1).trim() : '0');
|
|
@@ -2269,7 +2286,7 @@ async function verifyPublicUrl(
|
|
|
2269
2286
|
`${hostname}:443:${resolvedIp}`,
|
|
2270
2287
|
target,
|
|
2271
2288
|
],
|
|
2272
|
-
{ encoding: 'utf8' },
|
|
2289
|
+
{ windowsHide: true, encoding: 'utf8' },
|
|
2273
2290
|
);
|
|
2274
2291
|
if (result.status === 0) {
|
|
2275
2292
|
const output = result.stdout;
|
|
@@ -2331,7 +2348,7 @@ async function shareWorldReserved(name: string, options: ShareWorldOptions): Pro
|
|
|
2331
2348
|
// Each spawned tunnel owns one log generation. Truncating before spawn prevents a repeated
|
|
2332
2349
|
// share from accepting the previous process's URL handshake as the new process's endpoint.
|
|
2333
2350
|
const out = openPrivateLog(log, 'w');
|
|
2334
|
-
const child = spawn(tunnel.command, tunnel.args, {
|
|
2351
|
+
const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
|
|
2335
2352
|
cwd: root,
|
|
2336
2353
|
env: process.env,
|
|
2337
2354
|
detached: true,
|
|
@@ -2505,7 +2522,8 @@ async function probeExternalOnce(
|
|
|
2505
2522
|
if (!commandExists(ready.command)) {
|
|
2506
2523
|
return { ok: false, message: `readiness command \`${ready.command}\` not found on PATH` };
|
|
2507
2524
|
}
|
|
2508
|
-
const
|
|
2525
|
+
const [probeBin, ...probeArgs] = ownCommand([ready.command, ...(ready.args ?? [])]);
|
|
2526
|
+
const result = spawnSync(probeBin!, probeArgs, { windowsHide: true,
|
|
2509
2527
|
cwd, encoding: 'utf8', env, timeout: DOCTOR_EXTERNAL_PROBE_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024,
|
|
2510
2528
|
});
|
|
2511
2529
|
if (result.status === 0) return { ok: true, message: 'probe ok (command)' };
|
|
@@ -2860,7 +2878,7 @@ export function runWithWorldEnv(name: string, command: string[], root = process.
|
|
|
2860
2878
|
if (command.length === 0) throw new Error('Missing command after --');
|
|
2861
2879
|
const attached = worldAttachedCommandEnv(name, root, options.verbose);
|
|
2862
2880
|
return new Promise((resolveExit) => {
|
|
2863
|
-
const child = spawn(command[0]!, command.slice(1), {
|
|
2881
|
+
const child = spawn(command[0]!, command.slice(1), { windowsHide: true,
|
|
2864
2882
|
cwd: resolve(options.cwd ?? root),
|
|
2865
2883
|
env: attached.env,
|
|
2866
2884
|
stdio: 'inherit',
|
|
@@ -2933,7 +2951,7 @@ export async function attachWorld(name: string, command: string[], root = proces
|
|
|
2933
2951
|
let retired = true;
|
|
2934
2952
|
try {
|
|
2935
2953
|
return await new Promise<number>((done, reject) => {
|
|
2936
|
-
const child = spawn(command[0]!, command.slice(1), { cwd: resolve(options.cwd ?? root), env: attached.env, stdio: 'inherit', detached: commandProcessGroup });
|
|
2954
|
+
const child = spawn(command[0]!, command.slice(1), { windowsHide: true, cwd: resolve(options.cwd ?? root), env: attached.env, stdio: 'inherit', detached: commandProcessGroup });
|
|
2937
2955
|
const lifetime = commandLifetime(child);
|
|
2938
2956
|
let failure: unknown;
|
|
2939
2957
|
record.consumerPid = child.pid;
|
|
@@ -3014,7 +3032,7 @@ async function runWithWorldEnvLogged(name: string, command: string[], root: stri
|
|
|
3014
3032
|
let spawnError: string | undefined;
|
|
3015
3033
|
let child;
|
|
3016
3034
|
try {
|
|
3017
|
-
child = spawn(command[0]!, command.slice(1), {
|
|
3035
|
+
child = spawn(command[0]!, command.slice(1), { windowsHide: true,
|
|
3018
3036
|
cwd: resolve(options.cwd ?? root),
|
|
3019
3037
|
env: attached.env,
|
|
3020
3038
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
@@ -3096,7 +3114,7 @@ export async function shellWorld(name: string, root = process.cwd()): Promise<nu
|
|
|
3096
3114
|
process.stderr.write(`world '${name}' active in a subshell — vendor calls hit the twins (env-only; install openssl for ambient https redirect). type 'exit' to leave.\n`);
|
|
3097
3115
|
}
|
|
3098
3116
|
try {
|
|
3099
|
-
const result = spawnSync(shell, ['-i'], { cwd, env, stdio: 'inherit' });
|
|
3117
|
+
const result = spawnSync(shell, ['-i'], { windowsHide: true, cwd, env, stdio: 'inherit' });
|
|
3100
3118
|
return result.status ?? 0;
|
|
3101
3119
|
} finally {
|
|
3102
3120
|
if (proxy) {
|
|
@@ -3189,7 +3207,7 @@ function startProxyDaemonOwned(name: string, root: string, caCertPath: string, e
|
|
|
3189
3207
|
mkdirSync(dirname(proxyLogPath(root, name)), { recursive: true, mode: 0o700 });
|
|
3190
3208
|
const proxyOut = openPrivateLog(proxyLogPath(root, name), 'a');
|
|
3191
3209
|
try {
|
|
3192
|
-
child = spawn(process.execPath, [proxyDaemonEntry(), name, '--root', root, ...(envFile ? [`--env-file=${envFile}`] : [])], {
|
|
3210
|
+
child = spawn(process.execPath, [proxyDaemonEntry(), name, '--root', root, ...(envFile ? [`--env-file=${envFile}`] : [])], { windowsHide: true,
|
|
3193
3211
|
cwd: root, env: process.env, detached: true, stdio: ['ignore', proxyOut, proxyOut],
|
|
3194
3212
|
});
|
|
3195
3213
|
} finally { closeSync(proxyOut); }
|
package/src/schema.ts
CHANGED
|
@@ -173,6 +173,9 @@ export type WorldServiceConfig = {
|
|
|
173
173
|
* to loopback services directly while presenting a disguised world to the app/user.
|
|
174
174
|
*/
|
|
175
175
|
controlPlane?: boolean;
|
|
176
|
+
/** The account a vendor's own screens (its mirror) open signed in as: its handle, username or email, which the pack
|
|
177
|
+
* resolves against the accounts this World holds (`<vendor>MirrorSignIn`). Nothing secret is written here. */
|
|
178
|
+
signIn?: { as: string };
|
|
176
179
|
injectEnv?: string;
|
|
177
180
|
/**
|
|
178
181
|
* Additional env vars exported into the generated world env after this service starts.
|
|
@@ -704,7 +707,7 @@ export function assertWorldSelection(value: unknown, path: string): asserts valu
|
|
|
704
707
|
|
|
705
708
|
function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
|
|
706
709
|
if (!record(value)) throw new Error(`World service must be an object in ${path}`);
|
|
707
|
-
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
|
|
710
|
+
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root', 'signIn'], `Service ${String(value.id ?? '')}`, path, true);
|
|
708
711
|
const source = value.source === undefined ? {} : value.source;
|
|
709
712
|
const execution = value.execution === undefined ? {} : value.execution;
|
|
710
713
|
const endpoint = value.endpoint === undefined ? {} : value.endpoint;
|
|
@@ -721,6 +724,11 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
|
|
|
721
724
|
assertKeys(environment, ['values'], 'service.execution.environment', path);
|
|
722
725
|
if (typeof value.id !== 'string' || value.id === '') throw new Error(`World service must define string id in ${path}`);
|
|
723
726
|
if (value.description !== undefined && typeof value.description !== 'string') throw new Error(`Service "${value.id}" description must be a string in ${path}`);
|
|
727
|
+
if (value.signIn !== undefined) {
|
|
728
|
+
if (!record(value.signIn)) throw new Error(`Service "${value.id}" signIn must be an object in ${path}`);
|
|
729
|
+
assertKeys(value.signIn, ['as'], `Service "${value.id}" signIn`, path);
|
|
730
|
+
if (typeof value.signIn.as !== 'string' || value.signIn.as.trim() === '') throw new Error(`Service "${value.id}" signIn.as must name an account (a handle, username or email) in ${path}`);
|
|
731
|
+
}
|
|
724
732
|
if (value.description !== undefined && value['//'] !== undefined) throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
|
|
725
733
|
if (execution.cwd !== undefined && typeof execution.cwd !== 'string') throw new Error(`Service "${value.id}" execution.cwd must be a string in ${path}`);
|
|
726
734
|
if (execution.controlPlane !== undefined && typeof execution.controlPlane !== 'boolean') throw new Error(`Service "${value.id}" execution.controlPlane must be a boolean in ${path}`);
|
|
@@ -770,6 +778,7 @@ function serviceV2ToLegacy(value: unknown, path: string): WorldServiceConfig {
|
|
|
770
778
|
...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates as Record<string, string> }),
|
|
771
779
|
...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect as Record<string, string> }),
|
|
772
780
|
...(value.root === undefined ? {} : { root: value.root as WorldServiceConfig['root'] }),
|
|
781
|
+
...(value.signIn === undefined ? {} : { signIn: { as: (value.signIn as { as: string }).as } }),
|
|
773
782
|
...(external === undefined ? {} : { external }),
|
|
774
783
|
};
|
|
775
784
|
if (type !== 'external' && bindings.discover !== undefined) throw new Error(`Service "${legacy.id}" bindings.discover is only valid for external services in ${path}`);
|
|
@@ -889,6 +898,7 @@ function serviceToV2(service: WorldServiceConfig): Record<string, unknown> {
|
|
|
889
898
|
...(Object.keys(endpoint).length ? { endpoint } : {}),
|
|
890
899
|
...(Object.keys(bindings).length ? { bindings } : {}),
|
|
891
900
|
...(service.root === undefined ? {} : { root: service.root }),
|
|
901
|
+
...(service.signIn === undefined ? {} : { signIn: service.signIn }),
|
|
892
902
|
};
|
|
893
903
|
}
|
|
894
904
|
|