@volter/world 3.0.124 → 3.0.125
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/src/cli.js +14 -9
- package/package.json +5 -5
- package/src/cli.ts +13 -5
- package/src/journeys/tutorial.ts +5 -4
- package/src/journeys/tutorials.test.ts +10 -4
package/dist/src/cli.js
CHANGED
|
@@ -138,15 +138,20 @@ async function passthrough(cmd, cwd = process.cwd()) {
|
|
|
138
138
|
process.on(signal, forward(signal));
|
|
139
139
|
return await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? (signal ? 128 : 1))); });
|
|
140
140
|
}
|
|
141
|
-
/**
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
/** Read through EOF, including a delayed pipe producer. Bun's stdin iteration can leave the
|
|
142
|
+
* event loop unreferenced; the timer holds it until EOF or error and is released in either case. */
|
|
143
|
+
async function stdinText() {
|
|
144
|
+
const keepAlive = setInterval(() => { }, 1000);
|
|
145
|
+
try {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
for await (const chunk of process.stdin)
|
|
148
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
149
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
clearInterval(keepAlive);
|
|
153
|
+
}
|
|
146
154
|
}
|
|
147
|
-
catch {
|
|
148
|
-
return '';
|
|
149
|
-
} }
|
|
150
155
|
async function worldCommand(verb, args) {
|
|
151
156
|
const worldDir = value(args, '--world');
|
|
152
157
|
const pos = positionals(args, VALUE_FLAGS);
|
|
@@ -630,7 +635,7 @@ async function twinCommand(args) {
|
|
|
630
635
|
}
|
|
631
636
|
else if (verb === 'credential') {
|
|
632
637
|
// what is piped in, else the signed-in vault, else the repo's .env (company decision 0033: no vault required)
|
|
633
|
-
let input = process.stdin.isTTY ? '' : stdinText();
|
|
638
|
+
let input = process.stdin.isTTY ? '' : await stdinText();
|
|
634
639
|
if (input.trim() === '') {
|
|
635
640
|
if (!world.twin(vendor))
|
|
636
641
|
throw new Error(`volter twin ${vendor} credential: this World has no twin "${vendor}"`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.125",
|
|
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",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
33
|
-
"@volter/world-core": "3.0.
|
|
34
|
-
"@volter/world-runtime": "3.0.
|
|
33
|
+
"@volter/world-core": "3.0.125",
|
|
34
|
+
"@volter/world-runtime": "3.0.125",
|
|
35
35
|
"zod": "^4.6.5"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@volter/world-console": "3.0.
|
|
38
|
+
"@volter/world-console": "3.0.120"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@volter/world-console": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@volter/world-console": "3.0.
|
|
46
|
+
"@volter/world-console": "3.0.120"
|
|
47
47
|
},
|
|
48
48
|
"bin": {
|
|
49
49
|
"volter": "dist/src/cli.js"
|
package/src/cli.ts
CHANGED
|
@@ -124,10 +124,18 @@ async function passthrough(cmd: string[], cwd = process.cwd()): Promise<number>
|
|
|
124
124
|
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'] as const) process.on(signal, forward(signal));
|
|
125
125
|
return await new Promise<number>((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? (signal ? 128 : 1))); });
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
/** Read through EOF, including a delayed pipe producer. Bun's stdin iteration can leave the
|
|
128
|
+
* event loop unreferenced; the timer holds it until EOF or error and is released in either case. */
|
|
129
|
+
async function stdinText(): Promise<string> {
|
|
130
|
+
const keepAlive = setInterval(() => {}, 1000);
|
|
131
|
+
try {
|
|
132
|
+
const chunks: Buffer[] = [];
|
|
133
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
134
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
135
|
+
} finally {
|
|
136
|
+
clearInterval(keepAlive);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
131
139
|
|
|
132
140
|
async function worldCommand(verb: string, args: string[]): Promise<void> {
|
|
133
141
|
const worldDir = value(args, '--world');
|
|
@@ -454,7 +462,7 @@ async function twinCommand(args: string[]): Promise<void> {
|
|
|
454
462
|
world.setTwinRoot(vendor, { url, deploy, ...(value(rest, '--scope') ? { scope: value(rest, '--scope')! } : {}), ...(Object.keys(refresh).length ? { refresh } : {}) });
|
|
455
463
|
} else if (verb === 'credential') {
|
|
456
464
|
// what is piped in, else the signed-in vault, else the repo's .env (company decision 0033: no vault required)
|
|
457
|
-
let input = process.stdin.isTTY ? '' : stdinText();
|
|
465
|
+
let input = process.stdin.isTTY ? '' : await stdinText();
|
|
458
466
|
if (input.trim() === '') {
|
|
459
467
|
if (!world.twin(vendor)) throw new Error(`volter twin ${vendor} credential: this World has no twin "${vendor}"`);
|
|
460
468
|
const found = await world.rootCredentialInput(vendor);
|
package/src/journeys/tutorial.ts
CHANGED
|
@@ -136,7 +136,7 @@ const DONE = '__VOLTER_STEP_DONE__';
|
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
138
|
* Run a tutorial's steps in one bash session in the app, checking each expected line. Returns
|
|
139
|
-
*
|
|
139
|
+
* each executed step's result, stopping at the first failure; `failures()` names it.
|
|
140
140
|
*/
|
|
141
141
|
export async function runTutorial(tutorial: Tutorial, app: { dir: string; parent: string; env: Record<string, string> }, opts: { stepTimeoutMs?: number; onStep?: (r: StepResult) => void } = {}): Promise<StepResult[]> {
|
|
142
142
|
const shell = Bun.spawn({ cmd: ['bash', '--noprofile', '--norc'], cwd: app.dir, env: app.env, stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', windowsHide: true });
|
|
@@ -275,6 +275,7 @@ export async function runTutorial(tutorial: Tutorial, app: { dir: string; parent
|
|
|
275
275
|
const r: StepResult = { step, output: step.expect.length ? (fenceOutput.get(step.fence) ?? output) : output, missing, exit };
|
|
276
276
|
recordDescendants();
|
|
277
277
|
results.push(r); opts.onStep?.(r);
|
|
278
|
+
if (failures([r]).length) break;
|
|
278
279
|
}
|
|
279
280
|
} finally {
|
|
280
281
|
await cleanupShell();
|
|
@@ -420,10 +421,10 @@ export function packagesNamedIn(files: string[]): string[] {
|
|
|
420
421
|
export const TUTORIALS_DIR = resolve(import.meta.dir, '..', '..', '..', '..', 'docs');
|
|
421
422
|
/** Every tutorial page: getting-started, then the guides. */
|
|
422
423
|
export function tutorialFiles(): string[] {
|
|
423
|
-
const { readdirSync } = require('node:fs') as typeof import('node:fs');
|
|
424
|
-
const guides = readdirSync(join(TUTORIALS_DIR, 'guides')).filter((f) => f.endsWith('.md')).sort().map((f) => join(TUTORIALS_DIR, 'guides', f))
|
|
424
|
+
const { readdirSync, readFileSync } = require('node:fs') as typeof import('node:fs');
|
|
425
|
+
const guides = readdirSync(join(TUTORIALS_DIR, 'guides')).filter((f) => f.endsWith('.md')).sort().map((f) => join(TUTORIALS_DIR, 'guides', f))
|
|
426
|
+
.filter((file) => { const text = readFileSync(file, 'utf8'); return text.includes('<!-- journey -->') || parseTutorial(text, file).steps.some((step) => step.kind === 'file'); });
|
|
425
427
|
// a cookbook page is a journey too when it says so (`<!-- journey -->`): the same runner, the same proof
|
|
426
|
-
const { readFileSync } = require('node:fs') as typeof import('node:fs');
|
|
427
428
|
const cookbook = join(TUTORIALS_DIR, '..', 'cookbook');
|
|
428
429
|
const recipes = readdirSync(cookbook, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join(cookbook, e.name, 'README.md')).filter((f) => { try { return readFileSync(f, 'utf8').includes('<!-- journey -->'); } catch { return false; } }).sort();
|
|
429
430
|
return [join(TUTORIALS_DIR, 'getting-started.md'), ...guides, ...recipes];
|
|
@@ -8,13 +8,19 @@ import { relative } from 'node:path';
|
|
|
8
8
|
import { failures, packagesNamedIn, parseTutorial, removeTutorialApp, runTutorial, tutorialApp, tutorialFiles, TUTORIALS_DIR } from './tutorial.ts';
|
|
9
9
|
import { startRegistry, type Registry } from '../../../../scripts/registry.ts';
|
|
10
10
|
|
|
11
|
-
const only = process.env.TUTORIAL;
|
|
12
|
-
const
|
|
11
|
+
const only = process.env.TUTORIAL?.split(',').map((name) => name.trim()).filter(Boolean);
|
|
12
|
+
const matches = (file: string, name: string): boolean => file.endsWith(`/${name}.md`) || file.endsWith(`/${name}/README.md`);
|
|
13
|
+
const available = tutorialFiles();
|
|
14
|
+
for (const name of only ?? []) if (!available.some((file) => matches(file, name))) throw new Error(`No executable tutorial named ${name}`);
|
|
15
|
+
const pages = available.filter((file) => !only?.length || only.some((name) => matches(file, name)));
|
|
13
16
|
|
|
14
17
|
// one registry for the whole file: this checkout's packages, published to a registry on this
|
|
15
18
|
// machine, so every page's `bun add` is a real install
|
|
16
19
|
let registry: Registry | null = null;
|
|
17
|
-
beforeAll(async () => {
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
registry = await startRegistry({ only: packagesNamedIn(pages) });
|
|
22
|
+
console.error(`prepared tutorial packages: ${JSON.stringify(registry.prepared)}`);
|
|
23
|
+
}, 300_000);
|
|
18
24
|
afterAll(() => { registry?.stop(); });
|
|
19
25
|
|
|
20
26
|
for (const file of pages) {
|
|
@@ -38,7 +44,7 @@ for (const file of pages) {
|
|
|
38
44
|
// a page about self-hosting the platform brings its own identity provider: the values its provider gives back (scripts/issuer-stack.ts)
|
|
39
45
|
const platformOrigin = needsIssuer(readFileSync(file, 'utf8'));
|
|
40
46
|
if (platformOrigin) { issuer = await startIssuer(platformOrigin); Object.assign(app.env, issuer.env); }
|
|
41
|
-
results = await runTutorial(tutorial, app, { stepTimeoutMs: 180_000, ...(trace ? { onStep: trace } : {}) }); completed =
|
|
47
|
+
results = await runTutorial(tutorial, app, { stepTimeoutMs: 180_000, ...(trace ? { onStep: trace } : {}) }); completed = failures(results).length === 0;
|
|
42
48
|
} finally {
|
|
43
49
|
// each stack is stopped whatever the other or the page did; a stop's failure is said, never in place of the page's own
|
|
44
50
|
const stopped = await Promise.allSettled([hosted?.stop(), issuer?.stop()]);
|