@volter/world-runtime 3.0.37 → 3.0.39
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.
- package/dist/known-external-services.json +3 -10
- package/dist/src/actions-runner-cli.js +3 -3
- package/dist/src/actions-runner.js +2 -1
- package/dist/src/app-url.js +2 -1
- package/dist/src/backings.d.ts +10 -1
- package/dist/src/backings.js +66 -7
- package/dist/src/branch.js +85 -52
- package/dist/src/browser-routes.cjs +59 -0
- package/dist/src/ca-trust.js +4 -3
- package/dist/src/changeset.js +4 -3
- package/dist/src/cli-commands.js +25 -9
- package/dist/src/cli.js +2 -1
- package/dist/src/command-lifetime.d.mts +12 -0
- package/dist/src/command-lifetime.d.ts +1 -15
- package/dist/src/command-lifetime.js +1 -98
- package/dist/src/command-lifetime.mjs +90 -0
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.js +3 -2
- package/dist/src/covers.d.ts +3 -2
- package/dist/src/covers.js +95 -18
- package/dist/src/error-message.d.mts +1 -0
- package/dist/src/error-message.d.ts +1 -0
- package/dist/src/error-message.js +2 -0
- package/dist/src/error-message.mjs +10 -0
- package/dist/src/guest-command.d.mts +4 -0
- package/dist/src/guest-command.mjs +26 -0
- package/dist/src/guest-control.d.mts +1 -0
- package/dist/src/guest-control.mjs +17 -0
- package/dist/src/guest-entry.mjs +146 -0
- package/dist/src/guest-processes.d.mts +9 -0
- package/dist/src/guest-processes.mjs +47 -0
- package/dist/src/guest-runner.d.ts +6 -0
- package/dist/src/guest-runner.js +567 -0
- package/dist/src/host.js +5 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/infra-cli.js +4 -0
- package/dist/src/infra.js +133 -23
- package/dist/src/init.js +60 -23
- package/dist/src/lifecycle-record.d.ts +3 -1
- package/dist/src/lifecycle-record.js +29 -6
- package/dist/src/livekit-backing.mjs +37 -0
- package/dist/src/local-branches.js +11 -3
- package/dist/src/mongodb-backing.d.ts +7 -0
- package/dist/src/mongodb-backing.js +58 -0
- package/dist/src/native-postgres-backing.d.ts +4 -0
- package/dist/src/native-postgres-backing.js +404 -0
- package/dist/src/origin.js +2 -1
- package/dist/src/owned-path.d.ts +4 -0
- package/dist/src/owned-path.js +37 -0
- package/dist/src/pack-facts.d.ts +1 -0
- package/dist/src/pglite-backing.d.ts +4 -13
- package/dist/src/pglite-backing.js +198 -106
- package/dist/src/pglite-host.mjs +47 -17
- package/dist/src/placeholder.d.ts +3 -2
- package/dist/src/placeholder.js +9 -4
- package/dist/src/process-groups.d.mts +2 -0
- package/dist/src/process-groups.d.ts +1 -4
- package/dist/src/process-groups.js +1 -49
- package/dist/src/process-groups.mjs +59 -0
- package/dist/src/process-receipt.d.mts +6 -0
- package/dist/src/process-receipt.d.ts +11 -0
- package/dist/src/process-receipt.js +3 -0
- package/dist/src/process-receipt.mjs +45 -0
- package/dist/src/process-signal.d.mts +9 -0
- package/dist/src/process-signal.mjs +92 -0
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +4 -2
- package/dist/src/proxy-daemon.js +2 -1
- package/dist/src/redirect-proxy.js +186 -19
- package/dist/src/redis-backing.js +99 -35
- package/dist/src/reflect.js +2 -1
- package/dist/src/root.js +7 -6
- package/dist/src/run-task-worker.js +2 -1
- package/dist/src/runtime.d.ts +24 -7
- package/dist/src/runtime.js +703 -286
- package/dist/src/schema.d.ts +12 -1
- package/dist/src/schema.js +20 -5
- package/dist/src/served-world.d.ts +1 -0
- package/dist/src/served-world.js +26 -14
- package/dist/src/service-recorder.js +8 -3
- package/dist/src/signals.d.mts +1 -0
- package/dist/src/signals.d.ts +1 -1
- package/dist/src/signals.js +1 -11
- package/dist/src/signals.mjs +10 -0
- package/dist/src/tail.d.ts +2 -0
- package/dist/src/tail.js +9 -2
- package/dist/src/tcp-port.js +27 -13
- package/dist/src/temporal-backing.mjs +28 -0
- package/dist/src/up-cli.js +8 -3
- package/dist/src/up-task-worker.js +2 -1
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/websocket-relay.js +23 -10
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +3 -10
- package/package.json +12 -7
- package/src/actions-runner-cli.ts +3 -3
- package/src/actions-runner.ts +2 -1
- package/src/app-url.ts +2 -1
- package/src/backings.ts +65 -7
- package/src/branch.ts +73 -34
- package/src/browser-routes.cjs +59 -0
- package/src/ca-trust.ts +4 -3
- package/src/changeset.ts +4 -3
- package/src/cli-commands.ts +21 -9
- package/src/cli.ts +2 -1
- package/src/command-lifetime.d.mts +12 -0
- package/src/command-lifetime.mjs +90 -0
- package/src/command-lifetime.ts +1 -79
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +2 -1
- package/src/covers.ts +97 -19
- package/src/error-message.d.mts +1 -0
- package/src/error-message.mjs +10 -0
- package/src/error-message.ts +2 -0
- package/src/guest-command.d.mts +4 -0
- package/src/guest-command.mjs +26 -0
- package/src/guest-control.d.mts +1 -0
- package/src/guest-control.mjs +17 -0
- package/src/guest-entry.mjs +146 -0
- package/src/guest-processes.d.mts +9 -0
- package/src/guest-processes.mjs +47 -0
- package/src/guest-runner.ts +414 -0
- package/src/host.ts +5 -4
- package/src/index.ts +1 -0
- package/src/infra-cli.ts +4 -0
- package/src/infra.ts +103 -22
- package/src/init.ts +55 -22
- package/src/lifecycle-record.ts +21 -6
- package/src/livekit-backing.mjs +37 -0
- package/src/local-branches.ts +6 -3
- package/src/mongodb-backing.ts +55 -0
- package/src/native-postgres-backing.ts +329 -0
- package/src/origin.ts +2 -1
- package/src/owned-path.ts +32 -0
- package/src/pack-facts.ts +1 -0
- package/src/pglite-backing.ts +155 -79
- package/src/pglite-host.mjs +47 -17
- package/src/placeholder.ts +10 -5
- package/src/process-groups.d.mts +2 -0
- package/src/process-groups.mjs +59 -0
- package/src/process-groups.ts +1 -33
- package/src/process-receipt.d.mts +6 -0
- package/src/process-receipt.mjs +45 -0
- package/src/process-receipt.ts +5 -0
- package/src/process-signal.d.mts +9 -0
- package/src/process-signal.mjs +92 -0
- package/src/project-inspect.ts +4 -2
- package/src/proxy-daemon.ts +2 -1
- package/src/redirect-proxy.ts +153 -18
- package/src/redis-backing.ts +74 -25
- package/src/reflect.ts +2 -1
- package/src/root.ts +7 -6
- package/src/run-task-worker.ts +2 -1
- package/src/runtime.ts +596 -265
- package/src/schema.ts +30 -6
- package/src/served-world.ts +21 -15
- package/src/service-recorder.ts +4 -2
- package/src/signals.d.mts +1 -0
- package/src/signals.mjs +10 -0
- package/src/signals.ts +1 -10
- package/src/tail.ts +10 -3
- package/src/tcp-port.ts +24 -13
- package/src/temporal-backing.mjs +28 -0
- package/src/up-cli.ts +6 -3
- package/src/up-task-worker.ts +2 -1
- package/src/websocket-relay.ts +18 -11
- package/src/world-view.ts +2 -2
package/dist/src/cli-commands.js
CHANGED
|
@@ -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(`${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/src/configs.js
CHANGED
|
@@ -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
|
|
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';
|
package/dist/src/covers.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/src/covers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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';
|