@volter/world 3.0.184 → 3.0.186

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 (38) hide show
  1. package/dist/examples/customer-onboarding/.env.world +3 -0
  2. package/dist/examples/customer-onboarding/README.md +14 -13
  3. package/dist/examples/customer-onboarding/compose.yml +20 -0
  4. package/dist/examples/customer-onboarding/demo.mjs +1 -1
  5. package/dist/examples/customer-onboarding/gitignore.template +4 -0
  6. package/dist/examples/customer-onboarding/onboarding.mjs +1 -0
  7. package/dist/examples/customer-onboarding/package.json +5 -5
  8. package/dist/examples/customer-onboarding/public-files.json +2 -0
  9. package/dist/examples/index.json +2 -0
  10. package/dist/examples/integration-branches/README.md +4 -4
  11. package/dist/examples/integration-branches/package.json +5 -5
  12. package/dist/examples/workplace-agent/README.md +1 -1
  13. package/dist/examples/workplace-agent/package.json +5 -5
  14. package/dist/skills/volter-world/AGENTS.md +23 -14
  15. package/dist/src/cli.d.ts +1 -1
  16. package/dist/src/cli.js +33 -13
  17. package/dist/src/locate.d.ts +4 -3
  18. package/dist/src/locate.js +12 -8
  19. package/dist/src/world.d.ts +13 -4
  20. package/dist/src/world.js +17 -27
  21. package/examples/customer-onboarding/.env.world +3 -0
  22. package/examples/customer-onboarding/README.md +14 -13
  23. package/examples/customer-onboarding/compose.yml +20 -0
  24. package/examples/customer-onboarding/demo.mjs +1 -1
  25. package/examples/customer-onboarding/gitignore.template +4 -0
  26. package/examples/customer-onboarding/onboarding.mjs +1 -0
  27. package/examples/customer-onboarding/package.json +5 -5
  28. package/examples/customer-onboarding/public-files.json +2 -0
  29. package/examples/index.json +2 -0
  30. package/examples/integration-branches/README.md +4 -4
  31. package/examples/integration-branches/package.json +5 -5
  32. package/examples/workplace-agent/README.md +1 -1
  33. package/examples/workplace-agent/package.json +5 -5
  34. package/package.json +5 -5
  35. package/skills/volter-world/AGENTS.md +23 -14
  36. package/src/cli.ts +33 -16
  37. package/src/locate.ts +12 -8
  38. package/src/world.ts +19 -27
package/src/cli.ts CHANGED
@@ -3,16 +3,16 @@
3
3
  // verb. `volter world …` acts on the world of the cwd the way git acts on the repository of the
4
4
  // cwd; `volter twin <vendor> …` runs one twin on its own; `volter remote …` is the hosted remote.
5
5
  // One client of `World`; it adds nothing the SDK does not have.
6
- import { detectRepoVendors, errorMessage, formatInitReport, resolveCatalog, findInstalledPackage, packCli, serveConsoleFor, catalogDefaults, CATALOG_INDEX_PACKAGE } from '@volter/world-runtime';
7
- import { readFileSync, realpathSync } from 'node:fs';
6
+ import { isBuiltinTwinVendor, detectRepoVendors, errorMessage, formatInitReport, resolveCatalog, findInstalledPackage, packCli, serveConsoleFor, catalogDefaults, CATALOG_INDEX_PACKAGE } from '@volter/world-runtime';
7
+ import { realpathSync } from 'node:fs';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { spawn, spawnSync } from 'node:child_process';
10
10
  import { createHash } from 'node:crypto';
11
11
  import { hostname } from 'node:os';
12
- import { formatLedgerDelta, inWorldHistory, isWorldPerson, shownPersonFields } from '@volter/world-core/runtime';
12
+ import { inWorldHistory, isWorldPerson, shownPersonFields } from '@volter/world-core/runtime';
13
13
  import { existsSync } from 'node:fs';
14
14
  import { join, relative, resolve } from 'node:path';
15
- import { forgetPlatform, forgetToken, platformOrigin, requireToken, storePlatform, storeToken, tokenFor } from './credentials.ts';
15
+ import { forgetPlatform, forgetToken, platformOrigin, storePlatform, storeToken, tokenFor } from './credentials.ts';
16
16
  import { parseOriginUrl, World } from './world.ts';
17
17
  import { copyExample, examples } from './examples.ts';
18
18
  import { waitForServeShutdown } from './serve-shutdown.ts';
@@ -31,9 +31,10 @@ function openInBrowser(url: string): void {
31
31
 
32
32
  export const HELP = `volter — run your app against a world of twins
33
33
 
34
- world — the twins your app needs, running together (the world of the current directory)
34
+ world — a shared simulated internet (the world of the current directory)
35
35
  volter world init [--install] [--name <world>] [--twins a,b] [--source vendor=package] [--allow-unknown] detect or select vendors, choose installed publishers, write .volter/world.json
36
36
  volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)
37
+ volter world env [--template <file>] render deployment .env.world references into dotenv files
37
38
  volter world run [--verbose] -- <command...> run the app or its tests inside the world
38
39
  volter world activate eval "$(volter world activate)": vendor CLIs and curl in this shell reach the twins
39
40
  volter world shell a subshell with the world active
@@ -98,6 +99,10 @@ function value(args: string[], name: string): string | undefined {
98
99
  const i = args.indexOf(name);
99
100
  return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
100
101
  }
102
+ function values(args: string[]): string[] | undefined {
103
+ const found = args.flatMap((arg, index) => arg === '--template' && args[index + 1] ? [args[index + 1]!] : []);
104
+ return found.length ? found : undefined;
105
+ }
101
106
  function positionals(args: string[], valueFlags: string[]): string[] {
102
107
  const out: string[] = [];
103
108
  for (let i = 0; i < args.length; i += 1) {
@@ -109,7 +114,7 @@ function positionals(args: string[], valueFlags: string[]): string[] {
109
114
  return out;
110
115
  }
111
116
  function afterDashes(args: string[]): string[] { const i = args.indexOf('--'); return i < 0 ? [] : args.slice(i + 1); }
112
- const VALUE_FLAGS = ['--world', '--branch', '--name', '--token', '--admin-token', '-m', '--message', '--base', '--org', '--vendors', '--into', '--deploy', '--as', '--note', '--port', '--twins', '--source', '--host', '--origin', '--remote', '--dir', '--scope', '--refresh', '--at-most', '--at'];
117
+ const VALUE_FLAGS = ['--world', '--branch', '--name', '--token', '--admin-token', '-m', '--message', '--base', '--org', '--vendors', '--into', '--deploy', '--as', '--note', '--port', '--twins', '--source', '--host', '--origin', '--remote', '--dir', '--scope', '--refresh', '--at-most', '--at', '--template', '--output-root', '--app', '--app-url', '--template-out'];
113
118
 
114
119
  /** Help is resolved before opening a World; the child after `run --` owns its own flags. */
115
120
  function wantsHelp(noun: string | undefined, verb: string | undefined, args: string[]): boolean {
@@ -130,7 +135,6 @@ function subjectLabel(subject: { type: string; id: string }): string {
130
135
  }
131
136
  function out(text: string): void { process.stdout.write(text.endsWith('\n') ? text : `${text}\n`); }
132
137
  function json(value: unknown): void { out(JSON.stringify(value, null, 2)); }
133
- function when(iso: string): string { return iso.slice(11, 23); }
134
138
 
135
139
  /** Run another CLI in the foreground and adopt its exit code; a signal to us is a signal to it. */
136
140
  async function passthrough(cmd: string[], cwd = process.cwd()): Promise<number> {
@@ -153,9 +157,11 @@ async function stdinText(): Promise<string> {
153
157
  }
154
158
 
155
159
  async function worldCommand(verb: string, args: string[]): Promise<void> {
156
- const worldDir = value(args, '--world');
160
+ const boundary = args.indexOf('--');
161
+ const parentOptions = verb === 'run' && boundary >= 0 ? args.slice(0, boundary) : args;
162
+ const worldDir = value(parentOptions, '--world') ?? (verb === 'up' ? positionals(args, VALUE_FLAGS)[0] : undefined);
157
163
  const pos = positionals(args, VALUE_FLAGS);
158
- const asJson = flag(args, '--json');
164
+ const asJson = flag(parentOptions, '--json');
159
165
 
160
166
  if (verb === 'init' && flag(args, '--bare')) {
161
167
  if (args.some(arg => arg === '--source' || arg.startsWith('--source='))) throw new Error('--source selects installed publishers for app initialization; omit --bare and use world init --twins <vendors> --source vendor=package');
@@ -172,7 +178,7 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
172
178
  const app = resolve(worldDir ?? process.cwd());
173
179
  const selection = initSelection(args);
174
180
  await installDetectedTwins(app, flag(args, '--install'), selection);
175
- const { result } = World.init(app, { ...selection, ...(value(args, '--name') ? { name: value(args, '--name')! } : {}), force: flag(args, '--force'), allowUnknown: flag(args, '--allow-unknown') });
181
+ const { result } = World.init(app, { ...selection, ...(value(args, '--name') ? { name: value(args, '--name')! } : {}), force: flag(args, '--force'), update: flag(args, '--update'), templateOut: value(args, '--template-out'), allowUnknown: flag(args, '--allow-unknown') });
176
182
  if (asJson) json({ ok: result.ok, plan: result.plan, coverage: result.coverage, next: result.next });
177
183
  else {
178
184
  const twins = result.plan.vendors.filter((v) => v.service !== null).map((v) => v.vendor).sort();
@@ -199,7 +205,7 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
199
205
  process.exit(0);
200
206
  }
201
207
 
202
- const world = World.open({ ...(worldDir ? { root: worldDir } : {}), ...(value(args, '--branch') ? { name: value(args, '--branch')! } : {}) });
208
+ const world = World.open({ ...(worldDir ? { root: worldDir } : {}), ...(value(parentOptions, '--branch') ? { name: value(parentOptions, '--branch')! } : {}) });
203
209
 
204
210
  switch (verb) {
205
211
  case 'up': {
@@ -219,6 +225,13 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
219
225
  }
220
226
  case 'activate': { process.stdout.write(world.activateScript()); return; }
221
227
  case 'shell': { process.exitCode = await world.shell(); return; }
228
+ case 'env': {
229
+ const result = await world.env({ templates: values(args), outputRoot: value(args, '--output-root'), app: value(args, '--app'), appUrl: value(args, '--app-url') });
230
+ // Admission payloads are private. CLI output reports paths and hashes only.
231
+ if (asJson) json({ deploymentId: result.deploymentId, outputs: result.outputs.map(({ contents: _private, ...output }) => output) });
232
+ else for (const output of result.outputs) out(`wrote ${output.target}`);
233
+ return;
234
+ }
222
235
  case 'run': {
223
236
  const command = afterDashes(args);
224
237
  if (command.length === 0) throw new Error('volter world run: give the command after -- (volter world run -- npm test)');
@@ -226,7 +239,12 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
226
239
  let running = false; try { running = world.instance()?.running ?? false; } catch { /* never started */ }
227
240
  if (!running) { process.stderr.write(`${world.name} is not running: starting it (volter world up)
228
241
  `); await world.up({ seed: true }); }
229
- process.exitCode = await world.run(command, { verbose: flag(args.slice(0, args.indexOf('--')), '--verbose') });
242
+ process.exitCode = await world.run(command, {
243
+ verbose: flag(parentOptions, '--verbose'),
244
+ templates: values(parentOptions),
245
+ app: value(parentOptions, '--app'),
246
+ appUrl: value(parentOptions, '--app-url'),
247
+ });
230
248
  return;
231
249
  }
232
250
  case 'status': {
@@ -659,9 +677,8 @@ async function remoteCommand(verb: string, args: string[]): Promise<void> {
659
677
  // (docs/contributing/architecture.md, "The way in is the app's folder")
660
678
  if (made && world.hasSeed()) {
661
679
  const seeded = await world.seedServed(body.base, body.token);
662
- const issued = Object.entries(seeded.issued).map(([vendor, names]) => `${names.join(', ')} from ${vendor}`);
663
680
  if (seeded.exitCode !== 0) throw new Error(`the seed failed on ${served} (${seeded.entry} exited ${seeded.exitCode}): it holds what the seed wrote before it stopped, and is not marked`);
664
- out(`seeded ${served} ${relative(world.root, seeded.entry)} through its own twins${issued.length ? ` (${issued.join('; ')}, issued there)` : ''}`);
681
+ out(`seeded ${served} ${relative(world.root, seeded.entry)} through its own twins`);
665
682
  out(`marked ${served} its seed is where its changes count from${seeded.mark?.passed ? ` (${seeded.mark.passed} written by the seed)` : ''}`);
666
683
  }
667
684
  return;
@@ -774,13 +791,13 @@ function initSelection(args: string[]): { vendors?: string[]; sources: Record<st
774
791
  return { ...(vendors ? { vendors } : {}), sources };
775
792
  }
776
793
 
777
- async function installDetectedTwins(app: string, install: boolean, selection: { vendors?: string[]; sources?: Record<string, string> } = {}): Promise<void> {
794
+ async function installDetectedTwins(app: string, install: boolean, selection: { vendors?: string[]; sources?: Record<string, string> }): Promise<void> {
778
795
  let installed = new Set<string>();
779
796
  try { installed = new Set(resolveCatalog(app, selection).vendors()); } catch (error) {
780
797
  if (!(error instanceof Error) || !error.message.startsWith('No twins found for ')) throw error;
781
798
  }
782
799
  const found = detectRepoVendors(app).detected;
783
- const vendors = (selection.vendors ?? [...found.keys()]).filter((v) => !installed.has(v) && /^[a-z0-9][a-z0-9-]*$/.test(v)).sort();
800
+ const vendors = (selection.vendors ?? [...found.keys()]).filter((v) => !installed.has(v) && (selection.sources?.[v] !== undefined || !isBuiltinTwinVendor(v)) && /^[a-z0-9][a-z0-9-]*$/.test(v)).sort();
784
801
  if (vendors.length === 0) return; // init says what it found and how to add a twin by hand
785
802
  const index = findInstalledPackage(app, CATALOG_INDEX_PACKAGE) ?? findInstalledPackage(fileURLToPath(new URL('.', import.meta.url)), CATALOG_INDEX_PACKAGE);
786
803
  const recommended = index ? catalogDefaults(index, vendors) : undefined;
package/src/locate.ts CHANGED
@@ -1,5 +1,5 @@
1
- // Where the world is (docs/concepts/worlds.md#the-config-and-the-running-world): the nearest `.volter/world.json`
2
- // above the cwd names the world root — the app repo — and `.volter/current` names the branch that
1
+ // Where the world is (docs/concepts/worlds.md#the-config-and-the-running-world): the nearest standard
2
+ // World folder or `.volter/world.json` above the cwd names the root; `.volter/current` names the branch that
3
3
  // is checked out (absent: the main branch, the config's `id`). Every `volter` verb and
4
4
  // `World.open()` start here, the way `git` starts from the nearest `.git`.
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
@@ -11,15 +11,19 @@ export const WORLD_FILE = 'world.json';
11
11
 
12
12
  /** `<root>/.volter` */
13
13
  export function stateDir(root: string): string { return join(root, stateDirName()); }
14
- /** `<root>/.volter/world.json` — the config, relative to the root as `up` takes it. */
14
+ /** The in-repository config path used by initialization. */
15
15
  export function worldConfigRelative(): string { return join(stateDirName(), WORLD_FILE); }
16
- export function worldConfigPath(root: string): string { return join(stateDir(root), WORLD_FILE); }
16
+ /** A standalone World keeps its config at the folder root; an in-repository World uses `.volter`. */
17
+ export function worldConfigPath(root: string): string {
18
+ const standard = join(root, WORLD_FILE);
19
+ return existsSync(standard) ? standard : join(stateDir(root), WORLD_FILE);
20
+ }
17
21
  /** `<root>/.volter/world.env` — where `up` writes the live env (gitignored by init). */
18
22
  export function worldEnvPath(root: string): string { return join(stateDir(root), 'world.env'); }
19
- /** `<root>/.volter/seed.ts` — the default data, when the world has any. */
20
- export function worldSeedPath(root: string): string { return join(stateDir(root), 'seed.ts'); }
23
+ /** The standard seed lives beside the selected config. */
24
+ export function worldSeedPath(root: string): string { return join(dirname(worldConfigPath(root)), 'seed.ts'); }
21
25
 
22
- /** The world root above `from`, or null: the nearest directory holding `.volter/world.json`. */
26
+ /** The nearest directory holding a standard World config. */
23
27
  export function findWorldRoot(from: string = process.cwd()): string | null {
24
28
  let dir = resolve(from);
25
29
  for (;;) {
@@ -33,7 +37,7 @@ export function findWorldRoot(from: string = process.cwd()): string | null {
33
37
  export function requireWorldRoot(from: string = process.cwd()): string {
34
38
  const root = findWorldRoot(from);
35
39
  if (root === null) {
36
- throw new Error(`Not in a world: no ${worldConfigRelative()} here or above ${resolve(from)}. Run \`volter world init\` in your app, or pass --world <dir>.`);
40
+ throw new Error(`Not in a world: no world.json or ${worldConfigRelative()} here or above ${resolve(from)}. Run \`volter world init\` in your app, or pass --world <folder>.`);
37
41
  }
38
42
  return root;
39
43
  }
package/src/world.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import { renderDeploymentTemplates, type DeploymentTemplate } from '@volter/world-runtime/deployment-env';
1
2
  // WORLD — the twins an app needs, running together (docs/concepts/the-model.md,
2
3
  // docs/concepts/worlds.md#the-config-and-the-running-world): a branch with compute attached. `World.open()` finds the world of the cwd the
3
4
  // way git finds its repository; every method is a name for a world-runtime verb, in the words a
4
5
  // user reads. The `volter` command is one client of this class and adds nothing.
5
6
  import { platformPerson } from './credentials.ts';
6
- import { rebaseChangeset, worldBootMarker, type Changeset, type ChangesetApplication, type LedgerDelta, type PerformAction, type RemoteExecute, type TwinAction, type WorldMarker, type ApplyReceipt, listActions, inWorldHistory, pushablePendingActions, pendingActions, applyTwinWrite, twinResources, type TwinResource } from '@volter/world-core/runtime';
7
+ import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, listActions, inWorldHistory, pushablePendingActions, pendingActions, applyTwinWrite, twinResources, type TwinResource } from '@volter/world-core/runtime';
7
8
  import { spawnSync } from 'node:child_process';
8
9
  import clockForm from '@volter/world-core/world-clock';
9
10
  import {
@@ -15,7 +16,7 @@ import {
15
16
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
16
17
  import { dirname, join, resolve } from 'node:path';
17
18
  import { requireToken, storeToken } from './credentials.ts';
18
- import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from './locate.ts';
19
+ import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigPath, worldEnvPath, worldSeedPath } from './locate.ts';
19
20
  import { destinationOf, parentEntries, rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
20
21
 
21
22
  export type WorldRef = { name?: string; root?: string };
@@ -65,8 +66,8 @@ export function parseOriginUrl(url: string, worldRoot = process.cwd()): { url: s
65
66
  // a PATH is a world on this machine (git's file transport): its directory, and its served name
66
67
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
67
68
  const root = resolve(worldRoot, url);
68
- const configPath = join(root, '.volter', 'world.json');
69
- if (!existsSync(configPath)) throw new Error(`Not a remote URL or world directory: ${url} (want https://<host>/<org>/<world>, or a directory holding .volter/world.json)`);
69
+ const configPath = worldConfigPath(root);
70
+ if (!existsSync(configPath)) throw new Error(`Not a remote URL or world directory: ${url} (want https://<host>/<org>/<world>, or a standard World folder)`);
70
71
  const config = loadWorldConfig(configPath, root).config;
71
72
  return { url: root, namespace: config.bare?.name ?? `${config.id}/${config.id}` };
72
73
  }
@@ -80,14 +81,14 @@ export function parseOriginUrl(url: string, worldRoot = process.cwd()): { url: s
80
81
  export class World {
81
82
  /** The branch this handle is on — the instance name in the kernel. */
82
83
  readonly name: string;
83
- /** The world root: the app repo. */
84
+ /** The World folder, or the repository holding its `.volter` config. */
84
85
  readonly root: string;
85
86
  private constructor(name: string, root: string) { this.name = name; this.root = root; }
86
87
 
87
88
  // ── finding and making worlds ───────────────────────────────────────────────────────────
88
89
  /** The world of the cwd (or of `ref.root`), on its checked-out branch (or `ref.name`). */
89
90
  /** The config `loadWorldConfig` resolves for this world: the in-repo world.json, else the world's name. */
90
- private configRef(): string { const inRepo = join(this.root, '.volter', 'world.json'); return existsSync(inRepo) ? inRepo : this.name; }
91
+ private configRef(): string { const config = worldConfigPath(this.root); return existsSync(config) ? config : this.name; }
91
92
 
92
93
  static open(ref: WorldRef = {}): World {
93
94
  const root = ref.root === undefined ? requireWorldRoot() : requireWorldRoot(ref.root);
@@ -133,35 +134,28 @@ export class World {
133
134
  * stops compute, up resumes it — `reset` is the way back to the default data); a fresh branch
134
135
  * boots clean and loads the default data unless `seed: false`.
135
136
  */
136
- async up(opts: { mode?: WorldMode; seed?: boolean; cwd?: string } = {}): Promise<WorldInstance> {
137
+ async up(opts: { mode?: WorldMode; seed?: boolean } = {}): Promise<WorldInstance> {
137
138
  const existing = this.instance();
138
139
  if (existing?.running) return existing; // already up: nothing to do
139
- const instance = await upWorld(worldConfigRelative(), {
140
+ const instance = await upWorld(this.configRef(), {
140
141
  name: this.name, root: this.root, envFile: existing?.envFile ?? worldEnvPath(this.root),
141
142
  ...(opts.mode ? { mode: opts.mode } : existing ? { mode: existing.mode } : {}),
142
- ...(existing ? { keepState: true } : {}),
143
+ ...(existing ? { keepState: true } : {}), seed: opts.seed,
143
144
  });
144
- await this.finishStartup(instance, existing === null, opts);
145
145
  return instance;
146
146
  }
147
- /** Product startup completes its seed sequence; the runtime owns only the boot. */
148
- private async finishStartup(instance: WorldInstance, fresh: boolean, opts: { seed?: boolean; cwd?: string } = {}): Promise<void> {
149
- try {
150
- if (fresh && opts.seed !== false && existsSync(worldSeedPath(this.root))) {
151
- // a story that fails is a loud failure: the world is up, but not the world the app expects
152
- const seeded = await seedWorld(this.name, { root: this.root, cwd: opts.cwd ?? this.root });
153
- if (seeded.exitCode !== 0) throw new Error(`the story failed (${seeded.entry} exited ${seeded.exitCode}); state may be partially seeded — fix the seed, start this World and run \`volter world seed\``);
154
- }
155
- } finally {
156
- // a root set while the world was stopped reaches the twins' state after the default data (which a
157
- // fresh boot runs simulated), where the deploy reads it — also when the seed failed
158
- materializeRoots(loadWorldConfig(this.configRef(), this.root).config, instance.dirs.data, this.root);
159
- }
160
- }
161
147
  /** Stop the twins; the branch's state stays (`up` resumes). `purge` forgets it. */
162
148
  down(opts: { purge?: boolean } = {}): ReturnType<typeof downWorld> { return downWorld(this.name, this.root, opts); }
163
149
  /** Run a command inside the world: the app or its tests, with the twins' URLs and fake credentials in its env. */
164
- run(command: string[], opts: { cwd?: string; verbose?: boolean } = {}): Promise<number> { return runWithWorldEnv(this.name, command, this.root, { ...opts, cwd: opts.cwd ?? this.root }); }
150
+ async run(command: string[], opts: { cwd?: string; verbose?: boolean; templates?: DeploymentTemplate[]; app?: string; appUrl?: string } = {}): Promise<number> {
151
+ const cwd = opts.cwd ?? process.cwd();
152
+ const rendered = await renderDeploymentTemplates({ world: { name: this.name, root: this.root }, sourceRoot: cwd, mode: 'process', templates: opts.templates, app: opts.app, appUrl: opts.appUrl });
153
+ return runWithWorldEnv(this.name, command, this.root, { cwd, verbose: opts.verbose, env: rendered.env });
154
+ }
155
+ /** Render deployment templates into their selected dotenv files. */
156
+ env(opts: { cwd?: string; outputRoot?: string; templates?: DeploymentTemplate[]; app?: string; appUrl?: string } = {}): ReturnType<typeof renderDeploymentTemplates> {
157
+ return renderDeploymentTemplates({ world: { name: this.name, root: this.root }, sourceRoot: opts.cwd ?? process.cwd(), outputRoot: opts.outputRoot, mode: 'files', templates: opts.templates, app: opts.app, appUrl: opts.appUrl });
158
+ }
165
159
  /** The shell script that activates the world in the current shell: `eval "$(volter world activate)"` — vendor CLIs and curl reach the twins. */
166
160
  activateScript(): string { return activateScript(this.name, this.root); }
167
161
  /** A subshell with the world active; resolves to its exit code. */
@@ -364,7 +358,6 @@ export class World {
364
358
  serve(opts: { port?: number; host?: string; consolePort?: number; announce?: (info: { name: string; base: string; token: string; readToken: string; console: string | null }) => void } = {}): Promise<ServedWorld> { return serveWorld(this.name, { root: this.root, ...opts }); }
365
359
  /** `volter world view`: this World, its twins' screens, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
366
360
  async view(opts: { port?: number; host?: string; console?: ViewConsole; announce?: (line: string) => void; origins?: boolean; origin?: string } = {}): Promise<WorldView> {
367
- const fresh = this.instance() === null;
368
361
  const person = platformPerson();
369
362
  const announced: string[] = [];
370
363
  let ready = false;
@@ -372,7 +365,6 @@ export class World {
372
365
  try {
373
366
  const instance = this.instance();
374
367
  if (!instance) throw new Error('the viewer started without a recorded World instance');
375
- await this.finishStartup(instance, fresh);
376
368
  for (const line of announced) opts.announce?.(line);
377
369
  ready = true;
378
370
  return view;