@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.
- package/dist/examples/customer-onboarding/.env.world +3 -0
- package/dist/examples/customer-onboarding/README.md +14 -13
- package/dist/examples/customer-onboarding/compose.yml +20 -0
- package/dist/examples/customer-onboarding/demo.mjs +1 -1
- package/dist/examples/customer-onboarding/gitignore.template +4 -0
- package/dist/examples/customer-onboarding/onboarding.mjs +1 -0
- package/dist/examples/customer-onboarding/package.json +5 -5
- package/dist/examples/customer-onboarding/public-files.json +2 -0
- package/dist/examples/index.json +2 -0
- package/dist/examples/integration-branches/README.md +4 -4
- package/dist/examples/integration-branches/package.json +5 -5
- package/dist/examples/workplace-agent/README.md +1 -1
- package/dist/examples/workplace-agent/package.json +5 -5
- package/dist/skills/volter-world/AGENTS.md +23 -14
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +33 -13
- package/dist/src/locate.d.ts +4 -3
- package/dist/src/locate.js +12 -8
- package/dist/src/world.d.ts +13 -4
- package/dist/src/world.js +17 -27
- package/examples/customer-onboarding/.env.world +3 -0
- package/examples/customer-onboarding/README.md +14 -13
- package/examples/customer-onboarding/compose.yml +20 -0
- package/examples/customer-onboarding/demo.mjs +1 -1
- package/examples/customer-onboarding/gitignore.template +4 -0
- package/examples/customer-onboarding/onboarding.mjs +1 -0
- package/examples/customer-onboarding/package.json +5 -5
- package/examples/customer-onboarding/public-files.json +2 -0
- package/examples/index.json +2 -0
- package/examples/integration-branches/README.md +4 -4
- package/examples/integration-branches/package.json +5 -5
- package/examples/workplace-agent/README.md +1 -1
- package/examples/workplace-agent/package.json +5 -5
- package/package.json +5 -5
- package/skills/volter-world/AGENTS.md +23 -14
- package/src/cli.ts +33 -16
- package/src/locate.ts +12 -8
- 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 {
|
|
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 {
|
|
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,
|
|
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 —
|
|
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
|
|
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(
|
|
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(
|
|
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, {
|
|
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
|
|
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> }
|
|
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
|
|
2
|
-
// above the cwd names the
|
|
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
|
-
/**
|
|
14
|
+
/** The in-repository config path used by initialization. */
|
|
15
15
|
export function worldConfigRelative(): string { return join(stateDirName(), WORLD_FILE); }
|
|
16
|
-
|
|
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
|
-
/**
|
|
20
|
-
export function worldSeedPath(root: string): string { return join(
|
|
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
|
|
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 <
|
|
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
|
|
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,
|
|
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 =
|
|
69
|
-
if (!existsSync(configPath)) throw new Error(`Not a remote URL or world directory: ${url} (want https://<host>/<org>/<world>, or a
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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> {
|
|
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;
|