@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
@@ -59,7 +59,7 @@ const record = (entry, then) => {
59
59
  process.stdout.write(`${logEnd().atLineStart ? '' : '\n'}${line}`, () => then?.());
60
60
  };
61
61
  const started = Date.now();
62
- const child = spawn(command, args, { stdio: 'inherit', env });
62
+ const child = spawn(command, args, { windowsHide: true, stdio: 'inherit', env });
63
63
  if (child.pid !== undefined)
64
64
  record({ event: 'world.service.started', pid: child.pid, parentPid: process.pid });
65
65
  /** The log's last `bytes` (from a line start) as the rotation `<log>.1`: however far a runaway writer overshot the cap,
@@ -16,7 +16,7 @@ export function storagePoolResolver(onFailure) {
16
16
  let pool;
17
17
  if (process.platform === 'darwin') {
18
18
  stage = 'volume lookup';
19
- const df = execFileSync('/bin/df', ['-P', root], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'] });
19
+ const df = execFileSync('/bin/df', ['-P', root], { windowsHide: true, encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'] });
20
20
  const disk = df.trim().split('\n').at(-1)?.trim().split(/\s+/)[0];
21
21
  if (disk && /^\/dev\/disk\d+(?:s\d+)*$/.test(disk)) {
22
22
  stage = 'volume metadata';
@@ -51,7 +51,7 @@ export function storagePoolResolver(onFailure) {
51
51
  };
52
52
  }
53
53
  function diskInfo(disk) {
54
- return execFileSync('/usr/sbin/diskutil', ['info', '-plist', disk], {
54
+ return execFileSync('/usr/sbin/diskutil', ['info', '-plist', disk], { windowsHide: true,
55
55
  encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'ignore'],
56
56
  });
57
57
  }
@@ -8,7 +8,7 @@ import { siblingScript } from "./sibling.js";
8
8
  * is not the caller's to keep, so the boot completes and its outcome is written to the lifecycle log. */
9
9
  export function superviseWorldUp(configId, options) {
10
10
  return new Promise((done, reject) => {
11
- const child = spawn(process.execPath, [siblingScript(import.meta.url, 'up-task-worker')], {
11
+ const child = spawn(process.execPath, [siblingScript(import.meta.url, 'up-task-worker')], { windowsHide: true,
12
12
  cwd: process.cwd(), env: process.env, detached: process.platform !== 'win32',
13
13
  stdio: ['ignore', 'inherit', 'inherit', 'ipc'], serialization: 'json',
14
14
  });
@@ -0,0 +1,13 @@
1
+ export declare const LOCAL_ORIGIN_BASE = "localhost";
2
+ /** The origin a browser reaches `served` at on a local host listening on `port`, or null when its
3
+ * names make no label. */
4
+ export declare function localWorldOrigin(served: string, port: number, base?: string): string | null;
5
+ /** Which World a request's host names: undefined for a host that is not a World's origin (the host's
6
+ * own, path-addressed), null for a World-origin-shaped host naming no World served here. */
7
+ export declare function worldOfHost(hostname: string, served: Iterable<string>, base?: string): string | null | undefined;
8
+ /** Whether a path under the console's base is a door of a World served here: a World whose org shares the console's
9
+ * name (`console/<world>`) has its doors at `/-/console/<world>/…`, and they are the World's, never the console's. */
10
+ export declare function worldDoorUnderConsole(path: string, served: (name: string) => boolean, consoleBase: string): boolean;
11
+ /** Whether a path on a World's origin belongs to that World (its wire, its doors, its mirrors) or to
12
+ * what every origin carries (`/-/ping`, the console): a World's origin serves no other World. */
13
+ export declare function pathOfWorld(path: string, served: string, consoleBase: string): boolean;
@@ -0,0 +1,37 @@
1
+ // WORLD ORIGINS for a local host (docs/contributing/architecture.md, "Viewing a World"): each World a
2
+ // browser steps into is reached at an origin of its own, `http://<world>--<org>.localhost:<port>`, so no
3
+ // World's page shares an origin with another World's session. Browsers and curl resolve `*.localhost`
4
+ // to loopback. Callers that present a token keep path addressing on the host's own origin.
5
+ import { worldOriginLabel } from "./served-world.js";
6
+ export const LOCAL_ORIGIN_BASE = 'localhost';
7
+ /** The origin a browser reaches `served` at on a local host listening on `port`, or null when its
8
+ * names make no label. */
9
+ export function localWorldOrigin(served, port, base = LOCAL_ORIGIN_BASE) {
10
+ const label = worldOriginLabel(served);
11
+ return label ? `http://${label}.${base}:${port}` : null;
12
+ }
13
+ /** Which World a request's host names: undefined for a host that is not a World's origin (the host's
14
+ * own, path-addressed), null for a World-origin-shaped host naming no World served here. */
15
+ export function worldOfHost(hostname, served, base = LOCAL_ORIGIN_BASE) {
16
+ const suffix = `.${base}`;
17
+ if (!hostname.endsWith(suffix))
18
+ return undefined;
19
+ const label = hostname.slice(0, -suffix.length);
20
+ for (const name of served)
21
+ if (worldOriginLabel(name) === label)
22
+ return name;
23
+ return null;
24
+ }
25
+ /** Whether a path under the console's base is a door of a World served here: a World whose org shares the console's
26
+ * name (`console/<world>`) has its doors at `/-/console/<world>/…`, and they are the World's, never the console's. */
27
+ export function worldDoorUnderConsole(path, served, consoleBase) {
28
+ if (path !== consoleBase && !path.startsWith(`${consoleBase}/`))
29
+ return false;
30
+ const m = /^\/-\/([^/]+)\/([^/]+)/.exec(path);
31
+ return Boolean(m && served(`${m[1]}/${m[2]}`));
32
+ }
33
+ /** Whether a path on a World's origin belongs to that World (its wire, its doors, its mirrors) or to
34
+ * what every origin carries (`/-/ping`, the console): a World's origin serves no other World. */
35
+ export function pathOfWorld(path, served, consoleBase) {
36
+ return path === '/-/ping' || path === consoleBase || path.startsWith(`${consoleBase}/`) || path.startsWith(`/${served}/`) || path.startsWith(`/-/${served}/`) || path === `/${served}` || path === `/-/${served}`;
37
+ }
@@ -0,0 +1,25 @@
1
+ /** A console as a host mounts it (`@volter/world-console`): answer a request under `base`, or null. */
2
+ export type ViewConsole = {
3
+ handle: (request: Request, base: string, opts?: {
4
+ worldOrigin?: boolean;
5
+ }) => Promise<Response | null>;
6
+ };
7
+ export declare const VIEW_CONSOLE_BASE = "/-/console";
8
+ export type WorldView = {
9
+ url: string;
10
+ base: string;
11
+ served: string;
12
+ token: string;
13
+ readToken: string;
14
+ console: string | null; /** the World's own browser origin, when it has one */
15
+ origin: string | null;
16
+ stop: () => Promise<void>;
17
+ };
18
+ export declare function serveWorldView(name: string, opts?: {
19
+ root?: string;
20
+ port?: number;
21
+ host?: string;
22
+ console?: ViewConsole;
23
+ announce?: (line: string) => void; /** each World a browser steps into at an origin of its own (on by default on loopback) */
24
+ origins?: boolean;
25
+ }): Promise<WorldView>;
@@ -0,0 +1,110 @@
1
+ // THE LOCAL VIEW: one World served for a person to step into — its doors, its mirrors, its branches
2
+ // ("as of" views) and the console, on one origin (docs/contributing/architecture.md, "Viewing a
3
+ // World"). `volter world view` is this; the runtime's `up` gains no step. A host that serves many
4
+ // Worlds (world-host) answers the same doors; this serves one, and the branches made of it.
5
+ import { existsSync, readdirSync } from 'node:fs';
6
+ import { loadWorldConfig } from "./configs.js";
7
+ import { join, resolve } from 'node:path';
8
+ import { serveHttp, stateDirName } from '@volter/world-core';
9
+ import { LocalBranches } from "./local-branches.js";
10
+ import { mountWorld } from "./served-world.js";
11
+ import { localWorldOrigin, pathOfWorld, worldDoorUnderConsole, worldOfHost } from "./world-origins.js";
12
+ export const VIEW_CONSOLE_BASE = '/-/console';
13
+ /** The branch Worlds already under `dir` (`<dir>/<org>/<world>/.volter/world.json`). */
14
+ function branchRoots(dir) {
15
+ if (!existsSync(dir))
16
+ return [];
17
+ const out = [];
18
+ for (const org of readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory())) {
19
+ for (const world of readdirSync(join(dir, org.name), { withFileTypes: true }).filter((e) => e.isDirectory())) {
20
+ const root = join(dir, org.name, world.name);
21
+ if (existsSync(join(root, stateDirName(), 'world.json')))
22
+ out.push(root);
23
+ }
24
+ }
25
+ return out.sort();
26
+ }
27
+ export async function serveWorldView(name, opts = {}) {
28
+ const root = resolve(opts.root ?? process.cwd());
29
+ const host = opts.host ?? '127.0.0.1';
30
+ const worlds = new Map();
31
+ let loopback = '';
32
+ let port = 0;
33
+ const loopbackBind = host === '127.0.0.1' || host === 'localhost' || host === '::1';
34
+ // each World's browser session on an origin of its own, where the view listens on loopback (*.localhost)
35
+ const origins = opts.origins ?? loopbackBind;
36
+ // bound to loopback with an origin per World, the view is for the person at this machine: its pages need no token
37
+ // (served-world.ts localSession). Without origins every World's pages share one, and none is handed a session.
38
+ const local = loopbackBind && origins;
39
+ // a non-loopback --host turns origins off by default: say so (the World and its branches then share one origin)
40
+ if (opts.origins === undefined && !origins)
41
+ opts.announce?.(`origins off (bound to ${host}): the World and its branches share one origin, where a browser session is only path-scoped; bind loopback for an origin per World`);
42
+ const browserOrigin = origins ? (served) => (port ? localWorldOrigin(served, port) : null) : undefined;
43
+ const mountAt = (at) => mountWorld(loadWorldConfig(join(at, stateDirName(), 'world.json'), at).config.id, { root: at, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}) });
44
+ const branches = new LocalBranches({ dir: join(root, stateDirName(), 'branches'), origin: () => loopback, worlds, mount: mountAt, ...(opts.announce ? { announce: opts.announce } : {}) });
45
+ const world = await mountWorld(name, { root, branches: (served) => branches.doorsFor(served), ...(browserOrigin ? { browserOrigin } : {}), ...(local ? { localTrust: true } : {}) });
46
+ worlds.set(world.served, world);
47
+ const server = await serveHttp({
48
+ // a pushed blob (a release video) is one PUT: bodies up to 1 GiB, as `serve` takes them
49
+ hostname: host, port: opts.port ?? 0, maxRequestBodySize: 1024 * 1024 * 1024,
50
+ // this front serves a World and its branches; each World's own twins journal their requests
51
+ twinRequestJournal: false,
52
+ async fetch(request) {
53
+ const at = new URL(request.url);
54
+ const path = at.pathname;
55
+ // a World's own origin serves that World (and the ping and the console) and no other
56
+ const own = origins ? worldOfHost(at.hostname, worlds.keys()) : undefined;
57
+ if (own === null)
58
+ return Response.json({ error: `no world at ${at.host}` }, { status: 404 });
59
+ // the bare address, typed into a browser, is the World's page: at its own origin when it has one
60
+ if (opts.console && path === '/' && request.method === 'GET' && (request.headers.get('accept') ?? '').includes('text/html')) {
61
+ const served = own ?? world.served;
62
+ const page = `${VIEW_CONSOLE_BASE}/${served}`;
63
+ return Response.redirect(own ? `${at.origin}${page}` : `${browserOrigin?.(served) ?? at.origin}${page}`, 302);
64
+ }
65
+ if (own && !pathOfWorld(path, own, VIEW_CONSOLE_BASE))
66
+ return Response.json({ error: `this origin serves ${own} only` }, { status: 404 });
67
+ if (path === '/-/ping')
68
+ return Response.json({ ok: true, worlds: own ? [own] : [...worlds.keys()], ...(opts.console ? { console: `${VIEW_CONSOLE_BASE}/` } : {}) });
69
+ if (opts.console && (path === VIEW_CONSOLE_BASE || path.startsWith(`${VIEW_CONSOLE_BASE}/`)) && !worldDoorUnderConsole(path, (n) => worlds.has(n), VIEW_CONSOLE_BASE)) {
70
+ // on a World's own origin the console keeps no token script can read (its session is the credential)
71
+ const answered = await opts.console.handle(request, VIEW_CONSOLE_BASE, own ? { worldOrigin: true } : undefined);
72
+ if (answered)
73
+ return answered;
74
+ }
75
+ const m = /^\/(?:-\/)?([^/]+)\/([^/]+)/.exec(path);
76
+ const target = m ? worlds.get(`${m[1]}/${m[2]}`) : undefined;
77
+ if (!target)
78
+ return Response.json({ error: `no world at ${path}: this view serves ${[...worlds.keys()].join(', ')}` }, { status: 404 });
79
+ return target.handle(request);
80
+ },
81
+ });
82
+ port = server.port ?? 0;
83
+ const url = `http://${host}:${server.port}`;
84
+ loopback = `http://${host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host}:${server.port}`;
85
+ // a branch outlives the view that made it until its time runs out: stopping stops its compute only
86
+ const stop = async () => {
87
+ branches.stop();
88
+ await server.stop(true);
89
+ for (const w of [...worlds.values()].reverse())
90
+ await w.stop();
91
+ };
92
+ try {
93
+ await world.boot(url);
94
+ // branches an earlier view made: mounted again (the sweep then removes those whose time ran out)
95
+ for (const at of branchRoots(join(root, stateDirName(), 'branches'))) {
96
+ const b = await mountAt(at);
97
+ await b.boot(url);
98
+ worlds.set(b.served, b);
99
+ }
100
+ await branches.resume();
101
+ }
102
+ catch (error) {
103
+ await stop().catch(() => undefined);
104
+ throw error;
105
+ }
106
+ // the console opens on the World's own origin, where its session lives
107
+ const consoleUrl = opts.console ? `${browserOrigin?.(world.served) ?? url}${VIEW_CONSOLE_BASE}/${world.served}` : null;
108
+ opts.announce?.(`viewing ${world.served} ${url}/${world.served}${consoleUrl ? `\nconsole ${consoleUrl}` : ''}`);
109
+ return { url, base: `${url}/${world.served}`, served: world.served, get token() { return world.token; }, get readToken() { return world.readToken; }, console: consoleUrl, origin: browserOrigin?.(world.served) ?? null, stop };
110
+ }
@@ -373,10 +373,6 @@
373
373
  "disposition": "demanded",
374
374
  "reason": "vendor google-play (Play Developer API), ×1 — ladder classification 2026-09-02"
375
375
  },
376
- "@googleapis/calendar": {
377
- "disposition": "demanded",
378
- "reason": "vendor google-calendar, ×1 (plus the GOOGLE_CALENDAR_API_KEY stem, ×1) — ladder classification 2026-09-02"
379
- },
380
376
  "@googleapis/sheets": {
381
377
  "disposition": "demanded",
382
378
  "reason": "vendor google-sheets, ×1 — ladder classification 2026-09-02"
@@ -863,10 +859,6 @@
863
859
  "disposition": "demanded",
864
860
  "reason": "vendor ghost (VITE_PUBLIC_GHOST_CONTENT_API_KEY — the browser-side Content API key), ×1 — ladder classification 2026-09-02"
865
861
  },
866
- "googlecalendar": {
867
- "disposition": "demanded",
868
- "reason": "vendor google-calendar (GOOGLE_CALENDAR_API_KEY; the @googleapis/calendar client is the same demand), ×1 — ladder classification 2026-09-02"
869
- },
870
862
  "googlecloud": {
871
863
  "disposition": "demanded",
872
864
  "reason": "vendor googlecloud (GOOGLE_CLOUD_API_KEY; the roster's one consumer spends it on the Web Fonts Developer API at www.googleapis.com/webfonts/v1, so the stem is a Google Cloud API key rather than a googleoauth credential), ×1 — ladder classification 2026-09-02"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
5
5
  "keywords": [
6
6
  "twin",
@@ -59,6 +59,7 @@
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.2.20",
61
61
  "@types/node": "^24.0.0",
62
+ "mongodb": "~6.20.0",
62
63
  "pg": "8.16.3",
63
64
  "typescript": "^5.9.0"
64
65
  },
@@ -66,12 +67,14 @@
66
67
  "node": ">=22.3"
67
68
  },
68
69
  "peerDependencies": {
69
- "@volter/world-core": "2.0.0",
70
- "@volter/world-console": "2.0.0"
70
+ "@volter/world-core": "2.0.2",
71
+ "@volter/world-console": "2.0.2"
71
72
  },
72
73
  "dependencies": {
73
74
  "@electric-sql/pglite": "0.5.8",
74
- "@volter/world-core": "2.0.0",
75
+ "@electric-sql/pglite-pgvector": "0.0.9",
76
+ "@volter/world-access": "2.0.0",
77
+ "@volter/world-core": "2.0.2",
75
78
  "pg-gateway": "0.3.0-beta.4",
76
79
  "smol-toml": "^1.8.0"
77
80
  },
@@ -79,5 +82,8 @@
79
82
  "@volter/world-console": {
80
83
  "optional": true
81
84
  }
85
+ },
86
+ "optionalDependencies": {
87
+ "@volter/twin-mongodb": "0.1.2"
82
88
  }
83
89
  }
package/src/app-url.ts CHANGED
@@ -150,7 +150,7 @@ export function addAppHosts(name: string, hosts: string[], options: { root?: str
150
150
 
151
151
  /** The listening TCP ports of a live pid, via lsof (macOS + Linux). */
152
152
  function listeningTcpPorts(pid: number): number[] {
153
- const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { encoding: 'utf8' });
153
+ const result = spawnSync('lsof', ['-a', '-p', String(pid), '-iTCP', '-sTCP:LISTEN', '-P', '-n', '-Fn'], { windowsHide: true, encoding: 'utf8' });
154
154
  if (result.error) {
155
155
  throw new Error(`volter-world app-url: \`lsof\` is unavailable (${result.error.message}) — pass the port, or --set the URL directly`);
156
156
  }
package/src/branch.ts CHANGED
@@ -2,13 +2,18 @@
2
2
  // log of changes, and a world is a branch with compute attached (docs/concepts/the-model.md).
3
3
  // `branch <world> <name>` boots a new world from the base's config and forks every twin's mirror
4
4
  // into it (core's `forkTwin`: a POINTER to the base at its current position — the base's whole
5
- // history, its own entries included, is the branch's parent; nothing is copied). `checkout <name>`
5
+ // history, its own entries included, is the branch's parent; nothing is copied). A managed database (containerless
6
+ // Postgres) keeps no history, so it is the one thing copied: the stopped base's data, as of now. Two branches of one
7
+ // base running at once share the database port its definition names; run one at a time (the SDK does). `checkout <name>`
6
8
  // brings a stopped world back up from its own instance record, state intact.
7
- import { existsSync, readdirSync } from 'node:fs';
9
+ import { cpSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
8
10
  import { dirname, join, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
9
12
  import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withAncestryLock, worldPaths, worldStateRoot } from '@volter/world-core';
10
13
  import { downWorld, saveWorldInstance, statusWorld, upWorld } from './runtime.ts';
11
14
  import type { WorldInstance } from './schema.ts';
15
+ import { parseInfraDefinition, pgliteDown, pgliteUp } from './pglite-backing.ts';
16
+ import { redisTwinCli, redisTwinDown, redisTwinUp } from './redis-backing.ts';
12
17
 
13
18
  export type BranchOptions = { root?: string; envFile?: string; now?: Date; /** branch from a point in the base's history: an instant, or a position per twin (contract "Just like Neon", 3) */ at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string> } };
14
19
 
@@ -37,6 +42,12 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
37
42
  return { state, view, position, generation: stateGeneration(worldPaths(state, fromRoot).dir) };
38
43
  }) };
39
44
  }));
45
+ // the base's managed databases (checked before the branch boots: its database would share the base's port)
46
+ const databases = managedDatabases(from);
47
+ if (databases.length && (options.at?.instant || options.at?.positions || options.at?.views)) {
48
+ throw new Error(`volter-world branch: ${base} has a managed database, which keeps no history to branch from at an instant or position; branch from its current state`);
49
+ }
50
+ refuseLiveDatabases(from, databases);
40
51
  const instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile });
41
52
  const forked: Record<string, string[]> = {};
42
53
  try {
@@ -48,6 +59,7 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
48
59
  }
49
60
  if (states.length) forked[service] = states.map(s => s.state);
50
61
  }
62
+ await branchManagedDatabases(from, instance, databases);
51
63
  } catch (error) {
52
64
  await downWorld(name, root, { purge: true, expectedCreatedAt: instance.createdAt });
53
65
  throw error;
@@ -55,6 +67,58 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
55
67
  return { instance, forked };
56
68
  }
57
69
 
70
+ /** The base's containerless managed databases: each PGlite `pglite-<kind>-data` and the MongoDB twin's
71
+ * `twin-mongodb-data` (pglite-backing.ts), and the redis twin's `redis-twin` tree (redis-backing.ts). A stopped World's
72
+ * hosts leave no pid file; a docker-backed World keeps its data under the container's volume, which a branch does not
73
+ * carry. */
74
+ function managedDatabases(from: WorldInstance): string[] {
75
+ const data = from.dirs.data;
76
+ if (!existsSync(data)) return [];
77
+ return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'twin-mongodb-data' || n === 'redis-twin');
78
+ }
79
+
80
+ /** The pid files of the hosts serving one managed database directory. */
81
+ function hostPidFiles(data: string, db: string): string[] {
82
+ if (db === 'redis-twin') return readdirSync(data).filter((n) => /^redis-twin-\d+\.pid$/.test(n)).map((n) => join(data, n));
83
+ return [join(data, `${db.slice(0, -'-data'.length)}.pid`)];
84
+ }
85
+
86
+ /** A copy of a live database directory is not a consistent one, and the branch's database would answer on the base's
87
+ * port: the base must be stopped first (`volter world branch` stops it, as a branch replaces its base). */
88
+ function refuseLiveDatabases(from: WorldInstance, dbs: string[]): void {
89
+ for (const db of dbs) {
90
+ for (const pidFile of hostPidFiles(from.dirs.data, db)) {
91
+ const pid = existsSync(pidFile) ? Number(readFileSync(pidFile, 'utf8').trim()) : NaN;
92
+ let live = false;
93
+ if (Number.isInteger(pid) && pid > 0) { try { process.kill(pid, 0); live = true; } catch { live = false; } }
94
+ if (live) throw new Error(`volter-world branch: ${from.name}'s managed ${db} is running (pid ${pid}); stop ${from.name} first so its database can be copied into the branch consistently`);
95
+ }
96
+ }
97
+ }
98
+
99
+ /** The branch's managed databases as the base holds them: the branch's own, just booted empty by `up`, are stopped,
100
+ * replaced by a copy of the base's data, and started again, so a branch of a migrated and seeded World serves that
101
+ * data, as its twins serve the base's history. */
102
+ async function branchManagedDatabases(from: WorldInstance, instance: WorldInstance, dbs: string[]): Promise<void> {
103
+ if (!dbs.length) return;
104
+ const definition = join(dirname(instance.configPath), 'world.infrastructure.yml');
105
+ if (!existsSync(definition)) return;
106
+ const services = parseInfraDefinition(readFileSync(definition, 'utf8'));
107
+ const redis = services.filter((s) => s.kind === 'redis');
108
+ const hosted = services.filter((s) => s.kind !== 'redis');
109
+ const redisCli = dbs.includes('redis-twin') && redis.length ? redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(instance.configPath)]) : undefined;
110
+ await pgliteDown(hosted, instance.dirs.data);
111
+ if (redisCli) await redisTwinDown(redis, instance.dirs.data);
112
+ for (const db of dbs) {
113
+ if (db === 'redis-twin' && !redisCli) continue;
114
+ const to = join(instance.dirs.data, db);
115
+ rmSync(to, { recursive: true, force: true });
116
+ cpSync(join(from.dirs.data, db), to, { recursive: true });
117
+ }
118
+ if (dbs.some((db) => db !== 'redis-twin')) await pgliteUp(hosted, instance.dirs.data);
119
+ if (redisCli) await redisTwinUp(redis, instance.dirs.data, redisCli);
120
+ }
121
+
58
122
  /** A stopped world back up, from its own record. */
59
123
  export async function checkoutWorld(name: string, options: { root?: string } = {}): Promise<WorldInstance> {
60
124
  const root = resolve(options.root ?? process.cwd());
package/src/catalog.ts CHANGED
@@ -115,7 +115,7 @@ export function checkoutCatalog(root: string): Catalog {
115
115
  packDir: (vendor) => (isPackDir(join(packsDir, vendor)) ? join(packsDir, vendor) : undefined),
116
116
  version: (vendor) => packageVersion(join(packsDir, vendor)),
117
117
  sha: () => {
118
- const proc = spawnSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
118
+ const proc = spawnSync('git', ['-C', root, 'rev-parse', 'HEAD'], { windowsHide: true, encoding: 'utf8' });
119
119
  const sha = proc.status === 0 ? proc.stdout.trim() : '';
120
120
  return /^[0-9a-f]{40}$/.test(sha) ? sha : 'unknown';
121
121
  },
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { sessionTrustEnv } from './ca-trust.ts';
3
3
  import { spawn, spawnSync } from 'node:child_process';
4
4
  import { createRequire } from 'node:module';
5
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
5
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { dirname, join, resolve, resolve as resolvePath } from 'node:path';
8
8
  import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, upWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from './index.ts';
@@ -518,39 +518,61 @@ async function main(): Promise<void> {
518
518
  }
519
519
 
520
520
  if (cmd === 'clock') {
521
- // THE operator door for world time (physics): show / set <iso> / advance <duration>.
522
- // The clock is a frozen instant every twin reads per request (kernel worldNow()); setting
523
- // or advancing takes effect live, no restarts. `advance` requires a set clock (advancing
524
- // wall-clock would silently freeze time as a side effect).
521
+ // THE operator door for world time (physics): show / set <iso> / advance <duration> / shift <duration> / clear.
522
+ // The clock every twin reads per request (kernel worldNow()), and every application process through the injector
523
+ // (inject.cjs, WORLD TIME); a change takes effect live, no restarts. Its two forms (world-clock.cjs): `set` freezes
524
+ // time at an instant (scripted time: a life, a seed); `shift` moves a World forward while its time keeps running (a
525
+ // World serving an application, whose time a frozen instant would stop); `advance` moves either form; `clear`
526
+ // returns the World to the machine's time. `advance` refuses a World on the machine's time, which it would freeze.
525
527
  // argv shape here: cmd='clock', subject=<world>, rest=[action, value?, flags...]
528
+ const clockForm = createRequire(import.meta.url)('@volter/world-core/world-clock') as typeof import('@volter/world-core/world-clock');
526
529
  const name = subject;
527
530
  const [action, value] = rest;
528
- if (!name || !action || (action !== 'show' && action !== 'set' && action !== 'advance')) {
529
- console.error('usage: volter-world clock <world> show | set <iso-8601> | advance <N s|m|h|d> [--root <repo>]');
531
+ if (!name || !action || !['show', 'set', 'advance', 'shift', 'clear'].includes(action)) {
532
+ console.error('usage: volter-world clock <world> show | set <iso-8601> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear [--root <repo>]');
530
533
  process.exit(2);
531
534
  }
532
535
  const rootDir = resolve(optionValue(rest, '--root') ?? process.cwd());
533
536
  const file = clockFile(rootDir, name);
537
+ const current = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
538
+ const write = (clock: Parameters<typeof clockForm.formatClock>[0]) => {
539
+ mkdirSync(dirname(file), { recursive: true });
540
+ writeFileSync(file, `${clockForm.formatClock(clock)}\n`);
541
+ // the World's time now, bare, as scripts read it (`show` says which form)
542
+ console.log(new Date(clockForm.clockNowMs(clock, Date.now())).toISOString());
543
+ };
534
544
  if (action === 'show') {
535
- console.log(existsSync(file) ? `${readFileSync(file, 'utf8').trim()} (frozen)` : `${new Date().toISOString()} (wall clock — no world clock set)`);
545
+ if (!current) console.log(`${new Date().toISOString()} (wall clock — no world clock set)`);
546
+ else if (current.kind === 'frozen') console.log(`${new Date(current.at).toISOString()} (frozen)`);
547
+ else {
548
+ const ahead = current.at - current.since;
549
+ console.log(`${new Date(clockForm.clockNowMs(current, Date.now())).toISOString()} (running, ${ahead >= 0 ? '+' : '-'}${Math.abs(ahead) / 1000}s from the machine's time)`);
550
+ }
551
+ return;
552
+ }
553
+ if (action === 'clear') {
554
+ if (existsSync(file)) rmSync(file);
555
+ console.log(`${new Date().toISOString()} (wall clock — no world clock set)`);
536
556
  return;
537
557
  }
538
558
  if (action === 'set') {
539
559
  const parsed = Date.parse(value ?? '');
540
560
  if (Number.isNaN(parsed)) { console.error(`clock set: ${JSON.stringify(value)} is not an ISO-8601 instant`); process.exit(2); }
541
- mkdirSync(dirname(file), { recursive: true });
542
- writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
543
- console.log(new Date(parsed).toISOString());
561
+ write({ kind: 'frozen', at: parsed });
544
562
  return;
545
563
  }
546
564
  const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec((value ?? '').trim());
547
- if (!m) { console.error(`clock advance: ${JSON.stringify(value)} is not <N>(s|m|h|d)`); process.exit(2); }
548
- if (!existsSync(file)) { console.error('clock advance: no world clock is set (advance from wall-clock would freeze time as a side effect) — `clock set <iso>` first'); process.exit(2); }
549
- const base = Date.parse(readFileSync(file, 'utf8').trim());
550
- const unit = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
551
- const next = new Date(base + Number(m[1]) * unit).toISOString();
552
- writeFileSync(file, `${next}\n`);
553
- console.log(next);
565
+ if (!m) { console.error(`clock ${action}: ${JSON.stringify(value)} is not <N>(s|m|h|d)`); process.exit(2); }
566
+ const by = Number(m[1]) * { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
567
+ if (action === 'advance') {
568
+ if (!current) { console.error('clock advance: no world clock is set (advance from wall-clock would freeze time as a side effect) — `clock set <iso>` first, or `clock shift` to move a running World'); process.exit(2); }
569
+ write({ ...current, at: current.at + by });
570
+ return;
571
+ }
572
+ // shift: a World on the machine's time, or already running, moves forward and keeps running
573
+ if (current?.kind === 'frozen') { console.error('clock shift: this World\'s clock is frozen — `clock advance` moves it, or `clock clear` first'); process.exit(2); }
574
+ const wall = Date.now();
575
+ write(current ? { ...current, at: current.at + by } : { kind: 'running', at: wall + by, since: wall });
554
576
  return;
555
577
  }
556
578
 
@@ -838,7 +860,7 @@ async function main(): Promise<void> {
838
860
  // VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
839
861
  const commandEnv: Record<string, string | undefined> = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
840
862
  commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
841
- const child = spawn(command[0]!, command.slice(1), { env: commandEnv, stdio: 'inherit' });
863
+ const child = spawn(command[0]!, command.slice(1), { windowsHide: true, env: commandEnv, stdio: 'inherit' });
842
864
  const code = await new Promise<number>((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
843
865
  // a client that wrote and exited at once still has bytes on their way through a bridge
844
866
  await drainStreams();
@@ -862,7 +884,7 @@ async function main(): Promise<void> {
862
884
  const compose = splitDockerComposeArgs(command);
863
885
  if (compose) {
864
886
  // the consumer's own service names, from its own files, through its own docker
865
- const listed = spawnSync(compose.head[0]!, [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
887
+ const listed = spawnSync(compose.head[0]!, [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { windowsHide: true, env, stdio: ['ignore', 'pipe', 'pipe'] });
866
888
  if (listed.status !== 0) throw new Error(`volter-world attach --via reflect: docker compose config --services failed:\n${listed.stderr.toString().trim()}`);
867
889
  const services = listed.stdout.toString().split('\n').map((s) => s.trim()).filter(Boolean);
868
890
  const overridePath = join(instanceDir(attachRoot, resolved.ref), 'reflect-compose.override.yml');
@@ -870,7 +892,7 @@ async function main(): Promise<void> {
870
892
  toRun = dockerComposeWithOverride(command, overridePath);
871
893
  process.stderr.write(`volter-world attach --via reflect: ${services.length} service(s) attached — dns ${manifest.resolverIp}:53, front ${manifest.targetIp}:443, CA ${ATTACHED_CA_PATH}\n`);
872
894
  }
873
- const result = spawnSync(toRun[0]!, toRun.slice(1), { env, stdio: 'inherit' });
895
+ const result = spawnSync(toRun[0]!, toRun.slice(1), { windowsHide: true, env, stdio: 'inherit' });
874
896
  process.exit(result.status ?? 1);
875
897
  }
876
898
  if (via !== 'env') {
@@ -8,7 +8,7 @@
8
8
  import { serveHttp, type HttpServer } from '@volter/world-core';
9
9
 
10
10
  /** What a console offers a host: its shell and assets under `base`, told where the Worlds are served. */
11
- export type ConsoleMount = { handle: (request: Request, base: string, opts?: { worlds?: string; names?: string[] }) => Promise<Response | null> };
11
+ export type ConsoleMount = { handle: (request: Request, base: string, opts?: { worlds?: string; names?: string[]; worldOrigin?: boolean }) => Promise<Response | null> };
12
12
  export const CONSOLE_BASE = '/-/console';
13
13
 
14
14
  /** Serve `mount` on its own listener; doors under `/-/` go to `upstream` (the Worlds' origin). */
@@ -57,6 +57,9 @@ export async function serveConsoleFor(target: string, opts: { hostname?: string;
57
57
  throw error;
58
58
  }
59
59
  const hostname = opts.hostname ?? '127.0.0.1';
60
+ // the console's own origin, once it listens: a World that lets only its own origin frame its pages lets this
61
+ // console frame them too, as they are framed where it serves them itself
62
+ let consoleOrigin = '';
60
63
  const worldsServer = await serveHttp({
61
64
  hostname, port: opts.port !== undefined && opts.port !== 0 ? opts.port + 1 : 0, twinRequestJournal: false,
62
65
  async fetch(request) {
@@ -71,11 +74,14 @@ export async function serveConsoleFor(target: string, opts: { hostname?: string;
71
74
  // the host's own redirects (the session door's, a twin root's to its mirror) stay on this origin
72
75
  const location = out.get('location');
73
76
  if (location?.startsWith(origin)) out.set('location', `${url.origin}${location.slice(origin.length)}`);
77
+ const csp = out.get('content-security-policy');
78
+ if (csp && consoleOrigin) out.set('content-security-policy', csp.replace(/frame-ancestors ([^;]*)/, (_all, allowed: string) => (/'none'/.test(allowed) ? `frame-ancestors ${allowed}` : `frame-ancestors ${allowed} ${consoleOrigin}`)));
74
79
  return new Response(answer.body, { status: answer.status, headers: out });
75
80
  },
76
81
  });
77
82
  const worlds = `http://${hostname}:${worldsServer.port}`;
78
83
  const consoleServer = await serveConsoleApart(mount, { upstream: origin, worlds, names, hostname, ...(opts.port !== undefined ? { port: opts.port } : {}) });
84
+ consoleOrigin = new URL(consoleServer.url).origin;
79
85
  return { url: consoleServer.url, worlds, servers: [consoleServer.server, worldsServer] };
80
86
  }
81
87
 
package/src/covers.ts CHANGED
@@ -602,8 +602,10 @@ function usesOf(signals: Map<string, string[]>): Map<string, Array<{ usage: Worl
602
602
  export function detectRepoVendors(repoPath: string): RepoVendorSignals {
603
603
  const repo = resolve(repoPath);
604
604
  // the platform's own packages — the twins an app installed, the command, the runtime — are
605
- // never a vendor signal: an app that installed @volter/twin-slack did not gain a Slack dependency
606
- const dependencies = projectDependencies(repo).filter((dep) => !dep.startsWith('@volter/') && !dep.startsWith('pypi:volter'));
605
+ // never a vendor signal: an app that installed @volter/twin-slack did not gain a Slack dependency.
606
+ // A Volter package a pack names as its vendor's SDK is one (`@volter/identity`, the Volter identity
607
+ // service's client, is the volteridentity twin's), as any vendor's SDK is.
608
+ const dependencies = projectDependencies(repo).filter((dep) => Object.hasOwn(SDK_TWINS, dep) || (!dep.startsWith('@volter/') && !dep.startsWith('pypi:volter')));
607
609
  const envNames = projectEnvNames(repo);
608
610
  const npmRegistries = projectNpmRegistries(repo);
609
611