@vgai/sdk 0.5.14 → 0.5.16
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.
|
|
5
|
+
"version": "0.5.16",
|
|
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.
|
|
33
|
+
"@vgai/engine": "0.5.16",
|
|
34
34
|
"playwright": "^1.58.2",
|
|
35
35
|
"zod": "^4.3.6"
|
|
36
36
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* F1) — the exact follow-up `mcp-projection.ts`'s header reserved: "If the
|
|
4
|
-
* official SDK is later added, only a transport wrapper around `handle`
|
|
5
|
-
* changes; the derivation below does not."
|
|
2
|
+
* Official-SDK stdio transport for the MCP projection.
|
|
6
3
|
*
|
|
7
4
|
* This module is that transport wrapper and NOTHING more: it binds
|
|
8
5
|
* `@modelcontextprotocol/sdk`'s `Server` + `StdioServerTransport` around the
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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; //
|
|
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
|
-
`
|
|
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
|
-
/**
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
723
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|