@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.
Files changed (121) hide show
  1. package/dist/known-external-services.json +5 -4
  2. package/dist/src/actions-runner-cli.d.ts +2 -0
  3. package/dist/src/actions-runner-cli.js +25 -0
  4. package/dist/src/actions-runner.d.ts +18 -0
  5. package/dist/src/actions-runner.js +281 -0
  6. package/dist/src/ancestry.js +1 -1
  7. package/dist/src/app-url.js +1 -1
  8. package/dist/src/backings.d.ts +29 -0
  9. package/dist/src/backings.js +45 -0
  10. package/dist/src/branch.js +3 -2
  11. package/dist/src/browser-proxy-cli.js +1 -1
  12. package/dist/src/catalog-index.d.ts +38 -0
  13. package/dist/src/catalog-index.js +37 -0
  14. package/dist/src/catalog.d.ts +16 -3
  15. package/dist/src/catalog.js +69 -26
  16. package/dist/src/changeset.d.ts +1 -1
  17. package/dist/src/changeset.js +9 -11
  18. package/dist/src/cli-args.d.ts +4 -0
  19. package/dist/src/cli-args.js +18 -0
  20. package/dist/src/cli-commands.d.ts +1 -0
  21. package/dist/src/cli-commands.js +1216 -0
  22. package/dist/src/cli.js +8 -1300
  23. package/dist/src/configs.js +1 -1
  24. package/dist/src/console-apart.d.ts +2 -2
  25. package/dist/src/console-apart.js +5 -5
  26. package/dist/src/consumers.js +1 -1
  27. package/dist/src/covers.d.ts +1 -1
  28. package/dist/src/covers.js +94 -30
  29. package/dist/src/env-output.d.ts +1 -1
  30. package/dist/src/env-output.js +2 -2
  31. package/dist/src/fixture-env.js +18 -26
  32. package/dist/src/host-cli.js +4 -2
  33. package/dist/src/host-fault-fixture.js +6 -0
  34. package/dist/src/host-ready.d.ts +8 -2
  35. package/dist/src/host-ready.js +16 -4
  36. package/dist/src/host-worker.js +6 -1
  37. package/dist/src/host.d.ts +2 -0
  38. package/dist/src/host.js +9 -1
  39. package/dist/src/index.d.ts +2 -2
  40. package/dist/src/index.js +2 -2
  41. package/dist/src/init.d.ts +33 -23
  42. package/dist/src/init.js +228 -96
  43. package/dist/src/lifecycle-record.js +2 -2
  44. package/dist/src/local-branches.js +1 -1
  45. package/dist/src/origin.d.ts +1 -1
  46. package/dist/src/origin.js +12 -10
  47. package/dist/src/pack-facts.d.ts +30 -6
  48. package/dist/src/pack-facts.js +13 -3
  49. package/dist/src/pglite-backing.js +2 -1
  50. package/dist/src/placeholder.d.ts +1 -1
  51. package/dist/src/placeholder.js +12 -4
  52. package/dist/src/project-inspect.d.ts +1 -1
  53. package/dist/src/project-inspect.js +2 -2
  54. package/dist/src/redirect-proxy.d.ts +5 -0
  55. package/dist/src/redirect-proxy.js +8 -1
  56. package/dist/src/redis-backing.js +4 -2
  57. package/dist/src/reflect.js +1 -1
  58. package/dist/src/root.d.ts +11 -33
  59. package/dist/src/root.js +28 -59
  60. package/dist/src/runtime-test-support.js +1 -1
  61. package/dist/src/runtime.d.ts +7 -1
  62. package/dist/src/runtime.js +317 -53
  63. package/dist/src/schema.d.ts +27 -5
  64. package/dist/src/schema.js +13 -12
  65. package/dist/src/served-world.d.ts +11 -35
  66. package/dist/src/served-world.js +54 -141
  67. package/dist/src/tail.js +1 -1
  68. package/dist/src/up-cli.d.ts +3 -0
  69. package/dist/src/up-cli.js +55 -0
  70. package/dist/src/websocket-relay.d.ts +1 -1
  71. package/dist/src/world-origins.d.ts +1 -1
  72. package/dist/src/world-origins.js +1 -1
  73. package/dist/src/world-view.d.ts +1 -1
  74. package/dist/src/world-view.js +2 -2
  75. package/known-external-services.json +5 -4
  76. package/package.json +11 -11
  77. package/src/actions-runner-cli.ts +24 -0
  78. package/src/actions-runner.ts +244 -0
  79. package/src/ancestry.ts +1 -1
  80. package/src/app-url.ts +1 -1
  81. package/src/backings.ts +55 -0
  82. package/src/branch.ts +3 -2
  83. package/src/browser-proxy-cli.ts +1 -1
  84. package/src/catalog-index.ts +44 -0
  85. package/src/catalog.ts +67 -24
  86. package/src/changeset.ts +10 -13
  87. package/src/cli-args.ts +17 -0
  88. package/src/cli-commands.ts +1070 -0
  89. package/src/cli.ts +9 -1139
  90. package/src/configs.ts +1 -1
  91. package/src/console-apart.ts +5 -5
  92. package/src/consumers.ts +1 -1
  93. package/src/covers.ts +83 -30
  94. package/src/env-output.ts +2 -2
  95. package/src/fixture-env.ts +18 -26
  96. package/src/host-cli.ts +4 -2
  97. package/src/host-ready.ts +13 -4
  98. package/src/host-worker.ts +6 -2
  99. package/src/host.ts +11 -2
  100. package/src/index.ts +2 -2
  101. package/src/init.ts +222 -101
  102. package/src/lifecycle-record.ts +2 -2
  103. package/src/local-branches.ts +1 -1
  104. package/src/origin.ts +12 -10
  105. package/src/pack-facts.ts +22 -6
  106. package/src/pglite-backing.ts +2 -1
  107. package/src/placeholder.ts +13 -4
  108. package/src/project-inspect.ts +2 -2
  109. package/src/redirect-proxy.ts +9 -1
  110. package/src/redis-backing.ts +4 -2
  111. package/src/reflect.ts +1 -1
  112. package/src/root.ts +33 -65
  113. package/src/runtime-test-support.ts +1 -1
  114. package/src/runtime.ts +261 -55
  115. package/src/schema.ts +26 -13
  116. package/src/served-world.ts +50 -130
  117. package/src/tail.ts +1 -1
  118. package/src/up-cli.ts +55 -0
  119. package/src/websocket-relay.ts +1 -1
  120. package/src/world-origins.ts +1 -1
  121. 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 { DEFAULT_WORLD_SELECTION, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
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
- * `packages/twin/<vendor>/` dirs, or the `@volter/twin-<vendor>` packages installed above root). */
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 REAL endpoint env names for packs the injector cannot intercept — the other half of
59
- * `scripts/vendor-hosts.test.ts`'s descriptor hostsNone ruling. That allowlist states WHY each pack
60
- * has no VENDOR_HOSTS entry; this table states WHAT to wire instead, using the env var the vendor's
61
- * own SDK documents. Every entry is grounded in the SDK, not invented:
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 over the twin-only inspect sidecar: `world-smtp serve --inspect-port N`, then '
111
- + 'GET http://127.0.0.1:N/twin/messages/latest.',
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
- // descriptor-first migration (adding-a-twin.md §3): packs now declare their endpoint-env wiring (with its grounding
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/dynamodb/timestream here — the very keys
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, first definition winning, in a deterministic order:
437
- * root before workspace members (sorted), and within a dir the ENV_SOURCE_FILES priority. */
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 seen = new Set();
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 (seen.has(entry.name))
458
+ if (inDir.has(entry.name))
449
459
  continue;
450
- seen.add(entry.name);
451
- entries.push(entry);
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: `no packages/twin/${vendor} pack in this catalog — the proof reports it MISSING until one exists.`,
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.kind === 'installed'
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: [`packages/twin/${vendor}/src/cli.ts`, 'serve', ...(handlersArg !== null ? ['--scenario', handlersArg] : [])],
578
- ...(serveExport !== undefined ? { colocate: { module: `./packages/twin/${vendor}/src/index.ts`, export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
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
- // --- native frontends ------------------------------------------------------------------------
733
- // A detected native connection (Prisma over mysql://) that a SELECTED twin serves as a native
734
- // frontend of its own state (the descriptor's `nativeTransport`) is that twin, not managed
735
- // infrastructure: a separate database would split the state the app reads through the vendor's
736
- // HTTP client from the state it writes natively — the two-client bug this exists to prevent.
737
- for (const [kind, signals] of [...infraSignals.entries()]) {
738
- const owner = vendors.find((entry) => entry.service !== null && packFacts()[entry.vendor]?.nativeTransport?.protocol === kind);
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
- const native = packFacts()[owner.vendor].nativeTransport;
742
- const id = `${owner.vendor}-${kind}`;
743
- const frontend = {
744
- '//': `${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).`,
745
- id,
746
- type: 'twin',
747
- ...(catalogSource.kind === 'installed'
748
- ? { package: twinPackageName(owner.vendor), args: [native.flag] }
749
- : { command: 'bun', args: [`packages/twin/${owner.vendor}/src/cli.ts`, 'serve', native.flag] }),
750
- ...(catalogSource.version(owner.vendor) !== undefined ? { version: catalogSource.version(owner.vendor) } : {}),
751
- port: 'auto',
752
- injectEnvTemplates: Object.fromEntries([...signals].sort().map((name) => [name, `${kind}://twin:twin@${'${host}'}:${'${port}'}/twin`])),
753
- };
754
- services.splice(services.indexOf(owner.service) + 1, 0, frontend);
755
- for (const entry of infraEntries.filter((candidate) => candidate.kind === kind)) {
756
- 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)` });
757
- }
758
- infraSignals.delete(kind);
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";
@@ -1,4 +1,4 @@
1
- import { type HistoryReference } from '@volter/world-core';
1
+ import { type HistoryReference } from '@volter/world-core/runtime';
2
2
  import type { WorldInstance } from './schema.js';
3
3
  export type FetchOptions = {
4
4
  root?: string;
@@ -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, withAncestryLock, writeBranchMeta } from '@volter/world-core';
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
- withAncestryLock(() => {
51
- for (const service of services) {
52
- const controlRoot = join(instance.dirs.data, service);
53
- const meta = readBranchMeta(stateServiceOf(controlRoot, service), controlRoot);
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
- withAncestryLock(() => {
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
- withAncestryLock(() => {
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');