@volter/world 3.0.123 → 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 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
- /** All of stdin, as text: a synchronous read of fd 0 to EOF — the same on Bun and Node, and it
142
- * holds the process (an async iteration of `process.stdin` here let Bun exit 0 mid-read, the
143
- * credential unsealed and nothing said — found by the deploy-from-a-shared-world page). */
144
- function stdinText() { try {
145
- return readFileSync(0, 'utf8');
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.123",
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.123",
34
- "@volter/world-runtime": "3.0.123",
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.118"
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.118"
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
- /** All of stdin, as text: a synchronous read of fd 0 to EOF — the same on Bun and Node, and it
128
- * holds the process (an async iteration of `process.stdin` here let Bun exit 0 mid-read, the
129
- * credential unsealed and nothing said — found by the deploy-from-a-shared-world page). */
130
- function stdinText(): string { try { return readFileSync(0, 'utf8'); } catch { return ''; } }
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);
@@ -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
- * every step's result; `failures()` says which steps the page got wrong.
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 pages = tutorialFiles().filter((f) => !only || f.endsWith(`${only}.md`));
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 () => { registry = await startRegistry({ only: packagesNamedIn(pages) }); }, 300_000);
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 = true;
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()]);