Give AI agents a fast way to test your apps from the inside.
vitexec is for checks that are too slow, visual, or stateful for normal agent
browser control.
Use it to let an agent play through a scene, steer a camera, test game-like controls, inspect runtime state, and collect evidence without adding debug UI or temporary app code.
Install the package in the Vite app:
pnpm add -D vitexecInstall the agent skill:
npx skills add drawcall-ai/vitexecThen ask your agent to use it:
Use $vitexec to move through the scene and inspect the camera position.AI agents are slow when every action is a separate browser step.
Click, wait, inspect, press a key, wait again is expensive and often useless for interactive apps where movement, camera, physics, timing, and input all happen together.
The missing piece is a programmable control loop inside the running app.
vitexec lets the agent run a script inside the page instead of controlling the
browser one action at a time.
The snippet can import real Vite modules, drive real input, wait for frames, branch on state changes, and run full multi-step flows before logging the result.
Each run gets an isolated Vite server, its own Playwright browser, streamed logs, and optional screenshots, videos, traces, profiles, HARs, or heap snapshots.
Add the plugin to expose the scripts in ./vitexec as app pages in both the dev
server and production build:
import { defineConfig } from "vite";
import { vitexec } from "vitexec";
export default defineConfig({
plugins: [vitexec()]
});index.html
vitexec/
smoke.ts → /smoke.html
checkout.ts → /checkout.htmlEach generated page is the normal index.html plus its vitexec script. Only
top-level .js, .jsx, .mjs, .mts, .ts, and .tsx files become pages, so
scripts can import helpers from subdirectories.
Use an explicit mapping when the scripts live elsewhere:
vitexec({
directory: false,
pages: {
"/checkout.html": "/checks/checkout.ts"
}
});Calling vitexec() more than once is safe. Identical directories and mappings
are deduplicated; conflicting mappings fail the Vite config instead of depending
on plugin order.
The generated pages are included by vite build. Do not deploy them with
production code if they perform destructive or privileged actions.
See the plugin-only example for the same routes running under the normal Vite dev server and production build.
- Instead of adding test functions to the app: keep checks outside production code
- Instead of judging screenshots only: log exact runtime state
- Instead of one slow browser action at a time: run a state-aware script in-page
- Instead of hand-writing Playwright-style setup: give the agent one CLI tool
- Instead of sharing one dev server: run isolated checks in parallel
- Instead of fighting HMR during checks: use a Vite server with HMR disabled
vitexec --gpu --path /scene '
import { app } from "/src/app.ts";
window.dispatchEvent(new KeyboardEvent("keydown", { code: "KeyW" }));
await new Promise((resolve) => setTimeout(resolve, 1000));
window.dispatchEvent(new KeyboardEvent("keyup", { code: "KeyW" }));
const { object, camera } = app.getSnapshot();
console.log("moved", JSON.stringify({ object, camera }));
'logs:
[log] moved {"object":{"x":0,"y":0,"z":-4.2},"camera":{"yaw":0,"pitch":0}}No debug panel. No test-only app code. No guessing from pixels alone.
- Camera controls, pointer lock, drag interactions, and gamepad input
- Three.js scenes, physics simulations, canvas, WebGL, and WebXR
- Zustand, Redux, TanStack Query, or custom runtime stores
- Screenshots, videos, CPU profiles, HARs, traces, and heap snapshots
- Turning vague browser failures into readable logs
vitexec --gpu --path /scene 'console.log(location.pathname)'
vitexec --gpu --path /scene check-scene.tsFor a single argument, vitexec first checks the path as written, then checks the
same path under ./vitexec, and otherwise treats it as inline code. Thus
vitexec check-scene.ts runs ./vitexec/check-scene.ts.
| Option | Use |
|---|---|
--path /scene |
Open a specific route |
--config ./vite.config.ts |
Use a specific Vite config |
--gpu |
Use generic GPU/WebGPU-friendly Chromium flags |
--browser-ws-endpoint wss://... |
Connect to a Playwright browser WebSocket endpoint |
--browser-expose-network <loopback> |
Expose local network routes to a remote browser |
--screenshot ./page.png |
Capture a full-page screenshot |
--record ./run.webm |
Record browser video |
--cpu-profile ./cpu.cpuprofile |
Capture a Chrome/V8 CPU profile |
--network-trace ./network.har |
Capture network requests as HAR |
--performance-trace ./performance.trace.json |
Capture a Chrome performance trace |
--heap-snapshot ./heap.json |
Capture a jq-friendly decoded heap snapshot |
--viewport 390x844 |
Set the browser viewport (default 1280x720) |
--timeout 30 |
Set the maximum wait time |
CLI flags take precedence over environment variables.
| Environment variable | Equivalent option |
|---|---|
VITEXEC_BROWSER_WS_ENDPOINT |
--browser-ws-endpoint |
VITEXEC_BROWSER_EXPOSE_NETWORK |
--browser-expose-network |
VITEXEC_CONFIG |
--config |
VITEXEC_PATH |
--path |
VITEXEC_GPU |
--gpu |
VITEXEC_TIMEOUT |
--timeout |
VITEXEC_SCREENSHOT |
--screenshot |
VITEXEC_RECORD |
--record |
VITEXEC_CPU_PROFILE |
--cpu-profile |
VITEXEC_NETWORK_TRACE |
--network-trace |
VITEXEC_PERFORMANCE_TRACE |
--performance-trace |
VITEXEC_HEAP_SNAPSHOT |
--heap-snapshot |
VITEXEC_VIEWPORT |
--viewport |
When --browser-ws-endpoint is set, vitexec only sends browser-generic
GPU/WebGPU launch flags. Start the remote Playwright server with any
host-specific GPU policy that matches its platform.
By default vitexec launches its own Chromium. When you call it programmatically
you can instead hand it a Playwright browser, context, or page you already
own — the snippet runs in that browser with no second window. This is ideal
when a dev server already opens a visible, instrumented tab (e.g. a WebXR
emulator) and you want to watch the check run live in it.
import { runVitexec } from "vitexec/cli";
// Reuse a page you own. vitexec navigates it to its own per-run URL, runs the
// snippet, and never closes it — safe to reuse across many sequential runs.
for await (const line of runVitexec(code, { root, page })) console.log(line);
runVitexec(code, { root, context }); // open a fresh page in this context
runVitexec(code, { root, browser }); // open a fresh context + page in this browservitexec only ever closes handles it created itself: an adopted page is left
open, an adopted context keeps its own pages (vitexec closes just the page it
opened), and an adopted browser keeps running (vitexec closes just the context
it opened). Its own Vite server is always closed. --record and
--network-trace need a vitexec-created context, so they are skipped for an
adopted page or context.
This is also how you reuse one browser across many runs: connect or launch it once yourself and pass it in — vitexec never closes what it did not create. (The CLI has no flag for this; adoption is a programmatic-only capability.)