@volter/world-runtime 2.0.55 → 3.0.1

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