@volter/world-runtime 3.0.162 → 3.0.164
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/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +3 -2
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +445 -18
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +15 -10
- package/dist/src/service-exit.d.ts +3 -2
- package/dist/src/service-exit.js +7 -5
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +4 -4
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +4 -3
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +416 -17
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +14 -10
- package/src/service-exit.ts +5 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
package/src/cli-commands.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
+
import type { statusWorld } from './runtime.ts';
|
|
1
2
|
// Every volter-world command but `up` (./up-cli.ts), loaded by ./cli.ts only when one of them is asked for.
|
|
2
|
-
|
|
3
|
+
|
|
3
4
|
import { optionValue, ownerOption } from './cli-args.ts';
|
|
4
|
-
|
|
5
|
+
|
|
5
6
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
7
|
import { createRequire } from 'node:module';
|
|
7
8
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import { tmpdir } from 'node:os';
|
|
9
10
|
import { dirname, join, resolve, resolve as resolvePath } from 'node:path';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
13
14
|
import type { WorldManifest } from './attach.ts';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
18
19
|
import type { PrerequisiteId } from './index.ts';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
|
|
@@ -54,7 +55,7 @@ function acknowledgeOptions(args: string[], command: string): Record<string, str
|
|
|
54
55
|
|
|
55
56
|
function printHelp(): void {
|
|
56
57
|
process.stdout.write(`Usage:
|
|
57
|
-
volter-world init <name> --repo <path> [--out <dir>] [--force] [--allow-unknown] [--acknowledge vendor=reason]... [--json] [--root <repo>]
|
|
58
|
+
volter-world init <name> --repo <path> [--out <dir>] [--needs <needs.json>] [--force] [--allow-unknown] [--acknowledge vendor=reason]... [--json] [--root <repo>]
|
|
58
59
|
# the FRONT DOOR: detect the repo's vendors, EMIT a world config +
|
|
59
60
|
# env file OUTSIDE the repo (default <repo>/../<name>-pilot/, the
|
|
60
61
|
# repo is never written to), then run the coverage proof over the
|
|
@@ -62,6 +63,7 @@ function printHelp(): void {
|
|
|
62
63
|
# Deterministic: same repo + same pack catalog => identical bytes.
|
|
63
64
|
# init DIAGNOSES; it never boots — it prints the exact next commands
|
|
64
65
|
volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--app-url <url>] [--in-process] [--json] [--root <repo>]
|
|
66
|
+
volter-world up <config> --env-out=<path> --in-process --attach-after-line [--name <name>] [--keep-state] [--app-url <url>] [--root <repo>] -- <command...> # after its last boot line, waits for stdin "attach" before running the command
|
|
65
67
|
volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
|
|
66
68
|
volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
|
|
67
69
|
volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
|
|
@@ -184,6 +186,7 @@ export async function main(): Promise<void> {
|
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
if (cmd === 'consumers') {
|
|
189
|
+
const { retireWorldConsumers } = await import('./runtime.ts');
|
|
187
190
|
// `volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json]`: the owner's
|
|
188
191
|
// explicit resolution of attachments whose runner and command are provably gone.
|
|
189
192
|
const args = rest.filter((arg): arg is string => arg !== undefined);
|
|
@@ -205,6 +208,9 @@ export async function main(): Promise<void> {
|
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
if (cmd === 'resources' || cmd === 'prune') {
|
|
211
|
+
const { pruneWorlds, statusWorld } = await import('./runtime.ts');
|
|
212
|
+
const { worldResourceReport } = await import('./resources.ts');
|
|
213
|
+
const { worldConsumers, worldUsage } = await import('./consumers.ts');
|
|
208
214
|
const args = [subject, ...rest].filter((arg): arg is string => arg !== undefined);
|
|
209
215
|
const reportRoot = optionValue(args, '--root', process.cwd());
|
|
210
216
|
// Destructive commands must reject typos, not silently broaden a target to all Worlds.
|
|
@@ -263,6 +269,7 @@ export async function main(): Promise<void> {
|
|
|
263
269
|
|
|
264
270
|
|
|
265
271
|
if (cmd === 'down') {
|
|
272
|
+
const { downWorld } = await import('./runtime.ts');
|
|
266
273
|
if (!subject) throw new Error('volter-world down: missing world name');
|
|
267
274
|
const graceArg = optionValue(rest, '--grace-ms');
|
|
268
275
|
if (graceArg && !(Number(graceArg) >= 0)) throw new Error(`volter-world down: invalid --grace-ms "${graceArg}" (want a non-negative number)`);
|
|
@@ -298,6 +305,8 @@ export async function main(): Promise<void> {
|
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
if (cmd === 'events') {
|
|
308
|
+
const { statusWorld, worldEventsPath } = await import('./runtime.ts');
|
|
309
|
+
const { readWorldEvents, describeWorldEvent } = await import('./service-exit.ts');
|
|
301
310
|
if (!subject) throw new Error('volter-world events: missing world name');
|
|
302
311
|
const status = statusWorld(subject, root, { readOnly: true, light: true });
|
|
303
312
|
const events = readWorldEvents(worldEventsPath(status.dirs.logs));
|
|
@@ -308,6 +317,9 @@ export async function main(): Promise<void> {
|
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
if (cmd === 'status') {
|
|
320
|
+
const { statusWorld, worldEventsPath } = await import('./runtime.ts');
|
|
321
|
+
const { worldUsage } = await import('./consumers.ts');
|
|
322
|
+
const { readServiceEnd, describeServiceEnd, readWorldEvents, describeWorldEvent } = await import('./service-exit.ts');
|
|
311
323
|
if (!subject) throw new Error('volter-world status: missing world name');
|
|
312
324
|
const status = statusWorld(subject, root);
|
|
313
325
|
const usage = worldUsage(root, subject);
|
|
@@ -364,6 +376,7 @@ export async function main(): Promise<void> {
|
|
|
364
376
|
// `plan` and `review` retired 2026-09-05: the changeset verbs are the one review path (verify, approve, status, push).
|
|
365
377
|
|
|
366
378
|
if (cmd === 'doctor') {
|
|
379
|
+
const { doctorWorld } = await import('./runtime.ts');
|
|
367
380
|
if (!subject) throw new Error('volter-world doctor: missing world name');
|
|
368
381
|
const report = await doctorWorld(subject, { root, verifyPublic: rest.includes('--verify-public') });
|
|
369
382
|
if (rest.includes('--json')) {
|
|
@@ -377,6 +390,7 @@ export async function main(): Promise<void> {
|
|
|
377
390
|
}
|
|
378
391
|
|
|
379
392
|
if (cmd === 'doctor-prereqs') {
|
|
393
|
+
const { checkPrerequisites, formatPrerequisiteChecks } = await import('./prerequisites.ts');
|
|
380
394
|
const required = rest.flatMap((arg, index) => arg === '--require' ? [rest[index + 1]] : [])
|
|
381
395
|
.filter(Boolean) as PrerequisiteId[];
|
|
382
396
|
const checks = checkPrerequisites(required.length ? required : ['local-execution']);
|
|
@@ -392,6 +406,7 @@ export async function main(): Promise<void> {
|
|
|
392
406
|
}
|
|
393
407
|
|
|
394
408
|
if (cmd === 'inspect-project') {
|
|
409
|
+
const { inspectProject, formatProjectInspection } = await import('./project-inspect.ts');
|
|
395
410
|
const args = [...(subject ? [subject] : []), ...rest];
|
|
396
411
|
const path = args.find((arg) => !arg.startsWith('--')) || process.cwd();
|
|
397
412
|
const report = inspectProject(path);
|
|
@@ -400,6 +415,7 @@ export async function main(): Promise<void> {
|
|
|
400
415
|
}
|
|
401
416
|
|
|
402
417
|
if (cmd === 'migrate-config') {
|
|
418
|
+
const { migrateWorldConfig } = await import('./configs.ts');
|
|
403
419
|
if (!subject) throw new Error('volter-world migrate-config: missing config path or name');
|
|
404
420
|
const migration = migrateWorldConfig(subject, root);
|
|
405
421
|
if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(migration, null, 2)}\n`);
|
|
@@ -430,20 +446,26 @@ export async function main(): Promise<void> {
|
|
|
430
446
|
}
|
|
431
447
|
|
|
432
448
|
if (cmd === 'init') {
|
|
449
|
+
const { initWorld, formatInitReport } = await import('./init.ts');
|
|
433
450
|
if (!subject) throw new Error('volter-world init: missing world name (usage: volter-world init <name> --repo <path>)');
|
|
434
451
|
const repo = optionValue(rest, '--repo');
|
|
435
452
|
if (!repo) throw new Error('volter-world init: --repo <path> is required (the application repo to build the world for)');
|
|
436
453
|
const out = optionValue(rest, '--out');
|
|
454
|
+
const needs = optionValue(rest, '--needs');
|
|
437
455
|
const result = initWorld(subject, repo, {
|
|
438
456
|
root,
|
|
439
457
|
...(out ? { out } : {}),
|
|
458
|
+
...(needs ? { needs } : {}),
|
|
440
459
|
force: rest.includes('--force'),
|
|
441
460
|
allowUnknown: rest.includes('--allow-unknown'),
|
|
442
461
|
acknowledge: acknowledgeOptions(rest, 'init'),
|
|
443
462
|
});
|
|
444
463
|
process.stdout.write(rest.includes('--json') ? `${JSON.stringify(result, null, 2)}\n` : formatInitReport(result));
|
|
445
464
|
// Vendor coverage IS the exit code. App boot risks remain a separate, prominent report field.
|
|
446
|
-
|
|
465
|
+
// A World made from a stack's needs is the exception, and says so: it is made for the services the stack
|
|
466
|
+
// names, a vendor with no twin is its report and not its failure, and its maker goes by this exit alone.
|
|
467
|
+
if (!result.ok && needs) process.stdout.write('\nVENDOR COVERAGE: incomplete (the vendors above with no pack). With --needs that is reported and is not a failure: the World is written, exit 0.\n');
|
|
468
|
+
if (!result.ok && !needs) process.exit(1);
|
|
447
469
|
return;
|
|
448
470
|
}
|
|
449
471
|
|
|
@@ -451,9 +473,10 @@ export async function main(): Promise<void> {
|
|
|
451
473
|
// twins this world mounted; `audit` is each one's grade as the catalog records it. Both read
|
|
452
474
|
// instance.json and the committed artifacts — nothing is fetched.
|
|
453
475
|
if (cmd === 'outdated' || cmd === 'audit') {
|
|
476
|
+
const { statusWorld } = await import('./runtime.ts');
|
|
454
477
|
if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--json])`);
|
|
455
478
|
const status = statusWorld(subject, root);
|
|
456
|
-
const rows = worldUpkeep(status, cmd, root);
|
|
479
|
+
const rows = await worldUpkeep(status, cmd, root);
|
|
457
480
|
if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
|
|
458
481
|
else {
|
|
459
482
|
if (rows.length === 0) process.stdout.write(`World ${status.name}: no twin mounted with a resolved version — boot it once (up) so instance.json records what was mounted\n`);
|
|
@@ -463,6 +486,7 @@ export async function main(): Promise<void> {
|
|
|
463
486
|
return;
|
|
464
487
|
}
|
|
465
488
|
if (cmd === 'covers') {
|
|
489
|
+
const { coverWorld, formatCoverageReport, recordCoverage } = await import('./covers.ts');
|
|
466
490
|
if (!subject) throw new Error('volter-world covers: missing world name (usage: volter-world covers <world> --repo <path>)');
|
|
467
491
|
const repo = optionValue(rest, '--repo');
|
|
468
492
|
if (!repo) throw new Error('volter-world covers: --repo <path> is required (the application repo to prove coverage for)');
|
|
@@ -481,6 +505,8 @@ export async function main(): Promise<void> {
|
|
|
481
505
|
}
|
|
482
506
|
|
|
483
507
|
if (cmd === 'clock') {
|
|
508
|
+
const { clockFile } = await import('./runtime.ts');
|
|
509
|
+
const { writeWorldClock } = await import('./clock.ts');
|
|
484
510
|
// THE operator door for world time (physics): show / set <iso> / advance <duration> / shift <duration> / clear.
|
|
485
511
|
// The clock every twin reads per request (kernel worldNow()), and every application process through the injector
|
|
486
512
|
// (inject.cjs, WORLD TIME); a change takes effect live, no restarts. Its two forms (world-clock.cjs): `set` freezes
|
|
@@ -539,6 +565,7 @@ export async function main(): Promise<void> {
|
|
|
539
565
|
}
|
|
540
566
|
|
|
541
567
|
if (cmd === 'urls') {
|
|
568
|
+
const { urlsWorld } = await import('./runtime.ts');
|
|
542
569
|
if (!subject) throw new Error('volter-world urls: missing world name');
|
|
543
570
|
const urls = urlsWorld(subject, root);
|
|
544
571
|
if (rest.includes('--json')) {
|
|
@@ -553,6 +580,7 @@ export async function main(): Promise<void> {
|
|
|
553
580
|
}
|
|
554
581
|
|
|
555
582
|
if (cmd === 'url') {
|
|
583
|
+
const { urlWorld } = await import('./runtime.ts');
|
|
556
584
|
if (!subject) throw new Error('volter-world url: missing world name (usage: volter-world url <world> [service])');
|
|
557
585
|
const [service] = positionalArgs(rest);
|
|
558
586
|
const urls = urlWorld(subject, { root, ...(service === undefined ? {} : { service }) });
|
|
@@ -562,6 +590,7 @@ export async function main(): Promise<void> {
|
|
|
562
590
|
}
|
|
563
591
|
|
|
564
592
|
if (cmd === 'app-url') {
|
|
593
|
+
const { setAppUrl, detectAppUrl, addAppHosts, readAppUrl, appUrlUnsetMessage } = await import('./app-url.ts');
|
|
565
594
|
if (!subject) throw new Error('volter-world app-url: missing world name (usage: volter-world app-url <world> [--set <url>|--detect <pid|port>])');
|
|
566
595
|
const set = optionValue(rest, '--set');
|
|
567
596
|
const detect = optionValue(rest, '--detect');
|
|
@@ -592,6 +621,7 @@ export async function main(): Promise<void> {
|
|
|
592
621
|
}
|
|
593
622
|
|
|
594
623
|
if (cmd === 'tail' || cmd === 'log') {
|
|
624
|
+
const { tailWorldActions } = await import('./tail.ts');
|
|
595
625
|
if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world log <world> [service...])`);
|
|
596
626
|
const services = positionalArgs(rest);
|
|
597
627
|
const controller = new AbortController();
|
|
@@ -610,6 +640,7 @@ export async function main(): Promise<void> {
|
|
|
610
640
|
}
|
|
611
641
|
|
|
612
642
|
if (cmd === 'seed' || cmd === 'reset') {
|
|
643
|
+
const { seedWorld, resetWorld } = await import('./placeholder.ts');
|
|
613
644
|
if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--entry <file>] [--cwd <dir>])`);
|
|
614
645
|
if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file='))) throw new Error('--env-file is not an input for this command. Reset accepts --env-out=<new-output-path>.');
|
|
615
646
|
const envFile = optionValue(rest, '--env-out') || undefined;
|
|
@@ -624,6 +655,8 @@ export async function main(): Promise<void> {
|
|
|
624
655
|
}
|
|
625
656
|
|
|
626
657
|
if (cmd === 'branch') {
|
|
658
|
+
const { branchWorld } = await import('./branch.ts');
|
|
659
|
+
const { printReady } = await import('./up-cli.ts');
|
|
627
660
|
if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file='))) throw new Error('--env-file is not an input for this command. Use --env-out=<new-output-path>; never pass credentials or custody as an output.');
|
|
628
661
|
const [name] = positionalArgs(rest);
|
|
629
662
|
if (!subject || !name) throw new Error('volter-world branch: usage: volter-world branch <world> <name> [--env-out=<path>]');
|
|
@@ -634,12 +667,15 @@ export async function main(): Promise<void> {
|
|
|
634
667
|
return;
|
|
635
668
|
}
|
|
636
669
|
if (cmd === 'checkout') {
|
|
670
|
+
const { checkoutWorld } = await import('./branch.ts');
|
|
671
|
+
const { printReady } = await import('./up-cli.ts');
|
|
637
672
|
if (!subject) throw new Error('volter-world checkout: missing world name');
|
|
638
673
|
const instance = await checkoutWorld(subject, { root });
|
|
639
674
|
if (rest.includes('--json')) process.stdout.write(`${JSON.stringify({ name: instance.name, instance: instance.instance, createdAt: instance.createdAt }, null, 2)}\n`); else printReady(instance);
|
|
640
675
|
return;
|
|
641
676
|
}
|
|
642
677
|
if (cmd === 'fetch') {
|
|
678
|
+
const { fetchFromOrigin } = await import('./origin.ts');
|
|
643
679
|
if (!subject) throw new Error('volter-world fetch: missing world name (usage: volter-world fetch <world> --key <key> [--origin <url> --namespace <org>/<world>] [--full])');
|
|
644
680
|
const key = optionValue(rest, '--key') || process.env.VOLTER_TWINS_READ_KEY || process.env.VOLTER_TWINS_KEY || '';
|
|
645
681
|
if (!key) throw new Error('volter-world fetch: --key <twins key> is required (the read key for a fetch, the admin key for --full)');
|
|
@@ -652,6 +688,7 @@ export async function main(): Promise<void> {
|
|
|
652
688
|
return;
|
|
653
689
|
}
|
|
654
690
|
if (cmd === 'origin') {
|
|
691
|
+
const { worldOrigin } = await import('./origin.ts');
|
|
655
692
|
if (!subject) throw new Error('volter-world origin: missing world name');
|
|
656
693
|
const origin = worldOrigin(subject, root);
|
|
657
694
|
if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(origin, null, 2)}\n`);
|
|
@@ -660,6 +697,7 @@ export async function main(): Promise<void> {
|
|
|
660
697
|
}
|
|
661
698
|
|
|
662
699
|
if (cmd === 'mark') {
|
|
700
|
+
const { markWorld } = await import('./changeset.ts');
|
|
663
701
|
if (!subject) throw new Error('volter-world mark: missing world name (usage: volter-world mark <world> [--id <marker>] [--note <text>])');
|
|
664
702
|
const id = optionValue(rest, '--id');
|
|
665
703
|
const note = optionValue(rest, '--note');
|
|
@@ -670,6 +708,8 @@ export async function main(): Promise<void> {
|
|
|
670
708
|
}
|
|
671
709
|
|
|
672
710
|
if (cmd === 'diff') {
|
|
711
|
+
const { diffWorld } = await import('./changeset.ts');
|
|
712
|
+
const { formatLedgerDelta } = await import('@volter/world-core/runtime');
|
|
673
713
|
if (!subject) throw new Error('volter-world diff: missing world name (usage: volter-world diff <world> [--base <marker>])');
|
|
674
714
|
const base = optionValue(rest, '--base');
|
|
675
715
|
const delta = diffWorld(subject, { root, ...(base ? { base } : {}) });
|
|
@@ -678,6 +718,8 @@ export async function main(): Promise<void> {
|
|
|
678
718
|
}
|
|
679
719
|
|
|
680
720
|
if (cmd === 'changeset') {
|
|
721
|
+
const { parseVerifierExpression, formatChangeset, formatReplayReport, formatVerification, formatChangesetStatus, formatRebaseReport, formatApplication } = await import('@volter/world-core/runtime');
|
|
722
|
+
const { createWorldChangeset, findWorldChangeset, listWorldChangesets, replayWorldChangeset, verifyWorldChangeset, approveWorldChangeset, statusWorldChangeset, rebaseWorldChangeset, pushWorldChangeset } = await import('./changeset.ts');
|
|
681
723
|
const CHANGESET_VALUE_FLAGS = ['--root', '--base', '--into', '--world', '--verifier', '--as', '--note'];
|
|
682
724
|
const args = positionalArgs(rest, CHANGESET_VALUE_FLAGS);
|
|
683
725
|
const world = optionValue(rest, '--world');
|
|
@@ -773,6 +815,8 @@ export async function main(): Promise<void> {
|
|
|
773
815
|
}
|
|
774
816
|
|
|
775
817
|
if (cmd === 'list') {
|
|
818
|
+
const { listWorlds } = await import('./runtime.ts');
|
|
819
|
+
const { worldUsage } = await import('./consumers.ts');
|
|
776
820
|
const listArgs = [subject, ...rest].filter((arg): arg is string => arg !== undefined);
|
|
777
821
|
const listRoot = optionValue(listArgs, '--root', process.cwd());
|
|
778
822
|
const worlds = listWorlds(listRoot).map(world => ({ ...world, usage: worldUsage(listRoot, world.name) }));
|
|
@@ -785,6 +829,7 @@ export async function main(): Promise<void> {
|
|
|
785
829
|
}
|
|
786
830
|
|
|
787
831
|
if (cmd === 'env') {
|
|
832
|
+
const { attachWorld } = await import('./runtime.ts');
|
|
788
833
|
if (!subject) throw new Error('volter-world env: missing world name');
|
|
789
834
|
const split = rest.indexOf('--');
|
|
790
835
|
if (split < 0) throw new Error('volter-world env: pass command after --');
|
|
@@ -793,6 +838,10 @@ export async function main(): Promise<void> {
|
|
|
793
838
|
}
|
|
794
839
|
|
|
795
840
|
if (cmd === 'attach') {
|
|
841
|
+
// The local env attach, every app's CMD path, needs the World's ref and its runtime alone: the two load together,
|
|
842
|
+
// and the remote door (serve.ts) and the reflect front (reflect.ts, ca-trust.ts) load only on their own paths.
|
|
843
|
+
// All five were imported one after another first (S2 R16: Dub's attach, 225 bodies in 341 ms before its `next`).
|
|
844
|
+
const [{ resolveWorldRef, isRemoteWorldRef, worldManifest }, { withoutVaultSignIn, instanceDir, attachWorld }] = await Promise.all([import('./attach.ts'), import('./runtime.ts')]);
|
|
796
845
|
// `subject` may be the ref, a flag, or the `--` separator — reassemble and split.
|
|
797
846
|
const args = [subject, ...rest].filter((arg): arg is string => arg !== undefined);
|
|
798
847
|
const split = args.indexOf('--');
|
|
@@ -807,6 +856,7 @@ export async function main(): Promise<void> {
|
|
|
807
856
|
throw new Error('--owner is supported for local attach --via env commands');
|
|
808
857
|
}
|
|
809
858
|
if (isRemoteWorldRef(resolved.ref)) {
|
|
859
|
+
const { fetchRemoteManifest, remoteAttachEnv } = await import('./serve.ts');
|
|
810
860
|
// remote world: fetch the manifest, synthesize the attach env, exec
|
|
811
861
|
const attachToken = optionValue(head, '--token') || process.env.VOLTER_WORLD_TOKEN || undefined;
|
|
812
862
|
const manifest = await fetchRemoteManifest(resolved.ref, attachToken === undefined ? {} : { token: attachToken });
|
|
@@ -849,6 +899,7 @@ export async function main(): Promise<void> {
|
|
|
849
899
|
// gets the override — and everything else runs with the trust env and VOLTER_WORLD set, its DNS
|
|
850
900
|
// being the caller's to point (a container's --dns, a resolver on the host).
|
|
851
901
|
if (command.length === 0) throw new Error('volter-world attach: pass the command after --');
|
|
902
|
+
const [{ readReflectManifest, splitDockerComposeArgs, composeOverrideForReflect, dockerComposeWithOverride, ATTACHED_CA_PATH }, { sessionTrustEnv }] = await Promise.all([import('./reflect.ts'), import('./ca-trust.ts')]);
|
|
852
903
|
const manifest = readReflectManifest(attachRoot, resolved.ref);
|
|
853
904
|
if (!manifest) throw new Error(`volter-world attach --via reflect: no reflect front is running for world ${resolved.ref} — start one: volter-world reflect ${resolved.ref} --target-ip <ip> --port 443 --resolver-port 53`);
|
|
854
905
|
const env: Record<string, string> = { ...withoutVaultSignIn(process.env) as Record<string, string>, VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
|
|
@@ -879,12 +930,15 @@ export async function main(): Promise<void> {
|
|
|
879
930
|
}
|
|
880
931
|
|
|
881
932
|
if (cmd === 'manifest') {
|
|
933
|
+
const { worldManifest } = await import('./attach.ts');
|
|
882
934
|
if (!subject) throw new Error('volter-world manifest: missing world name');
|
|
883
935
|
process.stdout.write(`${JSON.stringify(worldManifest(subject, root), null, 2)}\n`);
|
|
884
936
|
return;
|
|
885
937
|
}
|
|
886
938
|
|
|
887
939
|
if (cmd === 'reflect') {
|
|
940
|
+
const { reflectRoutesPath, startReflectFront, startReflectResolver, readReflectRoutes, writeReflectManifest, clearReflectManifest } = await import('./reflect.ts');
|
|
941
|
+
const { statusWorld, instanceDir } = await import('./runtime.ts');
|
|
888
942
|
if (!subject) throw new Error('volter-world reflect: missing world name');
|
|
889
943
|
const targetIp = optionValue(rest, '--target-ip');
|
|
890
944
|
if (!targetIp) throw new Error('volter-world reflect: --target-ip <ip> is required (the address attachers reach the front at)');
|
|
@@ -918,6 +972,9 @@ export async function main(): Promise<void> {
|
|
|
918
972
|
}
|
|
919
973
|
|
|
920
974
|
if (cmd === 'serve') {
|
|
975
|
+
const { startReflectFront } = await import('./reflect.ts');
|
|
976
|
+
const { statusWorld, instanceDir } = await import('./runtime.ts');
|
|
977
|
+
const { startManifestServer, advertiseWorldManifest, MANIFEST_PATH } = await import('./serve.ts');
|
|
921
978
|
if (!subject) throw new Error('volter-world serve: missing world name');
|
|
922
979
|
const advertise = optionValue(rest, '--advertise');
|
|
923
980
|
if (!advertise) throw new Error('volter-world serve: --advertise <https-origin> is required (how attachers reach the door, e.g. https://worlds.corp:8443)');
|
|
@@ -956,6 +1013,7 @@ export async function main(): Promise<void> {
|
|
|
956
1013
|
}
|
|
957
1014
|
|
|
958
1015
|
if (cmd === 'route') {
|
|
1016
|
+
const { reflectRoutesPath, readReflectRoutes, writeReflectRoutes } = await import('./reflect.ts');
|
|
959
1017
|
if (!subject) throw new Error('volter-world route: missing world name');
|
|
960
1018
|
const [verb, host] = rest.filter((arg) => !arg.startsWith('--'));
|
|
961
1019
|
const routesPath = reflectRoutesPath(root, subject);
|
|
@@ -974,17 +1032,21 @@ export async function main(): Promise<void> {
|
|
|
974
1032
|
}
|
|
975
1033
|
|
|
976
1034
|
if (cmd === 'activate') {
|
|
1035
|
+
const { activateScript } = await import('./runtime.ts');
|
|
977
1036
|
if (!subject) throw new Error('volter-world activate: missing world name');
|
|
978
1037
|
process.stdout.write(activateScript(subject, root));
|
|
979
1038
|
return;
|
|
980
1039
|
}
|
|
981
1040
|
|
|
982
1041
|
if (cmd === 'shell') {
|
|
1042
|
+
const { shellWorld } = await import('./runtime.ts');
|
|
983
1043
|
if (!subject) throw new Error('volter-world shell: missing world name');
|
|
984
1044
|
process.exit(await shellWorld(subject, root));
|
|
985
1045
|
}
|
|
986
1046
|
|
|
987
1047
|
if (cmd === 'run') {
|
|
1048
|
+
const { runWorld } = await import('./runtime.ts');
|
|
1049
|
+
const { readServiceEnd, describeServiceEnd } = await import('./service-exit.ts');
|
|
988
1050
|
if (!subject) throw new Error('volter-world run: missing config');
|
|
989
1051
|
const split = rest.indexOf('--');
|
|
990
1052
|
if (split < 0) throw new Error('volter-world run: pass command after --');
|
|
@@ -1013,6 +1075,7 @@ export async function main(): Promise<void> {
|
|
|
1013
1075
|
}
|
|
1014
1076
|
|
|
1015
1077
|
if (cmd === 'share') {
|
|
1078
|
+
const { shareWorldServices } = await import('./runtime.ts');
|
|
1016
1079
|
if (!subject) throw new Error('volter-world share: missing world name');
|
|
1017
1080
|
const service = optionValue(rest, '--service') || undefined;
|
|
1018
1081
|
const verifyPath = rest.includes('--no-verify')
|
|
@@ -1044,6 +1107,7 @@ export async function main(): Promise<void> {
|
|
|
1044
1107
|
}
|
|
1045
1108
|
|
|
1046
1109
|
if (cmd === 'unshare') {
|
|
1110
|
+
const { unshareWorld } = await import('./runtime.ts');
|
|
1047
1111
|
if (!subject) throw new Error('volter-world unshare: missing world name');
|
|
1048
1112
|
const service = optionValue(rest, '--service') || undefined;
|
|
1049
1113
|
const result = await unshareWorld(subject, { root, service });
|
|
@@ -1059,7 +1123,10 @@ export async function main(): Promise<void> {
|
|
|
1059
1123
|
/** The rows `outdated` / `audit` print. `outdated`: pinned vs mounted vs catalog-current version and the
|
|
1060
1124
|
* protocol standing; `audit`: each mounted twin's grade, from the catalog's record. `attention` marks a row the
|
|
1061
1125
|
* operator should act on (exit 1). */
|
|
1062
|
-
function worldUpkeep(status: ReturnType<typeof statusWorld>, kind: 'outdated' | 'audit', root: string): Array<{ id: string; line: string; attention: boolean }
|
|
1126
|
+
async function worldUpkeep(status: ReturnType<typeof statusWorld>, kind: 'outdated' | 'audit', root: string): Promise<Array<{ id: string; line: string; attention: boolean }>> {
|
|
1127
|
+
const { resolveCatalog } = await import('./catalog.ts');
|
|
1128
|
+
const { loadWorldConfig } = await import('./configs.ts');
|
|
1129
|
+
const { vendorOfPackModule } = await import('@volter/world-core/runtime');
|
|
1063
1130
|
const catalog = resolveCatalog(root);
|
|
1064
1131
|
const config = loadWorldConfig(status.name, root).config;
|
|
1065
1132
|
const out: Array<{ id: string; line: string; attention: boolean }> = [];
|
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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
|
+
import { cliLoadStarted } from './cli-load-clock.ts';
|
|
4
5
|
import { optionValue } from './cli-args.ts';
|
|
5
6
|
import { errorMessage } from './error-message.ts';
|
|
6
7
|
|
|
7
8
|
const [cmd, subject, ...rest] = process.argv.slice(2);
|
|
8
9
|
const run = cmd === 'up' && subject !== '--help' && subject !== '-h'
|
|
9
|
-
? import('./up-cli.ts').then(({ upCommand }) =>
|
|
10
|
+
? import('./up-cli.ts').then(({ upCommand }) => {
|
|
11
|
+
process.stderr.write(`volter-world up: entry modules loaded in ${(performance.now() - cliLoadStarted).toFixed(1)} ms\n`);
|
|
12
|
+
return upCommand(subject, rest, optionValue(rest.includes('--attach-after-line') && rest.includes('--') ? rest.slice(0, rest.indexOf('--')) : rest, '--root', process.cwd()));
|
|
13
|
+
})
|
|
10
14
|
: import('./cli-commands.ts').then(({ main }) => main());
|
|
11
15
|
run.catch((error) => {
|
|
12
16
|
process.stderr.write(`${errorMessage(error)}\n`);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ClickHouse's managed ownership questions: HTTP ping, this child's kernel boot answer,
|
|
2
|
+
// and a native Hello/Ping. No SQL is executed by readiness.
|
|
3
|
+
// Packet fields: clickhouse-docs/docs/native-protocol/{client,server,basics}.md;
|
|
4
|
+
// revision54429 uses the reached clickhouse-driver0.2.10 layout retained with the pack.
|
|
5
|
+
import { connect } from 'node:net';
|
|
6
|
+
import { WORLD_BOOT_PATH } from '@volter/world-core/runtime';
|
|
7
|
+
import type { ManagedProtocol } from './twin-backing.ts';
|
|
8
|
+
import type { InfraService } from './infra-definition.ts';
|
|
9
|
+
|
|
10
|
+
async function answer(port: number, path: string): Promise<string | undefined> {
|
|
11
|
+
const controller = new AbortController(), timer = setTimeout(() => controller.abort(), 1000);
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(`http://127.0.0.1:${port}${path}`, { signal: controller.signal, redirect: 'error' });
|
|
14
|
+
if (!response.ok || !response.body) return undefined;
|
|
15
|
+
// This private proof contains a UUID boot nonce and a JavaScript-safe PID; ping is four bytes.
|
|
16
|
+
const reader = response.body.getReader(); let text = '', bytes = 0;
|
|
17
|
+
try { while (true) { const part = await reader.read(); if (part.done) break; bytes += part.value.length; if (bytes > 256) return undefined; text += new TextDecoder().decode(part.value); } }
|
|
18
|
+
finally { await reader.cancel().catch(() => undefined); }
|
|
19
|
+
return text;
|
|
20
|
+
} catch { return undefined; } finally { clearTimeout(timer); }
|
|
21
|
+
}
|
|
22
|
+
function number(value: number): number[] { const result: number[] = []; do { const byte = value % 128; value = Math.floor(value / 128); result.push(byte | (value ? 128 : 0)); } while (value); return result; }
|
|
23
|
+
function string(value: string): number[] { const bytes = new TextEncoder().encode(value); return [...number(bytes.length), ...bytes]; }
|
|
24
|
+
function nativeAnswers(service: InfraService): Promise<boolean> {
|
|
25
|
+
const port = service.sides?.native?.hostPort; if (!port) return Promise.resolve(false);
|
|
26
|
+
const users = service.configuration?.users as Record<string, string> | undefined;
|
|
27
|
+
const [user, password] = users && Object.hasOwn(users, 'default') ? ['default', users.default!] : Object.entries(users ?? { default: '' })[0] ?? [];
|
|
28
|
+
if (typeof user !== 'string' || typeof password !== 'string') return Promise.resolve(false);
|
|
29
|
+
const hello = Uint8Array.from([0, ...string('World ownership'), 1, 0, ...number(54429), ...string('default'), ...string(user), ...string(password)]);
|
|
30
|
+
return new Promise(resolve => {
|
|
31
|
+
const socket = connect({ host: '127.0.0.1', port, timeout: 1000 }); let bytes = new Uint8Array(), phase = 'hello', settled = false;
|
|
32
|
+
const end = (valid: boolean) => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); resolve(valid); };
|
|
33
|
+
// One complete ownership question has one deadline; incoming fragments never extend it.
|
|
34
|
+
const timer = setTimeout(() => end(false), 1000);
|
|
35
|
+
const missing = Symbol('incomplete');
|
|
36
|
+
socket.once('connect', () => socket.write(hello));
|
|
37
|
+
socket.on('data', chunk => {
|
|
38
|
+
// The declaring pack fixes its Hello name, timezone and display name;64KiB is beyond that packet domain.
|
|
39
|
+
if (bytes.length + chunk.length > 65536) { end(false); return; }
|
|
40
|
+
const joined = new Uint8Array(bytes.length + chunk.length); joined.set(bytes); joined.set(chunk, bytes.length); bytes = joined;
|
|
41
|
+
let at = 0;
|
|
42
|
+
const read = (): number => {
|
|
43
|
+
let value = 0n;
|
|
44
|
+
for (let shift = 0; shift < 70; shift += 7) { if (at >= bytes.length) throw missing; const byte = bytes[at++]!; value |= BigInt(byte & 127) << BigInt(shift); if (!(byte & 128)) { if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('Invalid native integer'); return Number(value); } }
|
|
45
|
+
throw new Error('Invalid native integer');
|
|
46
|
+
};
|
|
47
|
+
const text = (): string => { const length = read(); if (length > bytes.length - at) throw missing; const result = new TextDecoder().decode(bytes.subarray(at, at + length)); at += length; return result; };
|
|
48
|
+
try {
|
|
49
|
+
if (phase === 'hello') {
|
|
50
|
+
if (read() !== 0 || text() !== 'ClickHouse') { end(false); return; }
|
|
51
|
+
read(); read(); const revision = read(); if (revision !== 54429) { end(false); return; }
|
|
52
|
+
text(); text(); read(); bytes = bytes.slice(at); phase = 'pong'; socket.write(Uint8Array.of(4));
|
|
53
|
+
if (!bytes.length) return; at = 0;
|
|
54
|
+
}
|
|
55
|
+
end(read() === 4);
|
|
56
|
+
} catch (error) { if (error !== missing) end(false); }
|
|
57
|
+
});
|
|
58
|
+
socket.once('error', () => end(false)); socket.once('timeout', () => end(false)); socket.once('close', () => end(false));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export const CLICKHOUSE_PROTOCOL: ManagedProtocol = {
|
|
62
|
+
name: 'ClickHouse', bootProof: true,
|
|
63
|
+
async answers(port) { return await answer(port, '/ping') === 'Ok.\n'; },
|
|
64
|
+
async serves(port, pid, service, boot) {
|
|
65
|
+
if (!service || !boot || await answer(port, '/ping') !== 'Ok.\n') return false;
|
|
66
|
+
const body = await answer(port, WORLD_BOOT_PATH); if (!body) return false;
|
|
67
|
+
try { const held = JSON.parse(body); if (held.boot !== boot || held.pid !== pid) return false; } catch { return false; }
|
|
68
|
+
return nativeAnswers(service);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -4,7 +4,7 @@ export const COMMAND_GRACE_MS: number;
|
|
|
4
4
|
export const COMMAND_CONFIRM_MS: number;
|
|
5
5
|
export const commandProcessGroup: boolean;
|
|
6
6
|
export class CommandRetirementError extends Error {}
|
|
7
|
-
export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean): {
|
|
7
|
+
export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean, graceMs?: number): {
|
|
8
8
|
identity: Promise<SpawnIdentity | undefined>;
|
|
9
9
|
cancel(signal: NodeJS.Signals): void;
|
|
10
10
|
readonly forwarded: NodeJS.Signals | undefined;
|
package/src/command-lifetime.mjs
CHANGED
|
@@ -9,7 +9,10 @@ export class CommandRetirementError extends Error {}
|
|
|
9
9
|
|
|
10
10
|
/** The child owns a private POSIX group. A bounded retirement requires both closed output and
|
|
11
11
|
* a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
|
|
12
|
-
|
|
12
|
+
/** `graceMs` is how long a cancelled child is awaited before SIGKILL. A command gets the default; a STORE (a
|
|
13
|
+
* database, a twin that holds state) is given its lifecycle's budget, since what it does between the signal and
|
|
14
|
+
* its exit is write its state down, and a SIGKILL in the middle of that leaves a store nothing may take. */
|
|
15
|
+
export function commandLifetime(child, abort, preserveOnSuccess = false, graceMs = COMMAND_GRACE_MS) {
|
|
13
16
|
const identity = child.pid && commandProcessGroup ? childIdentity(child) : Promise.resolve(undefined);
|
|
14
17
|
let signalFailure;
|
|
15
18
|
let pendingSignals = 0;
|
|
@@ -62,7 +65,7 @@ export function commandLifetime(child, abort, preserveOnSuccess = false) {
|
|
|
62
65
|
}
|
|
63
66
|
};
|
|
64
67
|
const watch = () => {
|
|
65
|
-
if (!deadline) deadline = Date.now() +
|
|
68
|
+
if (!deadline) deadline = Date.now() + graceMs;
|
|
66
69
|
timer ??= setInterval(check, 20);
|
|
67
70
|
check();
|
|
68
71
|
};
|
package/src/configs.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { copyFileSync, constants, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime';
|
|
3
|
+
import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime-state';
|
|
4
4
|
import { assertWorldConfig, LEGACY_WORLD_SELECTION, worldConfigDocument, type WorldChangesetRules, type WorldConfig } from './schema.ts';
|
|
5
5
|
|
|
6
6
|
export function resolveConfigPath(config: string, root = process.cwd()): string {
|
package/src/consumers.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { hostname } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { stateDirName, withFileLock } from '@volter/world-core/runtime';
|
|
5
|
+
import { stateDirName, withFileLock } from '@volter/world-core/runtime-state';
|
|
6
6
|
|
|
7
7
|
export type WorldConsumer = {
|
|
8
8
|
id: string;
|
package/src/covers.ts
CHANGED
|
@@ -627,6 +627,8 @@ export function worldTwinInventory(world: string, root: string): WorldTwinInvent
|
|
|
627
627
|
export type CoverageOptions = {
|
|
628
628
|
root?: string;
|
|
629
629
|
allowUnknown?: boolean;
|
|
630
|
+
/** Application environment names already read from its declared stack. */
|
|
631
|
+
declaredEnvironmentNames?: readonly string[];
|
|
630
632
|
/**
|
|
631
633
|
* vendor → reason: vendors the operator ACKNOWLEDGES need no twin in this world — e.g. an SDK
|
|
632
634
|
* used verifier-only (svix's Webhook verifies incoming signatures locally and never calls out).
|
|
@@ -685,11 +687,11 @@ function usesOf(signals: Map<string, string[]>): Map<string, Array<{ usage: Worl
|
|
|
685
687
|
|
|
686
688
|
/** Detect every vendor (and every unmapped external-service-shaped signal) an application repo
|
|
687
689
|
* betrays: mapped npm dependencies across the root + all workspace members, vendor-shaped env
|
|
688
|
-
* names in committed `.env*` files, the SMTP/raw-protocol signal, and literal vendor URLs passed
|
|
690
|
+
* names in committed `.env*` files or its declared stack, the SMTP/raw-protocol signal, and literal vendor URLs passed
|
|
689
691
|
* directly to fetch in production source. URL attribution reads the injector's VENDOR_HOSTS table
|
|
690
692
|
* rather than re-encoding host rules here. Insertion order follows the sorted dependency, env-name,
|
|
691
693
|
* and source lists, so callers that preserve it are deterministic. */
|
|
692
|
-
export function detectRepoVendors(repoPath: string, services: Array<WorldServiceConfig | WorldServiceInstance> = []): RepoVendorSignals {
|
|
694
|
+
export function detectRepoVendors(repoPath: string, services: Array<WorldServiceConfig | WorldServiceInstance> = [], declaredEnvironmentNames: readonly string[] = []): RepoVendorSignals {
|
|
693
695
|
// Application aliases are names the selected backing actually injects, never global vendor adoption.
|
|
694
696
|
const backingEnvVendors = new Map<string, string>();
|
|
695
697
|
for (const service of services) {
|
|
@@ -703,7 +705,10 @@ export function detectRepoVendors(repoPath: string, services: Array<WorldService
|
|
|
703
705
|
// A Volter package a pack names as its vendor's SDK is one (`@volter/identity`, the Volter identity
|
|
704
706
|
// service's client, is the volteridentity twin's), as any vendor's SDK is.
|
|
705
707
|
const dependencies = projectDependencies(repo).filter((dep) => Object.hasOwn(SDK_TWINS, dep) || (!dep.startsWith('@volter/') && !dep.startsWith('pypi:volter')));
|
|
706
|
-
const
|
|
708
|
+
for (const name of declaredEnvironmentNames) {
|
|
709
|
+
if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error('Declared vendor environment name must be an identifier');
|
|
710
|
+
}
|
|
711
|
+
const envNames = [...new Set([...projectEnvNames(repo), ...declaredEnvironmentNames])].sort();
|
|
707
712
|
const npmRegistries = projectNpmRegistries(repo);
|
|
708
713
|
|
|
709
714
|
// vendor → detection sources
|
|
@@ -871,7 +876,7 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
|
|
|
871
876
|
const repo = resolve(repoPath);
|
|
872
877
|
const inventory = worldTwinInventory(world, root);
|
|
873
878
|
|
|
874
|
-
const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services);
|
|
879
|
+
const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services, options.declaredEnvironmentNames);
|
|
875
880
|
|
|
876
881
|
// Does ANY injector vendor key exist for this vendor? Decides whether an UNINTERCEPTABLE
|
|
877
882
|
// finding is acknowledgeable: a key that exists means the twin COULD be intercepted and the
|