@volter/world-runtime 2.0.54 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/known-external-services.json +5 -4
- package/dist/src/actions-runner-cli.d.ts +2 -0
- package/dist/src/actions-runner-cli.js +25 -0
- package/dist/src/actions-runner.d.ts +18 -0
- package/dist/src/actions-runner.js +281 -0
- package/dist/src/ancestry.js +1 -1
- package/dist/src/app-url.js +1 -1
- package/dist/src/backings.d.ts +29 -0
- package/dist/src/backings.js +45 -0
- package/dist/src/branch.js +3 -2
- package/dist/src/browser-proxy-cli.js +1 -1
- package/dist/src/catalog-index.d.ts +38 -0
- package/dist/src/catalog-index.js +37 -0
- package/dist/src/catalog.d.ts +16 -3
- package/dist/src/catalog.js +69 -26
- package/dist/src/changeset.d.ts +1 -1
- package/dist/src/changeset.js +9 -11
- package/dist/src/cli-args.d.ts +4 -0
- package/dist/src/cli-args.js +18 -0
- package/dist/src/cli-commands.d.ts +1 -0
- package/dist/src/cli-commands.js +1216 -0
- package/dist/src/cli.js +8 -1300
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.d.ts +2 -2
- package/dist/src/console-apart.js +5 -5
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +1 -1
- package/dist/src/covers.js +94 -30
- package/dist/src/env-output.d.ts +1 -1
- package/dist/src/env-output.js +2 -2
- package/dist/src/fixture-env.js +18 -26
- package/dist/src/host-cli.js +4 -2
- package/dist/src/host-fault-fixture.js +6 -0
- package/dist/src/host-ready.d.ts +8 -2
- package/dist/src/host-ready.js +16 -4
- package/dist/src/host-worker.js +6 -1
- package/dist/src/host.d.ts +2 -0
- package/dist/src/host.js +9 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/init.d.ts +33 -23
- package/dist/src/init.js +228 -96
- package/dist/src/lifecycle-record.js +2 -2
- package/dist/src/local-branches.js +1 -1
- package/dist/src/origin.d.ts +1 -1
- package/dist/src/origin.js +12 -10
- package/dist/src/pack-facts.d.ts +30 -6
- package/dist/src/pack-facts.js +13 -3
- package/dist/src/pglite-backing.js +2 -1
- package/dist/src/placeholder.d.ts +1 -1
- package/dist/src/placeholder.js +12 -4
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +2 -2
- package/dist/src/redirect-proxy.d.ts +5 -0
- package/dist/src/redirect-proxy.js +8 -1
- package/dist/src/redis-backing.js +4 -2
- package/dist/src/reflect.js +1 -1
- package/dist/src/root.d.ts +11 -33
- package/dist/src/root.js +28 -59
- package/dist/src/runtime-test-support.js +1 -1
- package/dist/src/runtime.d.ts +7 -1
- package/dist/src/runtime.js +317 -53
- package/dist/src/schema.d.ts +27 -5
- package/dist/src/schema.js +13 -12
- package/dist/src/served-world.d.ts +11 -35
- package/dist/src/served-world.js +54 -141
- package/dist/src/tail.js +1 -1
- package/dist/src/up-cli.d.ts +3 -0
- package/dist/src/up-cli.js +55 -0
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/world-origins.d.ts +1 -1
- package/dist/src/world-origins.js +1 -1
- package/dist/src/world-view.d.ts +1 -1
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +5 -4
- package/package.json +11 -11
- package/src/actions-runner-cli.ts +24 -0
- package/src/actions-runner.ts +244 -0
- package/src/ancestry.ts +1 -1
- package/src/app-url.ts +1 -1
- package/src/backings.ts +55 -0
- package/src/branch.ts +3 -2
- package/src/browser-proxy-cli.ts +1 -1
- package/src/catalog-index.ts +44 -0
- package/src/catalog.ts +67 -24
- package/src/changeset.ts +10 -13
- package/src/cli-args.ts +17 -0
- package/src/cli-commands.ts +1070 -0
- package/src/cli.ts +9 -1139
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +5 -5
- package/src/consumers.ts +1 -1
- package/src/covers.ts +83 -30
- package/src/env-output.ts +2 -2
- package/src/fixture-env.ts +18 -26
- package/src/host-cli.ts +4 -2
- package/src/host-ready.ts +13 -4
- package/src/host-worker.ts +6 -2
- package/src/host.ts +11 -2
- package/src/index.ts +2 -2
- package/src/init.ts +222 -101
- package/src/lifecycle-record.ts +2 -2
- package/src/local-branches.ts +1 -1
- package/src/origin.ts +12 -10
- package/src/pack-facts.ts +22 -6
- package/src/pglite-backing.ts +2 -1
- package/src/placeholder.ts +13 -4
- package/src/project-inspect.ts +2 -2
- package/src/redirect-proxy.ts +9 -1
- package/src/redis-backing.ts +4 -2
- package/src/reflect.ts +1 -1
- package/src/root.ts +33 -65
- package/src/runtime-test-support.ts +1 -1
- package/src/runtime.ts +261 -55
- package/src/schema.ts +26 -13
- package/src/served-world.ts +50 -130
- package/src/tail.ts +1 -1
- package/src/up-cli.ts +55 -0
- package/src/websocket-relay.ts +1 -1
- package/src/world-origins.ts +1 -1
- package/src/world-view.ts +3 -3
package/dist/src/init.js
CHANGED
|
@@ -34,20 +34,59 @@
|
|
|
34
34
|
// example value read by production source can block before listen even when vendor coverage is
|
|
35
35
|
// green. `init` never boots the world: it diagnoses, the operator (or the room) boots.
|
|
36
36
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
37
|
-
import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core';
|
|
37
|
+
import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core/runtime';
|
|
38
38
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
39
39
|
import { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorEnvNameForKey, injectorVendorKeysFor, isCredentialShapedEnvName, registryAcknowledgedReason, } from "./covers.js";
|
|
40
40
|
import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
|
|
41
|
-
import { resolveCatalog, twinPackageName } from "./catalog.js";
|
|
41
|
+
import { findInstalledPackage, packCli, packEntry, resolveCatalog, twinPackageName } from "./catalog.js";
|
|
42
|
+
import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-index.js";
|
|
42
43
|
import { projectEnvReads, projectManifestDirs } from "./project-inspect.js";
|
|
43
44
|
import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
|
|
44
|
-
import {
|
|
45
|
+
import { VENDOR_SERVER_BACKINGS } from "./backings.js";
|
|
46
|
+
import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
|
|
45
47
|
import { loadWorldConfig } from "./configs.js";
|
|
48
|
+
/** The names a pack's credential door fills with a file it writes (`file:<field>`, the descriptor's): a path, never a value. */
|
|
49
|
+
function fileCredentialNames() {
|
|
50
|
+
const out = new Set();
|
|
51
|
+
try {
|
|
52
|
+
for (const facts of Object.values(packFacts()))
|
|
53
|
+
for (const [name, spec] of Object.entries(facts?.credentialDoor?.fill ?? {}))
|
|
54
|
+
if (String(spec).startsWith('file:'))
|
|
55
|
+
out.add(name);
|
|
56
|
+
}
|
|
57
|
+
catch { /* no artifact: no file doors known */ }
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** The env stem of a detected vendor whose endpoint this is, when it points at a loopback server; otherwise null. */
|
|
61
|
+
function standInStem(name, value, detected) {
|
|
62
|
+
// an endpoint override only (`S3_ENDPOINT`, `AWS_ENDPOINT_URL`): a vendor's `*_URL` may be its only address (Supabase's
|
|
63
|
+
// project URL) or the app's own (`STRIPE_WEBHOOK_URL`), never a stand-in to drop
|
|
64
|
+
if (!/(^|_)(ENDPOINT|ENDPOINT_URL)$/.test(name) || !/^https?:\/\//.test(value))
|
|
65
|
+
return null;
|
|
66
|
+
let host;
|
|
67
|
+
try {
|
|
68
|
+
host = new URL(value).hostname.replace(/^\[|\]$/g, '');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (!['localhost', '127.0.0.1', '::1', '0.0.0.0', 'host.docker.internal'].includes(host))
|
|
74
|
+
return null;
|
|
75
|
+
for (const vendor of detected.keys()) {
|
|
76
|
+
for (const stem of packFacts()[vendor]?.adoption?.envStems ?? []) {
|
|
77
|
+
if (name.startsWith(`${stem}_`))
|
|
78
|
+
return stem;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/** Names that carry the application's own origin: its base URL (Next's public one included) and its auth library's. */
|
|
84
|
+
const APP_ORIGIN_ENV = /^(?:NEXT_PUBLIC_)?(?:APP_)?(?:BASE_)?URL$|^NEXT_PUBLIC_APP_(?:BASE_)?URL$|^(?:NEXTAUTH|BETTER_AUTH|AUTH)_URL$/;
|
|
46
85
|
// ---------------------------------------------------------------------------------------------
|
|
47
86
|
// The pack catalog
|
|
48
87
|
// ---------------------------------------------------------------------------------------------
|
|
49
88
|
/** The twins available at `root`, sorted — a deterministic input (catalog.ts: this checkout's
|
|
50
|
-
*
|
|
89
|
+
* vendor directories in the pack repository, or the `@volter/twin-<vendor>` packages installed above root). */
|
|
51
90
|
export function packCatalog(root) {
|
|
52
91
|
return resolveCatalog(root).vendors();
|
|
53
92
|
}
|
|
@@ -55,38 +94,13 @@ export function packCatalog(root) {
|
|
|
55
94
|
// Wiring knowledge
|
|
56
95
|
// ---------------------------------------------------------------------------------------------
|
|
57
96
|
/**
|
|
58
|
-
* The
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
|
|
64
|
-
* — inngest@4's own apiBaseUrl/eventBaseUrl getters.
|
|
65
|
-
* ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
|
|
66
|
-
* livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
|
|
67
|
-
* URL the SDK takes as its first constructor argument.
|
|
68
|
-
*
|
|
69
|
-
* A pack that is base-URL-configurable but has NO conventional env var (algolia, pinecone,
|
|
70
|
-
* replicate, fal, twilio, sendblue, figma, notion) is deliberately ABSENT: it gets a service
|
|
71
|
-
* with no endpoint env at all, and the proof reports it UNINTERCEPTABLE. Inventing a
|
|
72
|
-
* `<VENDOR>_BASE_URL` for it would make `covers` call the world covered (any app-read env counts)
|
|
73
|
-
* while the app, which reads no such var, still talks to the real vendor — the exact class of lie
|
|
74
|
-
* the proof exists to catch.
|
|
97
|
+
* The endpoint env for a pack the injector cannot intercept and whose descriptor cannot state it as `endpointEnv`
|
|
98
|
+
* (which receives the twin's URL): a raw protocol, whose client is pointed by host and port. Every name is one a real
|
|
99
|
+
* client documents. A pack with no such env is absent: it gets a service with no endpoint env at all, and the proof
|
|
100
|
+
* reports it UNINTERCEPTABLE. Inventing a `<VENDOR>_BASE_URL` for it would make `covers` call the world covered while
|
|
101
|
+
* the app, which reads no such var, still talks to the real vendor.
|
|
75
102
|
*/
|
|
76
103
|
export const APP_READ_ENDPOINT_ENV = {
|
|
77
|
-
tunnel: {
|
|
78
|
-
injectEnv: 'TUNNEL_SERVER_URL',
|
|
79
|
-
note: 'no injector entry: the native volter-tunnel CLI and @volter/tunnel WebSocket client are endpoint-configured through TUNNEL_SERVER_URL / the SDK host option; native cloudflared is outside Node HTTP injection.',
|
|
80
|
-
},
|
|
81
|
-
// (inngest moved to its pack descriptor's `endpointEnv` — the descriptor-first exemplar (adding-a-twin.md §3).)
|
|
82
|
-
'ai-gateway': {
|
|
83
|
-
injectEnv: 'AI_GATEWAY_BASE_URL',
|
|
84
|
-
note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
|
|
85
|
-
},
|
|
86
|
-
livekit: {
|
|
87
|
-
injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
|
|
88
|
-
note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
|
|
89
|
-
},
|
|
90
104
|
// RAW PROTOCOL. Not "no injector entry yet" — there is no host to map at all: SMTP is a line
|
|
91
105
|
// protocol over a raw TCP socket, so the injector's http/fetch patches never see it, and the
|
|
92
106
|
// relay hostname is whatever the operator configured rather than a fixed vendor host. The env
|
|
@@ -107,31 +121,24 @@ export const APP_READ_ENDPOINT_ENV = {
|
|
|
107
121
|
note: 'no injector entry and none possible: SMTP is a RAW TCP protocol, invisible to the http/fetch injector, '
|
|
108
122
|
+ 'with no fixed vendor host. Interception is the app-read env itself — point SMTP_HOST/SMTP_PORT (or Cal.com\'s '
|
|
109
123
|
+ 'EMAIL_SERVER_HOST/PORT, or NextAuth\'s EMAIL_SERVER url) at the twin\'s listener and an unmodified nodemailer '
|
|
110
|
-
+ 'sends there. Read the mail back
|
|
111
|
-
+ '
|
|
112
|
-
},
|
|
113
|
-
// RAW PROTOCOL, the gRPC kind. The Temporal SDKs dial the frontend address they are given
|
|
114
|
-
// (`Connection.connect({ address })`, `NativeConnection.connect({ address })`), conventionally read
|
|
115
|
-
// from TEMPORAL_ADDRESS — Postiz's temporal.module.ts, the SDK samples and the Temporal CLI all read
|
|
116
|
-
// that name — and speak gRPC over HTTP/2, which the http/fetch injector never sees. The value is an
|
|
117
|
-
// address (host:port), not a URL, which is why this lives here as a template and not as the
|
|
118
|
-
// descriptor's `endpointEnv` (whose `name` receives the twin's URL).
|
|
119
|
-
temporal: {
|
|
120
|
-
injectEnvTemplates: {
|
|
121
|
-
TEMPORAL_ADDRESS: '${host}:${port}',
|
|
122
|
-
},
|
|
123
|
-
note: 'no injector entry and none possible: the Temporal SDKs speak gRPC to the address they are configured with. '
|
|
124
|
-
+ 'TEMPORAL_ADDRESS=<host>:<port> points them at the twin (namespace `default` exists); an app that sets '
|
|
125
|
-
+ 'TEMPORAL_API_KEY turns TLS on in the SDK, which this loopback frontend does not speak — leave it unset.',
|
|
124
|
+
+ 'sends there. Read the mail back at the mailbox the World records beside it: `volter-world url <world> smtp.inspect` '
|
|
125
|
+
+ '(/_twin/messages?to=<address>, newest first; /_twin/messages/<id> for one).',
|
|
126
126
|
},
|
|
127
127
|
};
|
|
128
|
-
|
|
128
|
+
export { VENDOR_SERVER_BACKINGS } from "./backings.js";
|
|
129
|
+
// descriptor-first migration (architecture.md, "The descriptor"): packs now declare their endpoint-env wiring (with its grounding
|
|
129
130
|
// note) on the descriptor (`endpointEnv` on TwinPack); the table above shrinks toward empty as
|
|
130
131
|
// entries move. The overlay throws on a vendor declared in both homes.
|
|
131
132
|
overlayEndpointEnv(APP_READ_ENDPOINT_ENV);
|
|
132
133
|
/** The canonical wiring for one vendor's twin service, derived from the injector where possible. */
|
|
134
|
+
/** A twin service with its pack's credential door (`issues`) as the pack declares it now: a service kept from a saved
|
|
135
|
+
* config gets the current door, so its names are issued by the door the pack has, never by a copy it has since fixed. */
|
|
136
|
+
function withIssues(service) {
|
|
137
|
+
const door = service.type === 'twin' ? packFacts()[service.id]?.credentialDoor : undefined;
|
|
138
|
+
return door === undefined ? service : { ...service, issues: door };
|
|
139
|
+
}
|
|
133
140
|
function wiringFor(vendor) {
|
|
134
|
-
// RULE 2: ask the injector, never guess. `aws` yields s3/
|
|
141
|
+
// RULE 2: ask the injector, never guess. `aws` yields s3/secretsmanager here — the very keys
|
|
135
142
|
// the inert `AWS_TWIN_URL` does not.
|
|
136
143
|
const keys = injectorVendorKeysFor(vendor);
|
|
137
144
|
const twinUrlNames = keys.map(injectorEnvNameForKey);
|
|
@@ -433,25 +440,44 @@ export function parseEnvFile(text, source) {
|
|
|
433
440
|
}
|
|
434
441
|
return entries;
|
|
435
442
|
}
|
|
436
|
-
/** Every example-env entry in the repo,
|
|
437
|
-
*
|
|
443
|
+
/** Every example-env entry in the repo, in a deterministic order: root before workspace members (sorted), and within a
|
|
444
|
+
* dir the ENV_SOURCE_FILES priority. A name defined in several dirs with one value is that value; with different values
|
|
445
|
+
* it is the definition of the dir that holds the application (its Prisma schema or its vercel.json) when exactly one such
|
|
446
|
+
* dir defines it, and otherwise a conflict the operator decides, each definition listed (a landing page's base URL must
|
|
447
|
+
* never silently become the app's). */
|
|
438
448
|
function repoEnvEntries(repo) {
|
|
439
|
-
const
|
|
440
|
-
const entries = [];
|
|
449
|
+
const byName = new Map();
|
|
441
450
|
for (const dir of projectManifestDirs(repo)) {
|
|
451
|
+
const inDir = new Set();
|
|
442
452
|
for (const file of ENV_SOURCE_FILES) {
|
|
443
453
|
const path = resolve(dir, file);
|
|
444
454
|
if (!existsSync(path))
|
|
445
455
|
continue;
|
|
446
456
|
const source = relative(repo, path) || file;
|
|
447
457
|
for (const entry of parseEnvFile(readFileSync(path, 'utf8'), source)) {
|
|
448
|
-
if (
|
|
458
|
+
if (inDir.has(entry.name))
|
|
449
459
|
continue;
|
|
450
|
-
|
|
451
|
-
|
|
460
|
+
inDir.add(entry.name);
|
|
461
|
+
byName.set(entry.name, [...(byName.get(entry.name) ?? []), { ...entry, dir }]);
|
|
452
462
|
}
|
|
453
463
|
}
|
|
454
464
|
}
|
|
465
|
+
const holdsApp = (dir) => existsSync(resolve(dir, 'prisma', 'schema.prisma')) || existsSync(resolve(dir, 'vercel.json'));
|
|
466
|
+
const entries = [];
|
|
467
|
+
for (const defs of byName.values()) {
|
|
468
|
+
const first = defs[0];
|
|
469
|
+
const values = new Set(defs.map((d) => d.value).filter((v) => v !== ''));
|
|
470
|
+
if (values.size <= 1) {
|
|
471
|
+
entries.push({ name: first.name, value: defs.find((d) => d.value !== '')?.value ?? '', source: defs.find((d) => d.value !== '')?.source ?? first.source });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const app = defs.filter((d) => d.value !== '' && holdsApp(d.dir));
|
|
475
|
+
if (app.length === 1) {
|
|
476
|
+
entries.push({ name: first.name, value: app[0].value, source: app[0].source });
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
entries.push({ name: first.name, value: first.value, source: first.source, conflict: defs.filter((d) => d.value !== '').map((d) => `${d.source}=${d.value}`) });
|
|
480
|
+
}
|
|
455
481
|
return entries;
|
|
456
482
|
}
|
|
457
483
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -493,6 +519,15 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
493
519
|
const carried = (path) => (isInside(root, path) ? relative(root, path) : path);
|
|
494
520
|
const catalogSource = resolveCatalog(root);
|
|
495
521
|
const catalog = new Set(catalogSource.vendors());
|
|
522
|
+
// the catalog index's defaults (the installed `@volter/twin-catalog`), which name the twin to install for a vendor
|
|
523
|
+
// this catalog lacks
|
|
524
|
+
const indexDir = findInstalledPackage(root, CATALOG_INDEX_PACKAGE);
|
|
525
|
+
const indexDefaults = (() => { try {
|
|
526
|
+
return indexDir === undefined ? {} : catalogDefaults(indexDir);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return {};
|
|
530
|
+
} })();
|
|
496
531
|
// Pack defaults (defaults/handlers.json, defaults/seed.ts): copied VISIBLY into the world
|
|
497
532
|
// dir — the author's to edit or delete (copy = ownership; never baked into the pack).
|
|
498
533
|
const defaultsCopies = [];
|
|
@@ -520,6 +555,16 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
520
555
|
}
|
|
521
556
|
for (const vendor of [...detected.keys()].sort()) {
|
|
522
557
|
const detectedVia = [...(detected.get(vendor) ?? [])].sort();
|
|
558
|
+
const backing = catalog.has(vendor) ? undefined : VENDOR_SERVER_BACKINGS[vendor];
|
|
559
|
+
if (backing !== undefined) {
|
|
560
|
+
const service = existing?.services.find((entry) => entry.id === vendor) ?? backing.service;
|
|
561
|
+
if (!existingIds.has(vendor))
|
|
562
|
+
for (const name of endpointNames(service))
|
|
563
|
+
claimedEnv.set(name, vendor);
|
|
564
|
+
services.push(service);
|
|
565
|
+
vendors.push({ vendor, pack: null, detectedVia, wiring: 'backing', service, note: backing.service['//'] ?? '' });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
523
568
|
if (!catalog.has(vendor)) {
|
|
524
569
|
vendors.push({
|
|
525
570
|
vendor,
|
|
@@ -527,7 +572,9 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
527
572
|
detectedVia,
|
|
528
573
|
wiring: 'no-pack',
|
|
529
574
|
service: null,
|
|
530
|
-
note:
|
|
575
|
+
note: indexDefaults[vendor] !== undefined
|
|
576
|
+
? `no ${vendor} twin installed — the catalog index's default is ${indexDefaults[vendor].name}@${indexDefaults[vendor].version} (from ${indexDefaults[vendor].source}): install it (\`bun add -d ${indexDefaults[vendor].name}@${indexDefaults[vendor].version}\`) and run init again. Until then the proof reports it MISSING.`
|
|
577
|
+
: `no ${vendor} pack in this catalog — the proof reports it MISSING until one exists.`,
|
|
531
578
|
});
|
|
532
579
|
continue;
|
|
533
580
|
}
|
|
@@ -552,8 +599,15 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
552
599
|
defaultsCopies.push({ vendor, kind: 'handlers', from: defaultsHandlersPath, to: handlersTo });
|
|
553
600
|
const handlersArg = handlersTo === null ? null : carried(handlersTo);
|
|
554
601
|
const defaultsSeedPath = join(packDir, 'defaults', 'seed.ts');
|
|
602
|
+
// a protocol-3 pack's default data (src/semantics/seed.ts: calls to the vendor's own operations, as data) is copied
|
|
603
|
+
// beside a runner that makes each call against the twin, both the author's from then on
|
|
604
|
+
const semanticsSeedPath = join(packDir, 'src', 'semantics', 'seed.ts');
|
|
555
605
|
if (existsSync(defaultsSeedPath))
|
|
556
606
|
defaultsCopies.push({ vendor, kind: 'seed', from: defaultsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
|
|
607
|
+
else if (existsSync(semanticsSeedPath)) {
|
|
608
|
+
defaultsCopies.push({ vendor, kind: 'seed-data', from: semanticsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.data.ts`) });
|
|
609
|
+
defaultsCopies.push({ vendor, kind: 'seed', content: seedCallsRunner(vendor), to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
|
|
610
|
+
}
|
|
557
611
|
// COLOCATION (runtime contract R2a): a pack whose serve factory is known (derived into the
|
|
558
612
|
// pack-facts artifact) is emitted with `colocate`, so `up` boots it inside the ONE
|
|
559
613
|
// volter-world-host process instead of spawning a process per twin. The spawn `command`
|
|
@@ -566,7 +620,7 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
566
620
|
const serveExport = packFacts()[vendor]?.serveExport;
|
|
567
621
|
// In an app repo the twin is a PACKAGE, resolved from the world root at boot (hoisting-proof);
|
|
568
622
|
// inside this checkout it is the checkout-relative path, as the cookbook and the gates expect.
|
|
569
|
-
const boot = catalogSource.
|
|
623
|
+
const boot = catalogSource.kindOf(vendor) === 'installed'
|
|
570
624
|
? {
|
|
571
625
|
package: twinPackageName(vendor),
|
|
572
626
|
...(handlersArg !== null ? { args: ['--scenario', handlersArg] } : {}),
|
|
@@ -574,8 +628,8 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
574
628
|
}
|
|
575
629
|
: {
|
|
576
630
|
command: 'bun',
|
|
577
|
-
args: [
|
|
578
|
-
...(serveExport !== undefined ? { colocate: { module:
|
|
631
|
+
args: [relative(root, packCli(packDir)), 'serve', ...(handlersArg !== null ? ['--scenario', handlersArg] : [])],
|
|
632
|
+
...(serveExport !== undefined ? { colocate: { module: `./${relative(root, packEntry(packDir))}`, export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
|
|
579
633
|
};
|
|
580
634
|
const service = {
|
|
581
635
|
'//': note,
|
|
@@ -589,6 +643,8 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
589
643
|
...(conflict === undefined && wiring.injectEnvTemplates ? { injectEnvTemplates: wiring.injectEnvTemplates } : {}),
|
|
590
644
|
...(conflict === undefined && wiring.cliRedirect ? { cliRedirect: wiring.cliRedirect } : {}),
|
|
591
645
|
...(wiring.serviceEnv ? { env: wiring.serviceEnv } : {}),
|
|
646
|
+
// a twin that holds its keys issues the applications' credential at boot (its descriptor's credentialDoor)
|
|
647
|
+
...(packFacts()[vendor]?.credentialDoor !== undefined ? { issues: packFacts()[vendor].credentialDoor } : {}),
|
|
592
648
|
};
|
|
593
649
|
if (existing !== undefined && !existingIds.has(vendor)) {
|
|
594
650
|
// Check the complete proposed bindings, including any that the fresh-init conflict path
|
|
@@ -613,6 +669,7 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
613
669
|
wiring: savedService !== undefined ? 'explicit-config' : conflict === undefined ? wiring.wiring : 'explicit-config',
|
|
614
670
|
service: savedService ?? service,
|
|
615
671
|
note: savedService !== undefined ? `Retained saved service "${vendor}" and its configured bindings.` : note,
|
|
672
|
+
source: { kind: catalogSource.kindOf(vendor), ...(catalogSource.version(vendor) !== undefined ? { version: catalogSource.version(vendor) } : {}) },
|
|
616
673
|
});
|
|
617
674
|
}
|
|
618
675
|
// --- env ------------------------------------------------------------------------------------
|
|
@@ -652,16 +709,54 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
652
709
|
...(existing?.services ?? []),
|
|
653
710
|
...services.filter((service) => !existingIds.has(service.id)),
|
|
654
711
|
].flatMap(endpointNames));
|
|
712
|
+
// Every name a twin of this World issues (its pack's credentialDoor fill), by the twin: the pack names them, so a name
|
|
713
|
+
// is issued whatever its stem (PlanetScale's DATABASE_URL, Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
|
|
714
|
+
// that vendor's twin is in the World, and never when it is not.
|
|
715
|
+
const issuedBy = new Map();
|
|
716
|
+
for (const service of [...(existing?.services ?? []).map(withIssues), ...services.filter((service) => !existingIds.has(service.id))]) {
|
|
717
|
+
for (const name of Object.keys(service.issues?.fill ?? {}))
|
|
718
|
+
if (!issuedBy.has(name))
|
|
719
|
+
issuedBy.set(name, service.id);
|
|
720
|
+
}
|
|
721
|
+
const standInStems = new Set();
|
|
655
722
|
for (const entry of repoEnvEntries(repo)) {
|
|
656
723
|
if (!envSources.includes(entry.source))
|
|
657
724
|
envSources.push(entry.source);
|
|
658
|
-
const { name, value, source } = entry;
|
|
725
|
+
const { name, value, source, conflict } = entry;
|
|
659
726
|
// World-owned names: the world INJECTS these at boot from the services above. Carrying a repo's
|
|
660
727
|
// stale copy would shadow the live one.
|
|
661
728
|
if (/_TWIN_URL$/.test(name) || /^VOLTER_/.test(name) || worldInjected.has(name)) {
|
|
662
729
|
envRows.push({ name, disposition: 'unknown', source, reason: 'world-owned (injected at boot) — dropped from the emitted env' });
|
|
663
730
|
continue;
|
|
664
731
|
}
|
|
732
|
+
// The application's own origin (its base URL, its auth library's URL): only the booter knows where the app is
|
|
733
|
+
// reached (a local port, a browser tab's preview origin), so the World names it `$app-url` rather than copying the
|
|
734
|
+
// example's production address or faking one the app would then call itself at.
|
|
735
|
+
if (APP_ORIGIN_ENV.test(name)) {
|
|
736
|
+
env[name] = APP_URL_ENV;
|
|
737
|
+
envRows.push({ name, disposition: 'app-origin', source, reason: 'the application\'s own origin — `$app-url`: the address its booter records (`volter-world app-url <world> --set <url>`), resolved when the app attaches' });
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
// a name a twin of this World issues (its descriptor's credentialDoor): `$issue:<service>`, asked of the running twin
|
|
741
|
+
// at boot, so the application holds a key the twin made rather than a fixture it refuses
|
|
742
|
+
const issuer = issuedBy.get(name);
|
|
743
|
+
if (issuer !== undefined) {
|
|
744
|
+
env[name] = `${ISSUE_ENV_PREFIX}${issuer}`;
|
|
745
|
+
envRows.push({ name, disposition: 'issued', source, reason: `${issuer} credential — \`$issue:${issuer}\`: up asks the running twin to issue it` });
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
// a name some pack's credential door fills with a file (`file:<field>`) holds a path: with its issuer unselected, the
|
|
749
|
+
// example's path is never copied
|
|
750
|
+
if (fileCredentialNames().has(name)) {
|
|
751
|
+
env[name] = `/__volter_fake_credentials__/${name.toLowerCase().replace(/_/g, '-')}.json`;
|
|
752
|
+
envRows.push({ name, disposition: 'unknown', source, reason: 'credential file path — select its detected issuer to receive a World key file; the example path is never copied' });
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
// Several directories give it different values and none is the application's: the operator decides.
|
|
756
|
+
if (conflict !== undefined && !isCredentialShapedEnvName(name)) {
|
|
757
|
+
envRows.push({ name, disposition: 'unknown', source, reason: `defined differently in several places (${conflict.join('; ')}) and none of them is the application's directory — you decide`, unset: true });
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
665
760
|
// STRUCTURED credential shapes first: `GOOGLE_VERTEX_JSON` / `GOOGLE_API_CREDENTIALS` carry a
|
|
666
761
|
// whole JSON document the client SDK PARSES (and signs with) before any request leaves the
|
|
667
762
|
// process, and neither name ends in a credential SUFFIX — so the suffix heuristics below are
|
|
@@ -685,6 +780,12 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
685
780
|
});
|
|
686
781
|
continue;
|
|
687
782
|
}
|
|
783
|
+
const served = vendors.filter((v) => v.wiring === 'backing').map((v) => VENDOR_SERVER_BACKINGS[v.vendor]?.env[name]).find((value) => value !== undefined);
|
|
784
|
+
if (served !== undefined) {
|
|
785
|
+
env[name] = served;
|
|
786
|
+
envRows.push({ name, disposition: 'faked', source, reason: 'the key the World\'s own vendor server is started with' });
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
688
789
|
const vendor = envNameVendor(name);
|
|
689
790
|
if (vendor !== null) {
|
|
690
791
|
env[name] = fakeEnvValue(name);
|
|
@@ -699,6 +800,14 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
699
800
|
infraEntries.push({ name, source, kind: infraKind });
|
|
700
801
|
continue;
|
|
701
802
|
}
|
|
803
|
+
// A detected vendor's endpoint at a loopback server is the repository's local stand-in for the vendor (a garage for
|
|
804
|
+
// S3): the World routes the vendor's own host to its twin, so the name is left unset and the SDK addresses that host.
|
|
805
|
+
const standIn = standInStem(name, value, detected);
|
|
806
|
+
if (standIn !== null) {
|
|
807
|
+
standInStems.add(standIn);
|
|
808
|
+
envRows.push({ name, disposition: 'stand-in', source, reason: `a local stand-in for ${standIn}'s own host — left unset; the World routes that host to its twin`, unset: true });
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
702
811
|
// RULE 3: credential-shaped means faked, even when no vendor claims the stem. That covers
|
|
703
812
|
// app-local secrets (JWT_SECRET, ENCRYPTION_KEY) and untwinned vendors alike — and it is the
|
|
704
813
|
// reason a live key committed to `.env.example` can never reach the emitted world.
|
|
@@ -729,34 +838,32 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
729
838
|
envRows.push({ name, disposition: 'unknown', source, reason: 'empty in the example and not credential-shaped — you decide', unset: true });
|
|
730
839
|
}
|
|
731
840
|
}
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
for (const [
|
|
738
|
-
|
|
739
|
-
if (owner === undefined || owner.service === null)
|
|
841
|
+
// A twinned vendor's credential the app's code reads that no example env declares (a script, an app whose keys come
|
|
842
|
+
// from a secrets manager): the SDK refuses to start without it, so it is faked as a declared one would be. Only a
|
|
843
|
+
// credential: a base-URL override the code reads (`OPENAI_BASE_URL`) is optional there, and a fake one would send
|
|
844
|
+
// the SDK nowhere instead of through the injector; and never a framework's public name (`NEXT_PUBLIC_…`), whose
|
|
845
|
+
// value the app's own env files hold for the browser, which a set variable would override
|
|
846
|
+
for (const [name, readAt] of [...envReads.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
847
|
+
if (name in env || envRows.some((row) => row.name === name) || /_TWIN_URL$/.test(name) || /^VOLTER_/.test(name) || worldInjected.has(name))
|
|
740
848
|
continue;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
infraEntries.splice(0, infraEntries.length, ...infraEntries.filter((candidate) => candidate.kind !== kind));
|
|
849
|
+
if (/(?:^|_)(?:URL|URI|ENDPOINT|HOST|BASE|ORIGIN|DOMAIN|REGION|PORT)$/.test(name) || /^(?:NEXT_PUBLIC_|VITE_|REACT_APP_|EXPO_PUBLIC_|NUXT_PUBLIC_|PUBLIC_)/.test(name))
|
|
850
|
+
continue;
|
|
851
|
+
const vendor = envNameVendor(name);
|
|
852
|
+
if (vendor === null || !vendors.some((entry) => entry.vendor === vendor && entry.service !== null))
|
|
853
|
+
continue;
|
|
854
|
+
env[name] = fakeEnvValue(name);
|
|
855
|
+
envRows.push({ name, disposition: 'faked', source: readAt[0] ?? 'source', reason: `${vendor} credential read by the app's code; no example env declares it` });
|
|
856
|
+
}
|
|
857
|
+
// the stand-in's own region (garage's `garage`), where it is not a region of the vendor's shape (`eu-west-1`): the SDK
|
|
858
|
+
// would sign for a region the vendor does not have
|
|
859
|
+
for (const row of envRows) {
|
|
860
|
+
const stem = [...standInStems].find((candidate) => row.name === `${candidate}_REGION`);
|
|
861
|
+
if (stem === undefined || row.disposition !== 'kept' || /^[a-z]{2}(-[a-z]+)+-\d+$/.test(env[row.name] ?? ''))
|
|
862
|
+
continue;
|
|
863
|
+
delete env[row.name];
|
|
864
|
+
row.disposition = 'stand-in';
|
|
865
|
+
row.reason = `the local stand-in's region, not one ${stem}'s vendor has — left unset`;
|
|
866
|
+
row.unset = true;
|
|
760
867
|
}
|
|
761
868
|
// --- infra ----------------------------------------------------------------------------------
|
|
762
869
|
// Kinds with a managed recipe get a private definition next to the config, a declared external
|
|
@@ -823,13 +930,13 @@ export function planWorldInit(name, repoPath, options = {}) {
|
|
|
823
930
|
external: {
|
|
824
931
|
up: ['volter-world-infra', 'up'],
|
|
825
932
|
status: ['volter-world-infra', 'status'],
|
|
826
|
-
discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}
|
|
933
|
+
discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}`, ...(['postgres', 'mysql', 'mongodb', 'redis'].includes(service.kind) ? { protocol: service.kind } : {}) }))),
|
|
827
934
|
down: ['volter-world-infra', 'down'],
|
|
828
935
|
readyWhen: { command: 'volter-world-infra', args: ['status'], timeoutMs: 120000, intervalMs: 500 },
|
|
829
936
|
},
|
|
830
937
|
});
|
|
831
938
|
}
|
|
832
|
-
const mergedServices = existing === undefined ? services : [...existing.services, ...services.filter((service) => !existingIds.has(service.id))];
|
|
939
|
+
const mergedServices = existing === undefined ? services : [...existing.services.map(withIssues), ...services.filter((service) => !existingIds.has(service.id))];
|
|
833
940
|
if (existing !== undefined) {
|
|
834
941
|
const owners = new Map();
|
|
835
942
|
for (const service of mergedServices) {
|
|
@@ -1001,7 +1108,7 @@ export function writeWorldInit(plan, options = {}) {
|
|
|
1001
1108
|
for (const copy of plan.defaultsCopies) {
|
|
1002
1109
|
mkdirSync(dirname(copy.to), { recursive: true });
|
|
1003
1110
|
if (!(options.force === true && existsSync(copy.to)))
|
|
1004
|
-
writeFileSync(copy.to, readFileSync(copy.from));
|
|
1111
|
+
writeFileSync(copy.to, copy.content ?? readFileSync(copy.from));
|
|
1005
1112
|
}
|
|
1006
1113
|
if (plan.seedEntry !== null) {
|
|
1007
1114
|
mkdirSync(dirname(plan.seedEntry.storyPath), { recursive: true });
|
|
@@ -1023,6 +1130,30 @@ export function writeWorldInit(plan, options = {}) {
|
|
|
1023
1130
|
+ `\nawait story();\n`);
|
|
1024
1131
|
}
|
|
1025
1132
|
}
|
|
1133
|
+
/** The runner of a protocol-3 pack's default data (its `src/semantics/seed.ts`, copied beside it as `<vendor>.data.ts`):
|
|
1134
|
+
* each call, in order, against the twin at its `<VENDOR>_TWIN_URL`, a refusal stopping the seed with the twin's answer. */
|
|
1135
|
+
export function seedCallsRunner(vendor) {
|
|
1136
|
+
const stem = vendor.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
1137
|
+
return `// ${vendor}'s default data, as its twin's pack declares it (./${vendor}.data.ts: calls to the vendor's own
|
|
1138
|
+
// operations), made against the twin in order. Ownership transferred on copy: both files are yours — edit or delete.
|
|
1139
|
+
import { seed as calls } from './${vendor}.data.ts';
|
|
1140
|
+
|
|
1141
|
+
export async function seed(): Promise<void> {
|
|
1142
|
+
// a twin with a root is the vendor's real account: its default data is not written (VOLTER_SEED_SKIP)
|
|
1143
|
+
if ((process.env.VOLTER_SEED_SKIP ?? '').split(',').includes('${vendor}')) return;
|
|
1144
|
+
const base = process.env.${stem}_TWIN_URL;
|
|
1145
|
+
if (!base) throw new Error('${stem}_TWIN_URL is not set — bring the world up and run the seed with its env (volter-world attach/run)');
|
|
1146
|
+
for (const call of calls) {
|
|
1147
|
+
const res = await fetch(\`\${base.replace(/\\/$/, '')}\${call.path}\`, {
|
|
1148
|
+
method: call.method,
|
|
1149
|
+
headers: { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) },
|
|
1150
|
+
...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
|
|
1151
|
+
});
|
|
1152
|
+
if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
`;
|
|
1156
|
+
}
|
|
1026
1157
|
/**
|
|
1027
1158
|
* Plan, emit, and PROVE. The coverage proof runs against the just-written config (by path, so it
|
|
1028
1159
|
* reads exactly the bytes on disk rather than the in-memory plan) — `ok` is the proof's verdict and
|
|
@@ -1087,8 +1218,9 @@ export function formatInitReport(result) {
|
|
|
1087
1218
|
if (plan.vendors.length === 0)
|
|
1088
1219
|
lines.push(' (no external vendor dependencies detected in the repo)');
|
|
1089
1220
|
else {
|
|
1090
|
-
lines.push(...table(['vendor', 'wiring', 'inject-env', 'detected-via'], plan.vendors.map((vendor) => [
|
|
1221
|
+
lines.push(...table(['vendor', 'source', 'wiring', 'inject-env', 'detected-via'], plan.vendors.map((vendor) => [
|
|
1091
1222
|
vendor.vendor,
|
|
1223
|
+
vendor.source ? `${vendor.source.kind}${vendor.source.version ? ` ${vendor.source.version}` : ''}` : '-',
|
|
1092
1224
|
vendor.wiring,
|
|
1093
1225
|
[vendor.service?.injectEnv, ...Object.keys(vendor.service?.injectEnvTemplates ?? {})].filter(Boolean).join(', ') || '-',
|
|
1094
1226
|
vendor.detectedVia.join('; '),
|
|
@@ -1102,7 +1234,7 @@ export function formatInitReport(result) {
|
|
|
1102
1234
|
lines.push('');
|
|
1103
1235
|
const counts = (disposition) => plan.envRows.filter((row) => row.disposition === disposition);
|
|
1104
1236
|
lines.push(`Env (${plan.envRows.length} name(s) from ${plan.envSources.join(', ') || 'no example env file'}):`);
|
|
1105
|
-
for (const disposition of ['faked', 'kept', 'compose', 'placeholder', 'unknown']) {
|
|
1237
|
+
for (const disposition of ['faked', 'kept', 'app-origin', 'stand-in', 'compose', 'placeholder', 'unknown']) {
|
|
1106
1238
|
const rows = counts(disposition);
|
|
1107
1239
|
if (rows.length === 0)
|
|
1108
1240
|
continue;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Durable ownership of one World. This inventory does not allocate or reserve capacity.
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
-
import { volterHome } from '@volter/world-core';
|
|
4
|
+
import { volterHome } from '@volter/world-core/runtime';
|
|
5
5
|
import { hostname } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
7
|
-
import { stateDirName, withFileLock } from '@volter/world-core';
|
|
7
|
+
import { stateDirName, withFileLock } from '@volter/world-core/runtime';
|
|
8
8
|
import { checkedOwner } from "./consumers.js";
|
|
9
9
|
const directory = () => join(volterHome(), 'world-lifecycles');
|
|
10
10
|
const identity = (root, world) => createHash('sha256').update(`${resolve(root)}\0${world}`).digest('hex');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// and `volter world view` both make them with this; a hosted World's supervisor makes its own.
|
|
6
6
|
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
|
-
import { stateDirName, withStateRemoval } from '@volter/world-core';
|
|
8
|
+
import { stateDirName, withStateRemoval } from '@volter/world-core/runtime';
|
|
9
9
|
import { clockFile } from "./runtime.js";
|
|
10
10
|
import { loadWorldConfig } from "./configs.js";
|
|
11
11
|
import { TOKEN_HEADER } from "./served-world.js";
|
package/dist/src/origin.d.ts
CHANGED
package/dist/src/origin.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { captureHistory, historyDigest, publishOriginHistory, readHistoryView, historyLength, worldPaths } from '@volter/world-core';
|
|
1
|
+
import { captureHistory, historyDigest, publishOriginHistory, readHistoryView, historyLength, worldPaths } from '@volter/world-core/runtime';
|
|
2
2
|
// `volter world clone` and `fetch` — a world's ORIGIN is another world, served (docs/concepts/the-model.md
|
|
3
3
|
// #a-branch-is-a-position-not-a-copy: a clone is a branch whose parent is at a URL): its whole log per twin is this world's PARENT. `clone` records the origin
|
|
4
4
|
// and brings every twin's log in from position zero; `fetch` appends what the origin has past the
|
|
5
5
|
// recorded position. Nothing here touches a vendor: an origin is always a world, the token is the
|
|
6
6
|
// served world's, and what arrives are entries — the parent log grows, the branch log is untouched.
|
|
7
7
|
import { join, resolve } from 'node:path';
|
|
8
|
-
import { getActiveWorldStore, readBranchMeta,
|
|
8
|
+
import { getActiveWorldStore, readBranchMeta, withHistoryLock, writeBranchMeta } from '@volter/world-core/runtime';
|
|
9
9
|
import { saveWorldInstance, statusWorld } from "./runtime.js";
|
|
10
10
|
import { fetchReady, isPathRemote, localWorld, TOKEN_HEADER } from "./served-world.js";
|
|
11
11
|
import { loadWorldConfig, writeWorldConfig } from "./configs.js";
|
|
@@ -47,14 +47,15 @@ export async function fetchFromOrigin(name, options) {
|
|
|
47
47
|
const appended = {};
|
|
48
48
|
const behind = {};
|
|
49
49
|
const snapshots = {};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
for (const service of services) {
|
|
51
|
+
const controlRoot = join(instance.dirs.data, service);
|
|
52
|
+
const state = stateServiceOf(controlRoot, service);
|
|
53
|
+
withHistoryLock(worldPaths(state, controlRoot).dir, () => {
|
|
54
|
+
const meta = readBranchMeta(state, controlRoot);
|
|
54
55
|
if (meta?.parent && !meta.origin)
|
|
55
56
|
throw new Error('Cannot attach a first origin to an existing local fork; clone the origin before branching');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
58
59
|
for (const service of services) {
|
|
59
60
|
const controlRoot = join(instance.dirs.data, service);
|
|
60
61
|
const state = stateServiceOf(controlRoot, service);
|
|
@@ -62,7 +63,8 @@ export async function fetchFromOrigin(name, options) {
|
|
|
62
63
|
// A PARENT ON THIS MACHINE is read live through the pointer, like a local branch (contract
|
|
63
64
|
// "Just like Neon", 1): nothing is copied; a clone points at the parent's head, a fetch only
|
|
64
65
|
// says how far the parent has moved past this branch's position
|
|
65
|
-
|
|
66
|
+
// under the branch's history lock, as rebase is: a fetch cannot restore a pointer a concurrent rebase advanced
|
|
67
|
+
withHistoryLock(worldPaths(state, controlRoot).dir, () => {
|
|
66
68
|
const target = localWorld(origin.url);
|
|
67
69
|
const at = join(target.instance.dirs.data, service);
|
|
68
70
|
const parentState = stateServiceOf(at, service);
|
|
@@ -113,7 +115,7 @@ export async function fetchFromOrigin(name, options) {
|
|
|
113
115
|
}
|
|
114
116
|
if (historyDigest({ layout: first.layout, entries }) !== first.digest)
|
|
115
117
|
throw new Error('Origin history digest mismatch');
|
|
116
|
-
|
|
118
|
+
withHistoryLock(worldPaths(state, controlRoot).dir, () => {
|
|
117
119
|
const meta = readBranchMeta(state, controlRoot);
|
|
118
120
|
if (meta?.parent && !meta.origin)
|
|
119
121
|
throw new Error('Cannot attach a first origin to an existing local fork; clone the origin before branching');
|