@volter/world-runtime 2.0.55 → 3.0.1

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
@@ -0,0 +1,44 @@
1
+ // The catalog index as the runtime reads it (docs/contributing/architecture.md, "The catalog: where twins come from"):
2
+ // its sources, its vendors' packages and versions, and each vendor's default twin. The index is a directory of data (the
3
+ // `volter-ai/twin-catalog` repository, published as the npm package `@volter/twin-catalog`); scripts/twin-catalog.ts
4
+ // checks and writes it, and `init` reads the defaults from the installed package.
5
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ /** The index as npm carries it. */
9
+ export const CATALOG_INDEX_PACKAGE = '@volter/twin-catalog';
10
+
11
+ /** A source; `workflow` is the file under `.github/workflows/` its packages are released by (release.yml unless named). */
12
+ export type Source = { name: string; repository: string; scope: string; official: boolean; protocol: '3'; workflow?: string };
13
+ export type Version = { version: string; status: 'live' | 'pending' | 'rejected'; at: string; by: string; reason?: string; commit?: string };
14
+ export type Entry = { vendor: string; packages: Array<{ name: string; source: string; versions: Version[] }> };
15
+
16
+ export const readSources = (index: string): Source[] => JSON.parse(readFileSync(join(index, 'sources.json'), 'utf8')) as Source[];
17
+ export function readVendors(index: string): Entry[] {
18
+ const dir = join(index, 'vendors');
19
+ return existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith('.json')).sort().map((f) => JSON.parse(readFileSync(join(dir, f), 'utf8')) as Entry) : [];
20
+ }
21
+ /** A package's vendor: `@volter/twin-<vendor>`, or the part after `twin-` in any scope. */
22
+ export const vendorOf = (name: string): string => name.replace(/^@[^/]+\//, '').replace(/^twin-/, '');
23
+
24
+ const semver = (v: string): number[] => v.split(/[.-]/).slice(0, 3).map((n) => Number(n) || 0);
25
+ const newer = (a: string, b: string): number => { const x = semver(a); const y = semver(b); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i]! - y[i]!; return 0; };
26
+
27
+ /** Each vendor's default twin: its official package, else its live outside package; the
28
+ * highest live version of it. */
29
+ export function defaults(index: string): Record<string, { name: string; version: string; source: string }> {
30
+ const sources = new Map(readSources(index).map((s) => [s.name, s]));
31
+ const rank = (s: Source | undefined): number => (!s ? 0 : s.official ? 2 : 1);
32
+ const out: Record<string, { name: string; version: string; source: string }> = {};
33
+ for (const e of readVendors(index)) {
34
+ const candidates = e.packages
35
+ .map((p) => ({ p, s: sources.get(p.source), live: p.versions.filter((v) => v.status === 'live').sort((a, b) => newer(b.version, a.version))[0] }))
36
+ .filter((c) => c.live && c.s)
37
+ .sort((a, b) => rank(b.s) - rank(a.s));
38
+ const best = candidates[0];
39
+ // an outside twin is a default only where no official twin is
40
+ if (best && (best.s!.official || candidates.every((c) => !c.s!.official))) out[e.vendor] = { name: best.p.name, version: best.live!.version, source: best.p.source };
41
+ }
42
+ return out;
43
+ }
44
+
package/src/catalog.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  // The catalog — where the twins a world can run come from (docs/concepts/worlds.md
2
- // #the-config-and-the-running-world). Two sources answer one interface: this CHECKOUT (`packages/twin/<vendor>/`, the
3
- // catalog as the repo holds it) and an INSTALLED catalog (`@volter/twin-<vendor>` packages under
2
+ // #the-config-and-the-running-world). Two sources answer one interface: a pack-repository CHECKOUT and an
3
+ // INSTALLED catalog (`@volter/twin-<vendor>` packages under
4
4
  // `node_modules`, walked up from the world root — hoisting-proof, and independent of the
5
5
  // packages' `exports` maps, which do not expose `package.json`). A world born in an app repo
6
6
  // names its twins as packages and the runtime resolves them at boot; a world born here names the
7
- // checkout-relative paths, as before.
7
+ // selected checkout paths.
8
8
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { dirname, join, resolve } from 'node:path';
11
+ import { overlayPackFacts, type PackFacts } from './pack-facts.ts';
12
+ import { installedPackageDir, isDerivedPack, packRepository } from '@volter/world-core/runtime';
11
13
 
12
14
  export const TWIN_PACKAGE_PREFIX = '@volter/twin-';
13
15
 
14
- /** Packages under the `@volter/twin-` prefix that are NOT twins. */
15
-
16
16
  export function twinPackageName(vendor: string): string { return `${TWIN_PACKAGE_PREFIX}${vendor}`; }
17
17
 
18
18
  export type CatalogKind = 'checkout' | 'installed';
@@ -29,15 +29,22 @@ export type Catalog = {
29
29
  version(vendor: string): string | undefined;
30
30
  /** The birth stamp: the checkout's git sha, or a digest of the installed versions. */
31
31
  sha(): string;
32
+ /** Where the vendor's pack comes from: this checkout or an installed package (a merged catalog's sources differ per
33
+ * vendor, so a World boots each twin the way its own source serves it). */
34
+ kindOf(vendor: string): CatalogKind;
32
35
  };
33
36
 
37
+ /** A derived (Protocol 3) pack carries the surface derive-pack generated from its spec. */
38
+ const major = (version: string | undefined): number => Number(/^(\d+)/.exec(version ?? '')?.[1] ?? 0);
39
+
34
40
  function packageVersion(dir: string): string | undefined {
35
41
  const p = join(dir, 'package.json');
36
42
  if (!existsSync(p)) return undefined;
37
43
  try { const v = (JSON.parse(readFileSync(p, 'utf8')) as { version?: unknown }).version; return typeof v === 'string' ? v : undefined; } catch { return undefined; }
38
44
  }
39
45
 
40
- function isPackDir(dir: string): boolean { return packCli(dir) !== undefined; }
46
+ /** A twin a catalog lists: a derived pack with its CLI. */
47
+ function isPackDir(dir: string): boolean { return packCli(dir) !== undefined && isDerivedPack(dir); }
41
48
 
42
49
  type Manifest = { name?: string; exports?: unknown; main?: string; bin?: Record<string, string> | string };
43
50
  function manifest(dir: string): Manifest | undefined { const p = join(dir, 'package.json'); if (!existsSync(p)) return undefined; try { return JSON.parse(readFileSync(p, 'utf8')) as Manifest; } catch { return undefined; } }
@@ -62,16 +69,7 @@ export function packCli(dir: string): string | undefined {
62
69
  }
63
70
 
64
71
  /** The nearest `node_modules/<name>` above `from` that holds a package.json — node's own walk. */
65
- export function findInstalledPackage(from: string, name: string): string | undefined {
66
- let dir = resolve(from);
67
- for (;;) {
68
- const candidate = join(dir, 'node_modules', ...name.split('/'));
69
- if (existsSync(join(candidate, 'package.json'))) return candidate;
70
- const up = dirname(dir);
71
- if (up === dir) return undefined;
72
- dir = up;
73
- }
74
- }
72
+ export const findInstalledPackage = installedPackageDir;
75
73
 
76
74
  /** Every installed `@volter/twin-<vendor>` visible from `from`, nearest wins. */
77
75
  function installedTwins(from: string): Map<string, string> {
@@ -83,6 +81,7 @@ function installedTwins(from: string): Map<string, string> {
83
81
  for (const entry of readdirSync(scope, { withFileTypes: true })) {
84
82
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
85
83
  const name = `@volter/${entry.name}`;
84
+ if (name === '@volter/twin-standard') continue; // the platform's standard is tooling, not a vendor pack
86
85
  if (!name.startsWith(TWIN_PACKAGE_PREFIX)) continue; // the platform's packages are `@volter/world-*`: outside the prefix by name
87
86
  const vendor = name.slice(TWIN_PACKAGE_PREFIX.length);
88
87
  const packDir = join(scope, entry.name);
@@ -103,17 +102,18 @@ function fnv1a(text: string): string {
103
102
  }
104
103
 
105
104
  export function checkoutCatalog(root: string): Catalog {
106
- const packsDir = resolve(root, 'packages', 'twin');
105
+ const packsDir = resolve(root);
107
106
  const vendors = (): string[] => readdirSync(packsDir, { withFileTypes: true })
108
- .filter((entry) => entry.isDirectory() && isPackDir(join(packsDir, entry.name)))
107
+ .filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && manifest(join(packsDir, entry.name))?.name === twinPackageName(entry.name) && isPackDir(join(packsDir, entry.name)))
109
108
  .map((entry) => entry.name)
110
109
  .sort();
111
110
  return {
112
111
  kind: 'checkout',
113
112
  root: resolve(root),
114
113
  vendors,
115
- packDir: (vendor) => (isPackDir(join(packsDir, vendor)) ? join(packsDir, vendor) : undefined),
114
+ packDir: (vendor) => (/^[a-z][a-z0-9-]*$/.test(vendor) && manifest(join(packsDir, vendor))?.name === twinPackageName(vendor) && isPackDir(join(packsDir, vendor)) ? join(packsDir, vendor) : undefined),
116
115
  version: (vendor) => packageVersion(join(packsDir, vendor)),
116
+ kindOf: () => 'checkout',
117
117
  sha: () => {
118
118
  const proc = spawnSync('git', ['-C', root, 'rev-parse', 'HEAD'], { windowsHide: true, encoding: 'utf8' });
119
119
  const sha = proc.status === 0 ? proc.stdout.trim() : '';
@@ -130,22 +130,65 @@ export function installedCatalog(root: string): Catalog {
130
130
  vendors: () => [...twins.keys()].sort(),
131
131
  packDir: (vendor) => twins.get(vendor),
132
132
  version: (vendor) => { const dir = twins.get(vendor); return dir === undefined ? undefined : packageVersion(dir); },
133
+ kindOf: () => 'installed',
133
134
  sha: () => `installed-${fnv1a([...twins.keys()].sort().map((v) => `${v}@${packageVersion(twins.get(v)!) ?? '?'}`).join(','))}`,
134
135
  };
135
136
  }
136
137
 
137
- /** The catalog for a world root: the checkout when `packages/twin` is there, else what is installed. */
138
+ /**
139
+ * One catalog over several sources (docs/contributing/architecture.md, "The catalog: where twins come from"): each
140
+ * vendor is served by the pack of the higher major; on a tie the earlier source keeps it. `kind` is the first source's.
141
+ */
142
+ export function mergedCatalog(sources: Catalog[]): Catalog {
143
+ const chosen = new Map<string, Catalog>();
144
+ for (const source of sources) {
145
+ for (const vendor of source.vendors()) {
146
+ const held = chosen.get(vendor);
147
+ if (held === undefined) { chosen.set(vendor, source); continue; }
148
+ if (major(source.version(vendor)) > major(held.version(vendor))) chosen.set(vendor, source);
149
+ }
150
+ }
151
+ const of = (vendor: string): Catalog | undefined => chosen.get(vendor);
152
+ return {
153
+ kind: sources[0]!.kind,
154
+ root: sources[0]!.root,
155
+ vendors: () => [...chosen.keys()].sort(),
156
+ packDir: (vendor) => of(vendor)?.packDir(vendor),
157
+ version: (vendor) => of(vendor)?.version(vendor),
158
+ kindOf: (vendor) => of(vendor)?.kindOf(vendor) ?? sources[0]!.kind,
159
+ sha: () => sources.map((s) => s.sha()).join('+'),
160
+ };
161
+ }
162
+
163
+ /** Overlays one facts file on the kernel's (a file that does not read is skipped: the kernel's facts stand). */
164
+ export function overlayPackFactsFile(file: string): void {
165
+ try { overlayPackFacts((JSON.parse(readFileSync(file, 'utf8')) as { packs?: Record<string, PackFacts> }).packs ?? {}); } catch { /* the kernel's facts stand */ }
166
+ }
167
+
168
+ /** The facts file a released pack carries (`generated/pack-facts.json`, written by pack-release), when it has one. */
169
+ export function packFactsFile(dir: string): string | undefined {
170
+ const file = join(dir, 'generated', 'pack-facts.json');
171
+ return existsSync(file) ? file : undefined;
172
+ }
173
+
174
+ /** The catalog for a World root: its pack repository (or the one beside its platform checkout), merged with installed
175
+ * packages. Only the selected packs' own facts are overlaid on the kernel's. */
138
176
  export function resolveCatalog(root: string): Catalog {
139
177
  const resolved = resolve(root);
140
- if (existsSync(join(resolved, 'packages', 'twin'))) return checkoutCatalog(resolved);
141
178
  const installed = installedCatalog(resolved);
142
- if (installed.vendors().length === 0) {
179
+ const repo = packRepository(resolved);
180
+ const catalog = repo === undefined ? installed : mergedCatalog([checkoutCatalog(repo), installed]);
181
+ for (const vendor of catalog.vendors()) {
182
+ const file = packFactsFile(catalog.packDir(vendor)!);
183
+ if (file) overlayPackFactsFile(file);
184
+ }
185
+ if (catalog.vendors().length === 0) {
143
186
  throw new Error(
144
- `No twins found for ${resolved}: no \`packages/twin\` catalog here and no \`@volter/twin-<vendor>\` package installed above it. `
187
+ `No twins found for ${resolved}: no pack repository and no \`@volter/twin-<vendor>\` package installed above it. `
145
188
  + `Install the twins your app needs (e.g. \`bun add -d @volter/twin-stripe\`) and run init again.`,
146
189
  );
147
190
  }
148
- return installed;
191
+ return catalog;
149
192
  }
150
193
 
151
194
  /** The pack directory a service boots from: its `package` resolved from the world root at boot. */
package/src/changeset.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { worldChangesetRules } from './configs.ts';
2
- import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, originHead, withAncestryLock, worldPaths, type HistoryReference } from '@volter/world-core';
2
+ import { blobDigest, captureHistory, captureParentHistory, getActiveBlobStore, getActiveWorldStore, originHead, withHistoryLocks, worldPaths, type HistoryReference } from '@volter/world-core/runtime';
3
3
  // `volter-world mark` / `diff` / `changeset` — the WORLD-side verbs over the changeset
4
4
  // primitive (docs/concepts/the-model.md v0). Thin, exactly like `covers` and `tail` are thin: all
5
5
  // the state logic (markers, deltas, content hashing, replay) lives in @volter/world-core's
@@ -44,14 +44,14 @@ import {
44
44
  CHANGESET_KIND,
45
45
  MARKER_KIND,
46
46
  WORLD_BOOT_MARKER_ID,
47
- } from '@volter/world-core';
48
- import type { ApplyReceipt, Receipt, ChangesetApplication, RebaseReport, RebaseTarget, Changeset, ChangesetApproval, ChangesetReadiness, ChangesetVerification, ChangesetVerifier, LedgerDelta, LedgerRef, ReplayReport, ReplayTarget, WorldMarker } from '@volter/world-core';
47
+ } from '@volter/world-core/runtime';
48
+ import type { ApplyReceipt, Receipt, ChangesetApplication, RebaseReport, RebaseTarget, Changeset, ChangesetApproval, ChangesetReadiness, ChangesetVerification, ChangesetVerifier, LedgerDelta, LedgerRef, ReplayReport, ReplayTarget, WorldMarker } from '@volter/world-core/runtime';
49
49
  import { instanceDir, listWorlds, saveWorldInstance, statusWorld } from './runtime.ts';
50
50
  import { fetchFromOrigin } from './origin.ts';
51
51
  import type { WorldInstance } from './schema.ts';
52
52
  import { fetchReady, isPathRemote, landChangeset, landReceipts, localWorld, TOKEN_HEADER } from './served-world.ts';
53
53
  import { loadWorldChecks } from './root.ts';
54
- import { originEntries, parentEntries, readBranchMeta, readTree, rebaseBranch, runChecks, wholeLog, writeBranchMeta } from '@volter/world-core';
54
+ import { originEntries, parentEntries, readBranchMeta, readTree, rebaseBranch, runChecks, wholeLog, writeBranchMeta } from '@volter/world-core/runtime';
55
55
 
56
56
  export type WorldRootOptions = { root?: string };
57
57
 
@@ -68,7 +68,7 @@ function requireWorld(name: string, root: string): WorldInstance {
68
68
  } catch (error) {
69
69
  if (!/World instance not found/.test(String((error as Error).message))) throw error;
70
70
  const known = listWorlds(root).map((world) => world.name);
71
- throw new Error(`World "${name}" not found in ${root}${known.length ? ` (known worlds: ${known.join(', ')})` : ' (no worlds here)'} — boot it with \`volter-world up <config> --env-out <path> --name ${name}\``);
71
+ throw new Error(`World "${name}" not found in ${root}${known.length ? ` (known worlds: ${known.join(', ')})` : ' (no worlds here)'} — start it with \`volter world up\` in the app's folder (the operator's own: \`volter-world up <config> --env-out <path> --name ${name}\`)`);
72
72
  }
73
73
  }
74
74
 
@@ -215,7 +215,7 @@ export function createWorldChangeset(
215
215
  // re-cutting an existing name (overwrite) starts from that changeset's own base, not from its cut
216
216
  const base = options.base ?? (getActiveWorldStore().exists(path) ? readChangesetFile(path).base : undefined);
217
217
  const delta = diffWorld(world, { root, ...(base === undefined ? {} : { base }) });
218
- // where the parent log stands for each twin the changeset touches (PROTOCOL 2: the cut position a rebase reads from)
218
+ // where the parent log stands for each twin the changeset touches (the cut position a rebase reads from)
219
219
  const cutInstance = requireWorld(world, root);
220
220
  const cut: Record<string, HistoryReference> = {};
221
221
  for (const a of delta.actions) if (!(a.service in cut)) { const cr = join(cutInstance.dirs.data, a.service); const st = a.stateService ?? a.service; cut[a.service] = captureParentHistory(st, cr); }
@@ -614,13 +614,10 @@ export function rebaseWorldChangeset(name: string, options: WorldRootOptions & {
614
614
  const root = resolveRoot(options.root);
615
615
  const located = findWorldChangeset(name, { root, ...(options.world ? { world: options.world } : {}) });
616
616
  // the branch first: its pointer moves to the parent's current position (what a fetch brought in)
617
- withAncestryLock(() => {
618
- const inst = requireWorld(located.world, root);
619
- for (const service of new Set(located.changeset.actions.map((e) => e.service))) {
620
- const st = located.changeset.actions.find((e) => e.service === service)?.stateService ?? service;
621
- const controlRoot = join(inst.dirs.data, service);
622
- rebaseBranch(st, controlRoot, { origin: Boolean(readBranchMeta(st, controlRoot)?.origin) });
623
- }
617
+ const inst = requireWorld(located.world, root);
618
+ const moved = [...new Set(located.changeset.actions.map((e) => e.service))].map((service) => ({ st: located.changeset.actions.find((e) => e.service === service)?.stateService ?? service, controlRoot: join(inst.dirs.data, service) }));
619
+ withHistoryLocks(moved.map(({ st, controlRoot }) => worldPaths(st, controlRoot).dir), () => {
620
+ for (const { st, controlRoot } of moved) rebaseBranch(st, controlRoot, { origin: Boolean(readBranchMeta(st, controlRoot)?.origin) });
624
621
  });
625
622
  if (!changesetHashMatches(located.changeset)) throw new Error(`Refusing to rebase changeset "${name}": its stored contentHash does not match its body — the object drifted after authoring`);
626
623
  const instance = requireWorld(located.world, root);
@@ -0,0 +1,17 @@
1
+ /** The command line's option readers, shared by `up` (./up-cli.ts) and every other command (./cli-commands.ts). */
2
+
3
+ /** An option's value, written `--name value` or `--name=value`. */
4
+ export function optionValue(args: string[], name: string, fallback = ''): string {
5
+ const joined = args.find((arg) => arg.startsWith(`${name}=`));
6
+ if (joined !== undefined) return joined.slice(name.length + 1);
7
+ const index = args.indexOf(name);
8
+ return index >= 0 && index + 1 < args.length ? args[index + 1]! : fallback;
9
+ }
10
+
11
+ export function ownerOption(args: string[]): string | undefined {
12
+ const index = args.indexOf('--owner');
13
+ if (index < 0) return undefined;
14
+ const label = args[index + 1];
15
+ if (!label || label.startsWith('--')) throw new Error('--owner requires a task label');
16
+ return label;
17
+ }