@volter/world-runtime 2.0.0

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 (164) hide show
  1. package/LICENSE +202 -0
  2. package/dist/known-external-services.json +1108 -0
  3. package/dist/src/ancestry.d.ts +2 -0
  4. package/dist/src/ancestry.js +42 -0
  5. package/dist/src/app-url.d.ts +47 -0
  6. package/dist/src/app-url.js +239 -0
  7. package/dist/src/attach.d.ts +48 -0
  8. package/dist/src/attach.js +87 -0
  9. package/dist/src/branch.d.ts +20 -0
  10. package/dist/src/branch.js +65 -0
  11. package/dist/src/browser-proxy-cli.d.ts +2 -0
  12. package/dist/src/browser-proxy-cli.js +41 -0
  13. package/dist/src/ca-trust.d.ts +5 -0
  14. package/dist/src/ca-trust.js +64 -0
  15. package/dist/src/catalog.d.ts +31 -0
  16. package/dist/src/catalog.js +148 -0
  17. package/dist/src/changeset.d.ts +142 -0
  18. package/dist/src/changeset.js +570 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.js +1262 -0
  21. package/dist/src/command-lifetime.d.ts +15 -0
  22. package/dist/src/command-lifetime.js +98 -0
  23. package/dist/src/configs.d.ts +18 -0
  24. package/dist/src/configs.js +119 -0
  25. package/dist/src/console-apart.d.ts +38 -0
  26. package/dist/src/console-apart.js +107 -0
  27. package/dist/src/consumers.d.ts +46 -0
  28. package/dist/src/consumers.js +200 -0
  29. package/dist/src/covers.d.ts +183 -0
  30. package/dist/src/covers.js +800 -0
  31. package/dist/src/fixture-env.d.ts +42 -0
  32. package/dist/src/fixture-env.js +221 -0
  33. package/dist/src/host-cli.d.ts +2 -0
  34. package/dist/src/host-cli.js +92 -0
  35. package/dist/src/host-fault-fixture.d.ts +32 -0
  36. package/dist/src/host-fault-fixture.js +100 -0
  37. package/dist/src/host-worker.d.ts +1 -0
  38. package/dist/src/host-worker.js +23 -0
  39. package/dist/src/host.d.ts +38 -0
  40. package/dist/src/host.js +135 -0
  41. package/dist/src/index.d.ts +48 -0
  42. package/dist/src/index.js +35 -0
  43. package/dist/src/infra-cli.d.ts +2 -0
  44. package/dist/src/infra-cli.js +136 -0
  45. package/dist/src/init.d.ts +227 -0
  46. package/dist/src/init.js +1117 -0
  47. package/dist/src/inject-map.d.ts +34 -0
  48. package/dist/src/inject-map.js +56 -0
  49. package/dist/src/lifecycle-record.d.ts +47 -0
  50. package/dist/src/lifecycle-record.js +196 -0
  51. package/dist/src/origin.d.ts +31 -0
  52. package/dist/src/origin.js +139 -0
  53. package/dist/src/pack-facts.d.ts +75 -0
  54. package/dist/src/pack-facts.js +98 -0
  55. package/dist/src/pglite-backing.d.ts +21 -0
  56. package/dist/src/pglite-backing.js +158 -0
  57. package/dist/src/pglite-host.mjs +147 -0
  58. package/dist/src/placeholder.d.ts +20 -0
  59. package/dist/src/placeholder.js +100 -0
  60. package/dist/src/prerequisites.d.ts +21 -0
  61. package/dist/src/prerequisites.js +49 -0
  62. package/dist/src/process-groups.d.ts +4 -0
  63. package/dist/src/process-groups.js +49 -0
  64. package/dist/src/project-inspect.d.ts +109 -0
  65. package/dist/src/project-inspect.js +827 -0
  66. package/dist/src/proxy-daemon.d.ts +2 -0
  67. package/dist/src/proxy-daemon.js +18 -0
  68. package/dist/src/redirect-proxy.d.ts +105 -0
  69. package/dist/src/redirect-proxy.js +665 -0
  70. package/dist/src/reflect.d.ts +74 -0
  71. package/dist/src/reflect.js +392 -0
  72. package/dist/src/resources.d.ts +26 -0
  73. package/dist/src/resources.js +22 -0
  74. package/dist/src/root.d.ts +114 -0
  75. package/dist/src/root.js +312 -0
  76. package/dist/src/run-task-worker.d.ts +1 -0
  77. package/dist/src/run-task-worker.js +38 -0
  78. package/dist/src/run-task.d.ts +18 -0
  79. package/dist/src/run-task.js +48 -0
  80. package/dist/src/runtime-test-support.d.ts +59 -0
  81. package/dist/src/runtime-test-support.js +205 -0
  82. package/dist/src/runtime.d.ts +256 -0
  83. package/dist/src/runtime.js +3502 -0
  84. package/dist/src/schema.d.ts +449 -0
  85. package/dist/src/schema.js +605 -0
  86. package/dist/src/serve.d.ts +30 -0
  87. package/dist/src/serve.js +82 -0
  88. package/dist/src/served-world.d.ts +194 -0
  89. package/dist/src/served-world.js +986 -0
  90. package/dist/src/service-exit.d.ts +46 -0
  91. package/dist/src/service-exit.js +195 -0
  92. package/dist/src/service-recorder.d.ts +1 -0
  93. package/dist/src/service-recorder.js +121 -0
  94. package/dist/src/sibling.d.ts +1 -0
  95. package/dist/src/sibling.js +9 -0
  96. package/dist/src/signals.d.ts +1 -0
  97. package/dist/src/signals.js +11 -0
  98. package/dist/src/storage-capacity.d.ts +8 -0
  99. package/dist/src/storage-capacity.js +61 -0
  100. package/dist/src/tail.d.ts +30 -0
  101. package/dist/src/tail.js +160 -0
  102. package/dist/src/tcp-port.d.ts +2 -0
  103. package/dist/src/tcp-port.js +36 -0
  104. package/dist/src/up-task-worker.d.ts +1 -0
  105. package/dist/src/up-task-worker.js +61 -0
  106. package/dist/src/up-task.d.ts +17 -0
  107. package/dist/src/up-task.js +49 -0
  108. package/dist/src/websocket-relay.d.ts +3 -0
  109. package/dist/src/websocket-relay.js +40 -0
  110. package/known-external-services.json +1108 -0
  111. package/package.json +83 -0
  112. package/src/ancestry.ts +36 -0
  113. package/src/app-url.ts +253 -0
  114. package/src/attach.ts +117 -0
  115. package/src/branch.ts +63 -0
  116. package/src/browser-proxy-cli.ts +44 -0
  117. package/src/ca-trust.ts +57 -0
  118. package/src/catalog.ts +156 -0
  119. package/src/changeset.ts +627 -0
  120. package/src/cli.ts +1111 -0
  121. package/src/command-lifetime.ts +79 -0
  122. package/src/configs.ts +110 -0
  123. package/src/console-apart.ts +90 -0
  124. package/src/consumers.ts +185 -0
  125. package/src/covers.ts +934 -0
  126. package/src/fixture-env.ts +230 -0
  127. package/src/host-cli.ts +90 -0
  128. package/src/host-worker.ts +23 -0
  129. package/src/host.ts +169 -0
  130. package/src/index.ts +171 -0
  131. package/src/infra-cli.ts +133 -0
  132. package/src/init.ts +1316 -0
  133. package/src/inject-map.ts +72 -0
  134. package/src/lifecycle-record.ts +168 -0
  135. package/src/origin.ts +134 -0
  136. package/src/pack-facts.ts +128 -0
  137. package/src/pglite-backing.ts +141 -0
  138. package/src/pglite-host.mjs +147 -0
  139. package/src/placeholder.ts +89 -0
  140. package/src/prerequisites.ts +66 -0
  141. package/src/process-groups.ts +33 -0
  142. package/src/project-inspect.ts +770 -0
  143. package/src/proxy-daemon.ts +21 -0
  144. package/src/redirect-proxy.ts +684 -0
  145. package/src/reflect.ts +440 -0
  146. package/src/resources.ts +22 -0
  147. package/src/root.ts +290 -0
  148. package/src/run-task-worker.ts +27 -0
  149. package/src/run-task.ts +44 -0
  150. package/src/runtime-test-support.ts +208 -0
  151. package/src/runtime.ts +3357 -0
  152. package/src/schema.ts +922 -0
  153. package/src/serve.ts +102 -0
  154. package/src/served-world.ts +812 -0
  155. package/src/service-exit.ts +175 -0
  156. package/src/service-recorder.ts +89 -0
  157. package/src/sibling.ts +10 -0
  158. package/src/signals.ts +10 -0
  159. package/src/storage-capacity.ts +60 -0
  160. package/src/tail.ts +205 -0
  161. package/src/tcp-port.ts +35 -0
  162. package/src/up-task-worker.ts +40 -0
  163. package/src/up-task.ts +45 -0
  164. package/src/websocket-relay.ts +32 -0
@@ -0,0 +1,1262 @@
1
+ #!/usr/bin/env node
2
+ import { sessionTrustEnv } from "./ca-trust.js";
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, join, resolve } from 'node:path';
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, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
9
+ import { formatApplication, formatChangeset, formatRebaseReport, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, parseVerifierExpression } from '@volter/world-core';
10
+ import { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer } from "./serve.js";
11
+ import { clockFile, instanceDir, pruneWorlds } from "./runtime.js";
12
+ import { worldResourceReport } from "./resources.js";
13
+ import { worldConsumers, worldUsage } from "./consumers.js";
14
+ import { addAppHosts, appUrlUnsetMessage, detectAppUrl, readAppUrl, setAppUrl } from "./app-url.js";
15
+ import { describeServiceEnd, describeWorldEvent, readServiceEnd, readWorldEvents } from "./service-exit.js";
16
+ import { worldEventsPath } from "./runtime.js";
17
+ import { superviseWorldUp } from "./up-task.js";
18
+ /** An option's value, written `--name value` or `--name=value`. */
19
+ function optionValue(args, name, fallback = '') {
20
+ const joined = args.find((arg) => arg.startsWith(`${name}=`));
21
+ if (joined !== undefined)
22
+ return joined.slice(name.length + 1);
23
+ const index = args.indexOf(name);
24
+ return index >= 0 && index + 1 < args.length ? args[index + 1] : fallback;
25
+ }
26
+ function ownerOption(args) {
27
+ const index = args.indexOf('--owner');
28
+ if (index < 0)
29
+ return undefined;
30
+ const label = args[index + 1];
31
+ if (!label || label.startsWith('--'))
32
+ throw new Error('--owner requires a task label');
33
+ return label;
34
+ }
35
+ /** The non-flag args, skipping each value-taking flag's value (so `--root <path>` never
36
+ * masquerades as a positional service id). */
37
+ function positionalArgs(args, valueFlags = ['--root']) {
38
+ const positionals = [];
39
+ for (let index = 0; index < args.length; index += 1) {
40
+ const arg = args[index];
41
+ if (arg.startsWith('--')) {
42
+ if (valueFlags.includes(arg))
43
+ index += 1;
44
+ continue;
45
+ }
46
+ positionals.push(arg);
47
+ }
48
+ return positionals;
49
+ }
50
+ /** `--acknowledge vendor=reason` (repeatable) — the shared parse for `covers` and `init`. */
51
+ function acknowledgeOptions(args, command) {
52
+ const acknowledge = {};
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ if (args[index] !== '--acknowledge')
55
+ continue;
56
+ const pair = args[index + 1] ?? '';
57
+ const eq = pair.indexOf('=');
58
+ if (eq <= 0)
59
+ throw new Error(`volter-world ${command}: --acknowledge takes vendor=reason (e.g. --acknowledge "svix=verifier-only, no egress")`);
60
+ acknowledge[pair.slice(0, eq)] = pair.slice(eq + 1);
61
+ }
62
+ return acknowledge;
63
+ }
64
+ function printHelp() {
65
+ process.stdout.write(`Usage:
66
+ volter-world init <name> --repo <path> [--out <dir>] [--force] [--allow-unknown] [--acknowledge vendor=reason]... [--json] [--root <repo>]
67
+ # the FRONT DOOR: detect the repo's vendors, EMIT a world config +
68
+ # env file OUTSIDE the repo (default <repo>/../<name>-pilot/, the
69
+ # repo is never written to), then run the coverage proof over the
70
+ # emission. Exit 0 = ready to \`up\`; exit 1 = the honest worklist.
71
+ # Deterministic: same repo + same pack catalog => identical bytes.
72
+ # init DIAGNOSES; it never boots — it prints the exact next commands
73
+ volter-world up <config> --env-file=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--root <repo>]
74
+ volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
75
+ volter-world audit <world> [--json] — the catalog's upkeep verdicts for the mounted twins: matrix debt, census (R17)
76
+ volter-world down <name> [--grace-ms <ms>] [--purge] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
77
+ # --purge: after teardown, delete the instance dir (and per-service twin data) too — see docs/concepts/data-and-keys.md
78
+ volter-world status <name> [--json] [--root <repo>]
79
+ volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json] # resolve attachments whose runner, command and process group are gone
80
+ volter-world resources [--json] [--root <repo>] # actual filesystem capacity and lifecycle inventory; no reservations
81
+ # up/run/attach accept --owner <label> for task ownership
82
+ volter-world prune [name] [--apply] [--json] [--root <repo>]
83
+ # preview by default; --apply deletes only successfully stopped
84
+ # instance data/logs. Never stops live Worlds; sources survive
85
+ volter-world doctor <name> [--verify-public] [--json] [--root <repo>]
86
+ volter-world events <name> [--json] [--root <repo>] every service start and end, and the World's own stops
87
+ volter-world doctor-prereqs --require local-execution [--json] [--quiet]
88
+ volter-world inspect-project [path] [--json] # read-only adoption discovery
89
+ volter-world migrate-config <config> [--json] [--root <repo>] # backed-up, atomic format-1 → format-2 migration
90
+ volter-world fake-env <NAME...> [--json] # structurally valid fake env values for a world config
91
+ # (Google service-account JSON gets a real throwaway RSA key;
92
+ # opaque bearer keys get a twin-fake scalar)
93
+ volter-world covers <world> --repo <path> [--allow-unknown] [--acknowledge vendor=reason]... [--json] [--root <repo>]
94
+ # room-setup PROOF: every external vendor the repo talks to has a twin
95
+ # in the world. Exit 0 only when fully covered; missing twins and
96
+ # unmapped external-service-shaped deps exit 1 (--allow-unknown
97
+ # downgrades the latter to a warning)
98
+ volter-world clock <world> show | set <iso> | advance <N s|m|h|d> # THE WORLD CLOCK: frozen instant every twin stamps from; physics door
99
+ volter-world urls <name> [--json] [--root <repo>]
100
+ volter-world url <name> [service] [--root <repo>] # the clean base URL of one running service (no world.env parsing);
101
+ # without a service: every service, one \`<id> <url>\` per line
102
+ volter-world app-url <world> [--set <url>|--detect <pid|port>] [--host <name>...] [--json] [--root <repo>]
103
+ # the recorded APP endpoint of the instance — the attach pattern's
104
+ # missing output. The BOOTER records it as the last boot step
105
+ # (--set, or --detect an already-listening pid/port); consumers
106
+ # read it back (bare URL, or --json). Reading an UNSET record
107
+ # fails loudly with the registration recipe. A world that boots
108
+ # the app itself declares an "app" service (type "process") —
109
+ # its URL answers without any registration
110
+ volter-world log <name> [service...] [--no-follow] [--json] [--requests] [--root <repo>] # the log of changes (alias: tail)
111
+ # merged, occurredAt-ordered feed of the twins' action ledgers — one
112
+ # \`HH:MM:SS.mmm <service> <operation> <subjectId>\` line per action;
113
+ # follows until Ctrl-C (--no-follow: dump and exit; --json: raw JSONL;
114
+ # --requests: also merge the opt-in request journals — reads/404s,
115
+ # plus WHICH credential arrived in WHICH header/query param, named +
116
+ # fingerprinted, never the value — written by twins under
117
+ # VOLTER_TWIN_REQUEST_JOURNAL=1)
118
+ volter-world seed <world> [--entry <file>] [--cwd <dir>] [--json] [--root <repo>]
119
+ # pull from the placeholder remote: the seed entry runs with every twin's
120
+ # window open, so the defaults land as observed state (diff empty, nothing pending)
121
+ volter-world reset <world> [--entry <file>] [--cwd <dir>] [--json] [--root <repo>]
122
+ # back to the defaults: down --purge, up, seed (refuses a referenced parent)
123
+ volter-world branch <world> <name> [--env-file=<path>] [--json] [--root <repo>]
124
+ # a new world over the base's mirrors (its own log; the base's unpushed changes stay)
125
+ volter-world checkout <name> [--json] [--root <repo>] # a stopped world back up, state intact
126
+ volter-world fetch <world> [--origin <twins url> --namespace <org>/<world>] --key <key> [--full] [--service <id>]... [--json] [--root <repo>]
127
+ # the origin is a hosted twin's canonical history: --full clones its whole tree
128
+ # (admin key), plain fetch appends what it observed since the last fetch (read key)
129
+ volter-world origin <world> [--json] [--root <repo>] # the origin this world clones from
130
+ volter-world mark <world> [--id <marker>] [--json] [--root <repo>]
131
+ # capture a cross-service BASE MARKER (each twin ledger's offset +
132
+ # last action id) — the position \`diff\` measures from
133
+ volter-world diff <world> [--base <marker>] [--json] [--root <repo>]
134
+ # the ledger DELTA since a marker (default: the last mark, else
135
+ # world-boot), grouped by vendor, ordered by occurredAt
136
+ volter-world changeset create <world> <name> [--base <marker>] [--verifier "<service> <type>:<id> <field> <op> [value]"]... [--force] [--json] [--root <repo>]
137
+ volter-world changeset show <name> [--world <world>] [--json] [--root <repo>]
138
+ volter-world changeset list [--world <world>] [--json] [--root <repo>]
139
+ volter-world changeset replay <name> --into <world> [--world <world>] [--json] [--root <repo>]
140
+ # freeze a delta into a named, content-addressed changeset and replay
141
+ # it into another world's twins (the CI primitive; idempotent)
142
+ volter-world changeset verify <name> (--into <world> | --ephemeral) [--world <world>] [--json] [--root <repo>]
143
+ # replay into a clean target, run the changeset's verifiers against
144
+ # post-replay state, record the outcome ON the object (hash untouched)
145
+ volter-world changeset approve <name> --as <principal> [--note <text>] [--world <world>] [--json] [--root <repo>]
146
+ # append an approval bound to the current body hash (drift refuses)
147
+ volter-world changeset status <name> [--world <world>] [--json] [--root <repo>]
148
+ volter-world changeset rebase <name> [--world <world>] [--json] [--root <repo>]
149
+ # the merge onto a moved mirror (v3): re-stamp what still holds, name conflicts, drop approvals
150
+ volter-world changeset push <name> [--to <twins url> --namespace <org>/<world>] --key <namespace key> [--force] [--world <world>] [--json] [--root <repo>]
151
+ # the governed push (alias: apply): to the REMOTE — the world's origin by default —
152
+ # which refreshes, deploys to the vendor with its own keys, and answers with receipts
153
+ # one honest line: hash, verified?, approvals, ready|not-ready + why
154
+ # (exit 0 when ready; no pushing here — that is v2)
155
+ volter-world list [--json] [--root <repo>]
156
+ volter-world env <name> [--root <repo>] -- <command...>
157
+ volter-world attach [<world-ref>] [--via env|direct] [--root <repo>] [--verbose] [-- <command...>]
158
+ # ref: explicit → $VOLTER_WORLD → nearest .volter-world file (docs/guides/route-a-cli-through-the-world.md)
159
+ # --via env (default): run the command attached; --via direct: print the manifest JSON
160
+ volter-world manifest <name> [--root <repo>] # the world manifest (vendors map, CA, traffic proxy, suggested env) as JSON
161
+ volter-world reflect <name> --target-ip <ip> [--resolver-ip <ip>] [--port <p>] [--resolver-port <p>] [--upstream <dns-ip>] [--root <repo>]
162
+ # run the reflect front (SNI TLS door) + resolver in the foreground (docs/ATTACH.md);
163
+ # --port 443 --resolver-port 53 is what containers can reach (attach --via reflect);
164
+ # --resolver-ip when the Docker host's gateway address answers DNS itself (colima)
165
+ volter-world route <name> <add|rm|ls> [host] # attachment-scoped reflect routes (which hosts the resolver intercepts)
166
+ volter-world serve <name> --advertise <https-origin> [--port <front>] [--manifest-port <p>] [--token <t>] [--root <repo>]
167
+ # serve the world remotely: manifest endpoint + ONE advertised TLS door (docs/guides/route-a-cli-through-the-world.md)
168
+ # attachers: volter-world attach <http://host:manifest-port> -- <command...>
169
+ volter-world activate <name> [--root <repo>] # eval "$(volter-world activate <name>)" — a virtualenv for vendor APIs
170
+ volter-world shell <name> [--root <repo>] # drop into a subshell with the world active (any CLI hits the twins)
171
+ volter-world run <config> --env-file=<path> [--name <name>] [--mode local|share|sealed] [--keep] [--verbose] -- <command...>
172
+ volter-world share <name> [--service app] [--verify /health|--no-verify] [--provider cloudflare-quick|command] [--command <cmd> [-- <args…>]]
173
+ # {url} in command args = the service's local URL;
174
+ # --provider cloudflare-quick selects raw cloudflared and clears a configured custom command
175
+ volter-world unshare <name> [--service app]
176
+
177
+ Configs are stable JSON files, resolved from worlds/configs/<config>.json unless a path is passed.
178
+ `);
179
+ }
180
+ function printReady(instance) {
181
+ process.stdout.write(`World ${instance.name} is ready (${instance.config})
182
+
183
+ Services:
184
+ ${Object.values(instance.services).map((service) => ` ${service.id}: ${service.url ?? '(no listener — a World output)'}`).join('\n')}
185
+
186
+ This instance started CLEAN (a world's state lives only while it runs; the world dir is the
187
+ reproducible story) — set the clock and run your seed now, after every up.
188
+
189
+ The two moves: if the twin stores it, create it through the vendor's own API (seed as the
190
+ person, move the clock first for history). Everything else — judgment, lookups, faults — is
191
+ a handler in the world dir's handlers/<vendor>.json. EVERY twin explains itself at
192
+ GET <url>/twin; a scripted twin's GET <url>/twin/scenario lists handlers + misses.
193
+
194
+ Env:
195
+ ${instance.envFile}
196
+
197
+ Instance:
198
+ ${instance.dirs.instance}/instance.json
199
+
200
+ Stop:
201
+ volter-world down ${instance.name}
202
+
203
+ For a bounded job, use volter-world run ... -- <command> to tear down automatically on exit.
204
+ Persistent up does not expire or infer that your sessions are idle.
205
+ `);
206
+ }
207
+ async function main() {
208
+ const [cmd, subject, ...rest] = process.argv.slice(2);
209
+ const root = optionValue(rest, '--root', process.cwd());
210
+ if (!cmd || cmd === '--help' || cmd === '-h') {
211
+ printHelp();
212
+ return;
213
+ }
214
+ if (subject === '--help' || subject === '-h') {
215
+ printHelp();
216
+ return;
217
+ }
218
+ if (cmd === 'consumers') {
219
+ // `volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json]`: the owner's
220
+ // explicit resolution of attachments whose runner and command are provably gone.
221
+ const args = rest.filter((arg) => arg !== undefined);
222
+ if (subject !== 'retire')
223
+ throw new Error('volter-world consumers: usage: volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json]');
224
+ const world = positionalArgs(args)[0];
225
+ if (!world)
226
+ throw new Error('volter-world consumers retire: missing world name');
227
+ for (let i = 0; i < args.length; i++) {
228
+ if (args[i] === '--root' || args[i] === '--consumer') {
229
+ if (!args[i + 1] || args[i + 1].startsWith('--'))
230
+ throw new Error(`${args[i]} requires a value`);
231
+ i++;
232
+ }
233
+ else if (args[i].startsWith('--') && args[i] !== '--json')
234
+ throw new Error(`volter-world consumers retire: unknown option ${args[i]}`);
235
+ }
236
+ const consumer = args.includes('--consumer') ? optionValue(args, '--consumer', '') : undefined;
237
+ const results = retireWorldConsumers(world, optionValue(args, '--root', process.cwd()), consumer ? { consumer } : {});
238
+ if (args.includes('--json'))
239
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
240
+ else if (!results.length)
241
+ process.stdout.write(`World ${world}: no uncertain consumers.\n`);
242
+ else
243
+ for (const r of results)
244
+ process.stdout.write(`${r.retired ? 'RETIRED' : 'KEPT'} ${r.id}${r.owner ? ` (owner ${r.owner})` : ''}: ${r.reason}\n`);
245
+ return;
246
+ }
247
+ if (cmd === 'resources' || cmd === 'prune') {
248
+ const args = [subject, ...rest].filter((arg) => arg !== undefined);
249
+ const reportRoot = optionValue(args, '--root', process.cwd());
250
+ // Destructive commands must reject typos, not silently broaden a target to all Worlds.
251
+ for (let i = 0; i < args.length; i++) {
252
+ if (args[i] === '--root') {
253
+ if (!args[i + 1] || args[i + 1].startsWith('--'))
254
+ throw new Error('--root requires a path');
255
+ i++;
256
+ }
257
+ else if (args[i].startsWith('--') && args[i] !== '--json' && !(cmd === 'prune' && args[i] === '--apply')) {
258
+ throw new Error(`volter-world ${cmd}: unknown option ${args[i]}`);
259
+ }
260
+ }
261
+ const names = positionalArgs(args);
262
+ if (names.length > (cmd === 'prune' ? 1 : 0))
263
+ throw new Error(`volter-world ${cmd}: unexpected positional arguments`);
264
+ if (cmd === 'prune') {
265
+ const report = pruneWorlds({ root: reportRoot, name: names[0], apply: args.includes('--apply') });
266
+ if (args.includes('--json'))
267
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
268
+ else {
269
+ process.stdout.write(`${report.apply ? 'Prune result' : 'Preview only (use --apply to delete eligible instance data/logs)'}; never stops Worlds.\n`);
270
+ for (const entry of report.entries) {
271
+ process.stdout.write(`${entry.removed ? 'REMOVED' : entry.eligible ? 'ELIGIBLE' : 'KEEP'} ${entry.name}: ${entry.reason}\n ${entry.path}${entry.logicalBytes !== undefined ? ` (${entry.logicalBytes} logical bytes; actual disk reclaim may differ)` : ''}\n`);
272
+ }
273
+ }
274
+ if (report.apply && names.length && !report.entries[0]?.removed)
275
+ process.exitCode = 1;
276
+ }
277
+ else {
278
+ const report = worldResourceReport(reportRoot);
279
+ const consumers = report.worlds.map((claim) => {
280
+ let foreground;
281
+ try {
282
+ const record = claim.root ? statusWorld(claim.world, claim.root, { readOnly: true }).lastRun : undefined;
283
+ if (record)
284
+ foreground = { state: record.state, runnerPid: record.runnerPid, ...('consumerPid' in record ? { consumerPid: record.consumerPid } : {}), startedAt: record.startedAt };
285
+ }
286
+ catch { /* unknown */ }
287
+ const attached = claim.root ? worldConsumers(claim.root, claim.world) : { consumers: [], warnings: ['World root is unknown'] };
288
+ const usage = claim.root ? worldUsage(claim.root, claim.world, attached)
289
+ : { createdAt: null, lastUsedAt: null, lastUseSource: null, coverage: 'partial', warnings: attached.warnings };
290
+ return { identity: claim.identity, owner: claim.owner ?? null, attachments: attached.consumers, usage, warnings: attached.warnings, coverage: 'partial', foreground, otherConsumers: 'unknown (untracked synchronous SDK commands, shell/activate, reflect, remote clients and descendants are not enumerated)' };
291
+ });
292
+ if (args.includes('--json'))
293
+ process.stdout.write(`${JSON.stringify({ ...report, consumers }, null, 2)}\n`);
294
+ else {
295
+ const free = report.filesystem.availableBytes;
296
+ process.stdout.write(`Observed capacity at ${report.measuredAt}; no capacity reservations.\nFilesystem ${report.filesystem.path}: ${free === null ? 'unknown' : `${Math.floor(free / 1048576)} MiB available`}; pool=${report.filesystem.pool ?? 'unknown'}. Physical memory=${Math.floor(report.host.physicalMemoryBytes / 1048576)} MiB (not measured usage).\n`);
297
+ for (const claim of report.worlds) {
298
+ process.stdout.write(`${claim.phase} ${claim.world}: root=${claim.root}; owner=${claim.owner ?? 'unrecorded'}; instance=${claim.instanceCreatedAt}\n tracked service pids=${claim.pids.join(',') || 'none'}; log=${claim.log}\n`);
299
+ const attached = consumers.find((row) => row.identity === claim.identity);
300
+ process.stdout.write(` instance created: ${attached?.usage.createdAt ?? 'unknown'}; last observed command use: ${attached?.usage.lastUsedAt ?? 'unknown'} (${attached?.usage.lastUseSource ?? 'unrecorded'}; partial coverage)\n`);
301
+ for (const consumer of attached?.attachments ?? [])
302
+ process.stdout.write(` ${consumer.state} attachment ${consumer.id}: owner=${consumer.owner ?? 'unrecorded'} runner=${consumer.runnerPid} command-pid=${consumer.consumerPid ?? 'starting'} heartbeat=${consumer.heartbeatAt}\n`);
303
+ for (const warning of attached?.warnings ?? [])
304
+ process.stdout.write(` WARNING: ${warning}\n`);
305
+ for (const warning of attached?.usage.warnings ?? [])
306
+ if (!attached?.warnings.includes(warning))
307
+ process.stdout.write(` WARNING: ${warning}\n`);
308
+ }
309
+ for (const legacy of report.legacyRecords)
310
+ process.stdout.write(`LEGACY ${legacy.world ?? 'unknown'}: ${legacy.path}; use its pinned runtime for teardown; no capacity charged here\n`);
311
+ for (const warning of report.warnings)
312
+ process.stdout.write(`WARNING: ${warning}\n`);
313
+ process.stdout.write('Lifecycle records identify ownership, not measured usage. Consumer coverage is partial: local env attachments are registered; run records are available with --json. Untracked synchronous SDK commands, shell/activate, reflect, remote clients and descendants are unknown, not zero.\nReview ownership before explicit down <name>. prune previews stopped disk leftovers only. For bounded jobs, run cleans up on exit unless --keep.\n');
314
+ }
315
+ }
316
+ return;
317
+ }
318
+ if (cmd === 'up') {
319
+ if (!subject)
320
+ throw new Error('volter-world up: missing config');
321
+ const envFile = optionValue(rest, '--env-file');
322
+ if (!envFile)
323
+ throw new Error('volter-world up: missing required --env-file=<path>');
324
+ const name = optionValue(rest, '--name') || undefined;
325
+ const mode = optionValue(rest, '--mode', 'local');
326
+ const isolationArg = optionValue(rest, '--isolation');
327
+ if (isolationArg && isolationArg !== 'process' && isolationArg !== 'colocated' && isolationArg !== 'worker') {
328
+ throw new Error(`volter-world up: invalid --isolation "${isolationArg}" (want process|colocated|worker)`);
329
+ }
330
+ const isolation = (isolationArg || undefined);
331
+ // The boot runs in its own owner process (./up-task.ts): a closed terminal does not leave it half-done.
332
+ const instance = await superviseWorldUp(subject, { name, root, mode, envFile, owner: ownerOption(rest), ...(isolation ? { isolation } : {}) });
333
+ printReady(instance);
334
+ return;
335
+ }
336
+ if (cmd === 'down') {
337
+ if (!subject)
338
+ throw new Error('volter-world down: missing world name');
339
+ const graceArg = optionValue(rest, '--grace-ms');
340
+ if (graceArg && !(Number(graceArg) >= 0))
341
+ throw new Error(`volter-world down: invalid --grace-ms "${graceArg}" (want a non-negative number)`);
342
+ const purge = rest.includes('--purge');
343
+ const result = await downWorld(subject, root, {
344
+ ...(graceArg ? { graceMs: Number(graceArg) } : {}),
345
+ ...(purge ? { purge: true } : {}),
346
+ });
347
+ process.stdout.write(result.externalErrors.length ? `World ${result.name}: teardown incomplete; lifecycle evidence retained\n`
348
+ : `Stopped world ${result.name}${result.stopped.length ? `: ${result.stopped.join(', ')}` : ''}\n`);
349
+ if (result.escalated.length)
350
+ process.stdout.write(`Escalated to SIGKILL (ignored SIGTERM past the grace): ${result.escalated.join(', ')}\n`);
351
+ for (const line of result.notSignalled ?? [])
352
+ process.stdout.write(`Not signalled: ${line}\n`);
353
+ if (result.externalErrors.length) {
354
+ for (const message of result.externalErrors)
355
+ process.stderr.write(`External teardown failed: ${message}\n`);
356
+ }
357
+ if (result.purged) {
358
+ process.stdout.write(`Purged instance data: ${result.purged}\n`);
359
+ }
360
+ else if (purge && result.externalErrors.length) {
361
+ process.stdout.write(`Refused to purge instance data: an external service's teardown failed (see above) — its down command/discovered env must survive.\n`);
362
+ }
363
+ // Honest exit code (TWIN-61): a silent 0 here is exactly the gap a failed `supabase stop` +
364
+ // `--purge` exploited — a still-running external, a deleted stop recipe, and a green exit.
365
+ if (result.externalErrors.length)
366
+ process.exitCode = 1;
367
+ return;
368
+ }
369
+ if (cmd === 'events') {
370
+ if (!subject)
371
+ throw new Error('volter-world events: missing world name');
372
+ const status = statusWorld(subject, root, { readOnly: true, light: true });
373
+ const events = readWorldEvents(worldEventsPath(status.dirs.logs));
374
+ if (rest.includes('--json'))
375
+ process.stdout.write(`${JSON.stringify(events, null, 2)}\n`);
376
+ else if (!events.length)
377
+ process.stdout.write(`World ${subject}: no events recorded (${worldEventsPath(status.dirs.logs)})\n`);
378
+ else
379
+ for (const event of events)
380
+ process.stdout.write(`${describeWorldEvent(event)}\n`);
381
+ return;
382
+ }
383
+ if (cmd === 'status') {
384
+ if (!subject)
385
+ throw new Error('volter-world status: missing world name');
386
+ const status = statusWorld(subject, root);
387
+ const usage = worldUsage(root, subject);
388
+ // A stopped process service carries its own account of how it ended (./service-exit.ts).
389
+ const ends = Object.fromEntries(Object.values(status.services)
390
+ .filter((service) => status.serviceStates[service.id] !== 'running' && status.serviceStates[service.id] !== 'external')
391
+ .flatMap((service) => { const end = readServiceEnd(service.log); return end ? [[service.id, end]] : []; }));
392
+ if (rest.includes('--json')) {
393
+ const services = Object.fromEntries(Object.entries(status.services).map(([id, service]) => [id, ends[id] ? { ...service, end: ends[id] } : service]));
394
+ process.stdout.write(`${JSON.stringify({ ...status, services, usage }, null, 2)}\n`);
395
+ }
396
+ else {
397
+ const down = Object.entries(status.serviceStates).filter(([, state]) => state !== 'running' && state !== 'external').map(([id]) => id);
398
+ const headline = !status.running ? 'stopped' : status.degraded ? `degraded: ${down.join(', ')} not running` : 'running';
399
+ process.stdout.write(`World ${status.name}: ${headline} (${status.config})\n`);
400
+ process.stdout.write(` instance created: ${usage.createdAt ?? 'unknown'}\n last observed command use: ${usage.lastUsedAt ?? 'unknown'} (${usage.lastUseSource ?? 'unrecorded'}; partial coverage)\n`);
401
+ for (const warning of usage.warnings)
402
+ process.stdout.write(` WARNING: ${warning}\n`);
403
+ if (status.lastRun) {
404
+ if (status.lastRun.state === 'running') {
405
+ process.stdout.write(` foreground consumer: running (runner pid ${status.lastRun.runnerPid})\n log: ${status.lastRun.log}\n`);
406
+ }
407
+ else if (status.lastRun.state === 'abrupt') {
408
+ process.stdout.write(` foreground consumer: failed abruptly (${status.lastRun.error}; observed ${status.lastRun.observedAt})\n log: ${status.lastRun.log}\n`);
409
+ }
410
+ else {
411
+ const detail = status.lastRun.error ? `spawn error: ${status.lastRun.error}`
412
+ : status.lastRun.signal ? `terminated by ${status.lastRun.signal}`
413
+ : `exited ${status.lastRun.exitCode}`;
414
+ process.stdout.write(` foreground consumer: ${status.lastRun.exitCode === 0 ? 'succeeded' : 'failed'} (${detail})\n log: ${status.lastRun.log}\n`);
415
+ }
416
+ }
417
+ if (status.lifecycle)
418
+ process.stdout.write(` lifecycle: ownership recorded, no capacity reserved\n log: ${status.lifecycle.log}\n`);
419
+ else if (status.resources)
420
+ process.stdout.write(` legacy runtime ownership; use its pinned runtime for teardown\n log: ${status.resources.log}\n`);
421
+ for (const service of Object.values(status.services)) {
422
+ const state = status.serviceStates[service.id];
423
+ const lifecycle = state === 'external' ? 'externally managed'
424
+ : state === 'lingering' ? 'stopped; processes of its group are still alive, which `down` stops'
425
+ : state === 'gave-up' ? `worker gave up: ${service.workerGaveUp?.detail ?? 'exited'}, at ${service.workerGaveUp?.at ?? 'unknown'}`
426
+ : state;
427
+ const end = ends[service.id];
428
+ const how = end ? `\n it ${describeServiceEnd(end)}${end.lastOutput.length ? `; last output: ${end.lastOutput[end.lastOutput.length - 1]}` : ''}` : '';
429
+ process.stdout.write(` ${service.id}: ${service.url ?? '(no World URL)'} (${lifecycle})${how}\n log: ${service.log}\n`);
430
+ const own = service.log ? ownLogs(service.log) : [];
431
+ if (own.length)
432
+ process.stdout.write(` its own logs: ${own.join(', ')}\n`);
433
+ }
434
+ // The World's events: every service start and end, and the World's own stops.
435
+ const events = readWorldEvents(worldEventsPath(status.dirs.logs));
436
+ if (events.length) {
437
+ process.stdout.write(` recent events (${worldEventsPath(status.dirs.logs)}):\n`);
438
+ for (const event of events.slice(-8))
439
+ process.stdout.write(` ${describeWorldEvent(event)}\n`);
440
+ }
441
+ }
442
+ return;
443
+ }
444
+ // `plan` and `review` retired 2026-09-05: the changeset verbs are the one review path (verify, approve, status, push).
445
+ if (cmd === 'doctor') {
446
+ if (!subject)
447
+ throw new Error('volter-world doctor: missing world name');
448
+ const report = await doctorWorld(subject, { root, verifyPublic: rest.includes('--verify-public') });
449
+ if (rest.includes('--json')) {
450
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
451
+ }
452
+ else {
453
+ process.stdout.write(`World ${report.name}: ${report.ok ? 'ok' : 'failed'}\n`);
454
+ for (const check of report.checks)
455
+ process.stdout.write(` ${check.ok ? 'ok' : 'fail'} ${check.id}: ${check.message}\n`);
456
+ }
457
+ if (!report.ok)
458
+ process.exit(1);
459
+ return;
460
+ }
461
+ if (cmd === 'doctor-prereqs') {
462
+ const required = rest.flatMap((arg, index) => arg === '--require' ? [rest[index + 1]] : [])
463
+ .filter(Boolean);
464
+ const checks = checkPrerequisites(required.length ? required : ['local-execution']);
465
+ const ok = checks.every((check) => check.ok);
466
+ if (rest.includes('--json')) {
467
+ process.stdout.write(`${JSON.stringify({ ok, checks }, null, 2)}\n`);
468
+ }
469
+ else if (!rest.includes('--quiet')) {
470
+ const output = formatPrerequisiteChecks(checks);
471
+ process[ok ? 'stdout' : 'stderr'].write(`${output}\n`);
472
+ }
473
+ if (!ok)
474
+ process.exit(1);
475
+ return;
476
+ }
477
+ if (cmd === 'inspect-project') {
478
+ const args = [...(subject ? [subject] : []), ...rest];
479
+ const path = args.find((arg) => !arg.startsWith('--')) || process.cwd();
480
+ const report = inspectProject(path);
481
+ process.stdout.write(args.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatProjectInspection(report));
482
+ return;
483
+ }
484
+ if (cmd === 'migrate-config') {
485
+ if (!subject)
486
+ throw new Error('volter-world migrate-config: missing config path or name');
487
+ const migration = migrateWorldConfig(subject, root);
488
+ if (rest.includes('--json'))
489
+ process.stdout.write(`${JSON.stringify(migration, null, 2)}\n`);
490
+ else
491
+ process.stdout.write(`Migrated ${migration.path}: format ${migration.from} → ${migration.to}\nBackup ${migration.backup}\n${migration.dropped.length ? `Dropped ${migration.dropped.join(', ')}\n` : ''}`);
492
+ return;
493
+ }
494
+ if (cmd === 'fake-env') {
495
+ // Structurally valid fake credentials for a world config's env block. Most vendor keys
496
+ // are opaque bearer tokens (any `twin-fake-…` scalar works against a twin), but a value
497
+ // the CLIENT SDK parses/signs with (a Google service-account JSON) must be structurally
498
+ // real — full field set + a genuine throwaway RSA key — or the app throws before its
499
+ // first request. See fixture-env.ts for the pattern. Pure generator, no lifecycle.
500
+ const { fakeEnvValue } = await import("./fixture-env.js");
501
+ const names = [subject, ...rest].filter((a) => typeof a === 'string' && !a.startsWith('--'));
502
+ if (names.length === 0)
503
+ throw new Error('volter-world fake-env: pass one or more env var NAMES (e.g. `volter-world fake-env GOOGLE_VERTEX_JSON STRIPE_SECRET_KEY`)');
504
+ if (rest.includes('--json') || subject === '--json') {
505
+ process.stdout.write(`${JSON.stringify(Object.fromEntries(names.map((n) => [n, fakeEnvValue(n)])), null, 2)}\n`);
506
+ }
507
+ else {
508
+ for (const n of names) {
509
+ const v = fakeEnvValue(n);
510
+ // multiline values (SA JSON) print as NAME=<value> with the value JSON-escaped so the
511
+ // output stays one line per name and pastes into a world config env block directly.
512
+ process.stdout.write(`${n}=${v.includes('\n') ? JSON.stringify(v) : v}\n`);
513
+ }
514
+ }
515
+ return;
516
+ }
517
+ if (cmd === 'init') {
518
+ if (!subject)
519
+ throw new Error('volter-world init: missing world name (usage: volter-world init <name> --repo <path>)');
520
+ const repo = optionValue(rest, '--repo');
521
+ if (!repo)
522
+ throw new Error('volter-world init: --repo <path> is required (the application repo to build the world for)');
523
+ const out = optionValue(rest, '--out');
524
+ const result = initWorld(subject, repo, {
525
+ root,
526
+ ...(out ? { out } : {}),
527
+ force: rest.includes('--force'),
528
+ allowUnknown: rest.includes('--allow-unknown'),
529
+ acknowledge: acknowledgeOptions(rest, 'init'),
530
+ });
531
+ process.stdout.write(rest.includes('--json') ? `${JSON.stringify(result, null, 2)}\n` : formatInitReport(result));
532
+ // Vendor coverage IS the exit code. App boot risks remain a separate, prominent report field.
533
+ if (!result.ok)
534
+ process.exit(1);
535
+ return;
536
+ }
537
+ // R17 — the upkeep sweeps, surfaced per world the way npm does: `outdated` is the version
538
+ // and protocol drift of the twins this world mounted; `audit` is what the catalog knows about
539
+ // them (matrix debt, census baseline). Both read instance.json and the committed artifacts —
540
+ // nothing is fetched.
541
+ if (cmd === 'outdated' || cmd === 'audit') {
542
+ if (!subject)
543
+ throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--json])`);
544
+ const status = statusWorld(subject, root);
545
+ const rows = worldUpkeep(status, cmd);
546
+ if (rest.includes('--json'))
547
+ process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
548
+ else {
549
+ if (rows.length === 0)
550
+ process.stdout.write(`World ${status.name}: no twin mounted with a resolved version — boot it once (up) so instance.json records what was mounted\n`);
551
+ for (const r of rows)
552
+ process.stdout.write(`${r.line}\n`);
553
+ }
554
+ if (rows.some((r) => r.attention))
555
+ process.exit(1);
556
+ return;
557
+ }
558
+ if (cmd === 'covers') {
559
+ if (!subject)
560
+ throw new Error('volter-world covers: missing world name (usage: volter-world covers <world> --repo <path>)');
561
+ const repo = optionValue(rest, '--repo');
562
+ if (!repo)
563
+ throw new Error('volter-world covers: --repo <path> is required (the application repo to prove coverage for)');
564
+ const report = coverWorld(subject, repo, { root, allowUnknown: rest.includes('--allow-unknown'), acknowledge: acknowledgeOptions(rest, 'covers') });
565
+ process.stdout.write(rest.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatCoverageReport(report));
566
+ if (!report.ok)
567
+ process.exit(1);
568
+ return;
569
+ }
570
+ if (cmd === 'clock') {
571
+ // THE operator door for world time (physics): show / set <iso> / advance <duration>.
572
+ // The clock is a frozen instant every twin reads per request (kernel worldNow()); setting
573
+ // or advancing takes effect live, no restarts. `advance` requires a set clock (advancing
574
+ // wall-clock would silently freeze time as a side effect).
575
+ // argv shape here: cmd='clock', subject=<world>, rest=[action, value?, flags...]
576
+ const name = subject;
577
+ const [action, value] = rest;
578
+ if (!name || !action || (action !== 'show' && action !== 'set' && action !== 'advance')) {
579
+ console.error('usage: volter-world clock <world> show | set <iso-8601> | advance <N s|m|h|d> [--root <repo>]');
580
+ process.exit(2);
581
+ }
582
+ const rootDir = resolve(optionValue(rest, '--root') ?? process.cwd());
583
+ const file = clockFile(rootDir, name);
584
+ if (action === 'show') {
585
+ console.log(existsSync(file) ? `${readFileSync(file, 'utf8').trim()} (frozen)` : `${new Date().toISOString()} (wall clock — no world clock set)`);
586
+ return;
587
+ }
588
+ if (action === 'set') {
589
+ const parsed = Date.parse(value ?? '');
590
+ if (Number.isNaN(parsed)) {
591
+ console.error(`clock set: ${JSON.stringify(value)} is not an ISO-8601 instant`);
592
+ process.exit(2);
593
+ }
594
+ mkdirSync(dirname(file), { recursive: true });
595
+ writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
596
+ console.log(new Date(parsed).toISOString());
597
+ return;
598
+ }
599
+ const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec((value ?? '').trim());
600
+ if (!m) {
601
+ console.error(`clock advance: ${JSON.stringify(value)} is not <N>(s|m|h|d)`);
602
+ process.exit(2);
603
+ }
604
+ if (!existsSync(file)) {
605
+ console.error('clock advance: no world clock is set (advance from wall-clock would freeze time as a side effect) — `clock set <iso>` first');
606
+ process.exit(2);
607
+ }
608
+ const base = Date.parse(readFileSync(file, 'utf8').trim());
609
+ const unit = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
610
+ const next = new Date(base + Number(m[1]) * unit).toISOString();
611
+ writeFileSync(file, `${next}\n`);
612
+ console.log(next);
613
+ return;
614
+ }
615
+ if (cmd === 'urls') {
616
+ if (!subject)
617
+ throw new Error('volter-world urls: missing world name');
618
+ const urls = urlsWorld(subject, root);
619
+ if (rest.includes('--json')) {
620
+ process.stdout.write(`${JSON.stringify(urls, null, 2)}\n`);
621
+ }
622
+ else {
623
+ process.stdout.write(`World ${urls.name} (${urls.mode})\n`);
624
+ for (const [serviceId, service] of Object.entries(urls.services)) {
625
+ process.stdout.write(` ${serviceId}: ${service.localUrl}${service.publicUrl ? ` public=${service.publicUrl}` : ''}\n`);
626
+ }
627
+ }
628
+ return;
629
+ }
630
+ if (cmd === 'url') {
631
+ if (!subject)
632
+ throw new Error('volter-world url: missing world name (usage: volter-world url <world> [service])');
633
+ const [service] = positionalArgs(rest);
634
+ const urls = urlWorld(subject, { root, ...(service === undefined ? {} : { service }) });
635
+ if (service !== undefined)
636
+ process.stdout.write(`${urls[0].url}\n`);
637
+ else
638
+ for (const entry of urls)
639
+ process.stdout.write(`${entry.id} ${entry.url}\n`);
640
+ return;
641
+ }
642
+ if (cmd === 'app-url') {
643
+ if (!subject)
644
+ throw new Error('volter-world app-url: missing world name (usage: volter-world app-url <world> [--set <url>|--detect <pid|port>])');
645
+ const set = optionValue(rest, '--set');
646
+ const detect = optionValue(rest, '--detect');
647
+ if (set && detect)
648
+ throw new Error('volter-world app-url: pass --set OR --detect, not both');
649
+ const json = rest.includes('--json');
650
+ // --host (repeatable): a hostname the app answers as in production, routed to it inside the World
651
+ const hosts = rest.flatMap((arg, index) => (arg === '--host' ? [rest[index + 1] ?? ''] : []));
652
+ if (set || detect || hosts.length) {
653
+ let record = set ? setAppUrl(subject, set, { root }) : detect ? await detectAppUrl(subject, detect, { root }) : undefined;
654
+ if (hosts.length)
655
+ record = addAppHosts(subject, hosts, { root });
656
+ if (json)
657
+ process.stdout.write(`${JSON.stringify(record, null, 2)}\n`);
658
+ else
659
+ process.stdout.write(`Recorded app URL for world ${subject}: ${record.url}${record.detail ? ` (${record.detail})` : ''}${record.hosts?.length ? `; answering as ${record.hosts.join(', ')}` : ''}\n`);
660
+ return;
661
+ }
662
+ const record = readAppUrl(subject, { root });
663
+ if (record === null) {
664
+ // LOUD when unset — a consumer must never mistake "nobody registered it" for an endpoint.
665
+ process.stderr.write(`${appUrlUnsetMessage(subject)}\n`);
666
+ if (json)
667
+ process.stdout.write(`${JSON.stringify({ world: subject, url: null }, null, 2)}\n`);
668
+ process.exit(1);
669
+ }
670
+ // bare URL on stdout: ready for `curl "$(volter-world app-url <world>)/health"` interpolation
671
+ process.stdout.write(json ? `${JSON.stringify(record, null, 2)}\n` : `${record.url}\n`);
672
+ return;
673
+ }
674
+ if (cmd === 'tail' || cmd === 'log') {
675
+ if (!subject)
676
+ throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world log <world> [service...])`);
677
+ const services = positionalArgs(rest);
678
+ const controller = new AbortController();
679
+ process.once('SIGINT', () => controller.abort());
680
+ process.once('SIGTERM', () => controller.abort());
681
+ await tailWorldActions(subject, {
682
+ root,
683
+ ...(services.length ? { services } : {}),
684
+ follow: !rest.includes('--no-follow'),
685
+ json: rest.includes('--json'),
686
+ requests: rest.includes('--requests'),
687
+ signal: controller.signal,
688
+ });
689
+ return;
690
+ }
691
+ if (cmd === 'seed' || cmd === 'reset') {
692
+ if (!subject)
693
+ throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--entry <file>] [--cwd <dir>])`);
694
+ const entry = optionValue(rest, '--entry') || undefined;
695
+ const cwd = optionValue(rest, '--cwd') || undefined;
696
+ const json = rest.includes('--json');
697
+ const outcome = cmd === 'seed' ? await seedWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) }) : await resetWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) });
698
+ if (json)
699
+ process.stdout.write(`${JSON.stringify({ world: outcome.world, entry: outcome.entry, exitCode: outcome.exitCode, observed: outcome.observed }, null, 2)}\n`);
700
+ else
701
+ process.stdout.write(`${cmd === 'reset' ? 'Reset' : 'Seeded'} world ${subject} from the placeholder remote (${outcome.entry})\n${Object.entries(outcome.observed).map(([s, n]) => ` ${s}: ${n} observed`).join('\n')}\n${outcome.exitCode === 0 ? '' : ` seed entry exited ${outcome.exitCode}\n`}`);
702
+ process.exitCode = outcome.exitCode === 0 ? 0 : 1;
703
+ return;
704
+ }
705
+ if (cmd === 'branch') {
706
+ const [name] = positionalArgs(rest);
707
+ if (!subject || !name)
708
+ throw new Error('volter-world branch: usage: volter-world branch <world> <name> [--env-file=<path>]');
709
+ const envFile = optionValue(rest, '--env-file') || undefined;
710
+ const { instance, forked } = await branchWorld(subject, name, { root, ...(envFile ? { envFile } : {}) });
711
+ if (rest.includes('--json'))
712
+ process.stdout.write(`${JSON.stringify({ name: instance.name, base: subject, forked }, null, 2)}\n`);
713
+ else {
714
+ process.stdout.write(`Branched world ${name} from ${subject}\n${Object.entries(forked).map(([s, states]) => ` ${s}: mirror forked (${states.join(', ')})`).join('\n')}\n`);
715
+ printReady(instance);
716
+ }
717
+ return;
718
+ }
719
+ if (cmd === 'checkout') {
720
+ if (!subject)
721
+ throw new Error('volter-world checkout: missing world name');
722
+ const instance = await checkoutWorld(subject, { root });
723
+ if (rest.includes('--json'))
724
+ process.stdout.write(`${JSON.stringify({ name: instance.name }, null, 2)}\n`);
725
+ else
726
+ printReady(instance);
727
+ return;
728
+ }
729
+ if (cmd === 'fetch') {
730
+ if (!subject)
731
+ throw new Error('volter-world fetch: missing world name (usage: volter-world fetch <world> --key <key> [--origin <url> --namespace <org>/<world>] [--full])');
732
+ const key = optionValue(rest, '--key') || process.env.VOLTER_TWINS_READ_KEY || process.env.VOLTER_TWINS_KEY || '';
733
+ if (!key)
734
+ throw new Error('volter-world fetch: --key <twins key> is required (the read key for a fetch, the admin key for --full)');
735
+ const services = [];
736
+ for (let i = 0; i < rest.length; i++)
737
+ if (rest[i] === '--service' && rest[i + 1])
738
+ services.push(rest[i + 1]);
739
+ const url = optionValue(rest, '--origin') || undefined;
740
+ const namespace = optionValue(rest, '--namespace') || undefined;
741
+ const outcome = await fetchFromOrigin(subject, { root, key, ...(url ? { url } : {}), ...(namespace ? { namespace } : {}), ...(rest.includes('--full') ? { full: true } : {}), ...(services.length ? { services } : {}) });
742
+ if (rest.includes('--json'))
743
+ process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
744
+ else if (outcome.full)
745
+ process.stdout.write(`Cloned ${outcome.origin.namespace} from ${outcome.origin.url} into world ${subject} (${outcome.files} files)\n`);
746
+ else
747
+ process.stdout.write(`Fetched from ${outcome.origin.namespace} at ${outcome.origin.url} into world ${subject}\n${Object.entries(outcome.appended).map(([k, n]) => ` ${k}: ${n} observed`).join('\n')}\n`);
748
+ return;
749
+ }
750
+ if (cmd === 'origin') {
751
+ if (!subject)
752
+ throw new Error('volter-world origin: missing world name');
753
+ const origin = worldOrigin(subject, root);
754
+ if (rest.includes('--json'))
755
+ process.stdout.write(`${JSON.stringify(origin, null, 2)}\n`);
756
+ else
757
+ process.stdout.write(origin ? `${origin.namespace} at ${origin.url}${origin.fetchedAt ? ` (fetched ${origin.fetchedAt})` : ''}\n` : 'placeholder (no hosted origin recorded — `volter-world fetch <world> --full --origin <url> --namespace <org>/<world> --key <admin key>` clones one)\n');
758
+ return;
759
+ }
760
+ if (cmd === 'mark') {
761
+ if (!subject)
762
+ throw new Error('volter-world mark: missing world name (usage: volter-world mark <world> [--id <marker>] [--note <text>])');
763
+ const id = optionValue(rest, '--id');
764
+ const note = optionValue(rest, '--note');
765
+ const marker = markWorld(subject, { root, ...(id ? { id } : {}), ...(note ? { note } : {}) });
766
+ if (rest.includes('--json'))
767
+ process.stdout.write(`${JSON.stringify(marker, null, 2)}\n`);
768
+ else
769
+ process.stdout.write(`Marked world ${marker.world} at ${marker.id} (${marker.createdAt})\n ${marker.ledgers.length} ledger(s): ${marker.ledgers.map((ledger) => `${ledger.service}=${ledger.count}`).join(', ') || 'none yet'}\n`);
770
+ return;
771
+ }
772
+ if (cmd === 'diff') {
773
+ if (!subject)
774
+ throw new Error('volter-world diff: missing world name (usage: volter-world diff <world> [--base <marker>])');
775
+ const base = optionValue(rest, '--base');
776
+ const delta = diffWorld(subject, { root, ...(base ? { base } : {}) });
777
+ process.stdout.write(rest.includes('--json') ? `${JSON.stringify(delta, null, 2)}\n` : formatLedgerDelta(delta));
778
+ return;
779
+ }
780
+ if (cmd === 'changeset') {
781
+ const CHANGESET_VALUE_FLAGS = ['--root', '--base', '--into', '--world', '--verifier', '--as', '--note'];
782
+ const args = positionalArgs(rest, CHANGESET_VALUE_FLAGS);
783
+ const world = optionValue(rest, '--world');
784
+ const json = rest.includes('--json');
785
+ if (subject === 'create') {
786
+ const [worldName, name] = args;
787
+ if (!worldName || !name)
788
+ throw new Error('volter-world changeset create: usage: volter-world changeset create <world> <name> [--base <marker>] [--verifier "<service> <type>:<id> <field> <op> [value]"]...');
789
+ const base = optionValue(rest, '--base');
790
+ const verifiers = rest
791
+ .flatMap((flag, index) => (flag === '--verifier' && rest[index + 1] ? [rest[index + 1]] : []))
792
+ .map((expression, index) => parseVerifierExpression(expression, `v${index + 1}`));
793
+ const changeset = createWorldChangeset(worldName, name, { root, overwrite: rest.includes('--force'), ...(base ? { base } : {}), ...(verifiers.length ? { verifiers } : {}) });
794
+ process.stdout.write(json ? `${JSON.stringify(changeset, null, 2)}\n` : formatChangeset(changeset));
795
+ return;
796
+ }
797
+ if (subject === 'show') {
798
+ const [name] = args;
799
+ if (!name)
800
+ throw new Error('volter-world changeset show: usage: volter-world changeset show <name> [--world <world>]');
801
+ const located = findWorldChangeset(name, { root, ...(world ? { world } : {}) });
802
+ process.stdout.write(json ? `${JSON.stringify(located.changeset, null, 2)}\n` : formatChangeset(located.changeset));
803
+ return;
804
+ }
805
+ if (subject === 'list') {
806
+ const found = listWorldChangesets({ root, ...(world ? { world } : {}) });
807
+ if (json) {
808
+ process.stdout.write(`${JSON.stringify(found.map((entry) => ({ ...entry.changeset, path: entry.path })), null, 2)}\n`);
809
+ return;
810
+ }
811
+ if (!found.length)
812
+ process.stdout.write('No changesets yet — create one with `volter-world changeset create <world> <name>`\n');
813
+ for (const entry of found) {
814
+ process.stdout.write(`${entry.changeset.name}\t${entry.world}\t${entry.changeset.actions.length} action(s)\tbase=${entry.changeset.base}\t${entry.changeset.contentHash}\n`);
815
+ }
816
+ return;
817
+ }
818
+ if (subject === 'replay') {
819
+ const [name] = args;
820
+ if (!name)
821
+ throw new Error('volter-world changeset replay: usage: volter-world changeset replay <name> --into <world>');
822
+ const into = optionValue(rest, '--into');
823
+ if (!into)
824
+ throw new Error('volter-world changeset replay: --into <world> is required (the world to replay the changeset into)');
825
+ const report = await replayWorldChangeset(name, { root, into, ...(world ? { world } : {}) });
826
+ process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : formatReplayReport(report));
827
+ return;
828
+ }
829
+ if (subject === 'verify') {
830
+ const [name] = args;
831
+ if (!name)
832
+ throw new Error('volter-world changeset verify: usage: volter-world changeset verify <name> (--into <world> | --ephemeral)');
833
+ const into = optionValue(rest, '--into');
834
+ const outcome = await verifyWorldChangeset(name, { root, ...(into ? { into } : {}), ephemeral: rest.includes('--ephemeral'), ...(world ? { world } : {}) });
835
+ process.stdout.write(json ? `${JSON.stringify(outcome.verification, null, 2)}\n` : formatVerification(name, outcome.verification, outcome.report));
836
+ if (!outcome.verification.passed)
837
+ process.exitCode = 1;
838
+ return;
839
+ }
840
+ if (subject === 'approve') {
841
+ const [name] = args;
842
+ if (!name)
843
+ throw new Error('volter-world changeset approve: usage: volter-world changeset approve <name> --as <principal> [--note <text>]');
844
+ const principal = optionValue(rest, '--as');
845
+ if (!principal)
846
+ throw new Error('volter-world changeset approve: --as <principal> is required (who is signing)');
847
+ const note = optionValue(rest, '--note');
848
+ const outcome = approveWorldChangeset(name, { root, principal, ...(note ? { note } : {}), ...(world ? { world } : {}) });
849
+ if (json)
850
+ process.stdout.write(`${JSON.stringify(outcome.approval, null, 2)}\n`);
851
+ else
852
+ process.stdout.write(`Approved changeset ${name} as ${outcome.approval.principal} at ${outcome.approval.at}\n bound to ${outcome.approval.contentHash}\n`);
853
+ return;
854
+ }
855
+ if (subject === 'status') {
856
+ const [name] = args;
857
+ if (!name)
858
+ throw new Error('volter-world changeset status: usage: volter-world changeset status <name> [--world <world>]');
859
+ const readiness = statusWorldChangeset(name, { root, ...(world ? { world } : {}) });
860
+ process.stdout.write(json ? `${JSON.stringify(readiness, null, 2)}\n` : formatChangesetStatus(readiness));
861
+ if (!readiness.ready)
862
+ process.exitCode = 1;
863
+ return;
864
+ }
865
+ if (subject === 'rebase') {
866
+ const [name] = args;
867
+ if (!name)
868
+ throw new Error('volter-world changeset rebase: usage: volter-world changeset rebase <name> [--world <world>]');
869
+ const outcome = rebaseWorldChangeset(name, { root, ...(world ? { world } : {}) });
870
+ process.stdout.write(json ? `${JSON.stringify(outcome.report, null, 2)}\n` : formatRebaseReport(outcome.report));
871
+ if (outcome.report.outcome === 'conflict')
872
+ process.exitCode = 1;
873
+ return;
874
+ }
875
+ if (subject === 'apply' || subject === 'push') {
876
+ const [name] = args;
877
+ if (!name)
878
+ throw new Error('volter-world changeset push: usage: volter-world changeset push <name> --key <namespace key> [--to <twins url> --namespace <org>/<world>] [--force]');
879
+ const key = optionValue(rest, '--key') || process.env.VOLTER_TWINS_KEY || '';
880
+ if (!key)
881
+ throw new Error('volter-world changeset push: --key <namespace key> is required (a push is a write: the read key is refused; a vendor credential never leaves the remote)');
882
+ const to = optionValue(rest, '--to') || undefined;
883
+ const namespace = optionValue(rest, '--namespace') || undefined;
884
+ const outcome = await pushWorldChangeset(name, { root, key, ...(to ? { to } : {}), ...(namespace ? { namespace } : {}), ...(rest.includes('--force') ? { force: true } : {}), ...(world ? { world } : {}) });
885
+ process.stdout.write(json ? `${JSON.stringify(outcome.application, null, 2)}\n` : `${formatApplication(name, outcome.application)} remote: ${outcome.remote.namespace} at ${outcome.remote.url}\n`);
886
+ if (outcome.unsent)
887
+ process.stderr.write(`warning: ${outcome.unsent}\n`);
888
+ if (outcome.application.outcome !== 'applied')
889
+ process.exitCode = 1;
890
+ return;
891
+ }
892
+ throw new Error(`volter-world changeset: unknown subcommand ${JSON.stringify(subject ?? '')} (want create|show|list|replay|verify|approve|status|rebase|push)`);
893
+ }
894
+ if (cmd === 'list') {
895
+ const listArgs = [subject, ...rest].filter((arg) => arg !== undefined);
896
+ const listRoot = optionValue(listArgs, '--root', process.cwd());
897
+ const worlds = listWorlds(listRoot).map(world => ({ ...world, usage: worldUsage(listRoot, world.name) }));
898
+ if (rest.includes('--json') || subject === '--json')
899
+ process.stdout.write(`${JSON.stringify(worlds, null, 2)}\n`);
900
+ else
901
+ for (const world of worlds) {
902
+ process.stdout.write(`${world.name}\t${!world.running ? 'stopped' : world.degraded ? 'degraded' : 'running'}\t${world.config ?? ''}\tcreated=${world.usage.createdAt ?? 'unknown'}\tlast-observed-use=${world.usage.lastUsedAt ?? 'unknown'} (${world.usage.lastUseSource ?? 'unrecorded'}; partial coverage)\n`);
903
+ for (const warning of world.usage.warnings)
904
+ process.stdout.write(` WARNING: ${warning}\n`);
905
+ }
906
+ return;
907
+ }
908
+ if (cmd === 'env') {
909
+ if (!subject)
910
+ throw new Error('volter-world env: missing world name');
911
+ const split = rest.indexOf('--');
912
+ if (split < 0)
913
+ throw new Error('volter-world env: pass command after --');
914
+ const exitCode = await attachWorld(subject, rest.slice(split + 1), root, { owner: ownerOption(rest.slice(0, split)), verbose: rest.slice(0, split).includes('--verbose') });
915
+ process.exit(exitCode);
916
+ }
917
+ if (cmd === 'attach') {
918
+ // `subject` may be the ref, a flag, or the `--` separator — reassemble and split.
919
+ const args = [subject, ...rest].filter((arg) => arg !== undefined);
920
+ const split = args.indexOf('--');
921
+ const head = split >= 0 ? args.slice(0, split) : args;
922
+ const command = split >= 0 ? args.slice(split + 1) : [];
923
+ const explicit = head[0] !== undefined && !head[0].startsWith('-') ? head[0] : undefined;
924
+ const via = optionValue(head, '--via', 'env');
925
+ // when the ref is omitted, `subject` was a flag — the global root parse missed its value
926
+ const attachRoot = optionValue(head, '--root', process.cwd());
927
+ const resolved = resolveWorldRef(explicit, { cwd: process.cwd(), env: process.env });
928
+ if (ownerOption(head) !== undefined && (isRemoteWorldRef(resolved.ref) || via !== 'env')) {
929
+ throw new Error('--owner is supported for local attach --via env commands');
930
+ }
931
+ if (isRemoteWorldRef(resolved.ref)) {
932
+ // remote world: fetch the manifest, synthesize the attach env, exec
933
+ const attachToken = optionValue(head, '--token') || process.env.VOLTER_WORLD_TOKEN || undefined;
934
+ const manifest = await fetchRemoteManifest(resolved.ref, attachToken === undefined ? {} : { token: attachToken });
935
+ if (via === 'direct') {
936
+ process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
937
+ return;
938
+ }
939
+ if (via !== 'env')
940
+ throw new Error(`volter-world attach: --via ${via} is not available for a remote world (env|direct)`);
941
+ if (command.length === 0)
942
+ throw new Error('volter-world attach: pass the command after --');
943
+ let caFile;
944
+ if (manifest.ca !== null) {
945
+ caFile = join(mkdtempSync(join(tmpdir(), 'volter-attach-')), 'ca.pem');
946
+ writeFileSync(caFile, manifest.ca);
947
+ }
948
+ const injectPath = createRequire(import.meta.url).resolve('@volter/world-core/inject');
949
+ const env = remoteAttachEnv(manifest, { injectPath, ...(caFile === undefined ? {} : { caFile }) });
950
+ // the world's byte-stream doors, bridged to loopback listeners here, beside a command in any language
951
+ const { bridgeStreams, drainStreams } = createRequire(import.meta.url)('@volter/world-core/stream-bridge');
952
+ const bridged = manifest.streams ? await bridgeStreams(manifest.streams, attachToken ?? manifest.env.VOLTER_TWINS_KEY ?? '') : { env: {} };
953
+ // VOLTER_WORLD names the World to the injector: a request addressed to its paths carries its key
954
+ const commandEnv = { ...process.env, ...env, ...bridged.env, VOLTER_WORLD: resolved.ref };
955
+ commandEnv.VOLTER_TWIN_INJECT_QUIET = head.includes('--verbose') ? '0' : commandEnv.VOLTER_TWIN_INJECT_QUIET ?? '1';
956
+ const child = spawn(command[0], command.slice(1), { env: commandEnv, stdio: 'inherit' });
957
+ const code = await new Promise((resolveExit) => { child.on('exit', (status) => resolveExit(status ?? 1)); child.on('error', () => resolveExit(127)); });
958
+ // a client that wrote and exited at once still has bytes on their way through a bridge
959
+ await drainStreams();
960
+ process.exit(code);
961
+ }
962
+ if (via === 'direct') {
963
+ process.stdout.write(`${JSON.stringify(worldManifest(resolved.ref, attachRoot), null, 2)}\n`);
964
+ return;
965
+ }
966
+ if (via === 'reflect') {
967
+ // The reflect attachment (docs/ATTACH.md): the consumer is unmodified; its DNS is the world's resolver
968
+ // and its TLS trust is the session CA. A `docker compose` command is composed here — every service
969
+ // gets the override — and everything else runs with the trust env and VOLTER_WORLD set, its DNS
970
+ // being the caller's to point (a container's --dns, a resolver on the host).
971
+ if (command.length === 0)
972
+ throw new Error('volter-world attach: pass the command after --');
973
+ const manifest = readReflectManifest(attachRoot, resolved.ref);
974
+ if (!manifest)
975
+ throw new Error(`volter-world attach --via reflect: no reflect front is running for world ${resolved.ref} — start one: volter-world reflect ${resolved.ref} --target-ip <ip> --port 443 --resolver-port 53`);
976
+ const env = { ...process.env, VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
977
+ Object.assign(env, sessionTrustEnv(manifest.caCertPath, env));
978
+ let toRun = command;
979
+ const compose = splitDockerComposeArgs(command);
980
+ if (compose) {
981
+ // the consumer's own service names, from its own files, through its own docker
982
+ const listed = spawnSync(compose.head[0], [...compose.head.slice(1), ...compose.composeFlags, 'config', '--services'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
983
+ if (listed.status !== 0)
984
+ throw new Error(`volter-world attach --via reflect: docker compose config --services failed:\n${listed.stderr.toString().trim()}`);
985
+ const services = listed.stdout.toString().split('\n').map((s) => s.trim()).filter(Boolean);
986
+ const overridePath = join(instanceDir(attachRoot, resolved.ref), 'reflect-compose.override.yml');
987
+ writeFileSync(overridePath, composeOverrideForReflect(manifest, services, resolved.ref));
988
+ toRun = dockerComposeWithOverride(command, overridePath);
989
+ 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`);
990
+ }
991
+ const result = spawnSync(toRun[0], toRun.slice(1), { env, stdio: 'inherit' });
992
+ process.exit(result.status ?? 1);
993
+ }
994
+ if (via !== 'env') {
995
+ throw new Error(`volter-world attach: --via ${via} is not available (env|reflect|direct)`);
996
+ }
997
+ if (command.length === 0)
998
+ throw new Error('volter-world attach: pass the command after -- (or use --via direct for the manifest)');
999
+ // the world resolves at --root; the COMMAND runs where the caller stands —
1000
+ // an attached repo's `pnpm start` must run in the repo, not the twin root
1001
+ // (ponder blind-adoption finding)
1002
+ process.exit(await attachWorld(resolved.ref, command, attachRoot, { cwd: process.cwd(), owner: ownerOption(head), verbose: head.includes('--verbose') }));
1003
+ }
1004
+ if (cmd === 'manifest') {
1005
+ if (!subject)
1006
+ throw new Error('volter-world manifest: missing world name');
1007
+ process.stdout.write(`${JSON.stringify(worldManifest(subject, root), null, 2)}\n`);
1008
+ return;
1009
+ }
1010
+ if (cmd === 'reflect') {
1011
+ if (!subject)
1012
+ throw new Error('volter-world reflect: missing world name');
1013
+ const targetIp = optionValue(rest, '--target-ip');
1014
+ if (!targetIp)
1015
+ throw new Error('volter-world reflect: --target-ip <ip> is required (the address attachers reach the front at)');
1016
+ const routesPath = reflectRoutesPath(root, subject);
1017
+ const front = await startReflectFront({
1018
+ envLoader: () => statusWorld(subject, root).env,
1019
+ tlsDir: join(instanceDir(root, subject), 'tls'),
1020
+ port: Number(optionValue(rest, '--port', '0')) || 0,
1021
+ });
1022
+ const resolver = await startReflectResolver({
1023
+ routesLoader: () => readReflectRoutes(routesPath),
1024
+ targetIp,
1025
+ upstream: optionValue(rest, '--upstream') || undefined,
1026
+ port: Number(optionValue(rest, '--resolver-port', '0')) || 0,
1027
+ });
1028
+ const resolverIp = optionValue(rest, '--resolver-ip') || targetIp;
1029
+ writeReflectManifest(root, subject, { targetIp, resolverIp, frontPort: front.port, resolverPort: resolver.port, caCertPath: front.caCertPath });
1030
+ process.stdout.write(`Reflect attachment for world ${subject}:\n`);
1031
+ process.stdout.write(` front: ${targetIp}:${front.port} (SNI TLS door — session CA: ${front.caCertPath})\n`);
1032
+ process.stdout.write(` resolver: ${resolverIp}:${resolver.port} (point the attacher's DNS here, e.g. docker run --dns)\n`);
1033
+ process.stdout.write(` routes: ${routesPath} (volter-world route ${subject} add <host>)\n`);
1034
+ process.stdout.write('Foreground — Ctrl+C to stop. The runtime supervises nothing (see docs/guides/route-a-cli-through-the-world.md).\n');
1035
+ await new Promise((resolveSignal) => {
1036
+ process.once('SIGINT', () => resolveSignal());
1037
+ process.once('SIGTERM', () => resolveSignal());
1038
+ });
1039
+ clearReflectManifest(root, subject);
1040
+ await resolver.close();
1041
+ await front.close();
1042
+ return;
1043
+ }
1044
+ if (cmd === 'serve') {
1045
+ if (!subject)
1046
+ throw new Error('volter-world serve: missing world name');
1047
+ const advertise = optionValue(rest, '--advertise');
1048
+ if (!advertise)
1049
+ throw new Error('volter-world serve: --advertise <https-origin> is required (how attachers reach the door, e.g. https://worlds.corp:8443)');
1050
+ let advertisedUrl;
1051
+ try {
1052
+ advertisedUrl = new URL(advertise);
1053
+ }
1054
+ catch {
1055
+ throw new Error(`volter-world serve: --advertise must be an origin URL, got "${advertise}"`);
1056
+ }
1057
+ if (advertisedUrl.protocol !== 'https:')
1058
+ throw new Error('volter-world serve: the advertised origin must be https (the door terminates TLS with the session CA)');
1059
+ const frontPort = Number(advertisedUrl.port) || 443;
1060
+ const front = await startReflectFront({
1061
+ envLoader: () => statusWorld(subject, root).env,
1062
+ tlsDir: join(instanceDir(root, subject), 'tls'),
1063
+ port: Number(optionValue(rest, '--port', String(frontPort))) || frontPort,
1064
+ extraHosts: [advertisedUrl.hostname],
1065
+ });
1066
+ const serveToken = optionValue(rest, '--token') || undefined;
1067
+ const manifestServer = await startManifestServer({
1068
+ manifest: () => advertiseWorldManifest(subject, root, advertisedUrl.origin),
1069
+ port: Number(optionValue(rest, '--manifest-port', '0')) || 0,
1070
+ ...(serveToken === undefined ? {} : { token: serveToken }),
1071
+ });
1072
+ process.stdout.write(`Serving world ${subject}:\n`);
1073
+ process.stdout.write(` door: ${advertisedUrl.origin} (SNI TLS — vendors + the advertised name; session CA: ${front.caCertPath})\n`);
1074
+ process.stdout.write(` manifest: http://0.0.0.0:${manifestServer.port}${MANIFEST_PATH}\n`);
1075
+ process.stdout.write(` attach: volter-world attach http://<this-host>:${manifestServer.port} -- <command...>\n`);
1076
+ process.stdout.write('Foreground — Ctrl+C to stop. Read-only publication; the runtime supervises nothing.\n');
1077
+ await new Promise((resolveSignal) => {
1078
+ process.once('SIGINT', () => resolveSignal());
1079
+ process.once('SIGTERM', () => resolveSignal());
1080
+ });
1081
+ await manifestServer.close();
1082
+ await front.close();
1083
+ return;
1084
+ }
1085
+ if (cmd === 'route') {
1086
+ if (!subject)
1087
+ throw new Error('volter-world route: missing world name');
1088
+ const [verb, host] = rest.filter((arg) => !arg.startsWith('--'));
1089
+ const routesPath = reflectRoutesPath(root, subject);
1090
+ const routes = readReflectRoutes(routesPath);
1091
+ if (verb === 'ls' || verb === undefined) {
1092
+ for (const entry of [...routes].sort())
1093
+ process.stdout.write(`${entry}\n`);
1094
+ return;
1095
+ }
1096
+ if (!host)
1097
+ throw new Error(`volter-world route: ${verb} needs a host`);
1098
+ if (verb === 'add')
1099
+ routes.add(host.toLowerCase());
1100
+ else if (verb === 'rm')
1101
+ routes.delete(host.toLowerCase());
1102
+ else
1103
+ throw new Error(`volter-world route: unknown verb "${verb}" (add|rm|ls)`);
1104
+ writeReflectRoutes(routesPath, routes);
1105
+ process.stdout.write(`${[...routes].sort().join('\n')}${routes.size > 0 ? '\n' : ''}`);
1106
+ return;
1107
+ }
1108
+ if (cmd === 'activate') {
1109
+ if (!subject)
1110
+ throw new Error('volter-world activate: missing world name');
1111
+ process.stdout.write(activateScript(subject, root));
1112
+ return;
1113
+ }
1114
+ if (cmd === 'shell') {
1115
+ if (!subject)
1116
+ throw new Error('volter-world shell: missing world name');
1117
+ process.exit(await shellWorld(subject, root));
1118
+ }
1119
+ if (cmd === 'run') {
1120
+ if (!subject)
1121
+ throw new Error('volter-world run: missing config');
1122
+ const split = rest.indexOf('--');
1123
+ if (split < 0)
1124
+ throw new Error('volter-world run: pass command after --');
1125
+ const envFile = optionValue(rest, '--env-file');
1126
+ if (!envFile)
1127
+ throw new Error('volter-world run: missing required --env-file=<path>');
1128
+ const name = optionValue(rest, '--name') || undefined;
1129
+ const mode = optionValue(rest, '--mode', 'local');
1130
+ const result = await runWorld(subject, rest.slice(split + 1), { root, name, mode, envFile, owner: ownerOption(rest.slice(0, split)), keep: rest.slice(0, split).includes('--keep'), verbose: rest.slice(0, split).includes('--verbose') });
1131
+ if (result.exitCode !== 0) {
1132
+ const detail = result.outcome.error ? `could not start (${result.outcome.error})`
1133
+ : result.outcome.signal ? `was terminated by ${result.outcome.signal}`
1134
+ : `exited with code ${result.outcome.exitCode}`;
1135
+ process.stderr.write(`World ${result.instance.name}: foreground consumer failed: ${detail}\nLog: ${result.outcome.log}\n`);
1136
+ // A command that failed because a service under it died says so: each service that ended on its own during the
1137
+ // run (not by the World's teardown after it) is named with its end.
1138
+ for (const service of Object.values(result.instance.services)) {
1139
+ if (service.type === 'external' || !service.log)
1140
+ continue;
1141
+ const end = readServiceEnd(service.log);
1142
+ if (!end?.exited || end.exited.stopRequested)
1143
+ continue;
1144
+ process.stderr.write(` service ${service.id} ended during the run: it ${describeServiceEnd(end)}${end.lastOutput.length ? `; last output: ${end.lastOutput[end.lastOutput.length - 1]}` : ''}\n log: ${service.log}\n`);
1145
+ }
1146
+ }
1147
+ process.exit(result.exitCode);
1148
+ }
1149
+ if (cmd === 'share') {
1150
+ if (!subject)
1151
+ throw new Error('volter-world share: missing world name');
1152
+ const service = optionValue(rest, '--service') || undefined;
1153
+ const verifyPath = rest.includes('--no-verify')
1154
+ ? false
1155
+ : rest.includes('--verify')
1156
+ ? optionValue(rest, '--verify', '/health')
1157
+ : undefined;
1158
+ const split = rest.indexOf('--');
1159
+ const command = optionValue(rest, '--command') || undefined;
1160
+ const providerValue = optionValue(rest, '--provider') || undefined;
1161
+ if (providerValue !== undefined && providerValue !== 'cloudflare-quick' && providerValue !== 'command') {
1162
+ throw new Error('volter-world share: --provider must be cloudflare-quick or command');
1163
+ }
1164
+ const provider = providerValue;
1165
+ if (provider === 'cloudflare-quick' && command) {
1166
+ throw new Error('volter-world share: --provider cloudflare-quick uses the built-in cloudflared command and cannot be combined with --command');
1167
+ }
1168
+ if (split >= 0 && !command)
1169
+ throw new Error('volter-world share: -- <args> requires --command <cmd>');
1170
+ const args = command && split >= 0 ? rest.slice(split + 1) : undefined;
1171
+ const instance = await shareWorldServices(subject, { root, service, verifyPath, provider, command, args, ephemeral: rest.includes('--ephemeral') || undefined });
1172
+ const serviceIds = service ? [service] : Object.values(instance.services)
1173
+ .filter((candidate) => candidate.publicUrl)
1174
+ .map((candidate) => candidate.id);
1175
+ for (const serviceId of serviceIds) {
1176
+ const shared = instance.services[serviceId];
1177
+ process.stdout.write(`${shared?.publicReady ? 'Shared' : 'Shared (unverified)'} ${subject}/${serviceId}: ${shared?.publicUrl}\n`);
1178
+ }
1179
+ return;
1180
+ }
1181
+ if (cmd === 'unshare') {
1182
+ if (!subject)
1183
+ throw new Error('volter-world unshare: missing world name');
1184
+ const service = optionValue(rest, '--service') || undefined;
1185
+ const result = unshareWorld(subject, { root, service });
1186
+ process.stdout.write(`Unshared world ${subject}${service ? `/${service}` : ''}\n`);
1187
+ if (result.escalated.length)
1188
+ process.stdout.write(`Escalated to SIGKILL (tunnel ignored SIGTERM past the grace): ${result.escalated.join(', ')}\n`);
1189
+ return;
1190
+ }
1191
+ throw new Error(`Unknown command: ${cmd}`);
1192
+ }
1193
+ main().catch((error) => {
1194
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1195
+ process.exit(1);
1196
+ });
1197
+ /** The rows `outdated` / `audit` print. `outdated`: pinned vs mounted vs catalog-current version and the
1198
+ * protocol standing; `audit`: the catalog's upkeep verdicts for each mounted twin — the invariant
1199
+ * matrix's grandfathered cells and the spec census baseline — read from the pack's committed
1200
+ * census.json. `attention` marks a row the operator should act on (exit 1). */
1201
+ function worldUpkeep(status, kind) {
1202
+ const twinRoot = join(import.meta.dir, '..', '..', 'twin'); // the catalog: packages/twin
1203
+ const out = [];
1204
+ for (const [id, svc] of Object.entries(status.services)) {
1205
+ if (svc.type !== 'twin' && svc.resolvedVersion === undefined)
1206
+ continue;
1207
+ const vendor = id;
1208
+ let current;
1209
+ try {
1210
+ current = JSON.parse(readFileSync(join(twinRoot, vendor, 'package.json'), 'utf8')).version;
1211
+ }
1212
+ catch {
1213
+ current = undefined;
1214
+ }
1215
+ const pinned = status.services[id] && svc.version;
1216
+ if (kind === 'outdated') {
1217
+ const behind = svc.resolvedVersion !== undefined && current !== undefined && svc.resolvedVersion !== current;
1218
+ const standing = svc.protocol?.standing ?? 'unknown';
1219
+ const attention = behind || standing === 'deprecated' || standing === 'refused';
1220
+ out.push({ id, attention, line: `${id.padEnd(18)} mounted ${(svc.resolvedVersion ?? '?').padEnd(8)} catalog ${(current ?? '?').padEnd(8)} ${behind ? 'BEHIND' : 'current'} protocol ${svc.protocol ? `major ${svc.protocol.major} (${standing})` : 'unknown'}${pinned ? ` pinned ${pinned}` : ''}` });
1221
+ }
1222
+ else {
1223
+ let census;
1224
+ try {
1225
+ census = JSON.parse(readFileSync(join(twinRoot, vendor, 'census.json'), 'utf8'));
1226
+ }
1227
+ catch {
1228
+ census = undefined;
1229
+ }
1230
+ const debt = Object.entries(census?.invariants ?? {}).filter(([, v]) => v === 'grandfathered').map(([k]) => k);
1231
+ const attention = debt.length > 0;
1232
+ // R19 — the pin's age: MAINTAINERS.md's `last verified` cell is when this package's vendor
1233
+ // generation was last proven against a fresh spec; a stale one is drift nobody has looked for.
1234
+ let maintained = '';
1235
+ try {
1236
+ const row = readFileSync(join(twinRoot, '..', '..', 'policy', 'MAINTAINERS.md'), 'utf8').split('\n').find((l) => l.startsWith(`| ${vendor} |`));
1237
+ const cells = row ? row.split('|').map((c) => c.trim()) : [];
1238
+ if (cells.length > 8) {
1239
+ const days = Math.floor((Date.now() - Date.parse(cells[8])) / 86_400_000);
1240
+ maintained = ` ${cells[2]}, last verified ${cells[8]}${Number.isFinite(days) ? ` (${days}d ago)` : ''}`;
1241
+ if (Number.isFinite(days) && days > 30)
1242
+ maintained += ' STALE';
1243
+ }
1244
+ }
1245
+ catch {
1246
+ maintained = '';
1247
+ }
1248
+ out.push({ id, attention: attention || maintained.endsWith('STALE'), line: `${id.padEnd(18)} matrix debt: ${debt.length ? debt.join(', ') : 'none'} census: ${census ? 'committed' : 'NONE (not Supported)'}${maintained}` });
1249
+ }
1250
+ }
1251
+ return out;
1252
+ }
1253
+ /** The files a service wrote into its own log directory (`<log>.d`, VOLTER_WORLD_SERVICE_LOG_DIR), paths in full. */
1254
+ function ownLogs(log) {
1255
+ const dir = `${log.replace(/\.log$/, '')}.d`;
1256
+ try {
1257
+ return readdirSync(dir).map((entry) => join(dir, entry));
1258
+ }
1259
+ catch {
1260
+ return [];
1261
+ }
1262
+ }