@volter/world-runtime 3.0.38 → 3.0.40

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 (168) hide show
  1. package/dist/known-external-services.json +3 -10
  2. package/dist/src/actions-runner-cli.js +3 -3
  3. package/dist/src/actions-runner.js +2 -1
  4. package/dist/src/app-url.js +2 -1
  5. package/dist/src/backings.d.ts +10 -1
  6. package/dist/src/backings.js +66 -7
  7. package/dist/src/branch.js +85 -52
  8. package/dist/src/browser-routes.cjs +59 -0
  9. package/dist/src/ca-trust.js +4 -3
  10. package/dist/src/changeset.js +4 -3
  11. package/dist/src/cli-commands.js +25 -9
  12. package/dist/src/cli.js +2 -1
  13. package/dist/src/command-lifetime.d.mts +12 -0
  14. package/dist/src/command-lifetime.d.ts +1 -15
  15. package/dist/src/command-lifetime.js +1 -98
  16. package/dist/src/command-lifetime.mjs +90 -0
  17. package/dist/src/configs.js +1 -1
  18. package/dist/src/console-apart.js +3 -2
  19. package/dist/src/covers.d.ts +3 -2
  20. package/dist/src/covers.js +95 -18
  21. package/dist/src/error-message.d.mts +1 -0
  22. package/dist/src/error-message.d.ts +1 -0
  23. package/dist/src/error-message.js +2 -0
  24. package/dist/src/error-message.mjs +10 -0
  25. package/dist/src/guest-command.d.mts +4 -0
  26. package/dist/src/guest-command.mjs +26 -0
  27. package/dist/src/guest-control.d.mts +1 -0
  28. package/dist/src/guest-control.mjs +17 -0
  29. package/dist/src/guest-entry.mjs +146 -0
  30. package/dist/src/guest-processes.d.mts +9 -0
  31. package/dist/src/guest-processes.mjs +47 -0
  32. package/dist/src/guest-runner.d.ts +6 -0
  33. package/dist/src/guest-runner.js +567 -0
  34. package/dist/src/host.js +5 -4
  35. package/dist/src/index.d.ts +1 -0
  36. package/dist/src/index.js +1 -0
  37. package/dist/src/infra-cli.js +4 -0
  38. package/dist/src/infra.js +133 -23
  39. package/dist/src/init.js +60 -23
  40. package/dist/src/lifecycle-record.d.ts +3 -1
  41. package/dist/src/lifecycle-record.js +29 -6
  42. package/dist/src/livekit-backing.mjs +37 -0
  43. package/dist/src/local-branches.js +11 -3
  44. package/dist/src/mongodb-backing.d.ts +7 -0
  45. package/dist/src/mongodb-backing.js +58 -0
  46. package/dist/src/native-postgres-backing.d.ts +4 -0
  47. package/dist/src/native-postgres-backing.js +404 -0
  48. package/dist/src/origin.js +2 -1
  49. package/dist/src/owned-path.d.ts +4 -0
  50. package/dist/src/owned-path.js +37 -0
  51. package/dist/src/pack-facts.d.ts +1 -0
  52. package/dist/src/pglite-backing.d.ts +4 -13
  53. package/dist/src/pglite-backing.js +198 -106
  54. package/dist/src/pglite-host.mjs +47 -17
  55. package/dist/src/placeholder.d.ts +3 -2
  56. package/dist/src/placeholder.js +9 -4
  57. package/dist/src/process-groups.d.mts +2 -0
  58. package/dist/src/process-groups.d.ts +1 -4
  59. package/dist/src/process-groups.js +1 -49
  60. package/dist/src/process-groups.mjs +59 -0
  61. package/dist/src/process-receipt.d.mts +6 -0
  62. package/dist/src/process-receipt.d.ts +11 -0
  63. package/dist/src/process-receipt.js +3 -0
  64. package/dist/src/process-receipt.mjs +45 -0
  65. package/dist/src/process-signal.d.mts +9 -0
  66. package/dist/src/process-signal.mjs +92 -0
  67. package/dist/src/project-inspect.d.ts +1 -1
  68. package/dist/src/project-inspect.js +4 -2
  69. package/dist/src/proxy-daemon.js +2 -1
  70. package/dist/src/redirect-proxy.js +186 -19
  71. package/dist/src/redis-backing.js +99 -35
  72. package/dist/src/reflect.js +2 -1
  73. package/dist/src/root.js +7 -6
  74. package/dist/src/run-task-worker.js +2 -1
  75. package/dist/src/runtime.d.ts +24 -7
  76. package/dist/src/runtime.js +703 -286
  77. package/dist/src/schema.d.ts +12 -1
  78. package/dist/src/schema.js +20 -5
  79. package/dist/src/served-world.d.ts +1 -0
  80. package/dist/src/served-world.js +26 -14
  81. package/dist/src/service-recorder.js +8 -3
  82. package/dist/src/signals.d.mts +1 -0
  83. package/dist/src/signals.d.ts +1 -1
  84. package/dist/src/signals.js +1 -11
  85. package/dist/src/signals.mjs +10 -0
  86. package/dist/src/tail.d.ts +2 -0
  87. package/dist/src/tail.js +9 -2
  88. package/dist/src/tcp-port.js +27 -13
  89. package/dist/src/temporal-backing.mjs +28 -0
  90. package/dist/src/up-cli.js +8 -3
  91. package/dist/src/up-task-worker.js +2 -1
  92. package/dist/src/websocket-relay.d.ts +1 -1
  93. package/dist/src/websocket-relay.js +23 -10
  94. package/dist/src/world-view.js +2 -2
  95. package/known-external-services.json +3 -10
  96. package/package.json +12 -7
  97. package/src/actions-runner-cli.ts +3 -3
  98. package/src/actions-runner.ts +2 -1
  99. package/src/app-url.ts +2 -1
  100. package/src/backings.ts +65 -7
  101. package/src/branch.ts +73 -34
  102. package/src/browser-routes.cjs +59 -0
  103. package/src/ca-trust.ts +4 -3
  104. package/src/changeset.ts +4 -3
  105. package/src/cli-commands.ts +21 -9
  106. package/src/cli.ts +2 -1
  107. package/src/command-lifetime.d.mts +12 -0
  108. package/src/command-lifetime.mjs +90 -0
  109. package/src/command-lifetime.ts +1 -79
  110. package/src/configs.ts +1 -1
  111. package/src/console-apart.ts +2 -1
  112. package/src/covers.ts +97 -19
  113. package/src/error-message.d.mts +1 -0
  114. package/src/error-message.mjs +10 -0
  115. package/src/error-message.ts +2 -0
  116. package/src/guest-command.d.mts +4 -0
  117. package/src/guest-command.mjs +26 -0
  118. package/src/guest-control.d.mts +1 -0
  119. package/src/guest-control.mjs +17 -0
  120. package/src/guest-entry.mjs +146 -0
  121. package/src/guest-processes.d.mts +9 -0
  122. package/src/guest-processes.mjs +47 -0
  123. package/src/guest-runner.ts +414 -0
  124. package/src/host.ts +5 -4
  125. package/src/index.ts +1 -0
  126. package/src/infra-cli.ts +4 -0
  127. package/src/infra.ts +103 -22
  128. package/src/init.ts +55 -22
  129. package/src/lifecycle-record.ts +21 -6
  130. package/src/livekit-backing.mjs +37 -0
  131. package/src/local-branches.ts +6 -3
  132. package/src/mongodb-backing.ts +55 -0
  133. package/src/native-postgres-backing.ts +329 -0
  134. package/src/origin.ts +2 -1
  135. package/src/owned-path.ts +32 -0
  136. package/src/pack-facts.ts +1 -0
  137. package/src/pglite-backing.ts +155 -79
  138. package/src/pglite-host.mjs +47 -17
  139. package/src/placeholder.ts +10 -5
  140. package/src/process-groups.d.mts +2 -0
  141. package/src/process-groups.mjs +59 -0
  142. package/src/process-groups.ts +1 -33
  143. package/src/process-receipt.d.mts +6 -0
  144. package/src/process-receipt.mjs +45 -0
  145. package/src/process-receipt.ts +5 -0
  146. package/src/process-signal.d.mts +9 -0
  147. package/src/process-signal.mjs +92 -0
  148. package/src/project-inspect.ts +4 -2
  149. package/src/proxy-daemon.ts +2 -1
  150. package/src/redirect-proxy.ts +153 -18
  151. package/src/redis-backing.ts +74 -25
  152. package/src/reflect.ts +2 -1
  153. package/src/root.ts +7 -6
  154. package/src/run-task-worker.ts +2 -1
  155. package/src/runtime.ts +596 -265
  156. package/src/schema.ts +30 -6
  157. package/src/served-world.ts +21 -15
  158. package/src/service-recorder.ts +4 -2
  159. package/src/signals.d.mts +1 -0
  160. package/src/signals.mjs +10 -0
  161. package/src/signals.ts +1 -10
  162. package/src/tail.ts +10 -3
  163. package/src/tcp-port.ts +24 -13
  164. package/src/temporal-backing.mjs +28 -0
  165. package/src/up-cli.ts +6 -3
  166. package/src/up-task-worker.ts +2 -1
  167. package/src/websocket-relay.ts +18 -11
  168. package/src/world-view.ts +2 -2
@@ -57,10 +57,10 @@ function printHelp() {
57
57
  # emission. Exit 0 = ready to \`up\`; exit 1 = the honest worklist.
58
58
  # Deterministic: same repo + same pack catalog => identical bytes.
59
59
  # init DIAGNOSES; it never boots — it prints the exact next commands
60
- volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--root <repo>]
60
+ volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--json] [--root <repo>]
61
61
  volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
62
62
  volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
63
- volter-world down <name> [--grace-ms <ms>] [--purge] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
63
+ volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
64
64
  # --purge: after teardown, delete the instance dir (and per-service twin data) too — see docs/concepts/data-and-keys.md
65
65
  volter-world status <name> [--json] [--root <repo>]
66
66
  volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json] # resolve attachments whose runner, command and process group are gone
@@ -94,14 +94,15 @@ function printHelp() {
94
94
  # fails loudly with the registration recipe. A world that boots
95
95
  # the app itself declares an "app" service (type "process") —
96
96
  # its URL answers without any registration
97
- volter-world log <name> [service...] [--no-follow] [--json] [--requests] [--root <repo>] # the log of changes (alias: tail)
97
+ volter-world log <name> [service...] [--no-follow] [--json] [--requests] [--logs] [--root <repo>] # the log of changes (alias: tail)
98
98
  # merged, occurredAt-ordered feed of the twins' action ledgers — one
99
99
  # \`HH:MM:SS.mmm <service> <operation> <subjectId>\` line per action;
100
100
  # follows until Ctrl-C (--no-follow: dump and exit; --json: raw JSONL;
101
101
  # --requests: also merge the request journals — reads/404s,
102
102
  # plus WHICH credential arrived in WHICH header/query param, named +
103
103
  # fingerprinted, never the value — kept by every twin and link
104
- # unless VOLTER_TWIN_REQUEST_JOURNAL=0)
104
+ # unless VOLTER_TWIN_REQUEST_JOURNAL=0; --logs: also read the services'
105
+ # process output verbatim, labeled stdout — not shape-only journals)
105
106
  volter-world seed <world> [--entry <file>] [--cwd <dir>] [--json] [--root <repo>]
106
107
  # pull from the placeholder remote: the seed entry runs with every twin's
107
108
  # window open, so the defaults land as observed state (diff empty, nothing pending)
@@ -282,9 +283,18 @@ export async function main() {
282
283
  if (graceArg && !(Number(graceArg) >= 0))
283
284
  throw new Error(`volter-world down: invalid --grace-ms "${graceArg}" (want a non-negative number)`);
284
285
  const purge = rest.includes('--purge');
286
+ if (rest.some(arg => arg === '--if-created-at' || arg.startsWith('--if-created-at=')))
287
+ throw new Error('volter-world down: use --if-instance with the token returned by your own boot');
288
+ // absent flag: no ownership expectation (optionValue's '' fallback would read as an empty, refused token)
289
+ const ifInstance = rest.some(arg => arg === '--if-instance' || arg.startsWith('--if-instance='));
290
+ const expectedInstance = ifInstance ? optionValue(rest, '--if-instance') : undefined;
291
+ if (ifInstance
292
+ && (!expectedInstance || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(expectedInstance)))
293
+ throw new Error('volter-world down: invalid --if-instance token');
285
294
  const result = await downWorld(subject, root, {
286
295
  ...(graceArg ? { graceMs: Number(graceArg) } : {}),
287
296
  ...(purge ? { purge: true } : {}),
297
+ ...(expectedInstance === undefined ? {} : { expectedInstance }),
288
298
  });
289
299
  process.stdout.write(result.externalErrors.length ? `World ${result.name}: teardown incomplete; lifecycle evidence retained\n`
290
300
  : `Stopped world ${result.name}${result.stopped.length ? `: ${result.stopped.join(', ')}` : ''}\n`);
@@ -297,6 +307,8 @@ export async function main() {
297
307
  process.stderr.write(`External teardown failed: ${message}\n`);
298
308
  }
299
309
  if (result.purged) {
310
+ for (const message of result.externalAbandoned ?? [])
311
+ process.stdout.write(`Surrendered unavailable backend record (external stop unverified): ${message}\n`);
300
312
  process.stdout.write(`Purged instance data: ${result.purged}\n`);
301
313
  }
302
314
  else if (purge && result.externalErrors.length) {
@@ -657,6 +669,7 @@ export async function main() {
657
669
  follow: !rest.includes('--no-follow'),
658
670
  json: rest.includes('--json'),
659
671
  requests: rest.includes('--requests'),
672
+ logs: rest.includes('--logs'),
660
673
  signal: controller.signal,
661
674
  });
662
675
  return;
@@ -664,10 +677,13 @@ export async function main() {
664
677
  if (cmd === 'seed' || cmd === 'reset') {
665
678
  if (!subject)
666
679
  throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--entry <file>] [--cwd <dir>])`);
680
+ if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file=')))
681
+ throw new Error('--env-file is not an input for this command. Reset accepts --env-out=<new-output-path>.');
682
+ const envFile = optionValue(rest, '--env-out') || undefined;
667
683
  const entry = optionValue(rest, '--entry') || undefined;
668
684
  const cwd = optionValue(rest, '--cwd') || undefined;
669
685
  const json = rest.includes('--json');
670
- const outcome = cmd === 'seed' ? await seedWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) }) : await resetWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) });
686
+ const outcome = cmd === 'seed' ? await seedWorld(subject, { root, ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) }) : await resetWorld(subject, { root, ...(envFile ? { envFile } : {}), ...(entry ? { entry } : {}), ...(cwd ? { cwd } : {}) });
671
687
  if (json)
672
688
  process.stdout.write(`${JSON.stringify({ world: outcome.world, entry: outcome.entry, exitCode: outcome.exitCode, observed: outcome.observed }, null, 2)}\n`);
673
689
  else
@@ -684,7 +700,7 @@ export async function main() {
684
700
  const envFile = optionValue(rest, '--env-out') || undefined;
685
701
  const { instance, forked } = await branchWorld(subject, name, { root, forceEnvOutput: rest.includes('--force'), ...(envFile ? { envFile } : {}) });
686
702
  if (rest.includes('--json'))
687
- process.stdout.write(`${JSON.stringify({ name: instance.name, base: subject, forked }, null, 2)}\n`);
703
+ process.stdout.write(`${JSON.stringify({ name: instance.name, instance: instance.instance, createdAt: instance.createdAt, base: subject, forked }, null, 2)}\n`);
688
704
  else {
689
705
  process.stdout.write(`Branched world ${name} from ${subject}\n${Object.entries(forked).map(([s, states]) => ` ${s}: mirror forked (${states.join(', ')})`).join('\n')}\n`);
690
706
  printReady(instance);
@@ -696,7 +712,7 @@ export async function main() {
696
712
  throw new Error('volter-world checkout: missing world name');
697
713
  const instance = await checkoutWorld(subject, { root });
698
714
  if (rest.includes('--json'))
699
- process.stdout.write(`${JSON.stringify({ name: instance.name }, null, 2)}\n`);
715
+ process.stdout.write(`${JSON.stringify({ name: instance.name, instance: instance.instance, createdAt: instance.createdAt }, null, 2)}\n`);
700
716
  else
701
717
  printReady(instance);
702
718
  return;
@@ -1109,7 +1125,7 @@ export async function main() {
1109
1125
  throw new Error('volter-world run: missing required --env-out=<path>');
1110
1126
  const name = optionValue(worldArgs, '--name') || undefined;
1111
1127
  const mode = optionValue(worldArgs, '--mode', 'local');
1112
- 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') });
1128
+ const result = await runWorld(subject, rest.slice(split + 1), { root, cwd: process.cwd(), name, mode, envFile, forceEnvOutput: worldArgs.includes('--force'), owner: ownerOption(worldArgs), keep: worldArgs.includes('--keep'), verbose: worldArgs.includes('--verbose') });
1113
1129
  if (result.exitCode !== 0) {
1114
1130
  const detail = result.outcome.error ? `could not start (${result.outcome.error})`
1115
1131
  : result.outcome.signal ? `was terminated by ${result.outcome.signal}`
@@ -1164,7 +1180,7 @@ export async function main() {
1164
1180
  if (!subject)
1165
1181
  throw new Error('volter-world unshare: missing world name');
1166
1182
  const service = optionValue(rest, '--service') || undefined;
1167
- const result = unshareWorld(subject, { root, service });
1183
+ const result = await unshareWorld(subject, { root, service });
1168
1184
  process.stdout.write(`Unshared world ${subject}${service ? `/${service}` : ''}\n`);
1169
1185
  if (result.escalated.length)
1170
1186
  process.stdout.write(`Escalated to SIGKILL (tunnel ignored SIGTERM past the grace): ${result.escalated.join(', ')}\n`);
package/dist/src/cli.js CHANGED
@@ -2,11 +2,12 @@
2
2
  // volter-world: each command loads only what it needs. `up` (./up-cli.ts) starts the World's owner process
3
3
  // before any other command's modules load; every other command is ./cli-commands.ts.
4
4
  import { optionValue } from "./cli-args.js";
5
+ import { errorMessage } from "./error-message.js";
5
6
  const [cmd, subject, ...rest] = process.argv.slice(2);
6
7
  const run = cmd === 'up' && subject !== '--help' && subject !== '-h'
7
8
  ? import("./up-cli.js").then(({ upCommand }) => upCommand(subject, rest, optionValue(rest, '--root', process.cwd())))
8
9
  : import("./cli-commands.js").then(({ main }) => main());
9
10
  run.catch((error) => {
10
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
11
+ process.stderr.write(`${errorMessage(error)}\n`);
11
12
  process.exit(1);
12
13
  });
@@ -0,0 +1,12 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import type { SpawnIdentity } from './process-receipt.js';
3
+ export const COMMAND_GRACE_MS: number;
4
+ export const COMMAND_CONFIRM_MS: number;
5
+ export const commandProcessGroup: boolean;
6
+ export class CommandRetirementError extends Error {}
7
+ export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean): {
8
+ identity: Promise<SpawnIdentity | undefined>;
9
+ cancel(signal: NodeJS.Signals): void;
10
+ readonly forwarded: NodeJS.Signals | undefined;
11
+ finish(): Promise<{ code: number | null; signal: NodeJS.Signals | null }>;
12
+ };
@@ -1,15 +1 @@
1
- import type { ChildProcess } from 'node:child_process';
2
- export declare const COMMAND_GRACE_MS = 5000;
3
- export declare const commandProcessGroup: boolean;
4
- export declare class CommandRetirementError extends Error {
5
- }
6
- /** The child owns a private POSIX group. A bounded retirement requires both closed output and
7
- * a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
8
- export declare function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean): {
9
- cancel: (value: NodeJS.Signals) => void;
10
- readonly forwarded: NodeJS.Signals | undefined;
11
- finish(): Promise<{
12
- code: number | null;
13
- signal: NodeJS.Signals | null;
14
- }>;
15
- };
1
+ export { COMMAND_GRACE_MS, COMMAND_CONFIRM_MS, commandProcessGroup, CommandRetirementError, commandLifetime } from './command-lifetime.mjs';
@@ -1,98 +1 @@
1
- import { offSignal } from "./signals.js";
2
- import { survivingOwnedGroups } from "./process-groups.js";
3
- export const COMMAND_GRACE_MS = 5_000;
4
- export const commandProcessGroup = process.platform !== 'win32';
5
- export class CommandRetirementError extends Error {
6
- }
7
- /** The child owns a private POSIX group. A bounded retirement requires both closed output and
8
- * a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
9
- export function commandLifetime(child, abort, preserveOnSuccess = false) {
10
- let forwarded;
11
- let code = null;
12
- let exitSignal = null;
13
- let exited = false;
14
- let closed = false;
15
- let deadline = 0;
16
- let escalated = false;
17
- let timer;
18
- let resolveFinished;
19
- let rejectFinished;
20
- const finished = new Promise((resolve, reject) => { resolveFinished = resolve; rejectFinished = reject; });
21
- finished.catch(() => { });
22
- const signal = (value) => {
23
- if (!child.pid)
24
- return;
25
- try {
26
- if (commandProcessGroup)
27
- process.kill(-child.pid, value);
28
- else
29
- child.kill(value);
30
- }
31
- catch { /* retirement is proved below, never inferred from signal delivery */ }
32
- };
33
- const alive = () => {
34
- if (!child.pid)
35
- return false;
36
- return survivingOwnedGroups([child.pid]).length > 0;
37
- };
38
- const detach = () => {
39
- if (timer)
40
- clearInterval(timer);
41
- offSignal('SIGINT', onInt);
42
- offSignal('SIGTERM', onTerm);
43
- abort?.removeEventListener('abort', onAbort);
44
- };
45
- const check = () => {
46
- if (exited && closed && ((preserveOnSuccess && code === 0 && !forwarded) || !alive())) {
47
- detach();
48
- resolveFinished();
49
- return;
50
- }
51
- if (deadline && Date.now() >= deadline) {
52
- if (!escalated) {
53
- escalated = true;
54
- signal('SIGKILL');
55
- deadline = Date.now() + 1_000;
56
- }
57
- else {
58
- detach();
59
- child.stdout?.destroy();
60
- child.stderr?.destroy();
61
- rejectFinished(new CommandRetirementError('Command retirement could not be confirmed; ownership and cleanup evidence retained'));
62
- }
63
- }
64
- };
65
- const watch = () => {
66
- if (!deadline)
67
- deadline = Date.now() + COMMAND_GRACE_MS;
68
- timer ??= setInterval(check, 20);
69
- check();
70
- };
71
- const cancel = (value) => { forwarded ??= value; signal(value); watch(); };
72
- const onInt = () => cancel('SIGINT');
73
- const onTerm = () => cancel('SIGTERM');
74
- const onAbort = () => cancel(abort?.reason === 'SIGINT' ? 'SIGINT' : 'SIGTERM');
75
- process.on('SIGINT', onInt);
76
- process.on('SIGTERM', onTerm);
77
- abort?.addEventListener('abort', onAbort, { once: true });
78
- if (abort?.aborted)
79
- onAbort();
80
- child.once('exit', (value, signalValue) => {
81
- code = value;
82
- exitSignal = signalValue;
83
- exited = true;
84
- if (!(preserveOnSuccess && code === 0 && !forwarded))
85
- signal('SIGTERM');
86
- watch();
87
- });
88
- child.once('error', () => { if (!child.pid) {
89
- exited = true;
90
- watch();
91
- } });
92
- child.once('close', () => { closed = true; check(); });
93
- return {
94
- cancel,
95
- get forwarded() { return forwarded; },
96
- async finish() { await finished; return { code, signal: exitSignal }; },
97
- };
98
- }
1
+ export { COMMAND_GRACE_MS, COMMAND_CONFIRM_MS, commandProcessGroup, CommandRetirementError, commandLifetime } from './command-lifetime.mjs';
@@ -0,0 +1,90 @@
1
+ import { offSignal } from './signals.mjs';
2
+ import { survivingOwnedGroups } from './process-groups.mjs';
3
+ import { childIdentity, signalOwnedProcess, signalChild } from './process-signal.mjs';
4
+
5
+ export const COMMAND_GRACE_MS = 5_000;
6
+ export const COMMAND_CONFIRM_MS = 1_000;
7
+ export const commandProcessGroup = process.platform !== 'win32';
8
+ export class CommandRetirementError extends Error {}
9
+
10
+ /** The child owns a private POSIX group. A bounded retirement requires both closed output and
11
+ * a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
12
+ export function commandLifetime(child, abort, preserveOnSuccess = false) {
13
+ const identity = child.pid && commandProcessGroup ? childIdentity(child) : Promise.resolve(undefined);
14
+ let signalFailure;
15
+ let pendingSignals = 0;
16
+ let forwarded;
17
+ let code = null;
18
+ let exitSignal = null;
19
+ let exited = false;
20
+ let closed = false;
21
+ let deadline = 0;
22
+ let escalated = false;
23
+ let timer;
24
+ let resolveFinished;
25
+ let rejectFinished;
26
+ const finished = new Promise((resolve, reject) => { resolveFinished = resolve; rejectFinished = reject; });
27
+ finished.catch(() => {});
28
+ const signal = (value) => {
29
+ if (!child.pid) return;
30
+ try {
31
+ if (signalChild(child, value, commandProcessGroup)) return;
32
+ if (!commandProcessGroup || !alive()) return;
33
+ // The leader has exited: this is now retained numeric group ownership.
34
+ pendingSignals++;
35
+ void identity.then(birth => signalOwnedProcess(birth, value, true))
36
+ .catch(error => { signalFailure = error; })
37
+ .finally(() => { pendingSignals--; check(); });
38
+ } catch (error) { signalFailure = error; }
39
+ };
40
+ const alive = () => {
41
+ if (!child.pid) return false;
42
+ return survivingOwnedGroups([child.pid]).length > 0;
43
+ };
44
+ const detach = () => {
45
+ if (timer) clearInterval(timer);
46
+ offSignal('SIGINT', onInt);
47
+ offSignal('SIGTERM', onTerm);
48
+ abort?.removeEventListener('abort', onAbort);
49
+ };
50
+ const check = () => {
51
+ if (pendingSignals) return;
52
+ if (exited && closed && ((preserveOnSuccess && code === 0 && !forwarded) || !alive())) {
53
+ detach(); resolveFinished(); return;
54
+ }
55
+ if (deadline && Date.now() >= deadline) {
56
+ if (!escalated) { escalated = true; signal('SIGKILL'); deadline = Date.now() + COMMAND_CONFIRM_MS; }
57
+ else {
58
+ detach();
59
+ child.stdout?.destroy(); child.stderr?.destroy();
60
+ rejectFinished(new CommandRetirementError('Command retirement could not be confirmed; ownership and cleanup evidence retained', { cause: signalFailure }));
61
+ }
62
+ }
63
+ };
64
+ const watch = () => {
65
+ if (!deadline) deadline = Date.now() + COMMAND_GRACE_MS;
66
+ timer ??= setInterval(check, 20);
67
+ check();
68
+ };
69
+ const cancel = (value) => { forwarded ??= value; signal(value); watch(); };
70
+ const onInt = () => cancel('SIGINT');
71
+ const onTerm = () => cancel('SIGTERM');
72
+ const onAbort = () => cancel(abort?.reason === 'SIGINT' ? 'SIGINT' : 'SIGTERM');
73
+ process.on('SIGINT', onInt);
74
+ process.on('SIGTERM', onTerm);
75
+ abort?.addEventListener('abort', onAbort, { once: true });
76
+ if (abort?.aborted) onAbort();
77
+ child.once('exit', (value, signalValue) => {
78
+ code = value; exitSignal = signalValue; exited = true;
79
+ if (!(preserveOnSuccess && code === 0 && !forwarded)) signal('SIGTERM');
80
+ watch();
81
+ });
82
+ child.once('error', () => { if (!child.pid) { exited = true; watch(); } });
83
+ child.once('close', () => { closed = true; check(); });
84
+ return {
85
+ identity,
86
+ cancel,
87
+ get forwarded() { return forwarded; },
88
+ async finish() { await finished; await identity; return { code, signal: exitSignal }; },
89
+ };
90
+ }
@@ -29,7 +29,7 @@ export function writeWorldConfig(path, config) {
29
29
  getActiveWorldStore().write(path, `${JSON.stringify(document, null, 2)}\n`);
30
30
  }
31
31
  const LEGACY_TOP_LEVEL = new Set(['id', 'description', 'bare', 'remotes', 'resources', 'isolation', 'env', 'stripEnv', 'services', 'share', 'actors', 'fixtures', 'catalog', 'network']);
32
- const LEGACY_SERVICE = new Set(['id', 'type', 'package', 'command', 'args', 'cwd', 'port', 'portReason', 'env', 'controlPlane', 'injectEnv', 'injectEnvTemplates', 'root', 'rootArg', 'portArg', 'cliRedirect', 'ready', 'preload', 'external', 'colocate', 'version']);
32
+ const LEGACY_SERVICE = new Set(['id', 'type', 'package', 'command', 'args', 'cwd', 'port', 'portReason', 'env', 'controlPlane', 'branchState', 'injectEnv', 'injectEnvTemplates', 'root', 'rootArg', 'portArg', 'cliRedirect', 'ready', 'preload', 'external', 'colocate', 'version']);
33
33
  const LEGACY_NESTED = {
34
34
  bare: new Set(['name']), resources: new Set(['memoryMiB', 'writableStorageMiB']), catalog: new Set(['sha', 'protocol']),
35
35
  root: new Set(['url', 'deploy', 'refresh', 'scope']), refresh: new Set(['every', 'webhook']),
@@ -4,6 +4,7 @@
4
4
  // console on a listener of its own: the console's shell and bundle, and the host's `/-/` doors
5
5
  // forwarded to the host — never a twin's path, so no twin-served script ever runs on this origin.
6
6
  // The console opens a World's pages (a vendor's screens) on the Worlds' origin, which it is told.
7
+ import { errorMessage } from "./error-message.js";
7
8
  import { serveHttp } from '@volter/world-core/runtime';
8
9
  export const CONSOLE_BASE = '/-/console';
9
10
  /** Serve `mount` on its own listener; doors under `/-/` go to `upstream` (the Worlds' origin). */
@@ -62,8 +63,8 @@ export async function serveConsoleFor(target, opts = {}) {
62
63
  mount = (await import('@volter/world-console')).createConsole();
63
64
  }
64
65
  catch (error) {
65
- if (error instanceof Error && /Cannot find (module|package)/.test(error.message))
66
- throw new Error('the console is not installed: add @volter/world-console beside @volter/world');
66
+ if (error instanceof Error && /Cannot find (module|package)/.test(errorMessage(error)))
67
+ throw new Error('the console is not installed: add @volter/world-console beside @volter/world', { cause: error });
67
68
  throw error;
68
69
  }
69
70
  const hostname = opts.hostname ?? '127.0.0.1';
@@ -1,4 +1,4 @@
1
- import { type WorldSelection, type WorldUsage } from './schema.js';
1
+ import { type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.js';
2
2
  export type CoverageStatus = 'covered' | 'missing' | 'uninterceptable' | 'unknown-sdk' | 'acknowledged' | 'excluded';
3
3
  export type CoverageRow = {
4
4
  /** vendor key for mapped vendors; the raw dep/env name for unknown-sdk rows */
@@ -118,6 +118,7 @@ export type WorldTwinId = {
118
118
  interceptable: boolean;
119
119
  };
120
120
  type WorldTwinInventory = {
121
+ services: Array<WorldServiceConfig | WorldServiceInstance>;
121
122
  source: 'instance' | 'config';
122
123
  path: string;
123
124
  /** normalized identifier → id record (e.g. 'stripe' → { label: 'stripe (service)', … }) */
@@ -177,7 +178,7 @@ export type RepoVendorSignals = {
177
178
  * directly to fetch in production source. URL attribution reads the injector's VENDOR_HOSTS table
178
179
  * rather than re-encoding host rules here. Insertion order follows the sorted dependency, env-name,
179
180
  * and source lists, so callers that preserve it are deterministic. */
180
- export declare function detectRepoVendors(repoPath: string): RepoVendorSignals;
181
+ export declare function detectRepoVendors(repoPath: string, services?: Array<WorldServiceConfig | WorldServiceInstance>): RepoVendorSignals;
181
182
  export declare function coverWorld(world: string, repoPath: string, options?: CoverageOptions): CoverageReport;
182
183
  export declare function formatCoverageReport(report: CoverageReport): string;
183
184
  export {};
@@ -21,11 +21,13 @@
21
21
  // coverage. An external-service-shaped dep/env var with no vendor mapping →
22
22
  // unknown-sdk → exit 1 by default (`--allow-unknown` downgrades to a warning:
23
23
  // unknowns still print, but stop failing the proof).
24
+ import { errorMessage } from "./error-message.js";
24
25
  import { existsSync, readFileSync } from 'node:fs';
25
- import { dirname, join, relative, resolve } from 'node:path';
26
+ import { basename, dirname, join, relative, resolve } from 'node:path';
26
27
  import { fileURLToPath } from 'node:url';
27
28
  import { vendorOfPackModule } from '@volter/world-core/runtime';
28
29
  import { loadWorldConfig } from "./configs.js";
30
+ import { VENDOR_SERVER_BACKINGS } from "./backings.js";
29
31
  import { injectableVendorKeys, loadInject, twinUrlVendorFor } from "./inject-map.js";
30
32
  import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
31
33
  import { overlayCoversMaps, packFacts } from "./pack-facts.js";
@@ -247,16 +249,54 @@ export function registryAcknowledgedReason(name) {
247
249
  return null;
248
250
  return entries[0].reason;
249
251
  }
250
- function declaredTwin(services) {
252
+ /** Identity comes from the registered wrapper, not an arbitrary process with the vendor's name. */
253
+ function serverBacking(service) {
254
+ const backing = VENDOR_SERVER_BACKINGS[service.id];
255
+ if (!backing || service.type !== backing.service.type)
256
+ return undefined;
257
+ // an instance records the whole argv in `command`; a config splits it into `command` and `args`
258
+ const command = Array.isArray(service.command) ? service.command : [service.command, ...(('args' in service ? service.args : undefined) ?? [])];
259
+ if (!command[0] || basename(command[0]) !== backing.service.command || command[1] !== backing.service.args?.[0])
260
+ return undefined;
261
+ return backing;
262
+ }
263
+ function addBackingIds(ids, services, effectiveEnv) {
264
+ for (const service of services) {
265
+ const backing = serverBacking(service);
266
+ if (backing) {
267
+ const template = backing.facts.endpointEnv.templates[backing.facts.endpointEnv.name];
268
+ const instance = 'pid' in service;
269
+ const values = instance ? service.env : service.injectEnvTemplates ?? {};
270
+ const endpoint = instance && service.url && service.port ? template?.replaceAll('${host}', new URL(service.url).hostname)
271
+ .replaceAll('${port}', String(service.port)).replaceAll('${url}', service.url) : undefined;
272
+ const routed = template !== undefined && (instance
273
+ ? endpoint !== undefined && Object.entries(values).some(([name, value]) => value === endpoint && effectiveEnv?.[name] === value)
274
+ : service.port !== undefined && Object.values(values).includes(template));
275
+ addId(ids, normalizeId(service.id), `${service.id} (server backing)`, routed);
276
+ }
277
+ // A protocol already proved owned by this service can satisfy its declared native driver.
278
+ // HTTP-only siblings of the same vendor retain their independent interface obligations.
279
+ for (const binding of serviceConnections([service], effectiveEnv).values()) {
280
+ for (const [vendor, facts] of Object.entries(packFacts())) {
281
+ if (facts.transport === 'raw-tcp' && facts.managedService?.scheme === binding.protocol) {
282
+ addId(ids, normalizeId(vendor), `${binding.label}; managed ${facts.managedService.kind} backing`, true);
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ function declaredTwin(services, effectiveEnv) {
251
289
  const declared = new Map();
252
290
  for (const service of services) {
253
- if (service.type !== undefined && service.type !== 'twin')
291
+ if (service.type !== undefined && service.type !== 'twin' && !serverBacking(service))
254
292
  continue;
255
293
  const config = service;
256
294
  const identity = config.package?.match(/^@volter\/twin-(.+)$/)?.[1] ?? servicePackNames(config)[0] ?? service.id;
257
295
  const vendor = Object.keys(packFacts()).find((candidate) => worldIdsFor(candidate).includes(normalizeId(identity))) ?? identity;
258
296
  const ids = new Map();
259
- addServiceIds(ids, service);
297
+ if (!serverBacking(service))
298
+ addServiceIds(ids, service);
299
+ addBackingIds(ids, [service], effectiveEnv);
260
300
  const match = [...worldIdsFor(vendor), vendor].map(normalizeId).map((id) => ids.get(id)).find(Boolean);
261
301
  declared.set(vendor, match ?? { label: `${service.id} (declared twin)`, interceptable: false });
262
302
  }
@@ -278,7 +318,16 @@ function servicePackNames(service) {
278
318
  ...(service.args ?? []),
279
319
  ...(service.colocate?.module ? [service.colocate.module] : []),
280
320
  ];
281
- return commandParts.flatMap((part) => {
321
+ return commandParts.flatMap((argument, index) => {
322
+ // A shared host command is recorded on each service. Only that service's
323
+ // module identifies its pack; its neighbours' modules must not cover it.
324
+ let part = argument;
325
+ if (commandParts[index - 1] === '--spec') {
326
+ const [id, module] = argument.split('|');
327
+ if (id !== service.id || !module)
328
+ return [];
329
+ part = module;
330
+ }
282
331
  const fromPath = part.match(/(?:^|[/\\])twin-packs-p[23][/\\]([a-z0-9-]+)[/\\]/);
283
332
  const fromBin = part.match(/^world-([a-z0-9-]+)$/);
284
333
  const fromPkg = part.match(/(?:^|[/\\])@volter\/twin-([a-z0-9-]+)(?:[/\\]|$)/);
@@ -400,23 +449,27 @@ export function worldTwinInventory(world, root) {
400
449
  const status = statusWorld(world, root, { readOnly: true });
401
450
  const ids = new Map();
402
451
  for (const service of Object.values(status.services))
403
- addServiceIds(ids, service);
452
+ if (!serverBacking(service))
453
+ addServiceIds(ids, service);
454
+ addBackingIds(ids, Object.values(status.services), status.env);
404
455
  for (const key of Object.keys(status.env)) {
405
456
  const stem = key.match(/^(.+)_TWIN_URL$/);
406
457
  // a bare env `*_TWIN_URL` var is only a REACHABLE twin when the injector reads it
407
458
  if (stem)
408
459
  addId(ids, normalizeId(stem[1]), `env ${key}`, twinUrlVendorFor(key) !== null);
409
460
  }
410
- return { source: 'instance', path: `${status.dirs.instance}/instance.json`, ids, connections: serviceConnections(Object.values(status.services), status.env), selection: status.selection, declared: declaredTwin(Object.values(status.services)) };
461
+ return { services: Object.values(status.services), source: 'instance', path: `${status.dirs.instance}/instance.json`, ids, connections: serviceConnections(Object.values(status.services), status.env), selection: status.selection, declared: declaredTwin(Object.values(status.services), status.env) };
411
462
  }
412
463
  catch (error) {
413
- instanceError = error instanceof Error ? error.message : String(error);
464
+ instanceError = errorMessage(error);
414
465
  }
415
466
  try {
416
467
  const { path, config } = loadWorldConfig(world, root);
417
468
  const ids = new Map();
418
469
  for (const service of config.services)
419
- addServiceIds(ids, service);
470
+ if (!serverBacking(service))
471
+ addServiceIds(ids, service);
472
+ addBackingIds(ids, config.services);
420
473
  const connections = serviceConnections(config.services);
421
474
  // a name the World's env leaves to a twin's credential door (`$issue:<service>`) is that twin's, whatever scheme the
422
475
  // issued value has: the twin that issues it serves it
@@ -424,10 +477,10 @@ export function worldTwinInventory(world, root) {
424
477
  if (value.startsWith(ISSUE_ENV_PREFIX))
425
478
  connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
426
479
  }
427
- return { source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services) };
480
+ return { services: config.services, source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services) };
428
481
  }
429
482
  catch (error) {
430
- const configError = error instanceof Error ? error.message : String(error);
483
+ const configError = errorMessage(error);
431
484
  throw new Error(`volter-world covers: world "${world}" not found as an instance or a config.\n` +
432
485
  ` instance: ${instanceError}\n config: ${configError}`);
433
486
  }
@@ -464,7 +517,16 @@ function usesOf(signals) {
464
517
  * directly to fetch in production source. URL attribution reads the injector's VENDOR_HOSTS table
465
518
  * rather than re-encoding host rules here. Insertion order follows the sorted dependency, env-name,
466
519
  * and source lists, so callers that preserve it are deterministic. */
467
- export function detectRepoVendors(repoPath) {
520
+ export function detectRepoVendors(repoPath, services = []) {
521
+ // Application aliases are names the selected backing actually injects, never global vendor adoption.
522
+ const backingEnvVendors = new Map();
523
+ for (const service of services) {
524
+ if (!serverBacking(service))
525
+ continue;
526
+ const bindings = 'pid' in service ? service.env : service.injectEnvTemplates;
527
+ for (const name of Object.keys(bindings ?? {}))
528
+ backingEnvVendors.set(name, service.id);
529
+ }
468
530
  const repo = resolve(repoPath);
469
531
  // the platform's own packages — the twins an app installed, the command, the runtime — are
470
532
  // never a vendor signal: an app that installed @volter/twin-slack did not gain a Slack dependency.
@@ -512,7 +574,7 @@ export function detectRepoVendors(repoPath) {
512
574
  detected.set('smtp', [...(detected.get('smtp') ?? []), `env: ${name}`]);
513
575
  continue;
514
576
  }
515
- const vendor = envStemVendor(name, ENV_SUFFIX_BROAD);
577
+ const vendor = backingEnvVendors.get(name) ?? envStemVendor(name, ENV_SUFFIX_BROAD);
516
578
  if (vendor)
517
579
  detected.set(vendor, [...(detected.get(vendor) ?? []), `env: ${name}`]);
518
580
  }
@@ -535,6 +597,9 @@ export function detectRepoVendors(repoPath) {
535
597
  ...Object.values(SDK_TWINS).map((entry) => entry.vendor),
536
598
  ...Object.values(ENV_STEM_VENDORS),
537
599
  ...Object.keys(VENDOR_WORLD_IDS),
600
+ // a pack whose vendor is called only by its hosts (no SDK, no credential: Poly Haven's and ambientCG's public
601
+ // catalogs) is known by its descriptor's hosts, or a raw fetch matched to it would be dropped here
602
+ ...Object.entries(packFacts()).filter(([, facts]) => (facts.hosts ?? []).length).map(([vendor]) => vendor),
538
603
  ])
539
604
  knownVendorById.set(normalizeId(vendor), vendor);
540
605
  const canonicalVendor = (injectorKey) => {
@@ -632,7 +697,7 @@ export function detectRepoVendors(repoPath) {
632
697
  if (isSmtpEnvSignal(name))
633
698
  continue; // now a DETECTED vendor (above), no longer an unknown
634
699
  const stem = envVendorStem(name, ENV_SUFFIX_STRICT);
635
- if (stem === null || envStemVendor(name, ENV_SUFFIX_STRICT) || ENV_STEM_IGNORE.has(stem))
700
+ if (stem === null || backingEnvVendors.has(name) || envStemVendor(name, ENV_SUFFIX_STRICT) || ENV_STEM_IGNORE.has(stem))
636
701
  continue;
637
702
  // The registry's notExternal suppressions apply to ENV STEMS too — the census adjudicated
638
703
  // 40 app-internal stems ("names no vendor") that this loop kept flagging because only the
@@ -654,7 +719,7 @@ export function coverWorld(world, repoPath, options = {}) {
654
719
  }
655
720
  const repo = resolve(repoPath);
656
721
  const inventory = worldTwinInventory(world, root);
657
- const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo);
722
+ const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services);
658
723
  // Does ANY injector vendor key exist for this vendor? Decides whether an UNINTERCEPTABLE
659
724
  // finding is acknowledgeable: a key that exists means the twin COULD be intercepted and the
660
725
  // wiring is merely wrong (fix it, don't acknowledge it); no key at all means the pack is a
@@ -678,7 +743,14 @@ export function coverWorld(world, repoPath, options = {}) {
678
743
  // twin (no injector vendor, no app-read endpoint env) is a distinct, failing status.
679
744
  const candidates = worldIdsFor(vendor).filter((id) => inventory.ids.has(id));
680
745
  const matchId = candidates.find((id) => inventory.ids.get(id).interceptable) ?? candidates[0];
681
- const match = matchId === undefined ? null : inventory.ids.get(matchId);
746
+ const managed = packFacts()[vendor]?.managedService;
747
+ // Only a booted, service-owned endpoint establishes a managed pack's reachable wire.
748
+ const managedBinding = inventory.source === 'instance' && managed
749
+ ? [...inventory.connections.values()].find(binding => binding.protocol === managed.scheme)
750
+ : undefined;
751
+ const match = matchId === undefined
752
+ ? managedBinding ? { label: managedBinding.label, interceptable: true } : null
753
+ : inventory.ids.get(matchId);
682
754
  let status = match === null ? 'missing' : match.interceptable ? 'covered' : 'uninterceptable';
683
755
  let ackRefused;
684
756
  if (acknowledge[vendor] !== undefined) {
@@ -755,9 +827,14 @@ export function coverWorld(world, repoPath, options = {}) {
755
827
  uninterceptable.push(vendor);
756
828
  rows.push({ vendor, detectedVia: ['declared in world'], twinInWorld: declared.label, status });
757
829
  }
758
- for (const requirement of projectConnections(repo, new Set([...detected.values(), ...unknownSources.values()].flat().filter((source) => source.startsWith('env: ')).map((source) => source.slice(5))))) {
830
+ for (const requirement of projectConnections(repo, new Set([...detected.values(), ...unknownSources.values()].flat().filter((source) => source.startsWith('env: ')).map((source) => source.slice(5))), new Map([...inventory.connections].flatMap(([name, binding]) => binding.protocol && !['http', 'issued', 'incompatible'].includes(binding.protocol) ? [[name, binding.protocol]] : [])))) {
759
831
  const vendor = `${requirement.protocol ?? 'unknown-connection'}:${requirement.env ?? requirement.sources[0]}`;
760
- if (inventory.selection !== undefined && !worldSelectionIncludes(inventory.selection, vendor, 'application')) {
832
+ const connectionNames = [vendor, ...(requirement.protocol ? [requirement.protocol] : [])];
833
+ const selectorMatches = (selector) => (selector.usage === undefined || selector.usage === 'application') &&
834
+ (selector.vendor === undefined || connectionNames.includes(selector.vendor));
835
+ const connectionSelected = inventory.selection === undefined ||
836
+ (inventory.selection.include.some(selectorMatches) && !inventory.selection.exclude.some(selectorMatches));
837
+ if (!connectionSelected) {
761
838
  excluded.push(vendor);
762
839
  rows.push({
763
840
  vendor,
@@ -0,0 +1 @@
1
+ export function errorMessage(error: unknown): string;
@@ -0,0 +1 @@
1
+ export { errorMessage } from './error-message.mjs';
@@ -0,0 +1,2 @@
1
+ // The native Node hosts and typed runtime entrypoints use the same renderer.
2
+ export { errorMessage } from './error-message.mjs';