@volter/world-runtime 2.0.55 → 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/src/init.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
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 {
|
|
40
40
|
coverWorld,
|
|
@@ -48,12 +48,23 @@ import {
|
|
|
48
48
|
type CoverageReport,
|
|
49
49
|
} from './covers.ts';
|
|
50
50
|
import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from './fixture-env.ts';
|
|
51
|
-
import { resolveCatalog, twinPackageName, type Catalog } from './catalog.ts';
|
|
51
|
+
import { findInstalledPackage, packCli, packEntry, resolveCatalog, twinPackageName, type Catalog } from './catalog.ts';
|
|
52
|
+
import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from './catalog-index.ts';
|
|
52
53
|
import { projectEnvReads, projectManifestDirs } from './project-inspect.ts';
|
|
53
54
|
import { overlayEndpointEnv, packFacts } from './pack-facts.ts';
|
|
54
|
-
import {
|
|
55
|
+
import { VENDOR_SERVER_BACKINGS } from './backings.ts';
|
|
56
|
+
import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes, type WorldConfig, type WorldSelection, type WorldServiceConfig } from './schema.ts';
|
|
55
57
|
import { loadWorldConfig } from './configs.ts';
|
|
56
58
|
|
|
59
|
+
/** The names a pack's credential door fills with a file it writes (`file:<field>`, the descriptor's): a path, never a value. */
|
|
60
|
+
function fileCredentialNames(): Set<string> {
|
|
61
|
+
const out = new Set<string>();
|
|
62
|
+
try {
|
|
63
|
+
for (const facts of Object.values(packFacts())) for (const [name, spec] of Object.entries(facts?.credentialDoor?.fill ?? {})) if (String(spec).startsWith('file:')) out.add(name);
|
|
64
|
+
} catch { /* no artifact: no file doors known */ }
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
/** How a twin service in the emitted world is reached by the app's traffic. */
|
|
58
69
|
export type InitWiring =
|
|
59
70
|
/** the injector redirects the vendor's real hosts; `injectEnv` is a var it genuinely reads */
|
|
@@ -62,7 +73,9 @@ export type InitWiring =
|
|
|
62
73
|
| 'app-read'
|
|
63
74
|
/** no injector entry and no endpoint env: the SDK is pointed at the twin by explicit client config */
|
|
64
75
|
| 'explicit-config'
|
|
65
|
-
/**
|
|
76
|
+
/** the vendor's own open-source server runs as the World's service (VENDOR_SERVER_BACKINGS) */
|
|
77
|
+
| 'backing'
|
|
78
|
+
/** detected vendor with no vendor pack in the catalog — nothing to emit */
|
|
66
79
|
| 'no-pack';
|
|
67
80
|
|
|
68
81
|
export type InitVendorPlan = {
|
|
@@ -76,18 +89,47 @@ export type InitVendorPlan = {
|
|
|
76
89
|
service: WorldServiceConfig | null;
|
|
77
90
|
/** why this wiring — printed in the report and embedded in the service's `//` note */
|
|
78
91
|
note: string;
|
|
92
|
+
/** the source init chose the pack from (architecture, "The catalog: where twins come from"): where it comes from, its
|
|
93
|
+
* protocol and its version */
|
|
94
|
+
source?: { kind: 'checkout' | 'installed'; version?: string };
|
|
79
95
|
};
|
|
80
96
|
|
|
97
|
+
/** The env stem of a detected vendor whose endpoint this is, when it points at a loopback server; otherwise null. */
|
|
98
|
+
function standInStem(name: string, value: string, detected: ReadonlyMap<string, unknown>): string | null {
|
|
99
|
+
// an endpoint override only (`S3_ENDPOINT`, `AWS_ENDPOINT_URL`): a vendor's `*_URL` may be its only address (Supabase's
|
|
100
|
+
// project URL) or the app's own (`STRIPE_WEBHOOK_URL`), never a stand-in to drop
|
|
101
|
+
if (!/(^|_)(ENDPOINT|ENDPOINT_URL)$/.test(name) || !/^https?:\/\//.test(value)) return null;
|
|
102
|
+
let host: string;
|
|
103
|
+
try { host = new URL(value).hostname.replace(/^\[|\]$/g, ''); } catch { return null; }
|
|
104
|
+
if (!['localhost', '127.0.0.1', '::1', '0.0.0.0', 'host.docker.internal'].includes(host)) return null;
|
|
105
|
+
for (const vendor of detected.keys()) {
|
|
106
|
+
for (const stem of packFacts()[vendor]?.adoption?.envStems ?? []) {
|
|
107
|
+
if (name.startsWith(`${stem}_`)) return stem;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Names that carry the application's own origin: its base URL (Next's public one included) and its auth library's. */
|
|
114
|
+
const APP_ORIGIN_ENV = /^(?:NEXT_PUBLIC_)?(?:APP_)?(?:BASE_)?URL$|^NEXT_PUBLIC_APP_(?:BASE_)?URL$|^(?:NEXTAUTH|BETTER_AUTH|AUTH)_URL$/;
|
|
115
|
+
|
|
81
116
|
/** What `init` did with one env NAME found in the repo's example env files. */
|
|
82
117
|
export type EnvDisposition =
|
|
83
118
|
/** credential-shaped → replaced by a structurally valid fake (never the committed value) */
|
|
84
119
|
| 'faked'
|
|
120
|
+
/** a credential the vendor's twin issues at boot → `$issue:<vendor>` (the twin refuses a key it did not make) */
|
|
121
|
+
| 'issued'
|
|
85
122
|
/** non-credential app config → the example value copied verbatim */
|
|
86
123
|
| 'kept'
|
|
87
124
|
/** infra connection string → points at a World-managed infrastructure service */
|
|
88
125
|
| 'compose'
|
|
89
126
|
/** infra connection string → a documented placeholder the operator fills in */
|
|
90
127
|
| 'placeholder'
|
|
128
|
+
/** the application's own origin → `$app-url`, the address its booter records, resolved when it attaches */
|
|
129
|
+
| 'app-origin'
|
|
130
|
+
/** a detected vendor's endpoint pointed at a local stand-in server (a garage or MinIO for S3) → left unset, so the SDK
|
|
131
|
+
* addresses the vendor's own host, which the World routes to its twin */
|
|
132
|
+
| 'stand-in'
|
|
91
133
|
/** neither credential-shaped nor valued: the operator has to decide */
|
|
92
134
|
| 'unknown';
|
|
93
135
|
|
|
@@ -172,7 +214,8 @@ export type InitPlan = {
|
|
|
172
214
|
acknowledgedExternal: Array<{ name: string; reason: string }>;
|
|
173
215
|
/** pack `defaults/` files copied into the world dir (TWIN-PROGRAMMING-MODEL: default
|
|
174
216
|
* reasonable — visible starter seeds/handlers, ownership transfers on copy). */
|
|
175
|
-
|
|
217
|
+
/** `from` is copied byte for byte; `content` (a runner init writes for a protocol-3 pack's seed data) is written as given. */
|
|
218
|
+
defaultsCopies: Array<{ vendor: string; kind: 'handlers' | 'seed' | 'seed-data'; from?: string; content?: string; to: string }>;
|
|
176
219
|
/** emitted when any pack ships a default seed: the one explicit entry point + story skeleton */
|
|
177
220
|
seedEntry: { entryPath: string; storyPath: string } | null;
|
|
178
221
|
/** Explicit wiring for new default seeds when the existing entry point is operator-owned. */
|
|
@@ -206,7 +249,7 @@ export type InitResult = {
|
|
|
206
249
|
|
|
207
250
|
export type InitOptions = {
|
|
208
251
|
/** the WORLD root: where the emitted config boots from, and where the catalog is resolved —
|
|
209
|
-
*
|
|
252
|
+
* the sibling pack repository when init runs here, else the `@volter/twin-<vendor>`
|
|
210
253
|
* packages installed above it (docs/concepts/worlds.md#the-config-and-the-running-world). Defaults to the
|
|
211
254
|
* application repo. */
|
|
212
255
|
root?: string;
|
|
@@ -226,7 +269,7 @@ export type InitOptions = {
|
|
|
226
269
|
// ---------------------------------------------------------------------------------------------
|
|
227
270
|
|
|
228
271
|
/** The twins available at `root`, sorted — a deterministic input (catalog.ts: this checkout's
|
|
229
|
-
*
|
|
272
|
+
* vendor directories in the pack repository, or the `@volter/twin-<vendor>` packages installed above root). */
|
|
230
273
|
export function packCatalog(root: string): string[] {
|
|
231
274
|
return resolveCatalog(root).vendors();
|
|
232
275
|
}
|
|
@@ -236,38 +279,13 @@ export function packCatalog(root: string): string[] {
|
|
|
236
279
|
// ---------------------------------------------------------------------------------------------
|
|
237
280
|
|
|
238
281
|
/**
|
|
239
|
-
* The
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
|
|
245
|
-
* — inngest@4's own apiBaseUrl/eventBaseUrl getters.
|
|
246
|
-
* ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
|
|
247
|
-
* livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
|
|
248
|
-
* URL the SDK takes as its first constructor argument.
|
|
249
|
-
*
|
|
250
|
-
* A pack that is base-URL-configurable but has NO conventional env var (algolia, pinecone,
|
|
251
|
-
* replicate, fal, twilio, sendblue, figma, notion) is deliberately ABSENT: it gets a service
|
|
252
|
-
* with no endpoint env at all, and the proof reports it UNINTERCEPTABLE. Inventing a
|
|
253
|
-
* `<VENDOR>_BASE_URL` for it would make `covers` call the world covered (any app-read env counts)
|
|
254
|
-
* while the app, which reads no such var, still talks to the real vendor — the exact class of lie
|
|
255
|
-
* the proof exists to catch.
|
|
282
|
+
* The endpoint env for a pack the injector cannot intercept and whose descriptor cannot state it as `endpointEnv`
|
|
283
|
+
* (which receives the twin's URL): a raw protocol, whose client is pointed by host and port. Every name is one a real
|
|
284
|
+
* client documents. A pack with no such env is absent: it gets a service with no endpoint env at all, and the proof
|
|
285
|
+
* reports it UNINTERCEPTABLE. Inventing a `<VENDOR>_BASE_URL` for it would make `covers` call the world covered while
|
|
286
|
+
* the app, which reads no such var, still talks to the real vendor.
|
|
256
287
|
*/
|
|
257
288
|
export const APP_READ_ENDPOINT_ENV: Record<string, { injectEnv?: string; injectEnvTemplates?: Record<string, string>; note: string }> = {
|
|
258
|
-
tunnel: {
|
|
259
|
-
injectEnv: 'TUNNEL_SERVER_URL',
|
|
260
|
-
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.',
|
|
261
|
-
},
|
|
262
|
-
// (inngest moved to its pack descriptor's `endpointEnv` — the descriptor-first exemplar (adding-a-twin.md §3).)
|
|
263
|
-
'ai-gateway': {
|
|
264
|
-
injectEnv: 'AI_GATEWAY_BASE_URL',
|
|
265
|
-
note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
|
|
266
|
-
},
|
|
267
|
-
livekit: {
|
|
268
|
-
injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
|
|
269
|
-
note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
|
|
270
|
-
},
|
|
271
289
|
// RAW PROTOCOL. Not "no injector entry yet" — there is no host to map at all: SMTP is a line
|
|
272
290
|
// protocol over a raw TCP socket, so the injector's http/fetch patches never see it, and the
|
|
273
291
|
// relay hostname is whatever the operator configured rather than a fixed vendor host. The env
|
|
@@ -288,30 +306,25 @@ export const APP_READ_ENDPOINT_ENV: Record<string, { injectEnv?: string; injectE
|
|
|
288
306
|
note: 'no injector entry and none possible: SMTP is a RAW TCP protocol, invisible to the http/fetch injector, '
|
|
289
307
|
+ 'with no fixed vendor host. Interception is the app-read env itself — point SMTP_HOST/SMTP_PORT (or Cal.com\'s '
|
|
290
308
|
+ 'EMAIL_SERVER_HOST/PORT, or NextAuth\'s EMAIL_SERVER url) at the twin\'s listener and an unmodified nodemailer '
|
|
291
|
-
+ 'sends there. Read the mail back
|
|
292
|
-
+ '
|
|
293
|
-
},
|
|
294
|
-
// RAW PROTOCOL, the gRPC kind. The Temporal SDKs dial the frontend address they are given
|
|
295
|
-
// (`Connection.connect({ address })`, `NativeConnection.connect({ address })`), conventionally read
|
|
296
|
-
// from TEMPORAL_ADDRESS — Postiz's temporal.module.ts, the SDK samples and the Temporal CLI all read
|
|
297
|
-
// that name — and speak gRPC over HTTP/2, which the http/fetch injector never sees. The value is an
|
|
298
|
-
// address (host:port), not a URL, which is why this lives here as a template and not as the
|
|
299
|
-
// descriptor's `endpointEnv` (whose `name` receives the twin's URL).
|
|
300
|
-
temporal: {
|
|
301
|
-
injectEnvTemplates: {
|
|
302
|
-
TEMPORAL_ADDRESS: '${host}:${port}',
|
|
303
|
-
},
|
|
304
|
-
note: 'no injector entry and none possible: the Temporal SDKs speak gRPC to the address they are configured with. '
|
|
305
|
-
+ 'TEMPORAL_ADDRESS=<host>:<port> points them at the twin (namespace `default` exists); an app that sets '
|
|
306
|
-
+ 'TEMPORAL_API_KEY turns TLS on in the SDK, which this loopback frontend does not speak — leave it unset.',
|
|
309
|
+
+ 'sends there. Read the mail back at the mailbox the World records beside it: `volter-world url <world> smtp.inspect` '
|
|
310
|
+
+ '(/_twin/messages?to=<address>, newest first; /_twin/messages/<id> for one).',
|
|
307
311
|
},
|
|
308
312
|
};
|
|
309
|
-
|
|
313
|
+
export { VENDOR_SERVER_BACKINGS } from './backings.ts';
|
|
314
|
+
|
|
315
|
+
// descriptor-first migration (architecture.md, "The descriptor"): packs now declare their endpoint-env wiring (with its grounding
|
|
310
316
|
// note) on the descriptor (`endpointEnv` on TwinPack); the table above shrinks toward empty as
|
|
311
317
|
// entries move. The overlay throws on a vendor declared in both homes.
|
|
312
318
|
overlayEndpointEnv(APP_READ_ENDPOINT_ENV);
|
|
313
319
|
|
|
314
320
|
/** The canonical wiring for one vendor's twin service, derived from the injector where possible. */
|
|
321
|
+
/** A twin service with its pack's credential door (`issues`) as the pack declares it now: a service kept from a saved
|
|
322
|
+
* config gets the current door, so its names are issued by the door the pack has, never by a copy it has since fixed. */
|
|
323
|
+
function withIssues(service: WorldServiceConfig): WorldServiceConfig {
|
|
324
|
+
const door = service.type === 'twin' ? packFacts()[service.id]?.credentialDoor : undefined;
|
|
325
|
+
return door === undefined ? service : { ...service, issues: door };
|
|
326
|
+
}
|
|
327
|
+
|
|
315
328
|
function wiringFor(vendor: string): {
|
|
316
329
|
wiring: Exclude<InitWiring, 'no-pack'>;
|
|
317
330
|
injectEnv?: string;
|
|
@@ -320,7 +333,7 @@ function wiringFor(vendor: string): {
|
|
|
320
333
|
serviceEnv?: Record<string, string>;
|
|
321
334
|
note: string;
|
|
322
335
|
} {
|
|
323
|
-
// RULE 2: ask the injector, never guess. `aws` yields s3/
|
|
336
|
+
// RULE 2: ask the injector, never guess. `aws` yields s3/secretsmanager here — the very keys
|
|
324
337
|
// the inert `AWS_TWIN_URL` does not.
|
|
325
338
|
const keys = injectorVendorKeysFor(vendor);
|
|
326
339
|
const twinUrlNames = keys.map(injectorEnvNameForKey);
|
|
@@ -607,7 +620,7 @@ export function renderComposeFile(plan: InitPlan): string {
|
|
|
607
620
|
return `${lines.join('\n')}\n`;
|
|
608
621
|
}
|
|
609
622
|
|
|
610
|
-
export type ParsedEnvEntry = { name: string; value: string; source: string };
|
|
623
|
+
export type ParsedEnvEntry = { name: string; value: string; source: string; /** several dirs define it differently: each `source=value` */ conflict?: string[] };
|
|
611
624
|
|
|
612
625
|
/** Parse one dotenv-ish file into ordered NAME/value pairs. Deliberately conservative: the same
|
|
613
626
|
* `NAME=` line shape `projectEnvNames` detects, plus quote stripping, so the two views of a repo's
|
|
@@ -634,23 +647,36 @@ export function parseEnvFile(text: string, source: string): ParsedEnvEntry[] {
|
|
|
634
647
|
return entries;
|
|
635
648
|
}
|
|
636
649
|
|
|
637
|
-
/** Every example-env entry in the repo,
|
|
638
|
-
*
|
|
650
|
+
/** Every example-env entry in the repo, in a deterministic order: root before workspace members (sorted), and within a
|
|
651
|
+
* dir the ENV_SOURCE_FILES priority. A name defined in several dirs with one value is that value; with different values
|
|
652
|
+
* it is the definition of the dir that holds the application (its Prisma schema or its vercel.json) when exactly one such
|
|
653
|
+
* dir defines it, and otherwise a conflict the operator decides, each definition listed (a landing page's base URL must
|
|
654
|
+
* never silently become the app's). */
|
|
639
655
|
function repoEnvEntries(repo: string): ParsedEnvEntry[] {
|
|
640
|
-
const
|
|
641
|
-
const entries: ParsedEnvEntry[] = [];
|
|
656
|
+
const byName = new Map<string, Array<ParsedEnvEntry & { dir: string }>>();
|
|
642
657
|
for (const dir of projectManifestDirs(repo)) {
|
|
658
|
+
const inDir = new Set<string>();
|
|
643
659
|
for (const file of ENV_SOURCE_FILES) {
|
|
644
660
|
const path = resolve(dir, file);
|
|
645
661
|
if (!existsSync(path)) continue;
|
|
646
662
|
const source = relative(repo, path) || file;
|
|
647
663
|
for (const entry of parseEnvFile(readFileSync(path, 'utf8'), source)) {
|
|
648
|
-
if (
|
|
649
|
-
|
|
650
|
-
|
|
664
|
+
if (inDir.has(entry.name)) continue;
|
|
665
|
+
inDir.add(entry.name);
|
|
666
|
+
byName.set(entry.name, [...(byName.get(entry.name) ?? []), { ...entry, dir }]);
|
|
651
667
|
}
|
|
652
668
|
}
|
|
653
669
|
}
|
|
670
|
+
const holdsApp = (dir: string): boolean => existsSync(resolve(dir, 'prisma', 'schema.prisma')) || existsSync(resolve(dir, 'vercel.json'));
|
|
671
|
+
const entries: ParsedEnvEntry[] = [];
|
|
672
|
+
for (const defs of byName.values()) {
|
|
673
|
+
const first = defs[0]!;
|
|
674
|
+
const values = new Set(defs.map((d) => d.value).filter((v) => v !== ''));
|
|
675
|
+
if (values.size <= 1) { entries.push({ name: first.name, value: defs.find((d) => d.value !== '')?.value ?? '', source: defs.find((d) => d.value !== '')?.source ?? first.source }); continue; }
|
|
676
|
+
const app = defs.filter((d) => d.value !== '' && holdsApp(d.dir));
|
|
677
|
+
if (app.length === 1) { entries.push({ name: first.name, value: app[0]!.value, source: app[0]!.source }); continue; }
|
|
678
|
+
entries.push({ name: first.name, value: first.value, source: first.source, conflict: defs.filter((d) => d.value !== '').map((d) => `${d.source}=${d.value}`) });
|
|
679
|
+
}
|
|
654
680
|
return entries;
|
|
655
681
|
}
|
|
656
682
|
|
|
@@ -696,9 +722,13 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
696
722
|
|
|
697
723
|
const catalogSource: Catalog = resolveCatalog(root);
|
|
698
724
|
const catalog = new Set(catalogSource.vendors());
|
|
725
|
+
// the catalog index's defaults (the installed `@volter/twin-catalog`), which name the twin to install for a vendor
|
|
726
|
+
// this catalog lacks
|
|
727
|
+
const indexDir = findInstalledPackage(root, CATALOG_INDEX_PACKAGE);
|
|
728
|
+
const indexDefaults = (() => { try { return indexDir === undefined ? {} : catalogDefaults(indexDir); } catch { return {}; } })();
|
|
699
729
|
// Pack defaults (defaults/handlers.json, defaults/seed.ts): copied VISIBLY into the world
|
|
700
730
|
// dir — the author's to edit or delete (copy = ownership; never baked into the pack).
|
|
701
|
-
const defaultsCopies:
|
|
731
|
+
const defaultsCopies: InitPlan['defaultsCopies'] = [];
|
|
702
732
|
const signals = options.vendors ? null : detectRepoVendors(repo);
|
|
703
733
|
const detected = options.vendors
|
|
704
734
|
? new Map<string, string[]>(options.vendors.map((v) => [v, ['--twins']]))
|
|
@@ -723,6 +753,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
723
753
|
}
|
|
724
754
|
for (const vendor of [...detected.keys()].sort()) {
|
|
725
755
|
const detectedVia = [...(detected.get(vendor) ?? [])].sort();
|
|
756
|
+
const backing = catalog.has(vendor) ? undefined : VENDOR_SERVER_BACKINGS[vendor];
|
|
757
|
+
if (backing !== undefined) {
|
|
758
|
+
const service = existing?.services.find((entry) => entry.id === vendor) ?? backing.service;
|
|
759
|
+
if (!existingIds.has(vendor)) for (const name of endpointNames(service)) claimedEnv.set(name, vendor);
|
|
760
|
+
services.push(service);
|
|
761
|
+
vendors.push({ vendor, pack: null, detectedVia, wiring: 'backing', service, note: backing.service['//'] ?? '' });
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
726
764
|
if (!catalog.has(vendor)) {
|
|
727
765
|
vendors.push({
|
|
728
766
|
vendor,
|
|
@@ -730,7 +768,9 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
730
768
|
detectedVia,
|
|
731
769
|
wiring: 'no-pack',
|
|
732
770
|
service: null,
|
|
733
|
-
note:
|
|
771
|
+
note: indexDefaults[vendor] !== undefined
|
|
772
|
+
? `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.`
|
|
773
|
+
: `no ${vendor} pack in this catalog — the proof reports it MISSING until one exists.`,
|
|
734
774
|
});
|
|
735
775
|
continue;
|
|
736
776
|
}
|
|
@@ -753,7 +793,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
753
793
|
if (handlersTo !== null) defaultsCopies.push({ vendor, kind: 'handlers', from: defaultsHandlersPath, to: handlersTo });
|
|
754
794
|
const handlersArg = handlersTo === null ? null : carried(handlersTo);
|
|
755
795
|
const defaultsSeedPath = join(packDir, 'defaults', 'seed.ts');
|
|
796
|
+
// a protocol-3 pack's default data (src/semantics/seed.ts: calls to the vendor's own operations, as data) is copied
|
|
797
|
+
// beside a runner that makes each call against the twin, both the author's from then on
|
|
798
|
+
const semanticsSeedPath = join(packDir, 'src', 'semantics', 'seed.ts');
|
|
756
799
|
if (existsSync(defaultsSeedPath)) defaultsCopies.push({ vendor, kind: 'seed', from: defaultsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
|
|
800
|
+
else if (existsSync(semanticsSeedPath)) {
|
|
801
|
+
defaultsCopies.push({ vendor, kind: 'seed-data', from: semanticsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.data.ts`) });
|
|
802
|
+
defaultsCopies.push({ vendor, kind: 'seed', content: seedCallsRunner(vendor), to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
|
|
803
|
+
}
|
|
757
804
|
// COLOCATION (runtime contract R2a): a pack whose serve factory is known (derived into the
|
|
758
805
|
// pack-facts artifact) is emitted with `colocate`, so `up` boots it inside the ONE
|
|
759
806
|
// volter-world-host process instead of spawning a process per twin. The spawn `command`
|
|
@@ -766,7 +813,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
766
813
|
const serveExport = packFacts()[vendor]?.serveExport;
|
|
767
814
|
// In an app repo the twin is a PACKAGE, resolved from the world root at boot (hoisting-proof);
|
|
768
815
|
// inside this checkout it is the checkout-relative path, as the cookbook and the gates expect.
|
|
769
|
-
const boot: Pick<WorldServiceConfig, 'package' | 'command' | 'args' | 'colocate'> = catalogSource.
|
|
816
|
+
const boot: Pick<WorldServiceConfig, 'package' | 'command' | 'args' | 'colocate'> = catalogSource.kindOf(vendor) === 'installed'
|
|
770
817
|
? {
|
|
771
818
|
package: twinPackageName(vendor),
|
|
772
819
|
...(handlersArg !== null ? { args: ['--scenario', handlersArg] } : {}),
|
|
@@ -774,8 +821,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
774
821
|
}
|
|
775
822
|
: {
|
|
776
823
|
command: 'bun',
|
|
777
|
-
args: [
|
|
778
|
-
...(serveExport !== undefined ? { colocate: { module:
|
|
824
|
+
args: [relative(root, packCli(packDir)!), 'serve', ...(handlersArg !== null ? ['--scenario', handlersArg] : [])],
|
|
825
|
+
...(serveExport !== undefined ? { colocate: { module: `./${relative(root, packEntry(packDir))}`, export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
|
|
779
826
|
};
|
|
780
827
|
const service: WorldServiceConfig = {
|
|
781
828
|
'//': note,
|
|
@@ -789,6 +836,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
789
836
|
...(conflict === undefined && wiring.injectEnvTemplates ? { injectEnvTemplates: wiring.injectEnvTemplates } : {}),
|
|
790
837
|
...(conflict === undefined && wiring.cliRedirect ? { cliRedirect: wiring.cliRedirect } : {}),
|
|
791
838
|
...(wiring.serviceEnv ? { env: wiring.serviceEnv } : {}),
|
|
839
|
+
// a twin that holds its keys issues the applications' credential at boot (its descriptor's credentialDoor)
|
|
840
|
+
...(packFacts()[vendor]?.credentialDoor !== undefined ? { issues: packFacts()[vendor]!.credentialDoor! } : {}),
|
|
792
841
|
};
|
|
793
842
|
if (existing !== undefined && !existingIds.has(vendor)) {
|
|
794
843
|
// Check the complete proposed bindings, including any that the fresh-init conflict path
|
|
@@ -811,6 +860,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
811
860
|
wiring: savedService !== undefined ? 'explicit-config' : conflict === undefined ? wiring.wiring : 'explicit-config',
|
|
812
861
|
service: savedService ?? service,
|
|
813
862
|
note: savedService !== undefined ? `Retained saved service "${vendor}" and its configured bindings.` : note,
|
|
863
|
+
source: { kind: catalogSource.kindOf(vendor), ...(catalogSource.version(vendor) !== undefined ? { version: catalogSource.version(vendor) } : {}) },
|
|
814
864
|
});
|
|
815
865
|
}
|
|
816
866
|
|
|
@@ -847,15 +897,51 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
847
897
|
...(existing?.services ?? []),
|
|
848
898
|
...services.filter((service) => !existingIds.has(service.id)),
|
|
849
899
|
].flatMap(endpointNames));
|
|
900
|
+
// Every name a twin of this World issues (its pack's credentialDoor fill), by the twin: the pack names them, so a name
|
|
901
|
+
// is issued whatever its stem (PlanetScale's DATABASE_URL, Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
|
|
902
|
+
// that vendor's twin is in the World, and never when it is not.
|
|
903
|
+
const issuedBy = new Map<string, string>();
|
|
904
|
+
for (const service of [...(existing?.services ?? []).map(withIssues), ...services.filter((service) => !existingIds.has(service.id))]) {
|
|
905
|
+
for (const name of Object.keys(service.issues?.fill ?? {})) if (!issuedBy.has(name)) issuedBy.set(name, service.id);
|
|
906
|
+
}
|
|
907
|
+
const standInStems = new Set<string>();
|
|
850
908
|
for (const entry of repoEnvEntries(repo)) {
|
|
851
909
|
if (!envSources.includes(entry.source)) envSources.push(entry.source);
|
|
852
|
-
const { name, value, source } = entry;
|
|
910
|
+
const { name, value, source, conflict } = entry;
|
|
853
911
|
// World-owned names: the world INJECTS these at boot from the services above. Carrying a repo's
|
|
854
912
|
// stale copy would shadow the live one.
|
|
855
913
|
if (/_TWIN_URL$/.test(name) || /^VOLTER_/.test(name) || worldInjected.has(name)) {
|
|
856
914
|
envRows.push({ name, disposition: 'unknown', source, reason: 'world-owned (injected at boot) — dropped from the emitted env' });
|
|
857
915
|
continue;
|
|
858
916
|
}
|
|
917
|
+
// The application's own origin (its base URL, its auth library's URL): only the booter knows where the app is
|
|
918
|
+
// reached (a local port, a browser tab's preview origin), so the World names it `$app-url` rather than copying the
|
|
919
|
+
// example's production address or faking one the app would then call itself at.
|
|
920
|
+
if (APP_ORIGIN_ENV.test(name)) {
|
|
921
|
+
env[name] = APP_URL_ENV;
|
|
922
|
+
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' });
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
// a name a twin of this World issues (its descriptor's credentialDoor): `$issue:<service>`, asked of the running twin
|
|
926
|
+
// at boot, so the application holds a key the twin made rather than a fixture it refuses
|
|
927
|
+
const issuer = issuedBy.get(name);
|
|
928
|
+
if (issuer !== undefined) {
|
|
929
|
+
env[name] = `${ISSUE_ENV_PREFIX}${issuer}`;
|
|
930
|
+
envRows.push({ name, disposition: 'issued', source, reason: `${issuer} credential — \`$issue:${issuer}\`: up asks the running twin to issue it` });
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
// a name some pack's credential door fills with a file (`file:<field>`) holds a path: with its issuer unselected, the
|
|
934
|
+
// example's path is never copied
|
|
935
|
+
if (fileCredentialNames().has(name)) {
|
|
936
|
+
env[name] = `/__volter_fake_credentials__/${name.toLowerCase().replace(/_/g, '-')}.json`;
|
|
937
|
+
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' });
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
// Several directories give it different values and none is the application's: the operator decides.
|
|
941
|
+
if (conflict !== undefined && !isCredentialShapedEnvName(name)) {
|
|
942
|
+
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 });
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
859
945
|
// STRUCTURED credential shapes first: `GOOGLE_VERTEX_JSON` / `GOOGLE_API_CREDENTIALS` carry a
|
|
860
946
|
// whole JSON document the client SDK PARSES (and signs with) before any request leaves the
|
|
861
947
|
// process, and neither name ends in a credential SUFFIX — so the suffix heuristics below are
|
|
@@ -878,6 +964,12 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
878
964
|
});
|
|
879
965
|
continue;
|
|
880
966
|
}
|
|
967
|
+
const served = vendors.filter((v) => v.wiring === 'backing').map((v) => VENDOR_SERVER_BACKINGS[v.vendor]?.env[name]).find((value) => value !== undefined);
|
|
968
|
+
if (served !== undefined) {
|
|
969
|
+
env[name] = served;
|
|
970
|
+
envRows.push({ name, disposition: 'faked', source, reason: 'the key the World\'s own vendor server is started with' });
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
881
973
|
const vendor = envNameVendor(name);
|
|
882
974
|
if (vendor !== null) {
|
|
883
975
|
env[name] = fakeEnvValue(name);
|
|
@@ -892,6 +984,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
892
984
|
infraEntries.push({ name, source, kind: infraKind });
|
|
893
985
|
continue;
|
|
894
986
|
}
|
|
987
|
+
// A detected vendor's endpoint at a loopback server is the repository's local stand-in for the vendor (a garage for
|
|
988
|
+
// S3): the World routes the vendor's own host to its twin, so the name is left unset and the SDK addresses that host.
|
|
989
|
+
const standIn = standInStem(name, value, detected);
|
|
990
|
+
if (standIn !== null) {
|
|
991
|
+
standInStems.add(standIn);
|
|
992
|
+
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 });
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
895
995
|
// RULE 3: credential-shaped means faked, even when no vendor claims the stem. That covers
|
|
896
996
|
// app-local secrets (JWT_SECRET, ENCRYPTION_KEY) and untwinned vendors alike — and it is the
|
|
897
997
|
// reason a live key committed to `.env.example` can never reach the emitted world.
|
|
@@ -921,34 +1021,29 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
921
1021
|
envRows.push({ name, disposition: 'unknown', source, reason: 'empty in the example and not credential-shaped — you decide', unset: true });
|
|
922
1022
|
}
|
|
923
1023
|
}
|
|
1024
|
+
// A twinned vendor's credential the app's code reads that no example env declares (a script, an app whose keys come
|
|
1025
|
+
// from a secrets manager): the SDK refuses to start without it, so it is faked as a declared one would be. Only a
|
|
1026
|
+
// credential: a base-URL override the code reads (`OPENAI_BASE_URL`) is optional there, and a fake one would send
|
|
1027
|
+
// the SDK nowhere instead of through the injector; and never a framework's public name (`NEXT_PUBLIC_…`), whose
|
|
1028
|
+
// value the app's own env files hold for the browser, which a set variable would override
|
|
1029
|
+
for (const [name, readAt] of [...envReads.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1030
|
+
if (name in env || envRows.some((row) => row.name === name) || /_TWIN_URL$/.test(name) || /^VOLTER_/.test(name) || worldInjected.has(name)) continue;
|
|
1031
|
+
if (/(?:^|_)(?:URL|URI|ENDPOINT|HOST|BASE|ORIGIN|DOMAIN|REGION|PORT)$/.test(name) || /^(?:NEXT_PUBLIC_|VITE_|REACT_APP_|EXPO_PUBLIC_|NUXT_PUBLIC_|PUBLIC_)/.test(name)) continue;
|
|
1032
|
+
const vendor = envNameVendor(name);
|
|
1033
|
+
if (vendor === null || !vendors.some((entry) => entry.vendor === vendor && entry.service !== null)) continue;
|
|
1034
|
+
env[name] = fakeEnvValue(name);
|
|
1035
|
+
envRows.push({ name, disposition: 'faked', source: readAt[0] ?? 'source', reason: `${vendor} credential read by the app's code; no example env declares it` });
|
|
1036
|
+
}
|
|
924
1037
|
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const id = `${owner.vendor}-${kind}`;
|
|
935
|
-
const frontend: WorldServiceConfig = {
|
|
936
|
-
'//': `${owner.vendor}'s native ${kind} frontend: the same state as the "${owner.vendor}" twin over the ${kind} wire protocol (declare the HTTP twin first; it reads ${native.upstreamEnv} from it).`,
|
|
937
|
-
id,
|
|
938
|
-
type: 'twin',
|
|
939
|
-
...(catalogSource.kind === 'installed'
|
|
940
|
-
? { package: twinPackageName(owner.vendor), args: [native.flag] }
|
|
941
|
-
: { command: 'bun', args: [`packages/twin/${owner.vendor}/src/cli.ts`, 'serve', native.flag] }),
|
|
942
|
-
...(catalogSource.version(owner.vendor) !== undefined ? { version: catalogSource.version(owner.vendor) } : {}),
|
|
943
|
-
port: 'auto',
|
|
944
|
-
injectEnvTemplates: Object.fromEntries([...signals].sort().map((name) => [name, `${kind}://twin:twin@${'${host}'}:${'${port}'}/twin`])),
|
|
945
|
-
};
|
|
946
|
-
services.splice(services.indexOf(owner.service) + 1, 0, frontend);
|
|
947
|
-
for (const entry of infraEntries.filter((candidate) => candidate.kind === kind)) {
|
|
948
|
-
envRows.push({ name: entry.name, disposition: 'unknown', source: entry.source, reason: `${kind} connection string — injected at boot by the "${id}" twin service (${owner.vendor}'s native frontend over the same state)` });
|
|
949
|
-
}
|
|
950
|
-
infraSignals.delete(kind);
|
|
951
|
-
infraEntries.splice(0, infraEntries.length, ...infraEntries.filter((candidate) => candidate.kind !== kind));
|
|
1038
|
+
// 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
|
|
1039
|
+
// would sign for a region the vendor does not have
|
|
1040
|
+
for (const row of envRows) {
|
|
1041
|
+
const stem = [...standInStems].find((candidate) => row.name === `${candidate}_REGION`);
|
|
1042
|
+
if (stem === undefined || row.disposition !== 'kept' || /^[a-z]{2}(-[a-z]+)+-\d+$/.test(env[row.name] ?? '')) continue;
|
|
1043
|
+
delete env[row.name];
|
|
1044
|
+
row.disposition = 'stand-in';
|
|
1045
|
+
row.reason = `the local stand-in's region, not one ${stem}'s vendor has — left unset`;
|
|
1046
|
+
row.unset = true;
|
|
952
1047
|
}
|
|
953
1048
|
|
|
954
1049
|
// --- infra ----------------------------------------------------------------------------------
|
|
@@ -1017,14 +1112,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
|
|
|
1017
1112
|
external: {
|
|
1018
1113
|
up: ['volter-world-infra', 'up'],
|
|
1019
1114
|
status: ['volter-world-infra', 'status'],
|
|
1020
|
-
discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}
|
|
1115
|
+
discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}`, ...(['postgres', 'mysql', 'mongodb', 'redis'].includes(service.kind) ? { protocol: service.kind } : {}) }))),
|
|
1021
1116
|
down: ['volter-world-infra', 'down'],
|
|
1022
1117
|
readyWhen: { command: 'volter-world-infra', args: ['status'], timeoutMs: 120000, intervalMs: 500 },
|
|
1023
1118
|
},
|
|
1024
1119
|
});
|
|
1025
1120
|
}
|
|
1026
1121
|
|
|
1027
|
-
const mergedServices = existing === undefined ? services : [...existing.services, ...services.filter((service) => !existingIds.has(service.id))];
|
|
1122
|
+
const mergedServices = existing === undefined ? services : [...existing.services.map(withIssues), ...services.filter((service) => !existingIds.has(service.id))];
|
|
1028
1123
|
if (existing !== undefined) {
|
|
1029
1124
|
const owners = new Map<string, string>();
|
|
1030
1125
|
for (const service of mergedServices) {
|
|
@@ -1198,7 +1293,7 @@ export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}
|
|
|
1198
1293
|
// are the author's files now (edit or delete; regenerate a fresh world for fresh defaults).
|
|
1199
1294
|
for (const copy of plan.defaultsCopies) {
|
|
1200
1295
|
mkdirSync(dirname(copy.to), { recursive: true });
|
|
1201
|
-
if (!(options.force === true && existsSync(copy.to))) writeFileSync(copy.to, readFileSync(copy.from));
|
|
1296
|
+
if (!(options.force === true && existsSync(copy.to))) writeFileSync(copy.to, copy.content ?? readFileSync(copy.from!));
|
|
1202
1297
|
}
|
|
1203
1298
|
if (plan.seedEntry !== null) {
|
|
1204
1299
|
mkdirSync(dirname(plan.seedEntry.storyPath), { recursive: true });
|
|
@@ -1222,6 +1317,31 @@ export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}
|
|
|
1222
1317
|
}
|
|
1223
1318
|
}
|
|
1224
1319
|
|
|
1320
|
+
/** The runner of a protocol-3 pack's default data (its `src/semantics/seed.ts`, copied beside it as `<vendor>.data.ts`):
|
|
1321
|
+
* each call, in order, against the twin at its `<VENDOR>_TWIN_URL`, a refusal stopping the seed with the twin's answer. */
|
|
1322
|
+
export function seedCallsRunner(vendor: string): string {
|
|
1323
|
+
const stem = vendor.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
1324
|
+
return `// ${vendor}'s default data, as its twin's pack declares it (./${vendor}.data.ts: calls to the vendor's own
|
|
1325
|
+
// operations), made against the twin in order. Ownership transferred on copy: both files are yours — edit or delete.
|
|
1326
|
+
import { seed as calls } from './${vendor}.data.ts';
|
|
1327
|
+
|
|
1328
|
+
export async function seed(): Promise<void> {
|
|
1329
|
+
// a twin with a root is the vendor's real account: its default data is not written (VOLTER_SEED_SKIP)
|
|
1330
|
+
if ((process.env.VOLTER_SEED_SKIP ?? '').split(',').includes('${vendor}')) return;
|
|
1331
|
+
const base = process.env.${stem}_TWIN_URL;
|
|
1332
|
+
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)');
|
|
1333
|
+
for (const call of calls) {
|
|
1334
|
+
const res = await fetch(\`\${base.replace(/\\/$/, '')}\${call.path}\`, {
|
|
1335
|
+
method: call.method,
|
|
1336
|
+
headers: { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) },
|
|
1337
|
+
...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
|
|
1338
|
+
});
|
|
1339
|
+
if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
`;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1225
1345
|
/**
|
|
1226
1346
|
* Plan, emit, and PROVE. The coverage proof runs against the just-written config (by path, so it
|
|
1227
1347
|
* reads exactly the bytes on disk rather than the in-memory plan) — `ok` is the proof's verdict and
|
|
@@ -1287,9 +1407,10 @@ export function formatInitReport(result: InitResult): string {
|
|
|
1287
1407
|
if (plan.vendors.length === 0) lines.push(' (no external vendor dependencies detected in the repo)');
|
|
1288
1408
|
else {
|
|
1289
1409
|
lines.push(...table(
|
|
1290
|
-
['vendor', 'wiring', 'inject-env', 'detected-via'],
|
|
1410
|
+
['vendor', 'source', 'wiring', 'inject-env', 'detected-via'],
|
|
1291
1411
|
plan.vendors.map((vendor) => [
|
|
1292
1412
|
vendor.vendor,
|
|
1413
|
+
vendor.source ? `${vendor.source.kind}${vendor.source.version ? ` ${vendor.source.version}` : ''}` : '-',
|
|
1293
1414
|
vendor.wiring,
|
|
1294
1415
|
[vendor.service?.injectEnv, ...Object.keys(vendor.service?.injectEnvTemplates ?? {})].filter(Boolean).join(', ') || '-',
|
|
1295
1416
|
vendor.detectedVia.join('; '),
|
|
@@ -1303,7 +1424,7 @@ export function formatInitReport(result: InitResult): string {
|
|
|
1303
1424
|
lines.push('');
|
|
1304
1425
|
const counts = (disposition: EnvDisposition): InitEnvRow[] => plan.envRows.filter((row) => row.disposition === disposition);
|
|
1305
1426
|
lines.push(`Env (${plan.envRows.length} name(s) from ${plan.envSources.join(', ') || 'no example env file'}):`);
|
|
1306
|
-
for (const disposition of ['faked', 'kept', 'compose', 'placeholder', 'unknown'] as EnvDisposition[]) {
|
|
1427
|
+
for (const disposition of ['faked', 'kept', 'app-origin', 'stand-in', 'compose', 'placeholder', 'unknown'] as EnvDisposition[]) {
|
|
1307
1428
|
const rows = counts(disposition);
|
|
1308
1429
|
if (rows.length === 0) continue;
|
|
1309
1430
|
lines.push(` ${disposition.padEnd(11)} ${String(rows.length).padStart(3)} ${rows.map((row) => row.name).join(', ')}`);
|
package/src/lifecycle-record.ts
CHANGED
|
@@ -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.ts';
|
|
9
9
|
|
|
10
10
|
export type WorldLifecycleRecord = {
|
package/src/local-branches.ts
CHANGED
|
@@ -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.ts';
|
|
10
10
|
import { loadWorldConfig } from './configs.ts';
|
|
11
11
|
import { TOKEN_HEADER, type BranchDoors, type BranchRow, type MountedWorld } from './served-world.ts';
|