@volter/world 3.0.190 → 3.0.192
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/README.md +1 -1
- package/dist/examples/customer-onboarding/package.json +5 -5
- 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/src/cli.d.ts +1 -1
- package/dist/src/cli.js +19 -2
- package/dist/src/world-browser.d.ts +4 -0
- package/dist/src/world-browser.js +43 -0
- package/dist/src/world.d.ts +4 -1
- package/dist/src/world.js +4 -1
- package/examples/customer-onboarding/README.md +1 -1
- package/examples/customer-onboarding/package.json +5 -5
- 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 +10 -5
- package/src/cli.ts +17 -2
- package/src/handlers-in-repo.test.ts +6 -4
- package/src/init-selection.test.ts +23 -3
- package/src/journeys/kit.ts +1 -3
- package/src/journeys/tutorial.ts +2 -2
- package/src/world-browser.ts +37 -0
- package/src/world.ts +4 -1
|
@@ -13,7 +13,7 @@ You need Node 22.6+, pnpm 12.15.0, Docker Compose and registry access during ins
|
|
|
13
13
|
Copy this starter into a new folder:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.192 example customer-onboarding --dir onboarding
|
|
17
17
|
$ cd onboarding
|
|
18
18
|
$ pnpm install
|
|
19
19
|
```
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"pg": "8.16.3"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.192",
|
|
15
|
+
"@volter/world-core": "3.0.165",
|
|
16
|
+
"@volter/world-runtime": "3.0.190",
|
|
17
|
+
"@volter/world-console": "3.0.161",
|
|
18
18
|
"@volter/twin-stripe": "3.0.2",
|
|
19
19
|
"@volter/twin-resend": "1.0.1",
|
|
20
|
-
"@volter/world-ui": "1.0.
|
|
20
|
+
"@volter/world-ui": "1.0.164"
|
|
21
21
|
},
|
|
22
22
|
"packageManager": "pnpm@12.15.0",
|
|
23
23
|
"devEngines": {
|
|
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
|
|
|
13
13
|
Use an empty parent folder. Create the baseline starter there:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir baseline
|
|
17
17
|
$ cd baseline
|
|
18
18
|
$ pnpm install
|
|
19
19
|
$ ./node_modules/.bin/volter world init --name baseline --twins stripe
|
|
@@ -38,9 +38,9 @@ The stopped baseline retains its history and can supply local path remotes. Defa
|
|
|
38
38
|
Create three more copies in the same parent folder:
|
|
39
39
|
|
|
40
40
|
```console
|
|
41
|
-
$ npx @volter/world@3.0.
|
|
42
|
-
$ npx @volter/world@3.0.
|
|
43
|
-
$ npx @volter/world@3.0.
|
|
41
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir integration
|
|
42
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir worker-a
|
|
43
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir worker-b
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"stripe": "17.7.0"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@volter/world": "3.0.
|
|
13
|
-
"@volter/world-core": "3.0.
|
|
14
|
-
"@volter/world-runtime": "3.0.
|
|
15
|
-
"@volter/world-console": "3.0.
|
|
12
|
+
"@volter/world": "3.0.192",
|
|
13
|
+
"@volter/world-core": "3.0.165",
|
|
14
|
+
"@volter/world-runtime": "3.0.190",
|
|
15
|
+
"@volter/world-console": "3.0.161",
|
|
16
16
|
"@volter/twin-stripe": "3.0.2",
|
|
17
|
-
"@volter/world-ui": "1.0.
|
|
17
|
+
"@volter/world-ui": "1.0.164"
|
|
18
18
|
},
|
|
19
19
|
"packageManager": "pnpm@12.15.0",
|
|
20
20
|
"devEngines": {
|
|
@@ -19,7 +19,7 @@ The catalog offers current defaults. Keep this example's manifest and installati
|
|
|
19
19
|
together; its recorded workflow does not establish behavior for a different release.
|
|
20
20
|
|
|
21
21
|
```console
|
|
22
|
-
$ npx @volter/world@3.0.
|
|
22
|
+
$ npx @volter/world@3.0.192 example workplace-agent --dir workplace
|
|
23
23
|
$ cd workplace
|
|
24
24
|
$ pnpm install
|
|
25
25
|
$ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"@slack/web-api": "7.19.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.192",
|
|
15
|
+
"@volter/world-core": "3.0.165",
|
|
16
|
+
"@volter/world-runtime": "3.0.190",
|
|
17
|
+
"@volter/world-console": "3.0.161",
|
|
18
18
|
"@volter/twin-github": "3.0.9",
|
|
19
19
|
"@volter/twin-linear": "3.0.5",
|
|
20
20
|
"@volter/twin-slack": "3.0.8",
|
|
21
|
-
"@volter/world-ui": "1.0.
|
|
21
|
+
"@volter/world-ui": "1.0.164"
|
|
22
22
|
},
|
|
23
23
|
"packageManager": "pnpm@12.15.0",
|
|
24
24
|
"devEngines": {
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 a shared simulated internet (the world of the current directory)\n 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\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world env [--template <file>] render deployment .env.world references into dotenv files\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter example list ready application starters included in this CLI release\n volter example <name> [--dir <path>] copy a cookbook starter into an empty directory; does not install or start it\n volter world serve [--port <p>] [--console-port <c>] [--origin <url>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] [--origin <url>] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--remote <remote>] [--token <token>] [--force] send entries to the selected remote (default origin); vendor deployment follows its root policy\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved remote\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin submit --source <id> --vendor <vendor> --package <name> --version <exact> prepare a catalog submission\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n\nagents \u2014 your coding agent, driving the same verbs\n volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here\n volter mcp the MCP server (stdio) an agent starts: the verbs above as tools\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> [--token <personal token>] sign the CLI into the hosted platform: approve it in the browser (or a token made under Account, for CI)\n volter whoami the platform the CLI is signed into, as whom, and its orgs\n volter logout revoke the CLI's token at the platform and forget it here\n volter remote add <name> <url|path|org/world> [--token <token>] [--create] name a remote; org/world (or world, in one org) resolves through the platform you logged into; --create makes it there from this world's twins\n volter open [<org>/<world>] [--no-open] a World's dashboard: a hosted one through the platform, else this world's (as view)\n volter completion bash|zsh|fish|powershell print a completion script for your shell\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n --version the command's version\n";
|
|
2
|
+
export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 a shared simulated internet (the world of the current directory)\n 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\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world env [--template <file>] render deployment .env.world references into dotenv files\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter example list ready application starters included in this CLI release\n volter example <name> [--dir <path>] copy a cookbook starter into an empty directory; does not install or start it\n volter world serve [--port <p>] [--console-port <c>] [--origin <url>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] [--origin <url>] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world generate write seed.generated.ts from world.json's people, organizations and accounts\n volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--remote <remote>] [--token <token>] [--force] send entries to the selected remote (default origin); vendor deployment follows its root policy\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved remote\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin submit --source <id> --vendor <vendor> --package <name> --version <exact> prepare a catalog submission\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n\nagents \u2014 your coding agent, driving the same verbs\n volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here\n volter mcp the MCP server (stdio) an agent starts: the verbs above as tools\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> [--token <personal token>] sign the CLI into the hosted platform: approve it in the browser (or a token made under Account, for CI)\n volter whoami the platform the CLI is signed into, as whom, and its orgs\n volter logout revoke the CLI's token at the platform and forget it here\n volter remote add <name> <url|path|org/world> [--token <token>] [--create] name a remote; org/world (or world, in one org) resolves through the platform you logged into; --create makes it there from this world's twins\n volter open [<org>/<world>] [--no-open] a World's dashboard: a hosted one through the platform, else this world's (as view)\n volter completion bash|zsh|fish|powershell print a completion script for your shell\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n --version the command's version\n";
|
package/dist/src/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { forgetPlatform, forgetToken, platformOrigin, storePlatform, storeToken,
|
|
|
16
16
|
import { parseOriginUrl, World } from "./world.js";
|
|
17
17
|
import { copyExample, examples } from "./examples.js";
|
|
18
18
|
import { waitForServeShutdown } from "./serve-shutdown.js";
|
|
19
|
+
import { openWorldBrowser } from "./world-browser.js";
|
|
19
20
|
import { currentVersion, startUpdateCheck } from "./update-notice.js";
|
|
20
21
|
import { withoutVaultSignIn } from '@volter/world-runtime';
|
|
21
22
|
/** Open a URL in the person's browser; a machine without one only loses the convenience. */
|
|
@@ -56,6 +57,8 @@ world — a shared simulated internet (the world of the current directory)
|
|
|
56
57
|
volter world status world, branch, origin, unpushed changes, what is running
|
|
57
58
|
volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt
|
|
58
59
|
volter world diff [--json] what changed since the default data (or the last mark)
|
|
60
|
+
volter world generate write seed.generated.ts from world.json's people, organizations and accounts
|
|
61
|
+
volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)
|
|
59
62
|
volter world seed load the default data
|
|
60
63
|
volter world reset back to the default data
|
|
61
64
|
volter world down [--purge] stop the twins (--purge forgets this branch's state)
|
|
@@ -127,7 +130,7 @@ function positionals(args, valueFlags) {
|
|
|
127
130
|
return out;
|
|
128
131
|
}
|
|
129
132
|
function afterDashes(args) { const i = args.indexOf('--'); return i < 0 ? [] : args.slice(i + 1); }
|
|
130
|
-
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'];
|
|
133
|
+
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', '--cdp'];
|
|
131
134
|
/** Help is resolved before opening a World; the child after `run --` owns its own flags. */
|
|
132
135
|
function wantsHelp(noun, verb, args) {
|
|
133
136
|
const boundary = args.indexOf('--');
|
|
@@ -344,6 +347,20 @@ async function worldCommand(verb, args) {
|
|
|
344
347
|
out(` ${v.service}: ${v.count} change${v.count === 1 ? '' : 's'}`);
|
|
345
348
|
return;
|
|
346
349
|
}
|
|
350
|
+
case 'browser': {
|
|
351
|
+
const [name, url] = pos;
|
|
352
|
+
if (!name)
|
|
353
|
+
throw new Error('volter world browser <name> [url] [--cdp <endpoint>]');
|
|
354
|
+
await openWorldBrowser(world, name, url, value(args, '--cdp'), out);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
case 'generate': {
|
|
358
|
+
const r = world.generate();
|
|
359
|
+
out(`${r.changed ? 'wrote' : 'unchanged'} ${relative(process.cwd(), r.path) || r.path}`);
|
|
360
|
+
if (!r.imported)
|
|
361
|
+
out('note seed.ts beside it does not import ./seed.generated.ts, and the World runs only seed.ts');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
347
364
|
case 'seed': {
|
|
348
365
|
const r = await world.seed();
|
|
349
366
|
out(`Loaded the default data into ${r.world}: ${Object.entries(r.observed).map(([s, n]) => `${s} ${n}`).join(', ') || 'nothing recorded'}`);
|
|
@@ -718,7 +735,7 @@ async function twinCommand(args) {
|
|
|
718
735
|
out(t.ingest ? `${vendor} ingest ${t.ingest} (register it at the vendor as the webhook URL, with the signing secret sealed with the credential)` : `${vendor} ingest: a served World's door (volter world serve, or a remote) is the webhook URL to register at the vendor`);
|
|
719
736
|
return;
|
|
720
737
|
}
|
|
721
|
-
const dir = resolveCatalog(process.cwd()).packDir(vendor);
|
|
738
|
+
const dir = resolveCatalog(process.cwd(), { vendors: [vendor] }).packDir(vendor);
|
|
722
739
|
if (dir === undefined)
|
|
723
740
|
throw new Error(`No twin "${vendor}" here — \`pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/twin-${vendor}\`, or run from a checkout that has it`);
|
|
724
741
|
if (rest.length === 0 || rest[0].startsWith('-'))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// `volter world browser <name> [url] [--cdp <endpoint>]`: a person's saved World browser opened in a real browser
|
|
2
|
+
// (ADR 0016, rows 3 and 4). The profile is read through the World's own record reader and the browser is routed through
|
|
3
|
+
// the World's proxy with its CA trusted, as attach publishes them. Nothing is written back to the World.
|
|
4
|
+
import { deriveWorldManifest, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
|
|
5
|
+
export async function openWorldBrowser(world, name, url, cdp, say) {
|
|
6
|
+
const instance = statusWorld(world.name, world.root);
|
|
7
|
+
const record = worldBrowser(instance.dirs.data, name);
|
|
8
|
+
if (!record) {
|
|
9
|
+
const names = worldBrowsers(instance.dirs.data).map(row => row.name);
|
|
10
|
+
throw new Error(`World "${world.name}" has no browser "${name}" (has: ${names.join(', ') || 'none'})`);
|
|
11
|
+
}
|
|
12
|
+
const manifest = deriveWorldManifest(world.name, instance.env ?? {});
|
|
13
|
+
if (!instance.running || !manifest.browser)
|
|
14
|
+
throw new Error('the World is not running, so it has no proxy to route a browser through: start it with `volter world up`');
|
|
15
|
+
let chromium;
|
|
16
|
+
try {
|
|
17
|
+
({ chromium } = await import('playwright'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('volter world browser needs playwright: npm i playwright');
|
|
21
|
+
}
|
|
22
|
+
const profile = record.state;
|
|
23
|
+
if (cdp === undefined) {
|
|
24
|
+
const browser = await chromium.launch({ headless: false, args: manifest.browser.args });
|
|
25
|
+
const closed = new Promise(resolve => browser.on('disconnected', () => resolve()));
|
|
26
|
+
const context = await browser.newContext({ ...profile.context, storageState: profile.storageState });
|
|
27
|
+
const page = await context.newPage();
|
|
28
|
+
await page.goto(url ?? 'about:blank');
|
|
29
|
+
await closed;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const browser = await chromium.connectOverCDP(cdp);
|
|
33
|
+
const context = browser.contexts()[0];
|
|
34
|
+
if (!context)
|
|
35
|
+
throw new Error(`${cdp} has no default browser context to load ${name}'s cookies into`);
|
|
36
|
+
// An existing default context takes no context options (ADR 0016): its cookies are what is loaded.
|
|
37
|
+
if (Object.keys(profile.context).length)
|
|
38
|
+
say(`note ${name}'s context options (${Object.keys(profile.context).join(', ')}) were not applied: an existing browser's default context takes none`);
|
|
39
|
+
await context.addCookies(profile.storageState.cookies);
|
|
40
|
+
const page = await context.newPage();
|
|
41
|
+
await page.goto(url ?? 'about:blank');
|
|
42
|
+
await browser.close();
|
|
43
|
+
}
|
package/dist/src/world.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderDeploymentTemplates, type DeploymentTemplate } from '@volter/world-runtime/deployment-env';
|
|
2
2
|
import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, applyTwinWrite, type TwinResource } from '@volter/world-core/runtime';
|
|
3
|
-
import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedServedWorld, seedWorld, statusWorld, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, rootCredentialInput, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
|
|
3
|
+
import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedServedWorld, seedWorld, statusWorld, writeGeneratedSeed, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, rootCredentialInput, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
|
|
4
4
|
import { rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core/runtime';
|
|
5
5
|
export type WorldRef = {
|
|
6
6
|
name?: string;
|
|
@@ -146,6 +146,9 @@ export declare class World {
|
|
|
146
146
|
entry?: string;
|
|
147
147
|
cwd?: string;
|
|
148
148
|
}): ReturnType<typeof seedWorld>;
|
|
149
|
+
/** Write `seed.generated.ts` beside world.json from the people, organizations and accounts it declares (ADR 0033), as a
|
|
150
|
+
* lockfile is written from its manifest: whether it changed, and whether `seed.ts`, which the World runs, imports it. */
|
|
151
|
+
generate(): ReturnType<typeof writeGeneratedSeed>;
|
|
149
152
|
/** Whether this world has default data (`.volter/seed.ts`). */
|
|
150
153
|
hasSeed(): boolean;
|
|
151
154
|
/** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
|
package/dist/src/world.js
CHANGED
|
@@ -7,7 +7,7 @@ import { platformPerson } from "./credentials.js";
|
|
|
7
7
|
import { rebaseChangeset, worldBootMarker, listActions, inWorldHistory, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core/runtime';
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import clockForm from '@volter/world-core/world-clock';
|
|
10
|
-
import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld, materializeBrowserFiles, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
|
|
10
|
+
import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld, materializeBrowserFiles, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { requireToken, storeToken } from "./credentials.js";
|
|
@@ -248,6 +248,9 @@ export class World {
|
|
|
248
248
|
// ── default data ────────────────────────────────────────────────────────────────────────
|
|
249
249
|
/** Load the default data: the seed runs with every twin recording what it creates as data that was already there. */
|
|
250
250
|
seed(opts = {}) { return seedWorld(this.name, { root: this.root, cwd: this.root, ...opts }); }
|
|
251
|
+
/** Write `seed.generated.ts` beside world.json from the people, organizations and accounts it declares (ADR 0033), as a
|
|
252
|
+
* lockfile is written from its manifest: whether it changed, and whether `seed.ts`, which the World runs, imports it. */
|
|
253
|
+
generate() { return writeGeneratedSeed(worldConfigPath(this.root), this.root); }
|
|
251
254
|
/** Whether this world has default data (`.volter/seed.ts`). */
|
|
252
255
|
hasSeed() { return existsSync(worldSeedPath(this.root)); }
|
|
253
256
|
/** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
|
|
@@ -13,7 +13,7 @@ You need Node 22.6+, pnpm 12.15.0, Docker Compose and registry access during ins
|
|
|
13
13
|
Copy this starter into a new folder:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.192 example customer-onboarding --dir onboarding
|
|
17
17
|
$ cd onboarding
|
|
18
18
|
$ pnpm install
|
|
19
19
|
```
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"pg": "8.16.3"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.192",
|
|
15
|
+
"@volter/world-core": "3.0.165",
|
|
16
|
+
"@volter/world-runtime": "3.0.190",
|
|
17
|
+
"@volter/world-console": "3.0.161",
|
|
18
18
|
"@volter/twin-stripe": "3.0.2",
|
|
19
19
|
"@volter/twin-resend": "1.0.1",
|
|
20
|
-
"@volter/world-ui": "1.0.
|
|
20
|
+
"@volter/world-ui": "1.0.164"
|
|
21
21
|
},
|
|
22
22
|
"packageManager": "pnpm@12.15.0",
|
|
23
23
|
"devEngines": {
|
|
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
|
|
|
13
13
|
Use an empty parent folder. Create the baseline starter there:
|
|
14
14
|
|
|
15
15
|
```console
|
|
16
|
-
$ npx @volter/world@3.0.
|
|
16
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir baseline
|
|
17
17
|
$ cd baseline
|
|
18
18
|
$ pnpm install
|
|
19
19
|
$ ./node_modules/.bin/volter world init --name baseline --twins stripe
|
|
@@ -38,9 +38,9 @@ The stopped baseline retains its history and can supply local path remotes. Defa
|
|
|
38
38
|
Create three more copies in the same parent folder:
|
|
39
39
|
|
|
40
40
|
```console
|
|
41
|
-
$ npx @volter/world@3.0.
|
|
42
|
-
$ npx @volter/world@3.0.
|
|
43
|
-
$ npx @volter/world@3.0.
|
|
41
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir integration
|
|
42
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir worker-a
|
|
43
|
+
$ npx @volter/world@3.0.192 example integration-branches --dir worker-b
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"stripe": "17.7.0"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@volter/world": "3.0.
|
|
13
|
-
"@volter/world-core": "3.0.
|
|
14
|
-
"@volter/world-runtime": "3.0.
|
|
15
|
-
"@volter/world-console": "3.0.
|
|
12
|
+
"@volter/world": "3.0.192",
|
|
13
|
+
"@volter/world-core": "3.0.165",
|
|
14
|
+
"@volter/world-runtime": "3.0.190",
|
|
15
|
+
"@volter/world-console": "3.0.161",
|
|
16
16
|
"@volter/twin-stripe": "3.0.2",
|
|
17
|
-
"@volter/world-ui": "1.0.
|
|
17
|
+
"@volter/world-ui": "1.0.164"
|
|
18
18
|
},
|
|
19
19
|
"packageManager": "pnpm@12.15.0",
|
|
20
20
|
"devEngines": {
|
|
@@ -19,7 +19,7 @@ The catalog offers current defaults. Keep this example's manifest and installati
|
|
|
19
19
|
together; its recorded workflow does not establish behavior for a different release.
|
|
20
20
|
|
|
21
21
|
```console
|
|
22
|
-
$ npx @volter/world@3.0.
|
|
22
|
+
$ npx @volter/world@3.0.192 example workplace-agent --dir workplace
|
|
23
23
|
$ cd workplace
|
|
24
24
|
$ pnpm install
|
|
25
25
|
$ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"@slack/web-api": "7.19.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@volter/world": "3.0.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
14
|
+
"@volter/world": "3.0.192",
|
|
15
|
+
"@volter/world-core": "3.0.165",
|
|
16
|
+
"@volter/world-runtime": "3.0.190",
|
|
17
|
+
"@volter/world-console": "3.0.161",
|
|
18
18
|
"@volter/twin-github": "3.0.9",
|
|
19
19
|
"@volter/twin-linear": "3.0.5",
|
|
20
20
|
"@volter/twin-slack": "3.0.8",
|
|
21
|
-
"@volter/world-ui": "1.0.
|
|
21
|
+
"@volter/world-ui": "1.0.164"
|
|
22
22
|
},
|
|
23
23
|
"packageManager": "pnpm@12.15.0",
|
|
24
24
|
"devEngines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.192",
|
|
4
4
|
"description": "Run your app against a world of twins. `World` and `Repo` — one method per verb: init, up, run, log, diff, reset, branch, checkout, clone, fetch, changeset, push — and the `volter` command, one client of them.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"volter",
|
|
@@ -31,20 +31,25 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
34
|
-
"@volter/world-core": "3.0.
|
|
35
|
-
"@volter/world-runtime": "3.0.
|
|
34
|
+
"@volter/world-core": "3.0.165",
|
|
35
|
+
"@volter/world-runtime": "3.0.190",
|
|
36
36
|
"zod": "^4.6.5"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@volter/world-console": "3.0.
|
|
39
|
+
"@volter/world-console": "3.0.161",
|
|
40
|
+
"playwright": "^1.63.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependenciesMeta": {
|
|
42
43
|
"@volter/world-console": {
|
|
43
44
|
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"playwright": {
|
|
47
|
+
"optional": true
|
|
44
48
|
}
|
|
45
49
|
},
|
|
46
50
|
"devDependencies": {
|
|
47
|
-
"@volter/
|
|
51
|
+
"@volter/twin-anthropic": "1.0.6",
|
|
52
|
+
"@volter/world-console": "3.0.161"
|
|
48
53
|
},
|
|
49
54
|
"bin": {
|
|
50
55
|
"volter": "dist/src/cli.js"
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { forgetPlatform, forgetToken, platformOrigin, storePlatform, storeToken,
|
|
|
16
16
|
import { parseOriginUrl, World } from './world.ts';
|
|
17
17
|
import { copyExample, examples } from './examples.ts';
|
|
18
18
|
import { waitForServeShutdown } from './serve-shutdown.ts';
|
|
19
|
+
import { openWorldBrowser } from './world-browser.ts';
|
|
19
20
|
import { currentVersion, startUpdateCheck } from './update-notice.ts';
|
|
20
21
|
import type { ViewConsole } from '@volter/world-runtime';
|
|
21
22
|
import { withoutVaultSignIn } from '@volter/world-runtime';
|
|
@@ -47,6 +48,8 @@ world — a shared simulated internet (the world of the current directory)
|
|
|
47
48
|
volter world status world, branch, origin, unpushed changes, what is running
|
|
48
49
|
volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt
|
|
49
50
|
volter world diff [--json] what changed since the default data (or the last mark)
|
|
51
|
+
volter world generate write seed.generated.ts from world.json's people, organizations and accounts
|
|
52
|
+
volter world browser <name> [url] [--cdp <endpoint>] open a person's saved World browser, signed in, through the running World (--cdp loads its cookies into a browser already open)
|
|
50
53
|
volter world seed load the default data
|
|
51
54
|
volter world reset back to the default data
|
|
52
55
|
volter world down [--purge] stop the twins (--purge forgets this branch's state)
|
|
@@ -114,7 +117,7 @@ function positionals(args: string[], valueFlags: string[]): string[] {
|
|
|
114
117
|
return out;
|
|
115
118
|
}
|
|
116
119
|
function afterDashes(args: string[]): string[] { const i = args.indexOf('--'); return i < 0 ? [] : args.slice(i + 1); }
|
|
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'];
|
|
120
|
+
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', '--cdp'];
|
|
118
121
|
|
|
119
122
|
/** Help is resolved before opening a World; the child after `run --` owns its own flags. */
|
|
120
123
|
function wantsHelp(noun: string | undefined, verb: string | undefined, args: string[]): boolean {
|
|
@@ -281,6 +284,18 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
|
|
|
281
284
|
for (const v of delta.vendors) out(` ${v.service}: ${v.count} change${v.count === 1 ? '' : 's'}`);
|
|
282
285
|
return;
|
|
283
286
|
}
|
|
287
|
+
case 'browser': {
|
|
288
|
+
const [name, url] = pos;
|
|
289
|
+
if (!name) throw new Error('volter world browser <name> [url] [--cdp <endpoint>]');
|
|
290
|
+
await openWorldBrowser(world, name, url, value(args, '--cdp'), out);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
case 'generate': {
|
|
294
|
+
const r = world.generate();
|
|
295
|
+
out(`${r.changed ? 'wrote' : 'unchanged'} ${relative(process.cwd(), r.path) || r.path}`);
|
|
296
|
+
if (!r.imported) out('note seed.ts beside it does not import ./seed.generated.ts, and the World runs only seed.ts');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
284
299
|
case 'seed': {
|
|
285
300
|
const r = await world.seed();
|
|
286
301
|
out(`Loaded the default data into ${r.world}: ${Object.entries(r.observed).map(([s, n]) => `${s} ${n}`).join(', ') || 'nothing recorded'}`);
|
|
@@ -529,7 +544,7 @@ async function twinCommand(args: string[]): Promise<void> {
|
|
|
529
544
|
if (t.root) out(t.ingest ? `${vendor} ingest ${t.ingest} (register it at the vendor as the webhook URL, with the signing secret sealed with the credential)` : `${vendor} ingest: a served World's door (volter world serve, or a remote) is the webhook URL to register at the vendor`);
|
|
530
545
|
return;
|
|
531
546
|
}
|
|
532
|
-
const dir = resolveCatalog(process.cwd()).packDir(vendor);
|
|
547
|
+
const dir = resolveCatalog(process.cwd(), { vendors: [vendor] }).packDir(vendor);
|
|
533
548
|
if (dir === undefined) throw new Error(`No twin "${vendor}" here — \`pnpm add -D --package-import-method=hardlink --config.nodeLinker=hoisted @volter/twin-${vendor}\`, or run from a checkout that has it`);
|
|
534
549
|
if (rest.length === 0 || rest[0]!.startsWith('-')) throw new Error(`volter twin ${vendor}: which verb? serve | root | credential | refresh`);
|
|
535
550
|
const cli = packCli(dir); if (cli === undefined) throw new Error(`No cli for twin "${vendor}" (its manifest names no bin)`);
|
|
@@ -8,7 +8,7 @@ import { afterAll, describe, expect, test } from 'bun:test';
|
|
|
8
8
|
import { copyFileSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { installedPackageDir } from '@volter/world-core/runtime-state';
|
|
12
12
|
import { World } from './index.ts';
|
|
13
13
|
|
|
14
14
|
function app(): string {
|
|
@@ -19,9 +19,11 @@ function app(): string {
|
|
|
19
19
|
writeFileSync(join(dir, '.env.example'), 'ANTHROPIC_API_KEY=\n');
|
|
20
20
|
const installed = join(parent, 'node_modules', '@volter', 'twin-anthropic');
|
|
21
21
|
mkdirSync(installed, { recursive: true });
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// the published pack, this package's dev dependency (ADR 0034, decision 6)
|
|
23
|
+
const pack = installedPackageDir(import.meta.dir, '@volter/twin-anthropic')!;
|
|
24
|
+
for (const entry of readdirSync(pack)) {
|
|
25
|
+
if (entry === 'package.json') copyFileSync(join(pack, entry), join(installed, entry));
|
|
26
|
+
else symlinkSync(join(pack, entry), join(installed, entry));
|
|
25
27
|
}
|
|
26
28
|
return dir;
|
|
27
29
|
}
|
|
@@ -11,6 +11,14 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
|
11
11
|
|
|
12
12
|
const roots: string[] = [];
|
|
13
13
|
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
|
14
|
+
/** Each fixture vendor's own facts, as its release carries them (ADR 0034, decisions 2 and 6): Protocol 3, the SDK and
|
|
15
|
+
* env stem the app betrays it by, and the host its injector key serves. */
|
|
16
|
+
const FACTS: Record<string, unknown> = {
|
|
17
|
+
stripe: { protocol: { declared: '3', major: 3, standing: 'current' }, adoption: { sdks: ['stripe'], envStems: ['STRIPE'] }, hosts: [{ host: 'api.stripe.com' }] },
|
|
18
|
+
slack: { protocol: { declared: '3', major: 3, standing: 'current' }, adoption: { sdks: ['@slack/web-api'], envStems: ['SLACK'] }, hosts: [{ host: 'slack.com' }] },
|
|
19
|
+
};
|
|
20
|
+
/** An app calling Stripe and Slack, the named publisher fixtures installed, each with its own facts, and a catalog index
|
|
21
|
+
* whose captured releases (one publisher per vendor) let discovery see a vendor the selection leaves out. */
|
|
14
22
|
function app(packages = ['@one/twin-stripe']): string {
|
|
15
23
|
const root = mkdtempSync(join(tmpdir(), 'world-init-selection-')); roots.push(root);
|
|
16
24
|
writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'selection-app', private: true, dependencies: { stripe: '17.7.0', '@slack/web-api': '7.9.2' } }));
|
|
@@ -18,9 +26,21 @@ function app(packages = ['@one/twin-stripe']): string {
|
|
|
18
26
|
for (const name of packages) {
|
|
19
27
|
const dir = join(root, 'node_modules', name);
|
|
20
28
|
mkdirSync(join(dir, 'src', 'generated'), { recursive: true });
|
|
29
|
+
mkdirSync(join(dir, 'generated'), { recursive: true });
|
|
21
30
|
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version: name.startsWith('@one/') ? '1.0.0' : '9.0.0' }));
|
|
22
31
|
writeFileSync(join(dir, 'src', 'cli.ts'), '// Discovery fixture: never executed.\n');
|
|
23
32
|
writeFileSync(join(dir, 'src', 'generated', 'surface.gen.json'), '{}\n');
|
|
33
|
+
const vendor = /twin-([a-z]+)$/.exec(name)?.[1];
|
|
34
|
+
if (vendor) writeFileSync(join(dir, 'generated', 'pack-facts.json'), JSON.stringify({ packs: { [vendor]: FACTS[vendor] } }));
|
|
35
|
+
}
|
|
36
|
+
const index = join(root, 'node_modules', '@volter', 'twin-catalog');
|
|
37
|
+
mkdirSync(join(index, 'vendors'), { recursive: true }); mkdirSync(join(index, 'content'), { recursive: true });
|
|
38
|
+
writeFileSync(join(index, 'package.json'), JSON.stringify({ name: '@volter/twin-catalog', version: '0.0.0' }));
|
|
39
|
+
writeFileSync(join(index, 'sources.json'), JSON.stringify([{ name: 'one', scope: '@one', repository: 'one/twins', protocol: '3' }]));
|
|
40
|
+
for (const vendor of Object.keys(FACTS)) {
|
|
41
|
+
const release = { vendor, package: `@one/twin-${vendor}`, version: '1.0.0' };
|
|
42
|
+
writeFileSync(join(index, 'vendors', `${vendor}.json`), JSON.stringify({ vendor, packages: [{ name: release.package, source: 'one', versions: [{ version: '1.0.0', status: 'live' }] }] }));
|
|
43
|
+
writeFileSync(join(index, 'content', `${vendor}.json`), JSON.stringify({ schemaVersion: 1, release, facts: FACTS[vendor] }));
|
|
24
44
|
}
|
|
25
45
|
return root;
|
|
26
46
|
}
|
|
@@ -70,9 +90,9 @@ test('SDK initialization exposes the same vendor and publisher choice', () => {
|
|
|
70
90
|
test('an independent package name gets vendor identity from its published facts', () => {
|
|
71
91
|
const root = app(['@one/twin-stripe', '@two/stripe-simulator']);
|
|
72
92
|
const dir = join(root, 'node_modules', '@two', 'stripe-simulator', 'generated');
|
|
73
|
-
mkdirSync(dir);
|
|
74
|
-
|
|
75
|
-
writeFileSync(join(dir, 'pack-facts.json'), JSON.stringify({ packs: { stripe:
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
// the independently named publisher's own facts name its vendor (ADR 0034, decision 6)
|
|
95
|
+
writeFileSync(join(dir, 'pack-facts.json'), JSON.stringify({ packs: { stripe: FACTS.stripe } }));
|
|
76
96
|
const result = init(root, '--twins', 'stripe', '--source', 'stripe=@two/stripe-simulator');
|
|
77
97
|
expect(result.status).toBe(0);
|
|
78
98
|
expect(document(root).services[0].source.package).toBe('@two/stripe-simulator');
|
package/src/journeys/kit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What the tutorial runner and the docs' recorder share: where the commands
|
|
1
|
+
// What the tutorial runner and the docs' recorder share: where the commands are.
|
|
2
2
|
// A tutorial page is its own journey (tutorial.ts); nothing here runs a step of its own.
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
|
|
@@ -6,7 +6,5 @@ import { resolve } from 'node:path';
|
|
|
6
6
|
export const CLI = resolve(import.meta.dir, '..', 'cli.ts');
|
|
7
7
|
/** The operator's `volter-world` command, which ships with @volter/world-runtime. */
|
|
8
8
|
export const OPERATOR_CLI = resolve(import.meta.dir, '..', '..', '..', 'world-runtime', 'src', 'cli.ts');
|
|
9
|
-
/** The twins: the pack repository beside this or the main checkout. */
|
|
10
|
-
export { PACKS } from '../../../../scripts/pack-dir.ts';
|
|
11
9
|
/** @volter/world itself. */
|
|
12
10
|
export const SDK = resolve(import.meta.dir, '..', '..');
|
package/src/journeys/tutorial.ts
CHANGED
|
@@ -22,8 +22,8 @@ import { downWorld } from '@volter/world-runtime';
|
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import { basename, delimiter, dirname, join, resolve } from 'node:path';
|
|
24
24
|
import { randomUUID } from 'node:crypto';
|
|
25
|
-
import { CLI, OPERATOR_CLI,
|
|
26
|
-
void CLI; void OPERATOR_CLI; void
|
|
25
|
+
import { CLI, OPERATOR_CLI, SDK } from './kit.ts';
|
|
26
|
+
void CLI; void OPERATOR_CLI; void SDK;
|
|
27
27
|
|
|
28
28
|
export type Step =
|
|
29
29
|
| { kind: 'file'; path: string; content: string; line: number }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// `volter world browser <name> [url] [--cdp <endpoint>]`: a person's saved World browser opened in a real browser
|
|
2
|
+
// (ADR 0016, rows 3 and 4). The profile is read through the World's own record reader and the browser is routed through
|
|
3
|
+
// the World's proxy with its CA trusted, as attach publishes them. Nothing is written back to the World.
|
|
4
|
+
import { deriveWorldManifest, statusWorld, worldBrowser, worldBrowsers } from '@volter/world-runtime';
|
|
5
|
+
|
|
6
|
+
export async function openWorldBrowser(world: { name: string; root: string }, name: string, url: string | undefined, cdp: string | undefined, say: (line: string) => void): Promise<void> {
|
|
7
|
+
const instance = statusWorld(world.name, world.root);
|
|
8
|
+
const record = worldBrowser(instance.dirs.data, name);
|
|
9
|
+
if (!record) {
|
|
10
|
+
const names = worldBrowsers(instance.dirs.data).map(row => row.name);
|
|
11
|
+
throw new Error(`World "${world.name}" has no browser "${name}" (has: ${names.join(', ') || 'none'})`);
|
|
12
|
+
}
|
|
13
|
+
const manifest = deriveWorldManifest(world.name, instance.env ?? {});
|
|
14
|
+
if (!instance.running || !manifest.browser) throw new Error('the World is not running, so it has no proxy to route a browser through: start it with `volter world up`');
|
|
15
|
+
let chromium: typeof import('playwright').chromium;
|
|
16
|
+
try { ({ chromium } = await import('playwright')); }
|
|
17
|
+
catch { throw new Error('volter world browser needs playwright: npm i playwright'); }
|
|
18
|
+
const profile = record.state;
|
|
19
|
+
if (cdp === undefined) {
|
|
20
|
+
const browser = await chromium.launch({ headless: false, args: manifest.browser.args });
|
|
21
|
+
const closed = new Promise<void>(resolve => browser.on('disconnected', () => resolve()));
|
|
22
|
+
const context = await browser.newContext({ ...profile.context, storageState: profile.storageState } as Parameters<typeof browser.newContext>[0]);
|
|
23
|
+
const page = await context.newPage();
|
|
24
|
+
await page.goto(url ?? 'about:blank');
|
|
25
|
+
await closed;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const browser = await chromium.connectOverCDP(cdp);
|
|
29
|
+
const context = browser.contexts()[0];
|
|
30
|
+
if (!context) throw new Error(`${cdp} has no default browser context to load ${name}'s cookies into`);
|
|
31
|
+
// An existing default context takes no context options (ADR 0016): its cookies are what is loaded.
|
|
32
|
+
if (Object.keys(profile.context).length) say(`note ${name}'s context options (${Object.keys(profile.context).join(', ')}) were not applied: an existing browser's default context takes none`);
|
|
33
|
+
await context.addCookies(profile.storageState.cookies as Parameters<typeof context.addCookies>[0]);
|
|
34
|
+
const page = await context.newPage();
|
|
35
|
+
await page.goto(url ?? 'about:blank');
|
|
36
|
+
await browser.close();
|
|
37
|
+
}
|
package/src/world.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
9
9
|
import clockForm from '@volter/world-core/world-clock';
|
|
10
10
|
import {
|
|
11
11
|
activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedServedWorld, seedWorld, shellWorld,
|
|
12
|
-
materializeBrowserFiles, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
|
|
12
|
+
materializeBrowserFiles, statusWorld, writeGeneratedSeed, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
|
|
13
13
|
bindLegacyCredentials, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, type ServedWorld, type ViewConsole, type WorldView, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, rootCredentialInput, withoutVaultSignIn,
|
|
14
14
|
type DeployTwinOutcome, type MaterializedRoot,
|
|
15
15
|
type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots, instanceDir, worldChangesetRules } from '@volter/world-runtime';
|
|
@@ -241,6 +241,9 @@ export class World {
|
|
|
241
241
|
// ── default data ────────────────────────────────────────────────────────────────────────
|
|
242
242
|
/** Load the default data: the seed runs with every twin recording what it creates as data that was already there. */
|
|
243
243
|
seed(opts: { entry?: string; cwd?: string } = {}): ReturnType<typeof seedWorld> { return seedWorld(this.name, { root: this.root, cwd: this.root, ...opts }); }
|
|
244
|
+
/** Write `seed.generated.ts` beside world.json from the people, organizations and accounts it declares (ADR 0033), as a
|
|
245
|
+
* lockfile is written from its manifest: whether it changed, and whether `seed.ts`, which the World runs, imports it. */
|
|
246
|
+
generate(): ReturnType<typeof writeGeneratedSeed> { return writeGeneratedSeed(worldConfigPath(this.root), this.root); }
|
|
244
247
|
/** Whether this world has default data (`.volter/seed.ts`). */
|
|
245
248
|
hasSeed(): boolean { return existsSync(worldSeedPath(this.root)); }
|
|
246
249
|
/** Set up a served World this one is linked to from this world's seed: run there through its own twins, then marked. */
|