@vgai/sdk 0.5.13 → 0.5.15

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/sdk",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.13",
5
+ "version": "0.5.15",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.30.0",
33
- "@vgai/engine": "0.5.13",
33
+ "@vgai/engine": "0.5.15",
34
34
  "playwright": "^1.58.2",
35
35
  "zod": "^4.3.6"
36
36
  }
@@ -31,7 +31,7 @@ import { existsSync, readFileSync } from 'node:fs';
31
31
  import { isAbsolute, join } from 'node:path';
32
32
  import { z } from 'zod';
33
33
  import { ToolError } from '../errors.js';
34
- import { listProjectFiles } from '../project/shared.js';
34
+ import { isTheatreProjectPath, listProjectFiles } from '../project/shared.js';
35
35
  import { defineTool, type ToolErrorDefinition, type ToolRegistry } from '../registry.js';
36
36
  import type { ToolContext } from '../types.js';
37
37
 
@@ -153,7 +153,7 @@ export const cinematicProjectList = defineTool({
153
153
  permission: { risk: 'read', summary: 'Walks a file tree; no writes.' },
154
154
  async impl(input, ctx) {
155
155
  const root = resolveRoot(input.root, ctx);
156
- const theatreProjectFiles = listProjectFiles(root, (p) => p.endsWith('theatre-project.json'));
156
+ const theatreProjectFiles = listProjectFiles(root, isTheatreProjectPath);
157
157
  return { root, theatreProjectFiles };
158
158
  },
159
159
  });
@@ -159,7 +159,7 @@ export function createMcpServer(options: CreateMcpServerOptions = {}): VgaiMcpSe
159
159
  request.params.name === 'discover-project'
160
160
  ? 'Call project.discover, project.status, and project.manifest.read. Summarize each adapter root and then call editor.session.list. Do not write files.'
161
161
  : request.params.name === 'bring-existing-game'
162
- ? 'Call project.inspect before writing anything. Use its detected technologies, entry candidates, and blockers to identify Three.js/R3F, PixiJS, or React by renderer; explain the honest adapter tier and obtain consent before metadata or source writes.'
162
+ ? 'Call project.inspect before writing anything. Use its detected technologies, entry candidates, and blockers to identify Three.js/R3F, PixiJS, or React by renderer; name any capability the adapter has not yet reached and obtain consent before metadata or source writes.'
163
163
  : `Implement only this requested outcome: ${task}. Discover the project first, preserve unrelated work, use dryRun for file mutations when available, then verify through the exact matching editor with Play, status/logs, and a legible capture.`;
164
164
  return {
165
165
  description: requested.description,
@@ -9,7 +9,8 @@
9
9
  * - Stories (CSF) are ordinary `.tsx`/`.ts` modules (§5.2/C3) — VGAI stores
10
10
  * no duplicate story registry, so there is nothing to mutate here; listing
11
11
  * `*.stories.tsx`/`*.stories.ts` is the whole B2 surface.
12
- * - Theatre project state (`*.theatre-project.json`) is a real, versioned,
12
+ * - Theatre project state (`theatre-project.json` or `*.theatre-project.json`)
13
+ * is a real, versioned,
13
14
  * proprietary Theatre.js format this repo does not own (§3.2/§5.3 — "the
14
15
  * committed Theatre project state is authoritative authored data"). B2
15
16
  * validates only the OUTER shape (`sheetsById`/`definitionVersion`/
@@ -29,6 +30,7 @@ import { z } from 'zod';
29
30
  import { ToolError } from '../errors.js';
30
31
  import { defineTool, type ToolRegistry } from '../registry.js';
31
32
  import {
33
+ isTheatreProjectPath,
32
34
  listProjectFiles,
33
35
  NO_PROJECT_ROOT_ERROR,
34
36
  PATH_OUTSIDE_PROJECT_ERROR,
@@ -71,7 +73,7 @@ export const projectStoryDiscover = defineTool({
71
73
  export const projectTheatreDiscover = defineTool({
72
74
  name: 'project.theatre.discover',
73
75
  summary:
74
- 'List every committed Theatre project-state file (*.theatre-project.json) in the project.',
76
+ 'List every committed Theatre project-state file (theatre-project.json or *.theatre-project.json) in the project.',
75
77
  description:
76
78
  'List-only (§3.2/§5.3: the committed Theatre project state is authoritative authored data VGAI does not own the internal format of).',
77
79
  input: z.object({}),
@@ -86,9 +88,7 @@ export const projectTheatreDiscover = defineTool({
86
88
  permission: { risk: 'read', summary: 'Walks the project file tree; no writes.' },
87
89
  async impl(_input, ctx) {
88
90
  const projectRoot = requireProjectRoot(ctx);
89
- const theatreProjectFiles = listProjectFiles(projectRoot, (p) =>
90
- p.endsWith('.theatre-project.json'),
91
- );
91
+ const theatreProjectFiles = listProjectFiles(projectRoot, isTheatreProjectPath);
92
92
  return { theatreProjectFiles };
93
93
  },
94
94
  });
@@ -22,6 +22,7 @@ import {
22
22
  checkNotStale,
23
23
  DryRunField,
24
24
  FILE_NOT_FOUND_ERROR,
25
+ isTheatreProjectPath,
25
26
  listProjectFiles,
26
27
  mutationResultSchema,
27
28
  NO_PROJECT_ROOT_ERROR,
@@ -65,7 +66,7 @@ const ProjectDiscoverResult = z
65
66
  .describe('Every *.stories.tsx / *.stories.ts found under the project.'),
66
67
  theatreProjectFiles: z
67
68
  .array(z.string())
68
- .describe('Every *.theatre-project.json found under the project.'),
69
+ .describe('Every theatre-project.json / *.theatre-project.json found under the project.'),
69
70
  })
70
71
  .describe('A one-call survey of the project at ctx.projectRoot.');
71
72
 
@@ -115,9 +116,7 @@ export const projectDiscover = defineTool({
115
116
  projectRoot,
116
117
  (p) => p.endsWith('.stories.tsx') || p.endsWith('.stories.ts'),
117
118
  ),
118
- theatreProjectFiles: listProjectFiles(projectRoot, (p) =>
119
- p.endsWith('.theatre-project.json'),
120
- ),
119
+ theatreProjectFiles: listProjectFiles(projectRoot, isTheatreProjectPath),
121
120
  };
122
121
  },
123
122
  });
@@ -235,16 +235,78 @@ export type SessionJournalEvent =
235
235
  /** Two epochs beating under one tabId — "Duplicate Tab" copied sessionStorage. */
236
236
  | { readonly kind: 'tab-duplicated'; readonly tabId8: string }
237
237
  /**
238
- * The tab is present (its worker is beating) but its PAGE has never opened
239
- * a command channel this page-load. A heartbeat proves the tab exists, not
240
- * that the document works a main thread that died after the inline
241
- * bootstrap beats forever and can run nothing. Such a tab is passed over
242
- * for blessing and named in the refusal.
238
+ * The tab is present but its PAGE never came up this page-load. Presence
239
+ * proves the tab exists, not that the document works a main thread that
240
+ * died after the inline bootstrap keeps beating and holding its control
241
+ * connection, and can run nothing. Such a tab is passed over for blessing
242
+ * and named in the refusal.
243
+ *
244
+ * `reason` says which stage it is stuck at: `'no-channel'` (the page never
245
+ * opened a control connection at all) or `'no-command-listener'` (it did,
246
+ * and the module graph behind it never produced a listener).
243
247
  */
244
248
  | {
245
249
  readonly kind: 'tab-unresponsive';
246
250
  readonly tabId8: string;
247
- readonly noChannelForMs: number;
251
+ readonly reason: 'no-channel' | 'no-command-listener';
252
+ readonly unresponsiveForMs: number;
253
+ }
254
+ /**
255
+ * An uncaught error or unhandled rejection in a tab's PAGE, captured by
256
+ * `index.html`'s inline bootstrap — before the module graph, so it is
257
+ * recorded even when the boot that would have reported it is the thing that
258
+ * died. This is the line that says WHY a `tab-unresponsive` tab is
259
+ * unresponsive.
260
+ */
261
+ | {
262
+ readonly kind: 'page-error';
263
+ readonly tabId8: string;
264
+ readonly message: string;
265
+ }
266
+ /**
267
+ * One batch of occurrences of ONE console error/warning condition, as the
268
+ * server's unresolved-console ledger recorded it
269
+ * (`packages/editor/server/console-ledger.ts`).
270
+ *
271
+ * This is the DURABLE half of the loudness convention, and the reason it is
272
+ * a row per observation rather than a row per distinct message: `count` is
273
+ * the running total after this batch, so the file answers "how many times
274
+ * did this actually fire, and when did it stop" long after the ledger (and
275
+ * the tab, and the server) are gone. A repeat is never filtered out here —
276
+ * deduping repeats to silence is precisely the failure that made a session's
277
+ * eleven errors read as one line and then nothing.
278
+ */
279
+ | {
280
+ readonly kind: 'console-entry';
281
+ readonly id: string;
282
+ readonly severity: 'error' | 'warn';
283
+ readonly source: string | null;
284
+ /** Occurrences in THIS batch. */
285
+ readonly added: number;
286
+ /** Running total for this condition, across every page-load. */
287
+ readonly count: number;
288
+ readonly message: string;
289
+ }
290
+ /** A condition cleared by the honest re-test: the page reloaded and it did
291
+ * not recur. `count` is what it reached before it stopped. */
292
+ | {
293
+ readonly kind: 'console-retired';
294
+ readonly id: string;
295
+ readonly severity: 'error' | 'warn';
296
+ readonly count: number;
297
+ readonly message: string;
298
+ }
299
+ /** A condition waved through BY NAME (`vgai console ack`). The audit row is
300
+ * the whole point: an acknowledgment records who and why, and never erases
301
+ * what was acknowledged. */
302
+ | {
303
+ readonly kind: 'console-ack';
304
+ readonly id: string;
305
+ readonly severity: 'error' | 'warn';
306
+ readonly count: number;
307
+ readonly by: string;
308
+ readonly reason: string;
309
+ readonly message: string;
248
310
  };
249
311
 
250
312
  /** A parsed journal line: the event plus when it was appended. */
@@ -449,9 +511,22 @@ export function formatJournalLine(line: SessionJournalLine): string {
449
511
  case 'tab-duplicated':
450
512
  return `journal: ${at} tab-duplicated ${line.tabId8}`;
451
513
  case 'tab-unresponsive':
452
- return `journal: ${at} tab-unresponsive ${line.tabId8} no channel for ${Math.round(line.noChannelForMs / 1000)}s`;
514
+ return `journal: ${at} tab-unresponsive ${line.tabId8} ${line.reason} for ${Math.round(line.unresponsiveForMs / 1000)}s`;
515
+ case 'page-error':
516
+ return `journal: ${at} page-error ${line.tabId8} ${line.message}`;
453
517
  case 'tab-death-profile':
454
518
  return `journal: ${at} tab-death-profile ${line.tabId8} code ${line.code} — ${tabDeathProfileBody(line)}`;
519
+ // The CONSOLE arm. `count` is the running total for that condition, so a
520
+ // reader scanning the column sees a repeat climbing rather than the same
521
+ // line over and over with nothing to distinguish the fiftieth from the
522
+ // first. Every line leads with the ack id, because that is what a reader
523
+ // types next.
524
+ case 'console-entry':
525
+ return `journal: ${at} console-${line.severity} ${line.id} +${line.added} (total ${line.count}) ${line.source === null ? '' : `[${line.source}] `}${line.message.split('\n')[0]}`;
526
+ case 'console-retired':
527
+ return `journal: ${at} console-retired ${line.id} after ${line.count} — did not recur after reload`;
528
+ case 'console-ack':
529
+ return `journal: ${at} console-ack ${line.id} (×${line.count}) by ${line.by}: ${line.reason}`;
455
530
  }
456
531
  }
457
532
 
@@ -44,6 +44,12 @@ import { ToolError } from '../errors.js';
44
44
  import type { ToolErrorDefinition } from '../registry.js';
45
45
  import type { ToolContext } from '../types.js';
46
46
 
47
+ /** Both Theatre project-state names already established in shipped projects. */
48
+ export function isTheatreProjectPath(path: string): boolean {
49
+ const filename = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1);
50
+ return filename === 'theatre-project.json' || filename.endsWith('.theatre-project.json');
51
+ }
52
+
47
53
  // ---------------------------------------------------------------------------
48
54
  // Project-root resolution
49
55
  // ---------------------------------------------------------------------------
@@ -245,7 +245,7 @@ function keepFramesOverride(signal: AbortSignal | undefined): boolean | undefine
245
245
  }
246
246
 
247
247
  const DEFAULT_SEED = 0x5eed_c0de;
248
- /** Only actually used when `entry` is an http(s) URL (no dev server spawn — see {@link resolveEffectivePort}) or as this pure function's own documented default before that override applies. */
248
+ /** Only actually used when `entry` is an http(s) URL (no preview server spawn — see {@link resolveEffectivePort}) or as this pure function's own documented default before that override applies. */
249
249
  const DEFAULT_PORT = 5799;
250
250
 
251
251
  /**
@@ -282,7 +282,7 @@ export function findFreeRenderPort(): Promise<number> {
282
282
 
283
283
  /**
284
284
  * I7 fold-in (#9b): when the caller didn't pin an explicit `port`, resolve a
285
- * genuinely free one right before spawning the dev server, overriding the
285
+ * genuinely free one right before spawning the preview server, overriding the
286
286
  * resolved request's `DEFAULT_PORT` placeholder. Checked against the
287
287
  * ORIGINAL (unresolved) `request.port`, not `resolved.port` — the latter
288
288
  * already has `DEFAULT_PORT` filled in by {@link resolveRenderCinematicRequest}
@@ -471,7 +471,7 @@ export function resolveRenderCinematicRequest(
471
471
  keepFrames: req.keepFrames ?? false,
472
472
  // I7 fold-in #9b: this DEFAULT_PORT fallback is only actually used when
473
473
  // `resolveEffectivePort` (below) doesn't override it — i.e. `entry` is
474
- // an already-serving http(s) URL (no dev server is ever spawned, so no
474
+ // an already-serving http(s) URL (no preview server is ever spawned, so no
475
475
  // port collision is possible). Every Vite-config `entry` gets a REAL
476
476
  // free port at launch time instead — see `resolveEffectivePort`.
477
477
  port: req.port ?? DEFAULT_PORT,
@@ -607,23 +607,23 @@ async function waitForHttpOk(url: string, timeoutMs: number, signal?: AbortSigna
607
607
  const deadline = Date.now() + timeoutMs;
608
608
  let lastErr: unknown;
609
609
  while (Date.now() < deadline) {
610
- // I7 fold-in (#9a): don't keep polling a dev server that's booting for a
610
+ // I7 fold-in (#9a): don't keep polling a preview server that's booting for a
611
611
  // render that was already cancelled — return promptly so the caller's
612
612
  // own catch path (`launchEntryServer`) can group-kill the half-started
613
613
  // vite process right away instead of waiting out the full 30s timeout.
614
614
  if (signal?.aborted) {
615
- throw new Error('Render cancelled while waiting for the dev server to become ready.');
615
+ throw new Error('Render cancelled while waiting for the preview server to become ready.');
616
616
  }
617
617
  try {
618
618
  const res = await fetch(url);
619
- if (res.ok || res.status === 404) return; // dev server answering is enough; 404 still proves it's up
619
+ if (res.ok || res.status === 404) return; // a preview response is enough; 404 still proves it's up
620
620
  } catch (err) {
621
621
  lastErr = err;
622
622
  }
623
623
  await new Promise((r) => setTimeout(r, 200));
624
624
  }
625
625
  throw new Error(
626
- `Dev server at ${url} did not become ready within ${timeoutMs}ms` +
626
+ `Preview server at ${url} did not become ready within ${timeoutMs}ms` +
627
627
  (lastErr instanceof Error ? ` (last error: ${lastErr.message})` : ''),
628
628
  );
629
629
  }
@@ -633,16 +633,56 @@ export interface LaunchedServer {
633
633
  stop(): Promise<void>;
634
634
  }
635
635
 
636
- /** Resolve `entry` into a running dev server: reuse a URL as-is, or spawn `npx vite --config <entry>` (mirrors `run-doctor.ts`/`runConformance`'s `npx tsx <runner>` spawn idiom, `packages/vgai-cli/src/index.ts`). */
636
+ /** Build one Vite config as an exported artifact. A dev-served standalone game
637
+ * is intentionally trapped by `unexported-game-trap.ts`; render tooling must
638
+ * exercise either the editor or the export, never invent a bypass query. */
639
+ async function buildExportedEntry(
640
+ configPath: string,
641
+ cwd: string,
642
+ outDir: string,
643
+ signal?: AbortSignal,
644
+ ): Promise<string> {
645
+ const child = spawn('npx', ['vite', 'build', '--config', configPath, '--outDir', outDir], {
646
+ cwd,
647
+ stdio: ['ignore', 'pipe', 'pipe'],
648
+ detached: true,
649
+ });
650
+ let output = '';
651
+ child.stdout?.on('data', (data) => {
652
+ output += String(data);
653
+ });
654
+ child.stderr?.on('data', (data) => {
655
+ output += String(data);
656
+ });
657
+
658
+ const abort = () => void stopServerProcess(child);
659
+ signal?.addEventListener('abort', abort, { once: true });
660
+ try {
661
+ const code = await new Promise<number>((resolve, reject) => {
662
+ child.once('error', reject);
663
+ child.once('close', (exitCode) => resolve(exitCode ?? 1));
664
+ });
665
+ if (signal?.aborted) throw new Error('Render cancelled while building the exported game.');
666
+ if (code !== 0) {
667
+ throw new Error(`Vite production build failed (exit ${code}).\nOutput:\n${output}`);
668
+ }
669
+ return output;
670
+ } finally {
671
+ signal?.removeEventListener('abort', abort);
672
+ }
673
+ }
674
+
675
+ /** Resolve `entry` into a running exported page: reuse a URL as-is, or build
676
+ * the Vite config and spawn `vite preview`. */
637
677
  export async function launchEntryServer(
638
678
  entry: string,
639
679
  port: number,
640
- engineRoot: string,
680
+ _engineRoot: string,
641
681
  onProgress: ((e: RenderCinematicProgress) => void) | undefined,
642
682
  signal?: AbortSignal,
643
683
  options?: {
644
684
  /**
645
- * Extra args appended to the `npx vite` spawn (G3 fold-in). Motivating
685
+ * Extra args appended to the `npx vite preview` spawn (G3 fold-in). Motivating
646
686
  * case: the render-mode e2e fixtures all pin `server.host: '127.0.0.1'`
647
687
  * in their own vite configs, but a real project's config (an
648
688
  * `examples/<id>/vite.config.ts`) typically leaves `host` at Vite's
@@ -668,6 +708,15 @@ export async function launchEntryServer(
668
708
  }
669
709
  const baseUrl = `http://127.0.0.1:${port}`;
670
710
  onProgress?.({ phase: 'server-launch', url: baseUrl });
711
+ const projectDir = dirname(configPath);
712
+ const exportDir = await mkdtemp(join(tmpdir(), 'vgai-render-export-'));
713
+ let buildOutput: string;
714
+ try {
715
+ buildOutput = await buildExportedEntry(configPath, projectDir, exportDir, signal);
716
+ } catch (error) {
717
+ await rm(exportDir, { recursive: true, force: true });
718
+ throw error;
719
+ }
671
720
 
672
721
  // `detached: true` makes this child the leader of its OWN process group —
673
722
  // required for `stopServerProcess`'s `process.kill(-pid, sig)` group-kill
@@ -682,14 +731,17 @@ export async function launchEntryServer(
682
731
  'npx',
683
732
  [
684
733
  'vite',
734
+ 'preview',
685
735
  '--config',
686
736
  configPath,
737
+ '--outDir',
738
+ exportDir,
687
739
  '--port',
688
740
  String(port),
689
741
  '--strictPort',
690
742
  ...(options?.extraViteArgs ?? []),
691
743
  ],
692
- { cwd: engineRoot, stdio: ['ignore', 'pipe', 'pipe'], detached: true },
744
+ { cwd: projectDir, stdio: ['ignore', 'pipe', 'pipe'], detached: true },
693
745
  );
694
746
  let serverOutput = '';
695
747
  child.stdout?.on('data', (d) => {
@@ -708,10 +760,14 @@ export async function launchEntryServer(
708
760
  await waitForHttpOk(`${baseUrl}/`, 30_000, signal);
709
761
  } catch (err) {
710
762
  await stopServerProcess(child);
711
- throw new Error(`${(err as Error).message}\nVite dev server output:\n${serverOutput}`);
763
+ await rm(exportDir, { recursive: true, force: true });
764
+ throw new Error(
765
+ `${(err as Error).message}\nVite build output:\n${buildOutput}\nVite preview output:\n${serverOutput}`,
766
+ );
712
767
  }
713
768
  if (exited) {
714
- throw new Error(`Vite dev server exited before becoming ready.\nOutput:\n${serverOutput}`);
769
+ await rm(exportDir, { recursive: true, force: true });
770
+ throw new Error(`Vite preview exited before becoming ready.\nOutput:\n${serverOutput}`);
715
771
  }
716
772
 
717
773
  onProgress?.({ phase: 'server-ready', url: baseUrl });
@@ -719,8 +775,8 @@ export async function launchEntryServer(
719
775
  return {
720
776
  baseUrl,
721
777
  async stop() {
722
- if (exited) return;
723
- await stopServerProcess(child);
778
+ if (!exited) await stopServerProcess(child);
779
+ await rm(exportDir, { recursive: true, force: true });
724
780
  },
725
781
  };
726
782
  }
@@ -729,7 +785,7 @@ export async function launchEntryServer(
729
785
  * Terminate `proc` AND its whole process group (see the `detached: true`
730
786
  * comment at its call site above) — a plain `proc.kill()` leaves the real
731
787
  * `vite` node process (an `npx` grandchild) running, which both leaks a
732
- * port-bound dev server across renders and keeps THIS process's event loop
788
+ * port-bound preview server across renders and keeps THIS process's event loop
733
789
  * alive via the still-open stdout/stderr pipes, hanging `renderCinematic`'s
734
790
  * caller indefinitely after the render itself has already finished. Mirrors
735
791
  * `packages/editor/e2e/helpers/server.ts`'s `stopProcess` (POSIX process-
@@ -757,7 +813,7 @@ function stopServerProcess(proc: ChildProcess): Promise<void> {
757
813
  return;
758
814
  }
759
815
  const timer = setTimeout(() => {
760
- signalGroup('SIGKILL'); // belt and braces — don't hang the render on a stuck dev server
816
+ signalGroup('SIGKILL'); // belt and braces — don't hang the render on a stuck preview server
761
817
  resolvePromise();
762
818
  }, 5_000);
763
819
  proc.on('exit', () => {
@@ -1450,12 +1506,12 @@ async function openRenderPage(
1450
1506
  export interface RenderCinematicPageSession {
1451
1507
  readonly page: Page;
1452
1508
  readonly resolved: ResolvedRenderCinematicRequest;
1453
- /** Closes the page/context, the browser, and stops the spawned dev server (if any). */
1509
+ /** Closes the page/context, the browser, and stops the spawned preview server (if any). */
1454
1510
  close(): Promise<void>;
1455
1511
  }
1456
1512
 
1457
1513
  /**
1458
- * Open a live render-mode page — preflight, dev-server launch, headless
1514
+ * Open a live render-mode page — preflight, production build/preview launch, headless
1459
1515
  * Chromium launch, DOM/CSS-animation freeze, navigate, and wait for
1460
1516
  * `__vgaiRender.ready()` — WITHOUT running {@link renderCinematic}'s own
1461
1517
  * frame-capture/encode loop. This exposes the raw Playwright `Page` for a
@@ -1723,13 +1779,13 @@ export async function renderCinematic(
1723
1779
  // I7 fold-in (#9a — cancellation race): this is the EARLIEST possible
1724
1780
  // cancellation point — `preparePipelineOutputs` (immediately above)
1725
1781
  // already created `framesDir` on disk, so throwing here without cleanup
1726
- // leaked it (reproduced by a real abort landing before the dev server
1782
+ // leaked it (reproduced by a real abort landing before the preview server
1727
1783
  // even starts, not merely a hypothetical). `cleanupAfterFailure` is a
1728
1784
  // no-op on everything else at this point (no `outAbs` output could
1729
1785
  // possibly exist yet), so this is just the framesDir removal, done
1730
1786
  // explicitly rather than pulled into a broader try/catch this early.
1731
1787
  const err = new RenderCinematicCancelledError(
1732
- 'Render cancelled before the dev server launched.',
1788
+ 'Render cancelled before the preview server launched.',
1733
1789
  keepFramesOverride(signal),
1734
1790
  );
1735
1791
  await cleanupAfterFailure(err, resolved, outAbs, framesDir, signal);
@@ -1742,7 +1798,7 @@ export async function renderCinematic(
1742
1798
  // cancellation-driven failure — `waitForHttpOk`'s own `signal?.aborted`
1743
1799
  // check inside `launchEntryServer` throws a plain `Error` (wrapped again
1744
1800
  // by `launchEntryServer`'s own catch, losing any `RenderCinematicCancelledError`-
1745
- // ness entirely) if `signal` aborts while still waiting for the dev server
1801
+ // ness entirely) if `signal` aborts while still waiting for the preview server
1746
1802
  // to come up. Without this try/catch, THAT throw skipped
1747
1803
  // `cleanupAfterFailure` completely (it's outside the main try block) and
1748
1804
  // leaked the temp frames dir `preparePipelineOutputs` already created
@@ -1765,7 +1821,7 @@ export async function renderCinematic(
1765
1821
  let browser: Browser | undefined;
1766
1822
  const warnings: string[] = [];
1767
1823
  // I7 fold-in (#9a — cancellation/orphan): tear down the browser AND the
1768
- // vite dev server's WHOLE PROCESS GROUP the INSTANT `signal` aborts,
1824
+ // Vite preview server's WHOLE PROCESS GROUP the INSTANT `signal` aborts,
1769
1825
  // rather than waiting for the next cooperative per-frame check
1770
1826
  // (`captureFrames`'s `signal?.aborted` guard) to be reached. This is what
1771
1827
  // makes a slow/hung in-flight `page.evaluate`/`page.screenshot` call, or a