@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,8 @@
|
|
|
1
|
+
import type { InfraService } from './pglite-backing.js';
|
|
2
|
+
/** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
|
|
3
|
+
export declare function redisTwinCli(from: string[]): string | undefined;
|
|
4
|
+
/** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
|
|
5
|
+
export declare const redisTwinRoot: (dataDir: string) => string;
|
|
6
|
+
export declare function redisTwinUp(services: InfraService[], dataDir: string, cli: string): Promise<void>;
|
|
7
|
+
export declare function redisTwinStatus(services: InfraService[], dataDir: string): Promise<number>;
|
|
8
|
+
export declare function redisTwinDown(services: InfraService[], dataDir: string): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
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.js";
|
|
16
|
+
const PACKAGE = '@volter/twin-redis';
|
|
17
|
+
const READY_TIMEOUT_MS = 60_000;
|
|
18
|
+
/** The redis twin's cli: installed beside this runtime (its node_modules walk), else beside the World's config. */
|
|
19
|
+
export function redisTwinCli(from) {
|
|
20
|
+
for (const dir of from) {
|
|
21
|
+
const found = findInstalledPackage(dir, PACKAGE);
|
|
22
|
+
const cli = found === undefined ? undefined : packCli(found);
|
|
23
|
+
if (cli !== undefined && existsSync(cli))
|
|
24
|
+
return cli;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const pidPath = (dataDir, service) => join(dataDir, `redis-twin-${service.hostPort}.pid`);
|
|
29
|
+
/** Where the service's keys live: the World's tree for the `redis` service, under the World's data. */
|
|
30
|
+
export const redisTwinRoot = (dataDir) => join(dataDir, 'redis-twin');
|
|
31
|
+
function alive(pid) {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function listening(port) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const socket = connect({ host: '127.0.0.1', port, timeout: 1000 });
|
|
43
|
+
// a port that answers PING with PONG is this service; anything else is not ready (or not ours)
|
|
44
|
+
socket.once('connect', () => socket.write('PING\r\n'));
|
|
45
|
+
socket.once('data', (chunk) => { socket.destroy(); resolve(chunk.toString('latin1').startsWith('+PONG')); });
|
|
46
|
+
socket.on('error', () => resolve(false));
|
|
47
|
+
socket.on('timeout', () => { socket.destroy(); resolve(false); });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
export async function redisTwinUp(services, dataDir, cli) {
|
|
52
|
+
for (const service of services) {
|
|
53
|
+
const pidFile = pidPath(dataDir, service);
|
|
54
|
+
if (existsSync(pidFile) && alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort))
|
|
55
|
+
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))
|
|
59
|
+
throw new Error(`port ${service.hostPort} already serves Redis for another process`);
|
|
60
|
+
const root = redisTwinRoot(dataDir);
|
|
61
|
+
mkdirSync(root, { recursive: true });
|
|
62
|
+
const log = join(dataDir, `redis-twin-${service.hostPort}.log`);
|
|
63
|
+
mkdirSync(dirname(log), { recursive: true });
|
|
64
|
+
// a real fd, not a pipe: the server outlives this process untethered
|
|
65
|
+
const logFd = openSync(log, 'a');
|
|
66
|
+
// the runtime this runs on: Bun runs the pack's TypeScript as it is; Node (volter-world-infra's shebang) needs
|
|
67
|
+
// type transformation, not only stripping (the kernel declares constructor parameter properties)
|
|
68
|
+
const runtime = typeof globalThis.Bun !== 'undefined' || !cli.endsWith('.ts') ? [] : ['--experimental-transform-types', '--no-warnings'];
|
|
69
|
+
const child = spawn(process.execPath, [...runtime, cli, 'serve', '--port', String(service.hostPort), '--root', root], {
|
|
70
|
+
detached: true,
|
|
71
|
+
stdio: ['ignore', logFd, logFd],
|
|
72
|
+
});
|
|
73
|
+
child.unref();
|
|
74
|
+
closeSync(logFd);
|
|
75
|
+
writeFileSync(pidFile, `${child.pid}\n`);
|
|
76
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
77
|
+
while (!(await listening(service.hostPort)) || !alive(child.pid)) {
|
|
78
|
+
if (!alive(child.pid))
|
|
79
|
+
throw new Error(`redis exited before serving — see ${log}`);
|
|
80
|
+
if (Date.now() > deadline)
|
|
81
|
+
throw new Error(`redis did not serve port ${service.hostPort} within ${READY_TIMEOUT_MS / 1000}s — see ${log}`);
|
|
82
|
+
await sleep(200);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function redisTwinStatus(services, dataDir) {
|
|
87
|
+
let ready = 0;
|
|
88
|
+
for (const service of services) {
|
|
89
|
+
const pidFile = pidPath(dataDir, service);
|
|
90
|
+
if (!existsSync(pidFile))
|
|
91
|
+
continue;
|
|
92
|
+
if (alive(Number(readFileSync(pidFile, 'utf8').trim())) && await listening(service.hostPort))
|
|
93
|
+
ready += 1;
|
|
94
|
+
}
|
|
95
|
+
return ready;
|
|
96
|
+
}
|
|
97
|
+
export async function redisTwinDown(services, dataDir) {
|
|
98
|
+
for (const service of services) {
|
|
99
|
+
const pidFile = pidPath(dataDir, service);
|
|
100
|
+
if (!existsSync(pidFile))
|
|
101
|
+
continue;
|
|
102
|
+
const pid = Number(readFileSync(pidFile, 'utf8').trim());
|
|
103
|
+
if (alive(pid)) {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 'SIGTERM');
|
|
106
|
+
}
|
|
107
|
+
catch { /* already gone */ }
|
|
108
|
+
const deadline = Date.now() + 5000;
|
|
109
|
+
while (alive(pid) && Date.now() < deadline)
|
|
110
|
+
await sleep(100);
|
|
111
|
+
if (alive(pid)) {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 'SIGKILL');
|
|
114
|
+
}
|
|
115
|
+
catch { /* raced */ }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
rmSync(pidFile, { force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/src/root.d.ts
CHANGED
|
@@ -36,6 +36,29 @@ export declare function rootForControlRoot(controlRoot: string, service: string)
|
|
|
36
36
|
/** The pack's mirror module (`src/<vendor>-mirror-ui.ts`: its shell, client and stylesheet), read as the
|
|
37
37
|
* hosted mirror build reads it (apps/cloud/scripts/mirrors.ts); a pack's index need not re-export them. */
|
|
38
38
|
export declare function packMirrorExports(vendor: string, from: string): Promise<Record<string, unknown> | null>;
|
|
39
|
+
/** A pack's mirror as the pack builds it (`<vendor>MirrorHtml`, `build<Vendor>MirrorClient`,
|
|
40
|
+
* `<vendor>MirrorStyles`), found the way a World finds the pack; null when the pack has none. The
|
|
41
|
+
* client builds at serve under Bun and is served prebuilt under Node, by the pack's own builder. */
|
|
42
|
+
export type PackMirror = {
|
|
43
|
+
html: () => string;
|
|
44
|
+
client: () => Promise<string>;
|
|
45
|
+
styles?: () => Promise<string>; /** the account and a credential its screens sign in with (`<vendor>MirrorSignIn`): the account found in the twin's
|
|
46
|
+
* state at `root`, the credential minted for it through the twin's own doors (`twin`), never a stored one read back */
|
|
47
|
+
signIn?: (as: string, ctx: MirrorSignInContext) => Promise<{
|
|
48
|
+
account: string;
|
|
49
|
+
token: string;
|
|
50
|
+
} | null> | {
|
|
51
|
+
account: string;
|
|
52
|
+
token: string;
|
|
53
|
+
} | null;
|
|
54
|
+
};
|
|
55
|
+
/** What a pack's `<vendor>MirrorSignIn` is given: the twin's state root (to find the account) and the twin's own doors
|
|
56
|
+
* (to mint a credential for it, as the World's seed does through `/_twin/…`). */
|
|
57
|
+
export type MirrorSignInContext = {
|
|
58
|
+
root: string;
|
|
59
|
+
twin: (path: string, init?: RequestInit) => Promise<Response>;
|
|
60
|
+
};
|
|
61
|
+
export declare function packMirror(vendor: string, from: string): Promise<PackMirror | null>;
|
|
39
62
|
/** The real state system's adapters for a vendor: registered with the pack (protocol 2), else the
|
|
40
63
|
* pack's anchored exports (protocol 1: `perform<Name>Action`, `sync<Name>FromRemote`). */
|
|
41
64
|
export type IngestAdapter = (request: Request, ctx: {
|
package/dist/src/root.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
// A TWIN'S ROOT (docs/concepts/the-model.md): the vendor's real account
|
|
10
2
|
// behind a twin on a shared world. `volter twin <vendor> root <url> --deploy <policy>` records it,
|
|
11
3
|
// key-free, in `.volter/world.json` (committed) and, at boot, in the twin's state dir (core's
|
|
@@ -22,6 +14,7 @@ import { authStrategyFor, getActiveWorldStore, getPack, sealingKey, buildRemoteE
|
|
|
22
14
|
import { loadWorldConfig, writeWorldConfig } from "./configs.js";
|
|
23
15
|
import { changesetReadiness, collectObservations, foldObservations, loadChecks, openRootCredential, performEntries, rootCustody, protocolStanding } from '@volter/world-core';
|
|
24
16
|
import { findWorldChangeset, listWorldChangesets } from "./changeset.js";
|
|
17
|
+
import { importModule } from "./import-module.js";
|
|
25
18
|
import { statusWorld } from "./runtime.js";
|
|
26
19
|
import { findInstalledPackage, twinPackageName } from "./catalog.js";
|
|
27
20
|
// ── the user's sealing key ──────────────────────────────────────────────────────────────────
|
|
@@ -125,7 +118,7 @@ async function packModule(vendor, from) {
|
|
|
125
118
|
if (registered)
|
|
126
119
|
return { pack: registered };
|
|
127
120
|
const entry = packEntryPath(vendor, from);
|
|
128
|
-
return entry ?
|
|
121
|
+
return entry ? await importModule(entry) : null;
|
|
129
122
|
}
|
|
130
123
|
/** The pack's module itself, installed beside the World or in this checkout: its every export (a
|
|
131
124
|
* mirror's shell and client), where the registry holds only the descriptor. */
|
|
@@ -144,7 +137,26 @@ export async function packMirrorExports(vendor, from) {
|
|
|
144
137
|
return null;
|
|
145
138
|
const dir = dirname(entry);
|
|
146
139
|
const file = existsSync(dir) ? readdirSync(dir).find((f) => /-mirror-ui\.(ts|js)$/.test(f) && !f.includes('.test.')) : undefined;
|
|
147
|
-
return (
|
|
140
|
+
return importModule(file ? join(dir, file) : entry);
|
|
141
|
+
}
|
|
142
|
+
export async function packMirror(vendor, from) {
|
|
143
|
+
const installed = findInstalledPackage(from, twinPackageName(vendor));
|
|
144
|
+
const dir = installed ?? resolve(import.meta.dir, '..', '..', 'twin', vendor);
|
|
145
|
+
// the pack's mirror module, as the hosted build reads it (its index need not re-export every builder)
|
|
146
|
+
const src = join(dir, 'src');
|
|
147
|
+
const file = existsSync(src) ? readdirSync(src).find((f) => f.endsWith('-mirror-ui.ts')) : undefined;
|
|
148
|
+
const entry = file ? join(src, file) : installed ? packEntry(installed) : join(src, 'index.ts');
|
|
149
|
+
if (!existsSync(entry))
|
|
150
|
+
return null;
|
|
151
|
+
const mod = await importModule(entry);
|
|
152
|
+
const pick = (re) => { const k = Object.keys(mod).find((key) => re.test(key) && typeof mod[key] === 'function'); return k ? mod[k] : undefined; };
|
|
153
|
+
const html = pick(/^\w+MirrorHtml$/);
|
|
154
|
+
const client = pick(/^build\w+MirrorClient$/);
|
|
155
|
+
const styles = pick(/^\w+MirrorStyles$/);
|
|
156
|
+
const signIn = pick(/^\w+MirrorSignIn$/);
|
|
157
|
+
if (!html || !client)
|
|
158
|
+
return null;
|
|
159
|
+
return { html: html, client: client, ...(styles ? { styles: styles } : {}), ...(signIn ? { signIn: signIn } : {}) };
|
|
148
160
|
}
|
|
149
161
|
export async function adaptersFor(vendor, from) {
|
|
150
162
|
const registered = stateSystemFor(vendor);
|
package/dist/src/run-task.js
CHANGED
|
@@ -7,7 +7,7 @@ import { siblingScript } from "./sibling.js";
|
|
|
7
7
|
* The task process is outside the caller's POSIX process group and owns boot through teardown. */
|
|
8
8
|
export function superviseWorldRun(configId, command, options) {
|
|
9
9
|
return new Promise((done, reject) => {
|
|
10
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'run-task-worker')], {
|
|
10
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'run-task-worker')], { windowsHide: true,
|
|
11
11
|
cwd: process.cwd(), env: process.env, detached: process.platform !== 'win32',
|
|
12
12
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'], serialization: 'json',
|
|
13
13
|
});
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -122,6 +122,7 @@ export declare function pruneWorlds(options?: {
|
|
|
122
122
|
entries: WorldCleanupEntry[];
|
|
123
123
|
};
|
|
124
124
|
export declare function parseCloudflareQuickPublicUrl(candidate: string): string | undefined;
|
|
125
|
+
export declare function ownCommand(command: string[]): string[];
|
|
125
126
|
export declare function shareWorld(name: string, options?: ShareWorldOptions): Promise<WorldInstance>;
|
|
126
127
|
export declare function shareWorldServices(name: string, options?: ShareWorldServicesOptions): Promise<WorldInstance>;
|
|
127
128
|
/** TWIN-62: `unshare` must give tunnels the same contract plain `downWorld` already gives owned
|
package/dist/src/runtime.js
CHANGED
|
@@ -17,7 +17,7 @@ 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";
|
|
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 { loadWorldConfig } from "./configs.js";
|
|
22
22
|
import { createWorldNetworkPolicy, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
|
|
23
23
|
import { resolveServicePackage } from "./catalog.js";
|
|
@@ -65,7 +65,7 @@ function spawnRecorded(serviceId, command, args, cwd, env, log) {
|
|
|
65
65
|
recorderEnv.VOLTER_WORLD_SERVICE_NODE_OPTIONS = recorderEnv.NODE_OPTIONS;
|
|
66
66
|
delete recorderEnv.NODE_OPTIONS;
|
|
67
67
|
}
|
|
68
|
-
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], {
|
|
68
|
+
const child = spawn(process.execPath, [siblingScript(import.meta.url, 'service-recorder'), command, ...args], { windowsHide: true,
|
|
69
69
|
cwd,
|
|
70
70
|
env: recorderEnv,
|
|
71
71
|
detached: true,
|
|
@@ -614,7 +614,7 @@ function processStarts(pids) {
|
|
|
614
614
|
}
|
|
615
615
|
return starts;
|
|
616
616
|
}
|
|
617
|
-
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' } });
|
|
617
|
+
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' } });
|
|
618
618
|
for (const line of (result.stdout ?? '').split('\n')) {
|
|
619
619
|
const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
|
|
620
620
|
if (match)
|
|
@@ -762,13 +762,13 @@ function getByJsonPath(value, jsonPath) {
|
|
|
762
762
|
return current;
|
|
763
763
|
}
|
|
764
764
|
async function runExternalCommand(command, cwd, serviceId, phase, env = process.env, abort, timeoutMs = 60_000) {
|
|
765
|
-
const [bin, ...args] = command;
|
|
765
|
+
const [bin, ...args] = ownCommand(command);
|
|
766
766
|
if (!bin)
|
|
767
767
|
throw new Error(`External service "${serviceId}": ${phase} command is empty`);
|
|
768
768
|
if (!commandExists(bin))
|
|
769
769
|
throw new Error(`External service "${serviceId}": \`${bin}\` not found on PATH (the ${phase} command's tool is not installed)`);
|
|
770
770
|
abort?.throwIfAborted();
|
|
771
|
-
const child = spawn(bin, args, { cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
771
|
+
const child = spawn(bin, args, { windowsHide: true, cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
772
772
|
const lifetime = commandLifetime(child, abort, true);
|
|
773
773
|
let stdout = '';
|
|
774
774
|
let stderr = '';
|
|
@@ -803,7 +803,7 @@ 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
|
-
const [bin, ...args] = command;
|
|
806
|
+
const [bin, ...args] = ownCommand(command);
|
|
807
807
|
if (!bin)
|
|
808
808
|
throw new Error(`External service "${serviceId}": up command is empty`);
|
|
809
809
|
if (!commandExists(bin)) {
|
|
@@ -815,7 +815,7 @@ async function runExternalUp(command, cwd, serviceId, env, logPath, abort) {
|
|
|
815
815
|
const chunks = [];
|
|
816
816
|
try {
|
|
817
817
|
abort?.throwIfAborted();
|
|
818
|
-
const child = spawn(bin, args, { cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
818
|
+
const child = spawn(bin, args, { windowsHide: true, cwd, env, detached: commandProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
819
819
|
const lifetime = commandLifetime(child, abort, true);
|
|
820
820
|
let failure;
|
|
821
821
|
child.on('error', error => { failure = error; });
|
|
@@ -1453,7 +1453,8 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1453
1453
|
VOLTER_WORLD_MODE: mode,
|
|
1454
1454
|
VOLTER_WORLD_CONFIG: loaded.path,
|
|
1455
1455
|
VOLTER_WORLD_INSTANCE: instanceFile,
|
|
1456
|
-
|
|
1456
|
+
// every service's root is `<data>/<service id>`; the kernel finds another pack's store across them (A3, ownerStoreRoots)
|
|
1457
|
+
[WORLD_DATA_ENV]: data,
|
|
1457
1458
|
// the live env file this `up` writes: what a service that runs apps elsewhere (world-machine's guest) carries there
|
|
1458
1459
|
VOLTER_WORLD_ENV_FILE: envFile,
|
|
1459
1460
|
// THE WORLD CLOCK: every service stamps from this file via the kernel's worldNow();
|
|
@@ -2220,11 +2221,26 @@ function sleep(ms) {
|
|
|
2220
2221
|
}
|
|
2221
2222
|
function commandExists(command) {
|
|
2222
2223
|
// Pass the live env so PATH reflects the current process (some runtimes snapshot env otherwise).
|
|
2223
|
-
|
|
2224
|
-
|
|
2224
|
+
return OWN_COMMANDS[command] !== undefined || onPath(command);
|
|
2225
|
+
}
|
|
2226
|
+
/** A lifecycle command naming this runtime's own bin (`volter-world-infra`, which `init` names in every managed
|
|
2227
|
+
* infrastructure's lifecycle) runs this runtime's own script with the executable running it, when nothing on PATH
|
|
2228
|
+
* answers the name: a bare `volter-world down` (no runner that put node_modules/.bin on PATH) failed "not found on
|
|
2229
|
+
* PATH" and left the infrastructure up. Only the name is mapped — no directory joins PATH — and a same-named
|
|
2230
|
+
* command on PATH still wins, as before. */
|
|
2231
|
+
const OWN_COMMANDS = { 'volter-world-infra': 'infra-cli' };
|
|
2232
|
+
export function ownCommand(command) {
|
|
2233
|
+
const [bin, ...args] = command;
|
|
2234
|
+
const stem = bin === undefined ? undefined : OWN_COMMANDS[bin];
|
|
2235
|
+
if (stem === undefined || onPath(bin))
|
|
2236
|
+
return command;
|
|
2237
|
+
return [process.execPath, siblingScript(import.meta.url, stem), ...args];
|
|
2238
|
+
}
|
|
2239
|
+
function onPath(command) {
|
|
2240
|
+
return spawnSync('which', [command], { windowsHide: true, stdio: 'ignore', env: process.env }).status === 0;
|
|
2225
2241
|
}
|
|
2226
2242
|
function resolveWithPublicDns(hostname) {
|
|
2227
|
-
const result = spawnSync('dig', ['+short', '@1.1.1.1', hostname, 'A'], {
|
|
2243
|
+
const result = spawnSync('dig', ['+short', '@1.1.1.1', hostname, 'A'], { windowsHide: true,
|
|
2228
2244
|
encoding: 'utf8',
|
|
2229
2245
|
});
|
|
2230
2246
|
if (result.status !== 0)
|
|
@@ -2260,7 +2276,7 @@ async function verifyPublicUrl(publicUrl, path, timeoutMs) {
|
|
|
2260
2276
|
|| (isIP(normalizedHostname) === 4 && normalizedHostname.split('.')[0] === '127');
|
|
2261
2277
|
if (local) {
|
|
2262
2278
|
const target = publicHealthUrl(publicUrl, path);
|
|
2263
|
-
const result = spawnSync('curl', ['--noproxy', '*', '-sS', '-o', '-', '-w', '\n%{http_code}', '--max-time', String(Math.max(1, Math.ceil(timeoutMs / 1000))), target], { encoding: 'utf8' });
|
|
2279
|
+
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' });
|
|
2264
2280
|
const separator = result.stdout.lastIndexOf('\n');
|
|
2265
2281
|
const body = separator >= 0 ? result.stdout.slice(0, separator).trim() : result.stdout.trim();
|
|
2266
2282
|
const status = Number(separator >= 0 ? result.stdout.slice(separator + 1).trim() : '0');
|
|
@@ -2291,7 +2307,7 @@ async function verifyPublicUrl(publicUrl, path, timeoutMs) {
|
|
|
2291
2307
|
'--resolve',
|
|
2292
2308
|
`${hostname}:443:${resolvedIp}`,
|
|
2293
2309
|
target,
|
|
2294
|
-
], { encoding: 'utf8' });
|
|
2310
|
+
], { windowsHide: true, encoding: 'utf8' });
|
|
2295
2311
|
if (result.status === 0) {
|
|
2296
2312
|
const output = result.stdout;
|
|
2297
2313
|
const separator = output.lastIndexOf('\n');
|
|
@@ -2354,7 +2370,7 @@ async function shareWorldReserved(name, options) {
|
|
|
2354
2370
|
// Each spawned tunnel owns one log generation. Truncating before spawn prevents a repeated
|
|
2355
2371
|
// share from accepting the previous process's URL handshake as the new process's endpoint.
|
|
2356
2372
|
const out = openPrivateLog(log, 'w');
|
|
2357
|
-
const child = spawn(tunnel.command, tunnel.args, {
|
|
2373
|
+
const child = spawn(tunnel.command, tunnel.args, { windowsHide: true,
|
|
2358
2374
|
cwd: root,
|
|
2359
2375
|
env: process.env,
|
|
2360
2376
|
detached: true,
|
|
@@ -2523,7 +2539,8 @@ async function probeExternalOnce(ready, cwd, env, logPath) {
|
|
|
2523
2539
|
if (!commandExists(ready.command)) {
|
|
2524
2540
|
return { ok: false, message: `readiness command \`${ready.command}\` not found on PATH` };
|
|
2525
2541
|
}
|
|
2526
|
-
const
|
|
2542
|
+
const [probeBin, ...probeArgs] = ownCommand([ready.command, ...(ready.args ?? [])]);
|
|
2543
|
+
const result = spawnSync(probeBin, probeArgs, { windowsHide: true,
|
|
2527
2544
|
cwd, encoding: 'utf8', env, timeout: DOCTOR_EXTERNAL_PROBE_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024,
|
|
2528
2545
|
});
|
|
2529
2546
|
if (result.status === 0)
|
|
@@ -2897,7 +2914,7 @@ export function runWithWorldEnv(name, command, root = process.cwd(), options = {
|
|
|
2897
2914
|
throw new Error('Missing command after --');
|
|
2898
2915
|
const attached = worldAttachedCommandEnv(name, root, options.verbose);
|
|
2899
2916
|
return new Promise((resolveExit) => {
|
|
2900
|
-
const child = spawn(command[0], command.slice(1), {
|
|
2917
|
+
const child = spawn(command[0], command.slice(1), { windowsHide: true,
|
|
2901
2918
|
cwd: resolve(options.cwd ?? root),
|
|
2902
2919
|
env: attached.env,
|
|
2903
2920
|
stdio: 'inherit',
|
|
@@ -2981,7 +2998,7 @@ export async function attachWorld(name, command, root = process.cwd(), options =
|
|
|
2981
2998
|
let retired = true;
|
|
2982
2999
|
try {
|
|
2983
3000
|
return await new Promise((done, reject) => {
|
|
2984
|
-
const child = spawn(command[0], command.slice(1), { cwd: resolve(options.cwd ?? root), env: attached.env, stdio: 'inherit', detached: commandProcessGroup });
|
|
3001
|
+
const child = spawn(command[0], command.slice(1), { windowsHide: true, cwd: resolve(options.cwd ?? root), env: attached.env, stdio: 'inherit', detached: commandProcessGroup });
|
|
2985
3002
|
const lifetime = commandLifetime(child);
|
|
2986
3003
|
let failure;
|
|
2987
3004
|
record.consumerPid = child.pid;
|
|
@@ -3082,7 +3099,7 @@ async function runWithWorldEnvLogged(name, command, root, options = {}) {
|
|
|
3082
3099
|
let spawnError;
|
|
3083
3100
|
let child;
|
|
3084
3101
|
try {
|
|
3085
|
-
child = spawn(command[0], command.slice(1), {
|
|
3102
|
+
child = spawn(command[0], command.slice(1), { windowsHide: true,
|
|
3086
3103
|
cwd: resolve(options.cwd ?? root),
|
|
3087
3104
|
env: attached.env,
|
|
3088
3105
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
@@ -3189,7 +3206,7 @@ export async function shellWorld(name, root = process.cwd()) {
|
|
|
3189
3206
|
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`);
|
|
3190
3207
|
}
|
|
3191
3208
|
try {
|
|
3192
|
-
const result = spawnSync(shell, ['-i'], { cwd, env, stdio: 'inherit' });
|
|
3209
|
+
const result = spawnSync(shell, ['-i'], { windowsHide: true, cwd, env, stdio: 'inherit' });
|
|
3193
3210
|
return result.status ?? 0;
|
|
3194
3211
|
}
|
|
3195
3212
|
finally {
|
|
@@ -3297,7 +3314,7 @@ function startProxyDaemonOwned(name, root, caCertPath, envFile, boot, caller = p
|
|
|
3297
3314
|
mkdirSync(dirname(proxyLogPath(root, name)), { recursive: true, mode: 0o700 });
|
|
3298
3315
|
const proxyOut = openPrivateLog(proxyLogPath(root, name), 'a');
|
|
3299
3316
|
try {
|
|
3300
|
-
child = spawn(process.execPath, [proxyDaemonEntry(), name, '--root', root, ...(envFile ? [`--env-file=${envFile}`] : [])], {
|
|
3317
|
+
child = spawn(process.execPath, [proxyDaemonEntry(), name, '--root', root, ...(envFile ? [`--env-file=${envFile}`] : [])], { windowsHide: true,
|
|
3301
3318
|
cwd: root, env: process.env, detached: true, stdio: ['ignore', proxyOut, proxyOut],
|
|
3302
3319
|
});
|
|
3303
3320
|
}
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -190,6 +190,11 @@ export type WorldServiceConfig = {
|
|
|
190
190
|
* to loopback services directly while presenting a disguised world to the app/user.
|
|
191
191
|
*/
|
|
192
192
|
controlPlane?: boolean;
|
|
193
|
+
/** The account a vendor's own screens (its mirror) open signed in as: its handle, username or email, which the pack
|
|
194
|
+
* resolves against the accounts this World holds (`<vendor>MirrorSignIn`). Nothing secret is written here. */
|
|
195
|
+
signIn?: {
|
|
196
|
+
as: string;
|
|
197
|
+
};
|
|
193
198
|
injectEnv?: string;
|
|
194
199
|
/**
|
|
195
200
|
* Additional env vars exported into the generated world env after this service starts.
|
package/dist/src/schema.js
CHANGED
|
@@ -358,7 +358,7 @@ export function assertWorldSelection(value, path) {
|
|
|
358
358
|
function serviceV2ToLegacy(value, path) {
|
|
359
359
|
if (!record(value))
|
|
360
360
|
throw new Error(`World service must be an object in ${path}`);
|
|
361
|
-
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
|
|
361
|
+
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root', 'signIn'], `Service ${String(value.id ?? '')}`, path, true);
|
|
362
362
|
const source = value.source === undefined ? {} : value.source;
|
|
363
363
|
const execution = value.execution === undefined ? {} : value.execution;
|
|
364
364
|
const endpoint = value.endpoint === undefined ? {} : value.endpoint;
|
|
@@ -379,6 +379,13 @@ function serviceV2ToLegacy(value, path) {
|
|
|
379
379
|
throw new Error(`World service must define string id in ${path}`);
|
|
380
380
|
if (value.description !== undefined && typeof value.description !== 'string')
|
|
381
381
|
throw new Error(`Service "${value.id}" description must be a string in ${path}`);
|
|
382
|
+
if (value.signIn !== undefined) {
|
|
383
|
+
if (!record(value.signIn))
|
|
384
|
+
throw new Error(`Service "${value.id}" signIn must be an object in ${path}`);
|
|
385
|
+
assertKeys(value.signIn, ['as'], `Service "${value.id}" signIn`, path);
|
|
386
|
+
if (typeof value.signIn.as !== 'string' || value.signIn.as.trim() === '')
|
|
387
|
+
throw new Error(`Service "${value.id}" signIn.as must name an account (a handle, username or email) in ${path}`);
|
|
388
|
+
}
|
|
382
389
|
if (value.description !== undefined && value['//'] !== undefined)
|
|
383
390
|
throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
|
|
384
391
|
if (execution.cwd !== undefined && typeof execution.cwd !== 'string')
|
|
@@ -437,6 +444,7 @@ function serviceV2ToLegacy(value, path) {
|
|
|
437
444
|
...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates }),
|
|
438
445
|
...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect }),
|
|
439
446
|
...(value.root === undefined ? {} : { root: value.root }),
|
|
447
|
+
...(value.signIn === undefined ? {} : { signIn: { as: value.signIn.as } }),
|
|
440
448
|
...(external === undefined ? {} : { external }),
|
|
441
449
|
};
|
|
442
450
|
if (type !== 'external' && bindings.discover !== undefined)
|
|
@@ -573,6 +581,7 @@ function serviceToV2(service) {
|
|
|
573
581
|
...(Object.keys(endpoint).length ? { endpoint } : {}),
|
|
574
582
|
...(Object.keys(bindings).length ? { bindings } : {}),
|
|
575
583
|
...(service.root === undefined ? {} : { root: service.root }),
|
|
584
|
+
...(service.signIn === undefined ? {} : { signIn: service.signIn }),
|
|
576
585
|
};
|
|
577
586
|
}
|
|
578
587
|
/** Serialize normalized intent as the committed format-2 manifest. */
|