@volter/world-runtime 3.0.163 → 3.0.165

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 (137) hide show
  1. package/dist/src/ancestry.js +2 -2
  2. package/dist/src/app-url.d.ts +5 -1
  3. package/dist/src/app-url.js +14 -1
  4. package/dist/src/branch.js +28 -8
  5. package/dist/src/browser-files.d.ts +5 -0
  6. package/dist/src/browser-files.js +14 -0
  7. package/dist/src/browsers.d.ts +2 -3
  8. package/dist/src/browsers.js +4 -4
  9. package/dist/src/catalog.d.ts +2 -1
  10. package/dist/src/catalog.js +9 -3
  11. package/dist/src/changeset.js +1 -1
  12. package/dist/src/cli-commands.js +71 -18
  13. package/dist/src/cli-load-clock.d.ts +1 -0
  14. package/dist/src/cli-load-clock.js +2 -0
  15. package/dist/src/cli.js +5 -1
  16. package/dist/src/clickhouse-backing.d.ts +2 -0
  17. package/dist/src/clickhouse-backing.js +145 -0
  18. package/dist/src/command-lifetime.d.mts +1 -1
  19. package/dist/src/command-lifetime.mjs +5 -2
  20. package/dist/src/configs.js +1 -1
  21. package/dist/src/consumers.js +1 -1
  22. package/dist/src/covers.d.ts +4 -2
  23. package/dist/src/covers.js +8 -4
  24. package/dist/src/go-mysql-binding.d.ts +19 -0
  25. package/dist/src/go-mysql-binding.js +51 -0
  26. package/dist/src/guest-processes.d.mts +1 -1
  27. package/dist/src/guest-processes.mjs +22 -14
  28. package/dist/src/host-cli.js +8 -1
  29. package/dist/src/host-ready.d.ts +16 -2
  30. package/dist/src/host-ready.js +66 -39
  31. package/dist/src/host-worker.js +7 -1
  32. package/dist/src/host.d.ts +26 -0
  33. package/dist/src/host.js +93 -3
  34. package/dist/src/infra-definition.d.ts +35 -0
  35. package/dist/src/infra-definition.js +146 -0
  36. package/dist/src/infra.d.ts +3 -0
  37. package/dist/src/infra.js +375 -105
  38. package/dist/src/init.d.ts +46 -0
  39. package/dist/src/init.js +446 -19
  40. package/dist/src/lifecycle-record.d.ts +5 -1
  41. package/dist/src/lifecycle-record.js +27 -4
  42. package/dist/src/local-branches.js +1 -1
  43. package/dist/src/mariadb-bootstrap.d.ts +32 -0
  44. package/dist/src/mariadb-bootstrap.js +90 -0
  45. package/dist/src/mariadb-supplier.d.ts +23 -0
  46. package/dist/src/mariadb-supplier.js +215 -0
  47. package/dist/src/mongodb-backing.d.ts +4 -0
  48. package/dist/src/mongodb-backing.js +71 -1
  49. package/dist/src/native-mariadb-backing.d.ts +13 -0
  50. package/dist/src/native-mariadb-backing.js +512 -0
  51. package/dist/src/native-postgres-backing.d.ts +4 -10
  52. package/dist/src/native-postgres-backing.js +428 -43
  53. package/dist/src/native-server-inputs.d.ts +16 -0
  54. package/dist/src/native-server-inputs.js +204 -0
  55. package/dist/src/origin.js +3 -3
  56. package/dist/src/original-inputs.d.ts +14 -0
  57. package/dist/src/original-inputs.js +84 -0
  58. package/dist/src/pack-facts.d.ts +9 -1
  59. package/dist/src/pglite-backing.d.ts +41 -17
  60. package/dist/src/pglite-backing.js +213 -79
  61. package/dist/src/pglite-host.mjs +482 -10
  62. package/dist/src/process-groups.d.mts +6 -1
  63. package/dist/src/process-groups.d.ts +2 -1
  64. package/dist/src/process-groups.js +1 -1
  65. package/dist/src/process-groups.mjs +82 -15
  66. package/dist/src/process-signal.d.mts +2 -2
  67. package/dist/src/process-signal.mjs +29 -9
  68. package/dist/src/project-inspect.d.ts +12 -0
  69. package/dist/src/project-inspect.js +41 -4
  70. package/dist/src/proxy-env.d.ts +34 -0
  71. package/dist/src/proxy-env.js +114 -0
  72. package/dist/src/redirect-proxy.d.ts +1 -31
  73. package/dist/src/redirect-proxy.js +3 -112
  74. package/dist/src/redis-backing.d.ts +6 -4
  75. package/dist/src/redis-backing.js +20 -168
  76. package/dist/src/runtime.d.ts +1 -0
  77. package/dist/src/runtime.js +503 -218
  78. package/dist/src/schema.d.ts +1 -0
  79. package/dist/src/schema.js +5 -3
  80. package/dist/src/served-world.js +3 -3
  81. package/dist/src/twin-backing.d.ts +47 -0
  82. package/dist/src/twin-backing.js +343 -0
  83. package/dist/src/up-cli.js +56 -3
  84. package/package.json +3 -3
  85. package/src/ancestry.ts +2 -2
  86. package/src/app-url.ts +9 -2
  87. package/src/branch.ts +26 -8
  88. package/src/browser-files.ts +15 -0
  89. package/src/browsers.ts +4 -4
  90. package/src/catalog.ts +9 -4
  91. package/src/changeset.ts +1 -1
  92. package/src/cli-commands.ts +85 -18
  93. package/src/cli-load-clock.ts +2 -0
  94. package/src/cli.ts +5 -1
  95. package/src/clickhouse-backing.ts +70 -0
  96. package/src/command-lifetime.d.mts +1 -1
  97. package/src/command-lifetime.mjs +5 -2
  98. package/src/configs.ts +1 -1
  99. package/src/consumers.ts +1 -1
  100. package/src/covers.ts +9 -4
  101. package/src/go-mysql-binding.ts +49 -0
  102. package/src/guest-processes.d.mts +1 -1
  103. package/src/guest-processes.mjs +22 -14
  104. package/src/host-cli.ts +7 -1
  105. package/src/host-ready.ts +62 -31
  106. package/src/host-worker.ts +6 -2
  107. package/src/host.ts +88 -6
  108. package/src/infra-definition.ts +135 -0
  109. package/src/infra.ts +250 -45
  110. package/src/init.ts +417 -18
  111. package/src/lifecycle-record.ts +18 -4
  112. package/src/local-branches.ts +1 -1
  113. package/src/mariadb-bootstrap.ts +107 -0
  114. package/src/mariadb-supplier.ts +199 -0
  115. package/src/mongodb-backing.ts +59 -1
  116. package/src/native-mariadb-backing.ts +418 -0
  117. package/src/native-postgres-backing.ts +353 -41
  118. package/src/native-server-inputs.ts +169 -0
  119. package/src/origin.ts +3 -3
  120. package/src/original-inputs.ts +72 -0
  121. package/src/pack-facts.ts +1 -1
  122. package/src/pglite-backing.ts +192 -80
  123. package/src/pglite-host.mjs +482 -10
  124. package/src/process-groups.d.mts +6 -1
  125. package/src/process-groups.mjs +82 -15
  126. package/src/process-groups.ts +2 -1
  127. package/src/process-signal.d.mts +2 -2
  128. package/src/process-signal.mjs +29 -9
  129. package/src/project-inspect.ts +37 -4
  130. package/src/proxy-env.ts +128 -0
  131. package/src/redirect-proxy.ts +3 -125
  132. package/src/redis-backing.ts +19 -136
  133. package/src/runtime.ts +342 -107
  134. package/src/schema.ts +5 -4
  135. package/src/served-world.ts +3 -3
  136. package/src/twin-backing.ts +300 -0
  137. package/src/up-cli.ts +52 -3
@@ -2,7 +2,7 @@
2
2
  // carry parent-side pins, including clones across checkouts, without a global World registry.
3
3
  import { existsSync, readdirSync } from 'node:fs';
4
4
  import { join, relative, resolve, sep } from 'node:path';
5
- import { assertStateRemovable, readBranchMeta, stateDirName, withAncestryLock, withStateRemoval, getActiveWorldStore, worldPaths, worldStateRoot } from '@volter/world-core/runtime';
5
+ import { assertStateRemovable, readBranchMeta, stateDirName, withAncestryLock, withStateRemoval, getActiveWorldStore, worldPaths, worldStateRoot } from '@volter/world-core/runtime-state';
6
6
  function checkWorldAncestry(root, path) {
7
7
  const base = join(root, stateDirName(), 'worlds');
8
8
  if (!existsSync(base))
@@ -35,7 +35,7 @@ function checkWorldAncestry(root, path) {
35
35
  }
36
36
  }
37
37
  export function assertWorldStateRemovable(root, path) {
38
- withAncestryLock(() => { checkWorldAncestry(root, path); assertStateRemovable(path); });
38
+ withAncestryLock(() => { checkWorldAncestry(root, path); assertStateRemovable(path); }, { operation: 'removal preflight' });
39
39
  }
40
40
  export function removeWorldState(root, path, remove) {
41
41
  return withStateRemoval(path, remove, () => checkWorldAncestry(root, path));
@@ -22,7 +22,11 @@ export declare function setAppUrl(name: string, url: string, options?: {
22
22
  root?: string;
23
23
  via?: Exclude<AppUrlSource, 'service'>;
24
24
  detail?: string;
25
- app?: string;
25
+ app?: string; /** An instrument: filled with how long the World's status and the record's write each took. */
26
+ timings?: {
27
+ statusMs?: number;
28
+ writeMs?: number;
29
+ };
26
30
  }): AppUrlRecord;
27
31
  /** Record `hosts` as the app's own production hostnames, beside its recorded URL (or its `app` service's). */
28
32
  export declare function addAppHosts(name: string, hosts: string[], options?: {
@@ -21,7 +21,7 @@
21
21
  // instance it described.
22
22
  import { errorMessage } from "./error-message.js";
23
23
  import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
24
- import { withFileLock } from '@volter/world-core/runtime';
24
+ import { withFileLock } from '@volter/world-core/runtime-state';
25
25
  import { join } from 'node:path';
26
26
  import { spawnSync } from 'node:child_process';
27
27
  import net from 'node:net';
@@ -89,8 +89,21 @@ function assertHttpUrl(url) {
89
89
  export function setAppUrl(name, url, options = {}) {
90
90
  const root = options.root ?? process.cwd();
91
91
  const app = appName(options.app);
92
+ const began = Date.now();
92
93
  const status = statusWorld(name, root); // throws "World instance not found" — the loud path
94
+ const statusRead = Date.now();
95
+ if (options.timings)
96
+ options.timings.statusMs = statusRead - began;
93
97
  assertHttpUrl(url);
98
+ try {
99
+ return setAppUrlEntry(root, name, app, url, status, options);
100
+ }
101
+ finally {
102
+ if (options.timings)
103
+ options.timings.writeMs = Date.now() - statusRead;
104
+ }
105
+ }
106
+ function setAppUrlEntry(root, name, app, url, status, options) {
94
107
  return writeEntry(root, name, app, (file) => {
95
108
  // a new URL keeps the hosts recorded for the app (they name the app, not where it listens)
96
109
  const hosts = entryOf(file, app)?.hosts;
@@ -7,7 +7,7 @@
7
7
  // base running at once share the database port its definition names; run one at a time (the SDK does). `checkout <name>`
8
8
  // brings a stopped world back up from its own instance record, state intact.
9
9
  import { runInfrastructure } from "./infra.js";
10
- import { cpSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
10
+ import { cpSync, existsSync, lstatSync, readdirSync, readFileSync, rmSync } from 'node:fs';
11
11
  import { dirname, join, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { captureHistory, historyAtInstant, forkTwin, stateGeneration, withHistoryLocks, worldPaths, worldStateRoot } from '@volter/world-core/runtime';
@@ -48,7 +48,7 @@ export async function branchWorld(base, name, options = {}) {
48
48
  const position = options.at?.positions?.[service] ?? options.at?.positions?.[state] ?? timed?.position;
49
49
  return { state, view, position, generation: stateGeneration(worldPaths(state, fromRoot).dir) };
50
50
  }) };
51
- }));
51
+ }), { operation: 'capture' });
52
52
  // the base's managed databases (checked before the branch boots: its database would share the base's port)
53
53
  const databases = managedDatabases(from);
54
54
  if (databases.length && (options.at?.instant || options.at?.positions || options.at?.views)) {
@@ -90,32 +90,51 @@ export async function branchWorld(base, name, options = {}) {
90
90
  return { instance, forked };
91
91
  }
92
92
  /** The base's containerless managed databases: each PGlite `pglite-<kind>-data`, MongoDB's
93
- * `mongod-mongodb-data` (pglite-backing.ts), the redis twin's `redis-twin` tree (redis-backing.ts), and native PostgreSQL. A stopped World's
93
+ * `mongod-mongodb-data` (pglite-backing.ts), the declared Redis/ClickHouse twin trees, and native PostgreSQL. A stopped World's
94
94
  * hosts leave no pid file; a docker-backed World keeps its data under the container's volume, which a branch does not
95
95
  * carry. */
96
96
  function managedDatabases(from) {
97
97
  const data = from.dirs.data;
98
98
  if (!existsSync(data))
99
99
  return [];
100
- const known = (name) => /^pglite-[a-z0-9_-]+-data$/.test(name) || name === 'mongod-mongodb-data' || name === 'native-postgres-data';
100
+ const known = (name) => /^pglite-[a-z0-9_-]+-data$/.test(name) || name === 'mongod-mongodb-data' || ['native-postgres-data', 'native-mariadb-data'].includes(name);
101
101
  if (readdirSync(data).some(name => name.endsWith('-data') && !known(name) && !from.services[name])) {
102
102
  throw new Error('volter-world branch: unrecognised storage; retain the original World');
103
103
  }
104
- return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'mongod-mongodb-data' || n === 'redis-twin' || n === 'native-postgres-data');
104
+ return readdirSync(data).filter((n) => /^pglite-[a-z0-9_-]+-data$/.test(n) || n === 'mongod-mongodb-data' || ['redis-twin', 'clickhouse-twin', 'native-postgres-data', 'native-mariadb-data'].includes(n));
105
105
  }
106
106
  /** The pid files of the hosts serving one managed database directory. */
107
107
  function hostPidFiles(data, db) {
108
+ if (db === 'native-mariadb-data')
109
+ return readdirSync(data).filter(name => /^native-mariadb\.(?:owner|starting)\.json(?:\.spawn\.json)?$/u.test(name)
110
+ || /^native-mariadb-tool-.*\.json(?:\.spawn\.json)?$/u.test(name)).map(name => join(data, name));
108
111
  if (db === 'native-postgres-data')
109
112
  return [join(data, 'native-postgres.owner.json')];
110
113
  if (db === 'redis-twin')
111
114
  return readdirSync(data).filter((n) => /^redis-twin-\d+\.pid$/.test(n)).map((n) => join(data, n));
115
+ if (db === 'clickhouse-twin')
116
+ return [...new Set(readdirSync(data)
117
+ .filter(n => new RegExp(`^${db}-\\d+\\.pid(?:\\.spawn\\.json)?$`).test(n))
118
+ .map(n => join(data, n.replace(/\.spawn\.json$/, ''))))];
112
119
  return [join(data, `${db.slice(0, -'-data'.length)}.pid`)];
113
120
  }
114
121
  /** A copy of a live database directory is not a consistent one, and the branch's database would answer on the base's
115
122
  * port: the base must be stopped first (`volter world branch` stops it, as a branch replaces its base). */
116
123
  function refuseLiveDatabases(from, dbs) {
117
124
  for (const db of dbs) {
125
+ if (db === 'native-mariadb-data' && (existsSync(join(from.dirs.data, db, 'server.pid')) || existsSync(join(from.dirs.data, db, 'server.sock'))))
126
+ throw new Error(`volter-world branch: ${from.name}'s managed MariaDB retains PID/socket files; verified down is required`);
127
+ if (db === 'native-mariadb-data') {
128
+ const stamp = join(from.dirs.data, db, '.world-policy.json');
129
+ if (!existsSync(stamp) || !lstatSync(stamp).isFile() || existsSync(join(from.dirs.data, db, '.world-initializing.json')))
130
+ throw new Error(`volter-world branch: ${from.name}'s MariaDB has incomplete initialization; original data retained`);
131
+ }
118
132
  for (const pidFile of hostPidFiles(from.dirs.data, db)) {
133
+ // Successful managed-twin down removes both receipts. A partial publication or an
134
+ // uncertain retirement must finish there before branch copies this store.
135
+ if (db === 'clickhouse-twin' || db === 'native-mariadb-data') {
136
+ throw new Error(`volter-world branch: ${from.name}'s managed ${db} retains ownership receipts; stop ${from.name} before copying its store`);
137
+ }
119
138
  const recorded = existsSync(pidFile) ? readFileSync(pidFile, 'utf8').trim() : '';
120
139
  const pid = db === 'native-postgres-data' && recorded ? JSON.parse(recorded).pid : Number(recorded);
121
140
  let live = false;
@@ -137,6 +156,7 @@ function refuseLiveDatabases(from, dbs) {
137
156
  function requireMatchingBackings(source, child) {
138
157
  const kinds = {
139
158
  postgres: ['native-postgres-data', 'pglite-postgres-data'],
159
+ mysql: ['native-mariadb-data'],
140
160
  mongodb: ['mongod-mongodb-data'],
141
161
  };
142
162
  for (const [kind, formats] of Object.entries(kinds)) {
@@ -179,17 +199,17 @@ async function branchManagedDatabases(from, instance, dbs) {
179
199
  if (!infrastructure)
180
200
  throw new Error(`volter-world branch: ${instance.name}'s managed databases have no declared infrastructure service`);
181
201
  const launch = { env: externalServiceEnv(infrastructure, instance.dirs, instance.env, config.stripEnv) };
182
- if (dbs.includes('native-postgres-data')) {
202
+ if (dbs.some(directory => ['native-postgres-data', 'native-mariadb-data', 'clickhouse-twin'].includes(directory))) {
183
203
  const io = { out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text) };
184
204
  if (await runInfrastructure('down', launch.env, io) !== 0)
185
- throw new Error('managed native database branch could not stop its new backing');
205
+ throw new Error('managed database branch could not stop its new backing');
186
206
  for (const db of dbs) {
187
207
  const to = join(instance.dirs.data, db);
188
208
  rmSync(to, { recursive: true, force: true });
189
209
  cpSync(join(from.dirs.data, db), to, { recursive: true });
190
210
  }
191
211
  if (await runInfrastructure('up', launch.env, io) !== 0)
192
- throw new Error('managed native database branch could not resume its copied backing');
212
+ throw new Error('managed database branch could not resume its copied backing');
193
213
  return;
194
214
  }
195
215
  const redisCli = dbs.includes('redis-twin') && redis.length ? redisTwinCli([dirname(fileURLToPath(import.meta.url)), dirname(instance.configPath)]) : undefined;
@@ -0,0 +1,5 @@
1
+ /** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
2
+ export declare const browsersDir: (data: string) => string;
3
+ /** Empty Worlds need no browser validators or cookie/load machinery. A prior
4
+ * projection still loads the ordinary materializer to remove stale files. */
5
+ export declare function materializeWorldBrowserFiles(data: string, root: string, name: string, main?: string): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime-state';
2
+ import { dirname, join } from 'node:path';
3
+ /** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
4
+ export const browsersDir = (data) => join(dirname(data), 'browser-records');
5
+ /** Empty Worlds need no browser validators or cookie/load machinery. A prior
6
+ * projection still loads the ordinary materializer to remove stale files. */
7
+ export async function materializeWorldBrowserFiles(data, root, name, main = name) {
8
+ const store = getActiveWorldStore();
9
+ if (!store.list(browsersDir(data)).some(file => file.endsWith('.json'))
10
+ && store.read(join(root, stateDirName(), 'browsers', '.source.json')) === null)
11
+ return;
12
+ const { materializeBrowserFiles } = await import("./browsers.js");
13
+ materializeBrowserFiles(data, root, name, main);
14
+ }
@@ -1,11 +1,10 @@
1
- import { type BrowserState } from '@volter/world-core';
1
+ import { type BrowserState } from '@volter/world-core/browser-state';
2
2
  export type WorldBrowser = {
3
3
  name: string;
4
4
  state: BrowserState;
5
5
  version: string;
6
6
  };
7
- /** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
8
- export declare const browsersDir: (data: string) => string;
7
+ export { browsersDir } from './browser-files.js';
9
8
  export declare function worldBrowser(data: string, name: string): WorldBrowser | null;
10
9
  export declare function worldBrowsers(data: string): WorldBrowser[];
11
10
  export declare function putWorldBrowser(data: string, name: string, state: unknown): WorldBrowser;
@@ -1,9 +1,9 @@
1
- import { assertBrowserName, validateBrowserState } from '@volter/world-core';
2
- import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime';
1
+ import { assertBrowserName, validateBrowserState } from '@volter/world-core/browser-state';
2
+ import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime-state';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { dirname, join } from 'node:path';
5
- /** Instance bookkeeping, alongside the clock; retained at down, discarded at reset. */
6
- export const browsersDir = (data) => join(dirname(data), 'browser-records');
5
+ import { browsersDir } from "./browser-files.js";
6
+ export { browsersDir } from "./browser-files.js";
7
7
  const path = (data, name) => { assertBrowserName(name); return join(browsersDir(data), `${name}.json`); };
8
8
  const records = new WeakMap();
9
9
  export function worldBrowser(data, name) {
@@ -1,4 +1,4 @@
1
- import { installedPackageDir } from '@volter/world-core/runtime';
1
+ import { installedPackageDir } from '@volter/world-core/runtime-state';
2
2
  export declare const TWIN_PACKAGE_PREFIX = "@volter/twin-";
3
3
  export declare function twinPackageName(vendor: string): string;
4
4
  export type CatalogKind = 'checkout' | 'installed';
@@ -45,6 +45,7 @@ export declare function packFactsFile(dir: string): string | undefined;
45
45
  export declare function resolveCatalog(root: string, options?: {
46
46
  sources?: Readonly<Record<string, string>>;
47
47
  vendors?: readonly string[];
48
+ mayBeEmpty?: boolean;
48
49
  }): Catalog;
49
50
  /** The pack directory a service boots from: its `package` resolved from the world root at boot. */
50
51
  export declare function resolveServicePackage(root: string, pkg: string): string;
@@ -9,7 +9,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
10
  import { join, resolve } from 'node:path';
11
11
  import { overlayPackFacts } from "./pack-facts.js";
12
- import { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository } from '@volter/world-core/runtime';
12
+ import { installedPackageDir, installedVendorPacks, isDerivedPack, packRepository } from '@volter/world-core/runtime-state';
13
13
  export const TWIN_PACKAGE_PREFIX = '@volter/twin-';
14
14
  export function twinPackageName(vendor) { return `${TWIN_PACKAGE_PREFIX}${vendor}`; }
15
15
  /** A derived (Protocol 3) pack carries the surface derive-pack generated from its spec. */
@@ -85,7 +85,10 @@ export function checkoutCatalog(root) {
85
85
  };
86
86
  }
87
87
  export function installedCatalog(root, options = {}) {
88
- const twins = new Map([...installedVendorPacks(root, options)].filter(([, dir]) => packCli(dir) !== undefined));
88
+ // An explicit empty selection needs no package discovery or publisher choice.
89
+ const twins = options.vendors?.length === 0
90
+ ? new Map()
91
+ : new Map([...installedVendorPacks(root, options)].filter(([, dir]) => packCli(dir) !== undefined));
89
92
  return {
90
93
  kind: 'installed',
91
94
  root: resolve(root),
@@ -152,6 +155,8 @@ export function packFactsFile(dir) {
152
155
  export function resolveCatalog(root, options = {}) {
153
156
  const resolved = resolve(root);
154
157
  const installed = installedCatalog(resolved, options);
158
+ if (options.vendors?.length === 0)
159
+ return installed;
155
160
  const repo = packRepository(resolved);
156
161
  const catalog = repo === undefined ? installed : mergedCatalog([checkoutCatalog(repo), installed]);
157
162
  for (const vendor of catalog.vendors()) {
@@ -159,7 +164,8 @@ export function resolveCatalog(root, options = {}) {
159
164
  if (file)
160
165
  overlayPackFactsFile(file);
161
166
  }
162
- if (catalog.vendors().length === 0) {
167
+ // A World whose application runs beside infrastructure alone has no twin to find (`init --needs`, architecture).
168
+ if (catalog.vendors().length === 0 && !options.mayBeEmpty) {
163
169
  throw new Error(`No twins found for ${resolved}: no pack repository and no \`@volter/twin-<vendor>\` package installed above it. `
164
170
  + `Install the twins your app needs (e.g. \`bun add -d @volter/twin-stripe\`) and run init again.`);
165
171
  }
@@ -586,7 +586,7 @@ export function rebaseWorldChangeset(name, options = {}) {
586
586
  withHistoryLocks(moved.map(({ st, controlRoot }) => worldPaths(st, controlRoot).dir), () => {
587
587
  for (const { st, controlRoot } of moved)
588
588
  rebaseBranch(st, controlRoot, { origin: Boolean(readBranchMeta(st, controlRoot)?.origin) });
589
- });
589
+ }, { operation: 'branch-pointer adjustment' });
590
590
  if (!changesetHashMatches(located.changeset))
591
591
  throw new Error(`Refusing to rebase changeset "${name}": its stored contentHash does not match its body — the object drifted after authoring`);
592
592
  const instance = requireWorld(located.world, root);