@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.
Files changed (78) hide show
  1. package/dist/known-external-services.json +0 -8
  2. package/dist/src/app-url.js +1 -1
  3. package/dist/src/branch.js +79 -2
  4. package/dist/src/catalog.js +1 -1
  5. package/dist/src/cli.js +50 -22
  6. package/dist/src/console-apart.d.ts +1 -0
  7. package/dist/src/console-apart.js +7 -0
  8. package/dist/src/covers.js +4 -2
  9. package/dist/src/fixture-env.d.ts +3 -0
  10. package/dist/src/fixture-env.js +24 -0
  11. package/dist/src/host-worker.js +2 -9
  12. package/dist/src/host.js +2 -9
  13. package/dist/src/import-module.d.ts +1 -0
  14. package/dist/src/import-module.js +16 -0
  15. package/dist/src/index.d.ts +7 -2
  16. package/dist/src/index.js +4 -1
  17. package/dist/src/infra-cli.js +61 -12
  18. package/dist/src/init.d.ts +1 -1
  19. package/dist/src/init.js +34 -5
  20. package/dist/src/local-branches.d.ts +37 -0
  21. package/dist/src/local-branches.js +193 -0
  22. package/dist/src/pglite-backing.d.ts +8 -0
  23. package/dist/src/pglite-backing.js +121 -35
  24. package/dist/src/pglite-host.mjs +520 -14
  25. package/dist/src/prerequisites.js +1 -1
  26. package/dist/src/process-groups.js +1 -1
  27. package/dist/src/redirect-proxy.d.ts +1 -1
  28. package/dist/src/redirect-proxy.js +4 -4
  29. package/dist/src/redis-backing.d.ts +8 -0
  30. package/dist/src/redis-backing.js +120 -0
  31. package/dist/src/root.d.ts +23 -0
  32. package/dist/src/root.js +22 -10
  33. package/dist/src/run-task.js +1 -1
  34. package/dist/src/runtime.d.ts +1 -0
  35. package/dist/src/runtime.js +37 -20
  36. package/dist/src/schema.d.ts +5 -0
  37. package/dist/src/schema.js +10 -1
  38. package/dist/src/served-world.d.ts +196 -9
  39. package/dist/src/served-world.js +847 -103
  40. package/dist/src/service-recorder.js +1 -1
  41. package/dist/src/storage-capacity.js +2 -2
  42. package/dist/src/up-task.js +1 -1
  43. package/dist/src/world-origins.d.ts +13 -0
  44. package/dist/src/world-origins.js +37 -0
  45. package/dist/src/world-view.d.ts +25 -0
  46. package/dist/src/world-view.js +110 -0
  47. package/known-external-services.json +0 -8
  48. package/package.json +10 -4
  49. package/src/app-url.ts +1 -1
  50. package/src/branch.ts +66 -2
  51. package/src/catalog.ts +1 -1
  52. package/src/cli.ts +43 -21
  53. package/src/console-apart.ts +7 -1
  54. package/src/covers.ts +4 -2
  55. package/src/fixture-env.ts +25 -0
  56. package/src/host-worker.ts +2 -1
  57. package/src/host.ts +2 -1
  58. package/src/import-module.ts +9 -0
  59. package/src/index.ts +7 -2
  60. package/src/infra-cli.ts +56 -12
  61. package/src/init.ts +34 -5
  62. package/src/local-branches.ts +173 -0
  63. package/src/pglite-backing.ts +112 -36
  64. package/src/pglite-host.mjs +520 -14
  65. package/src/prerequisites.ts +1 -1
  66. package/src/process-groups.ts +1 -1
  67. package/src/redirect-proxy.ts +4 -4
  68. package/src/redis-backing.ts +107 -0
  69. package/src/root.ts +27 -2
  70. package/src/run-task.ts +1 -1
  71. package/src/runtime.ts +38 -20
  72. package/src/schema.ts +11 -1
  73. package/src/served-world.ts +762 -85
  74. package/src/service-recorder.ts +1 -1
  75. package/src/storage-capacity.ts +2 -2
  76. package/src/up-task.ts +1 -1
  77. package/src/world-origins.ts +38 -0
  78. 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 ? ((await import(entry)) as Record<string, unknown>) : null;
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 (await import(file ? join(dir, file) : entry)) as Record<string, unknown>;
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
- VOLTER_WORLD_DATA: data,
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
- const result = spawnSync('which', [command], { stdio: 'ignore', env: process.env });
2194
- return result.status === 0;
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 result = spawnSync(ready.command, ready.args ?? [], {
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