Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Command line

The spawnite command comes from @spawnite/cli, which a game that spawnite create writes carries as a dev dependency. spawnite help prints every command. This page adds what the help does not say, grouped by what a command does; the help alone covers a command this page leaves out. A coding agent reaches the same commands as MCP tools.

Every command except completion takes --json, which prints one object with "schemaVersion": 1 in place of the lines a person reads. A command that runs long, model make, model check, play profile and play join, prints each phase to stderr as it starts, such as Running models/imp.py in Blender, so stdout keeps the reply alone. The MCP’s tools send the same lines as progress notifications. Every command also takes --ports <first-last> and --room-ports <first-last>. --ports keeps each server it starts, the dev server, the build’s preview and the room, to the first free port of the range that no other server it starts was given. --room-ports keeps the room to a range of its own. GAME_PORTS and GAME_ROOM_PORTS set the same. Without them, the system picks each port. A refusal prints its message alone to stderr, or { "schemaVersion": 1, "error": { "type", "message" } } to stdout with --json. The exit code is 0 on success, 1 on a refusal, and 2 for a command line the cli does not accept.

pnpm spawnite with no command, in a terminal, opens a dashboard built with Ink. Its Games panel lists the games in the folder, one row each, pinned games first with a ★, and its Other panel under it lists the screens that are not a game’s: New game and Account. Beside them, in a terminal 80 columns wide or wider, a panel says what the selected row holds: for a game, whether a playtest runs, at which address and whether your agent’s MCP server started it, how many replay sessions it keeps, and its folder.

↑↓ or the wheel move through both panels, tab jumps between them, ⏎ or a second click opens the row, p pins or unpins the selected game, esc goes back, a click on a crumb in the top bar goes back to it, and q quits. The pins are kept per machine in pinned-games.json, beside the login. A folder that is itself a game opens on that game.

A game’s Playtest tab has a Play button: with no playtest running, it starts one as spawnite play start does and opens it in your browser, and with one running, whoever started it, it opens that one. While one runs, the tab shows the agent’s session: the copy of the game its headless browser holds paused until the agent steps it, with who started it and so what stops it. The tab also shows a Stop playtest button for one spawnite play start or the dashboard began, its devtools tree in a World panel and its newest console lines in a Console panel. o opens and x stops. ← and → or a click move between a game’s tabs.

Its Replays tab lists the sessions its replays keep, newest first, each with when it started, how long it runs, its marks and its size. Then it lists the saved clips and how much of the budget they use. It reads them through the game’s Vite, as spawnite replay list reads them. ⏎ opens a session: a timeline of the minutes it keeps with a ▲ under each mark, and its moments. The moments are the marks first, and then one every 10 s that has a line of the log within 5 s, so an idle stretch shows none. Beside them is the shot kept nearest the selected moment and its log within 5 s either side. ⏎ or o writes the shot to the temp folder and opens it in the machine’s viewer, and c saves the minutes around the moment as spawnite replay save --around does.

Its Publish tab shows the game’s state and its link, above its versions on Spawnite, newest first, with the live one marked. It runs the code the commands run, with their progress and their refusals in the tab:

  • u builds the game and uploads it as the next version, as spawnite upload does.
  • p makes the selected version live, as spawnite publish <version> does.
  • v puts the game on the store or takes it off, as spawnite visibility does.

p, and v when it would put the game on the store, ask first: the tab names the version or the game, and the same key pressed again acts, while esc or any other key cancels. Signed out, the tab says how to log in. For a game whose package.json names no id yet, it says to run spawnite games create <name> in its folder.

New game shows the registry’s templates as spawnite list reads them, one card each with its description and whether it plays solo, multiplayer, or solo or multiplayer. ⏎ picks one and asks for a folder name of letters, digits, - and _, where q types a q. It asks under a preview of the files it writes: each file at the top, then each folder with how many files it holds, which the wheel scrolls. ⏎ again writes the game to games/<name> as spawnite create does and adds it to the Games panel.

The account shows who the machine is signed in as and where the login is kept, and logs in or out as spawnite login and spawnite logout do, with the code to check on screen while a login waits. Signed in, n asks for a new nickname, where q types a q, and ⏎ sets it as spawnite account name does, or shows why the backend refused it.

Without a terminal on both stdin and stdout, or with CI set, pnpm spawnite prints the help and exits 2 instead.

  • npx @spawnite/cli plugin install connects the Spawnite plugin to every coding agent on the PATH. In Claude Code, it adds the spawnite/plugin marketplace and installs spawnite@spawnite for the user. It enables one that is installed but disabled, and installs one that is only a project’s for the user. In Codex, it registers the MCP server as codex mcp add spawnite -- npx -y @spawnite/mcp does, and adds the long tool timeout under its table in ~/.codex/config.toml. Each agent loads the plugin when its next session starts, or Claude Code when the user types /reload-plugins, and the reply says so; nothing waits on it. With neither agent on the PATH, it refuses and names where to install one. --json carries what was done for each agent under connected.
  • pnpm exec spawnite completion install puts spawnite on your PATH and has bash complete its commands. It writes ~/.local/bin/spawnite, init.sh in the data folder env-paths names (~/.local/share/spawnite on Linux), and one line in ~/.bashrc and your login file.
  • spawnite dashboard screenshot draws the dashboard with no terminal, for the games under the working folder, presses --keys in order, each once the screen has settled, and prints the screen as text. --keys j,down,enter,esc takes a character or the name of up, down, left, right, enter, esc, tab, space or backspace. wait presses nothing but holds the next key until the screen draws something new, such as the replays a tab reads through the game’s Vite: --keys right,wait,enter. --size 100x30 sets the terminal’s columns and rows. --until <text> waits up to 10 s for the screen to show the text, such as an account a network read fills in, and fails with the screen it showed. --out <file> writes the screen as a PNG in its colours instead, shot in headless Chromium. --json carries the text as screen.
  • spawnite login signs this machine in to Spawnite through WorkOS AuthKit’s device flow, as gh auth login does. It opens the approval page in the browser, with the code filled in, prints the address and the code to stderr, and waits until the code is approved there, on this machine or any other. An agent runs it itself, since only the approval needs its user, and in a sandbox or over SSH hands them the address. Signing in the first time creates the account. --no-browser only prints.

    The login is kept in login.json under SPAWNITE_CONFIG_DIR, else under the platform’s config folder as env-paths names it. That folder is ~/Library/Preferences/spawnite on macOS, %APPDATA%\spawnite\Config on Windows, and $XDG_CONFIG_HOME/spawnite or ~/.config/spawnite on Linux. The file is readable by its owner alone, and its access token is refreshed when it expires. The backend is production, https://api.spawnite.com, for a cli installed from npm, and the dev deployment for a run from this repository’s checkout. SPAWNITE_CONVEX_URL, a backend’s Convex API address, names another for either, such as https://api.spawnite.com for production from the checkout. The backend names the AuthKit client.

  • spawnite logout ends the machine’s session with WorkOS, through the backend, which holds the API key WorkOS asks for, so a copy of the refresh token is spent. It then forgets the kept login, even when the session cannot be ended, and says why not: the backend refused, or the login was made against another backend, whose session is left to expire. A session whose refresh WorkOS already refuses counts as ended.

  • spawnite whoami prints the account the machine is signed in as, once the backend accepts its sign-in, its nickname and handle, whether the account still needs a nickname, and where the login is kept. It refuses when there is no login, and when the backend refuses it.

  • spawnite games create <name> creates a game on the signed-in account from a name. The name is up to 50 characters, with no word the store does not allow, and other games may share it. The command prints the game’s id and its slug, the readable text in its address, which is made from the name, as Publishing a game says. Run in a game’s folder, or with --project, it writes the id into the project’s package.json as "spawnite": { "game": "<id>" }, so the commands below find the game there. It refuses a project that already names a game, and --no-project writes the id nowhere.

  • spawnite upload [folder] builds the game into its dist/bundle, as spawnite build does, and uploads the bundle as the game’s next version, on the signed-in account. Given a folder spawnite build wrote, it uploads that folder as it is. Before it sends anything, it checks the bundle against the upload’s limits, the same numbers the backend reads from @spawnite/schema’s bundleLimits: 1 GB in all, 5,000 files and 50 MB of the page’s HTML, scripts and styles. It refuses with every finding, one per line: the limit, the bundle’s value, the limit’s value, the largest files or fullest folders, and what to change. Past 100 MB in all, it uploads and prints a warning that says why the size matters and what to check. It warns the same way of each file the page loads from another site, which a published page refuses.

    It then asks https://assets.spawnite.com for each shared file the bundle names by URL. It refuses a bundle that names one the site does not serve, or when it cannot reach the site, naming the file that holds the URL. WebAssembly is allowed. The backend checks the listing and the manifest again, then signs one URL per file. The cli sends each file straight to the games bucket, eight at a time, each up to four tries. Then the backend checks the bucket holds them all and records the version. spawnite publish, and the MCP’s upload_version and publish_version tools, upload through the same check.

    It prints the version’s number and the command that makes it live: the version is checked and not live. --engine, --pin and --no-pin choose the build, as spawnite build takes them. Publishing a game says what the folder holds and what the check refuses.

  • spawnite publish builds the game, uploads it as the next version and makes that version live at the game’s link, in one command, then prints the link, the live version and the game’s state. A first publish is unlisted: the link carries the game’s key, and only people with it can play. spawnite publish <version> makes an uploaded version live as it is, newer or older than the live one, as a rollback, and builds nothing. --public also puts the game on the store, and refuses before it builds a game with no store page. A publish never takes a public game off the store. --slug <slug> sets the readable text in the game’s address before anything is built. Players load the new version within a minute.

    Before it uploads, and before it makes an uploaded version live, it compares the version’s save entry with every version the game ever made live, at the highest version each part of the save had. It refuses, naming the part, a version below that one and the same version with another schema fingerprint. The refusal says to raise version and add a migration step. No flag publishes past it. A fingerprint it cannot compare, missing or in another format, prints a warning and the publish goes on. spawnite upload alone runs no such check, since an uploaded version is live for nobody.

  • A read-only command reads a published game’s numbers on the signed-in account, for the game and for each published version:

    • its players
    • its sessions and how long they last
    • how many players came back the next day and the next week
    • rounds finished, where the game has rounds
    • purchases, with what was earned

    It prints them as text, or as JSON with --json. The numbers are totals for the game, start with its first player, and identify no player, as What you can see about your game says.

  • spawnite create <folder> --template <name> writes a new game project from a template in the registry. The project installs the published packages at the cli’s own version. It plays with the install and dev commands of its package manager, such as npm install then npm run dev. That manager is the one a lock file or a packageManager field above the folder names, else the one that ran create, such as the npm behind npx. The project gets pnpm-workspace.yaml, with pnpm’s own settings, only when pnpm installs it, and --tarballs refuses any other manager.

    The project starts with the template’s model files and the manifest baked from them. Its models script bakes public/assets/ into src/models.json, its dev and build scripts bake first, and its dev server rebakes when a model file appears, changes or goes. It also carries the template’s tests under test/, all but the browser smoke, any test that imports an internal entry of ours, which the published packages drop, and any test that imports such a test. Its test script runs them with Vitest against the manifest as the project holds it.

    The project is a git repository with one commit of what create wrote, and its .gitignore leaves out node_modules, dist, .replays and .spawnite. Its AGENTS.md, which its CLAUDE.md imports, does four things:

    • It tells the agent to prefer what the engine ships over its own code.
    • It holds the index of the engine’s guide pages that spawnite ai-files writes.
    • It tells the agent to run the project’s check script before it says a step is done. The script, spelled for its manager such as npm run check, runs tsc and ESLint with the engine’s rules for games.
    • It says that nothing backs the project up until it is pushed, so the agent offers the creator a GitHub repository for it.

    Where git is missing, create still writes the project and says so in one line. In a folder that is already inside a git repository, it starts no second one, and says to commit and push the project there. Without --template, it lists the templates and asks. --release <version> pins another version, and --tarballs <folder> installs from pnpm pack output instead, for a check before a tag.

    --room makes a game that plays alone, such as the example, a game its friends play together. The project’s package.json gets a spawnite.room naming the scene the room runs and its settings, and gets @spawnite/room as a dev dependency. A room game’s template, such as the arena, gets both without --room, and a template that cannot run in a room refuses it. A room game plays with the install, then spawnite play start through the manager, such as npm exec spawnite play start, which starts its room beside the dev server.

  • spawnite ai-files writes the index of the installed engine’s guide pages into the game’s AGENTS.md, between its BEGIN:spawnite-engine-index and END:spawnite-engine-index comments. The index holds the pages’ folder under node_modules/@spawnite/engine, then one line per folder naming each page and the first two things a creator asks for that it answers, from the page’s useCases. A last line says where a link inside a page leads. It makes the file where it is missing, keeps everything outside the comments, and leaves the file as it is when the index is already current.

    It then writes the Spawnite plugin’s skills, game-builder, model-builder and wiki, under the game’s .agents/skills/, where Codex reads a project’s skills. It replaces each of those three folders whole on every run, and keeps a skill of any other name. The cli’s build copies them from packages/plugin/skills, and create writes them too. A created game’s postinstall runs it, so the index matches the engine, and the skills the cli, that each install of the game’s packages brings. It refuses a game with no engine installed. The game is found from the working directory, or named with --project.

  • spawnite upgrade moves the game to the newest release. It sets every @spawnite package the game lists to that version together, and runs the install of the package manager whose lock file the game has. It then prints each change between the two versions, oldest first: what was renamed or removed, and what to write instead. A game on a beta, such as 0.1.0-beta.8, moves to the newest version under npm’s beta tag, or to the full release once it ships; a game on a full release follows latest. It refuses a game whose engine comes from a workspace or a folder. --json prints the two versions, the packages it moved and the changes as migrations. The changes come from migrations.json in the cli the install brought, so they need no network beyond the install.

  • spawnite list prints what the registry holds. First come the games in three groups: those that play in a room, those that play alone or in a room with --room, and those that play alone. Then come the parts spawnite add writes, then the games that cannot install from npm yet, each with the package it needs that no release publishes. create refuses those from the registry. With --json, each game carries room: always, optional or none, and a game that cannot install also carries unavailable: { "reason": "unpublished-package", "package": "@spawnite/auth" }. A game with no unavailable is one create writes.

  • spawnite add <item> [name] writes an item’s files into the game and prints the pnpm add lines for its dependencies. With a name, Feature in the template becomes that name: PascalCase for a behaviour, an entity, a panel, a hud part or a shader, kebab-case for a scene. The lines that register a scene, a panel or a hud part are printed for you to paste. spawnite add behaviour <Name> --system also writes the behaviour’s system beside it, src/behaviours/<Name>System.ts, a rule the step runs over every entity with the trait. It adds the system as the last rule of the game’s own plugin, src/rules.plugin.ts, and writes that file where the game has none. The first time, it prints the lines that list the plugin in src/game.ts. The rule’s entry declares no runsOn, so in a game with a room the room alone runs it.

    spawnite add hud <Name> --slot top-right writes src/hud/<Name>.tsx, a Panel at that slot of the screen with a Text and the player’s health Bar, and prints the <Name /> line to put inside a <Hud>. spawnite add dialog <npc> gives the NPC at src/npcs/<npc>.tsx a conversation in src/npcs/<npc>Dialog.tsx and prints the lines that place it inside its <Npc>; it refuses an NPC with no file there, or one that already holds a <Dialog>.

    A file that exists is never overwritten. create and add refuse an item whose file would land outside the game, under a .git folder, whose hooks git runs, through a symlink, or in a file stream named with a colon, and write none of its files. --dry-run refuses what a real run refuses, prints the same lines and each file’s content, and writes nothing; it does not apply to spawnite add asset. The game is found from the working directory, or named with --project as a path or a folder under games/.

  • spawnite add asset <id> downloads a shared asset from the manifest into the game’s public/assets/ folder, so the game’s own deploy serves it, and refuses a file whose sha256 differs from the manifest’s. It refuses a public/assets/ folder reached through a symlink, as add does.

    For a model it bakes the game’s public/assets/ folder into src/models.json, which measures the model. It then registers the model’s entry from that manifest, with no size in it, through the game’s src/models.ts. That is the module the scene imports, so a room registers what a page does. A src/models.ts that loops over the manifest registers the model already, and the command prints nothing to paste. One that names its entries one by one gets the registerModel line to paste, with whichever import it lacks. A game with none gets one written that loops over the manifest, and the line that imports it into the scene the game’s spawnite.room names, or into the app file of a game with no room. <Entity model="<id>"> then draws it.

    For a clip it prints the registerClip line to paste, with the served path, and the marks to add once spawnite play timeline --clip shows the frame. For a particle effect it writes src/particles/<id>.json instead, with its sprites inside it. It prints the line that imports it into the scene and the call that plays it: useParticles().spawn for an effect that plays once, <Emitter> for one that loops. --assets points it at another base URL, or at a folder holding assets/index.json; ASSETS_URL sets the default.

  • spawnite assets publish <path> publishes a model, an avatar, a clip, a texture, a sound or a particle effect to the shared assets on the signed-in account, so any game adds it with spawnite add asset. It publishes one file, or every such file under a folder. A .glb or .vrm is at most 50 MB, and a .vrma, .webp or .mp3 at most 20 MB, and a three.quarks effect’s .json at most 5 MB.

    Each file needs a row in the catalog.json of the folder, under its path from that folder: its name, category, licence and source, with optional tags and style, as the assets page shows. The command reads what the file tells: its triangles, clip names, whether it is rigged, its bounds, a texture’s resolution, a sound’s length and whether an effect loops. It sends the row to the backend, then sends the file to the upload URL the backend answers. An effect goes with each sprite it names beside it inside the file, and with a picture of it playing for its store card, which the command renders in headless Chromium. An effect three.quarks cannot parse is refused before anything is sent. The backend checks the file before it is public and draws the asset’s id from its name, such as flat-rock-48201973.

    The command prints one line per file:

    • published, with the id and the spawnite add asset command
    • present, for a file the account published before, which is sent nowhere
    • refused, with the reason: the backend’s, or the cli’s own for a file it cannot read as its type

    It exits 1 when a file was refused. A refused file stops no other, and a file’s name need not be an id: Flat Rock.GLB publishes. An account publishes at most 100 assets a day, and each counts against its storage. A file with no catalog row stops the publish before anything is sent. Needs spawnite login. --json carries the files as assets.

  • spawnite model make <name> runs models/<name>.py, a Python script, in the installed Blender with no window, and writes public/assets/<name>.glb. The script starts from an empty scene and builds only the model. The command then exports the whole scene with modifiers applied, animations included and +Y up, rebakes src/models.json, registers the model through src/models.ts as spawnite add asset does, and renders a Workbench still into a temporary folder. Applying modifiers drops shape keys: they survive only on a mesh whose one modifier is its armature.

    It prints the file, its triangle count, its clip names, its size and the still’s path. Then it prints the concept the game keeps beside the script as models/<name>-concept.png, .jpg or .webp, with the spawnite model check that lays it over the model. Then come a src/models.ts it wrote and the lines to paste, as spawnite add asset prints them. A script that throws stops the command with Blender’s traceback, which names the script’s line. Blender is BLENDER_PATH, else blender on the PATH, else the folder the OS’s installer puts it in. With none, the command prints the install command for the OS: brew install --cask blender, winget install BlenderFoundation.Blender or sudo snap install blender --classic. No game needs Blender: dev, build and the bake never call it.

  • spawnite model check <files...> reads GLB files, such as ones from Meshy, Tripo or Sketchfab, against the Models page, and draws them on one sheet to judge against their concept. A .vrma clip among the files prints its motion table instead: its length, where the arms peak, which a release mark starts from, the hips’ drop and travel, and each humanoid bone’s largest turn and the second it peaks. A clip renders nothing. For each file it prints the triangles it draws, its size in metres along x, y and z, its clips with their lengths in seconds, its bones and each warning the bake would give it, all measured as the bake measures them.

    It then renders a sheet in headless Chromium, into a temporary folder, with a column for each --angle, given once for each. An angle is any angle spawnite play screenshot takes:

    • front, the model’s +Z
    • three-quarter, 45° round toward +X at eye level
    • right, back, left, top, bottom or iso
    • azimuth,elevation in degrees

    Without one, the columns are front, three-quarter, right and back. Every tile is orthographic at one scale, each file one unit tall. --style shaded,silhouette,outline,wireframe,vertices draws a band of the sheet for each:

    • shaded, by default
    • silhouette, for proportions
    • outline, for the outer edge and creases with hidden lines removed
    • wireframe, for the edges with the faces in front hiding those behind
    • vertices, for the points a face does not hide

    --reference front=concept.png lays a concept image on a plain or transparent background over the rest pose’s front tile, in red. It prints the overlap of the two silhouettes, their intersection over their union, then:

    • each silhouette’s runs across at ten heights
    • the heights of the crotch, the shoulders and the neck on each silhouette
    • the three heights where the body, the run on the centre line, sits furthest from the concept’s, in centimetres

    A line says when the arms stand apart at other heights than the concept’s, as a T-pose concept’s do beside an A-pose model. --reference front,right,back=turnaround.png cuts a turnaround’s figures, left to right, at the widest gaps between them, and #x,y,width,height after the image crops it first. A side or three-quarter figure is also tried mirrored, and the line says when that matched better. Without --angle, the references’ angles are the sheet’s. --fit also fits each concept by the scale and offset that overlap best, for a concept whose height an antenna or a raised weapon sets. It prints the fit, and reads the widths, the landmarks and the red outline from the fitted concept.

    --clip jump --at 0.4,0.9 adds a row of the sheet for each moment of the clip under the rest pose’s. It also checks the whole clip 30 times a second for a forearm, hand, shin or foot more than 1 cm inside the head’s or the torso’s faces, naming the limb, the seconds and how deep. The upper limbs that sink into the body round their joints follow on one line after. A rig whose bones name no head, torso and limb is said to be unchecked. A file without the clip stands at rest, and the output says so.

    A .blend is refused: export it as a .glb first. It refuses an angle asked twice, a reference whose image holds fewer figures than its angles, a moment past a clip’s end, and a sheet past 64 tiles. When Chromium cannot download, or a page fails, it prints the rest and says why there is no sheet. --json carries the numbers as comparisons and crossings.

  • spawnite model avatar <file> turns a character rigged by Meshy into a VRM 1.0 avatar that plays every VRMA clip. It maps Meshy’s 22 humanoid bones onto the VRM humanoid, stands the skeleton in a T-pose facing +Z with the palms down, binds the mesh in that pose, bakes Meshy’s centimetre Armature into metres and leaves the clips out. It writes <file>.vrm beside the GLB, or --output. A rig that lacks one of Meshy’s bones is refused with each missing bone’s name.

  • spawnite sound check <files...> reads sound files with the installed ffmpeg, in any format it decodes, and prints each file’s length, peak, RMS loudness, and the level and spectral flatness of its quietest tenth of 46 ms frames. A sound of 1 s or more carries a constant noise, such as a recording’s hiss, when that tenth is louder than −65 dBFS and at a flatness of 0.6 or more; a shorter one is not judged, since its quietest tenth is its own decay. Assets says how the numbers were set. With no ffmpeg on the PATH, it refuses with the command that installs it.

  • spawnite build builds the game’s bundle, the version as the store keeps it, into the game’s dist/bundle, or the folder --out names, emptied first, and sends nothing. The bundle is laid out as the store keeps it:

    • page/, the page, with the engine and its peers left for an import map at an engine release
    • server/, the server half
    • manifest.json, which names the engine release, when the game plays in a room and its player cap, read from the spawnite.room in its package.json, and every other file by its size and SHA-256

    It builds against the release in the installed engine’s dist-browser, or the release folder --engine names. It fails where either half imports something the release lacks, naming the file, the import and the release.

    It prints, in order:

    • the bundle’s hash, the SHA-256 of its files’ sha256sum listing
    • each half’s size
    • Game code, the gzipped size of the game’s own code a player downloads before the game starts
    • the heaviest of the files a player downloads then, each from the page or the release, and a release file with the packages it holds
    • the --top heaviest modules of the game’s own code by rendered size, 10 by default
    • last, whether the bundle is within the upload’s limits, or each finding spawnite upload would refuse it for
    • then each warning the upload would print

    The warnings include one past 500 KB of game code gzipped, since a first-time player on a phone waits a second or two longer. They also include one that names each file the page’s HTML, stylesheets or scripts load from another site, since a published game’s page loads files from its own origin, the engine’s address and the shared assets site alone. --json carries the manifest, the game’s code bytes as code.gameBytes, the engine’s as code.engineBytes, every file under code.files, the findings as refusals and the warnings as warnings too. A bundle past a limit or a warning still builds, and the command exits 0. spawnite upload and spawnite publish name the game code in their Built the bundle line and print its warning with the upload’s.

    The pin lives in the project’s package.json as "spawnite": { "engine": { "pin": "exact" } }, which every build reads, upload’s and publish’s too. A pinned version keeps the release it is built against, and gets no engine fixes, when the platform serves a later one. An unpinned one runs the release the platform serves. --pin writes the pin into package.json and builds pinned; --no-pin removes it and builds unpinned. With neither flag, any other value under spawnite.engine is refused before the build; either flag replaces it. It names each shared file, from @spawnite/assets or the engine, by its URL on https://assets.spawnite.com and copies none, as a published page loads it. It sends nothing to check that the site serves it, so it runs offline.

    --serve <port> then serves the page at / and the release at /_engine/<release>/, until stopped. The site serves the release from the engine’s address instead, so spawnite play join --url can join it to a room. spawnite build --server builds only the server half a room runs: each scene under src/scenes as one .mjs module named for its file, with the engine and its peers left for the room’s own copy and no asset written. It writes them into the game’s dist-server, emptied first, or the folder --out names, where nothing is removed. Beside them it writes build.json, the game’s build, which a room on the half compares a joining page’s with: copy it with the modules. It prints the half’s bytes and each file. Publishing says more.

  • spawnite simulate --scene <name> --seconds <n> runs a scene in Node with no browser and no GPU, and prints the world’s dump with its hash and the steps’ wall time. --until <expression> steps until the expression holds, over the same entities, count, has, player and steps the playtest’s step reads, within --budget seconds of game time. It exits 1 when it did not. --keys w,a holds keys down for every step; w, a, s, d and the arrows steer the player’s character, and Space jumps it once. A key bound to a command, such as a dash on F, sends the command once, through her character. The run joins her player before the scene mounts, as a page played alone does, so the scene’s <CharacterSpawn> hands her her character.

    The command loads the engine and the scene’s file, src/scenes/<Name>.tsx, through the game’s own Vite. It mounts the component the file exports under that name through the engine’s headless mount, so nothing draws, no model or sound loads and no UI mounts. The components render between calls, not between steps, so a modal that opens on a step, a round screen for one, holds no step up: the round’s state is in the dump. Five seconds of game time take milliseconds, and two runs on one input print one hash.

    --profile times each system of the step and prints, slowest first, its mean and worst milliseconds per step over the last 60 steps; --json carries them as profile, and the hash is the same with it or without. --fields transform,wallet.coins prints only the traits and fields it names, and the entities that hold one, in place of the whole dump, with a line naming each field no entity holds; --json carries the same narrowed entities. --ai prints the AI tree after the steps in place of the dump: each described entity in words, seen from the player or from the entity row --subject <id|name> names, as a JSON list with one node a line. --json carries it as ai beside the dump.

    --save <file> starts the world from a save file, as a page loads a player’s save: the world’s saves and the persisted stores restore before the scene mounts, and her character’s as she spawns. A file the game would refuse stops the command with the part that failed.

  • spawnite play install downloads the Chromium the playtest drives, for the Playwright version the cli runs, into Playwright’s folder, which every game on the machine shares. Every command that opens Chromium downloads it the first time it finds none, with the progress on stderr, so run this one only to fetch it ahead, such as before going offline, or again after a download failed.

  • spawnite play start bakes the game’s public/assets/ folder into src/models.json, starts the game’s dev server, opens it in a headless Chromium with the world held still, and prints its URL and the devtools tree. A game with no model files and no src/models.json gets none. For a game whose package.json names a spawnite.room, the command also starts the game’s room on a free port, with the rest of the environment its env sets, such as MAX_PLAYERS, and passes that address in the page’s room parameter. It waits until the tree holds the player’s character the room streams before it prints the tree. It fails when she has not shown within 90 s. The room, the dev server and the browser start side by side, and the page loads while the room starts.

    The browser keeps the shaders it compiled for the game’s last session under the game’s node_modules/.cache/game/play/profile and nothing else of its profile, so a game’s saved storage never carries from one session to the next. The printed URL carries the room’s address. --scene <name> goes on from the game’s start scene to the scene registered under that name, as a button that opens it would, and prints that scene’s tree. Every session opens held and prints where its clock stands: a game’s world before its first step, or a room game’s room, which the session holds with its page once the page has joined. The room is started with ROOM_DIRECTOR=1, which lets the cli hold its clock.

    --bots <n> adds the room’s bots, as spawnite play room runs them, which play until the session stops. The page is a player too, so a game that waits on every player, such as a run that starts once each is ready, waits on it. --page-bot has the page’s own character play the part the scene’s bot plays for a room bot: the words a player’s keys and menus send, while its walk and its aim stay the agent’s. With --pages, each page’s character does. --pages <n> opens more players’ pages in the room, the session’s own among them, each a tab of its own in the session’s browser that joins before the bots and holds and steps with the room. --page <n> on screenshot, dump, eval, state and look reads the nth. --invulnerable spares every player’s character, and --room-env NAME=VALUE sets the room’s environment.

    --latency <ms>, --jitter <ms> and --stall <ms>/<every s> play each page over a slow link to the room: the round trip, how much each message’s trip varies, and a stall now and then. They are named in the page’s address and on play state’s Room: line. --replay <replay> plays a replay back on the game’s dev page instead, with the code as it stands, and --at <moment> stands it on a moment. --device <name> opens the pages as a device Playwright names, such as 'Pixel 7' or 'iPhone 15': its screen’s size and pixels, a touch screen, and a phone’s coarse pointer. --query <parameters> adds parameters to the address of each page the session opens, such as debug for a game that mounts its devtools behind ?debug, or 'map=town&debug'. The session’s own, such as room and hold, are set over them. The replay page says how to direct a session.

    The session stops itself once it goes --idle <minutes> with no command on it, 60 by default; --idle 0 keeps it until spawnite play stop. --save <file> starts the session from a save file in place of a new game: the page’s save in a game with no rooms, and every player’s in a room game’s room. The file is handed over once, so a reload or a rejoin keeps what was played since, and nothing is written over the file; without it the session starts clean. While a session of the game that spawnite play start began runs, a second start exits 1 and names its address and who started it, because someone may be playing it or driving it. --replace stops that session and starts in its place. A session whose dev server has ended counts as none.

  • spawnite play step steps the running playtest --steps <n> fixed steps, --seconds <s> of the game’s time, or until --until <expression> holds, and leaves it paused. A room session steps its room and its page together; toward a condition, it lets the room run at its own pace and holds it on the first frame the page sees the condition hold. A replay plays on, fast toward a condition, and then plays the last half second before where it stopped again at the page’s pace, so what the page animates in its DOM stands as it stood then. The expression reads entities, count, has, player and steps, and the engine’s own camera, loop, cursor and room, as spawnite play state prints them. In a replay, it also reads at, the moment the page stands on in seconds, such as --until 'at >= 58.55'. A pausing modal stops it early, and the command names the modal. It exits 1 when the condition did not hold, and says so when the round never started, naming the input that starts it.

    --keys KeyW,MouseLeft holds controls down while it steps, any the input layer names, below, and a control held already stays held after. --press <control@seconds> lands inputs on moments of the step, control@seconds a press and control@from-to a hold, in seconds of the step’s game time, such as --press MouseLeft@0-0.5,KeyR@1, the session held between. --sample <expression> reads the expression on each frame the page draws, as spawnite play eval reads one, and prints each frame’s value beside the world’s step and the canvas’s clock, such as --seconds 0.8 --sample camera.pitch for a recoil’s curve. A room session runs with its page at real time for it. A replay plays on with the player’s input on top of what it recorded. Your first game walks through a playtest.

  • spawnite play record start and spawnite play record stop record a video clip of the session’s page, as the dev tools’ Record button does, into the game’s .spawnite/clips: an H.264 MP4 with its stills, contact sheet and clip.json. Between them, any play command moves the world, such as spawnite play step --seconds 4 --press KeyW@0-3. spawnite play record --seconds <s> records that much of the game’s time in one go, with --press as spawnite play step takes it.

    The clip runs on the world’s steps, one frame a step at 1/60 s, so it shows the game’s time with no pause while a command waits, and drops no frame: a held world waits for the encoder. Its time is not the replay’s, so it keeps no replay window. Holding the world pauses the clip: a held session records nothing between commands, and spawnite play pause stops a running one’s clip until the next step or resume. --shape 9:16 letterboxes the page to a tall clip; 16:9 is the default. Stop prints the clip as spawnite clip show does. A page that plays a replay back records none.

  • spawnite play pause holds the session where it stands: a game’s world, a room game’s room with its page, or a replay’s playback.

  • spawnite play resume runs the session on, at --speed <factor> of real time, 1 by default.

  • spawnite play seek <moment> stands the session on a moment, held: seconds, a mark such as m1, a mark and seconds such as m1-2, or now and seconds such as now-5. A replay goes back as well as on, stands her character where its keyframe had her, and says when her character stands more than a centimetre from where the recording’s keyframes had her. A room session opens its page’s own replay in a second page, and spawnite play seek live goes back to the room.

  • spawnite play mark marks the moment on the page’s own replay, as F8 does.

  • spawnite play press <controls...> presses each control and lets it go, in order, on the page the session shows, as a player does. A control is one of these:

    • a key, by KeyboardEvent.code or Playwright’s name, such as KeyW, Digit1, Space or Shift
    • MouseLeft, MouseRight or MouseMiddle
    • a standard pad’s PadA to PadHome
    • a finger, Touch1 to Touch10, which lands at --at x,y

    Shift+KeyW is a chord. --hold <seconds> keeps each down that long in the game’s time, which a held session steps. Without it a control goes down and up at once, which a game that reads a control held each frame, such as a trigger, misses. --at x,y presses a mouse button at a point of the page, in its pixels. --on <subject> presses on an entity where it shows nearest the middle of what shows, as spawnite play see reads it, such as spawnite play press MouseLeft --on '[npc.name=Mira]' to walk to an NPC and talk. A subject that picks several entities, or one that shows nothing, fails saying why.

    While the engine holds the cursor captured, which it does with no pointer lock in an automated browser, the mouse reaches the canvas at its middle, as a pointer lock delivers it. Keys, and the mouse over a free cursor, are the browser’s own trusted input; a finger is Chromium’s touch input; a pad is a standard pad the page’s navigator.getGamepads() returns. spawnite play press Digit1 takes a card where a game binds 1 to one. It prints what is held after.

  • spawnite play hold <controls...> holds each control down until spawnite play release lets it go, across later commands, so a step or a resume plays with it held.

  • spawnite play release [controls...] lets each control go, or with none everything held, the last held first.

  • spawnite play move <control> moves what moves:

    • Mouse --by x,y, which turns the camera while the cursor is captured, or --to x,y for a free cursor
    • Wheel --by y, 100 a notch down
    • PadLeftStick or PadRightStick --to x,y, each from -1 to 1
    • PadLT or PadRT --to a pull from 0 to 1
    • a finger held, --to or --by
  • spawnite play see [subject] prints what the player’s camera sees of each entity a subject picks, or of every entity it draws, most showing first. For each, it prints the share of it that shows with the rest of the scene in front, its pixels showing and in view, the box they fill on the page and its share of the screen. --json also carries point, where it shows nearest the middle of what shows, which press --on presses. It also carries coveredBy, the element the page draws over that point, such as a dialog, which takes a click there and makes press --on refuse. --part <name> narrows each to the objects inside its drawn object whose name holds the text, such as the gun in a character’s hand. --json carries it as sightings.

  • spawnite play aim prints where the crosshair points: the first thing the camera’s ray through the canvas’s middle meets, by the engine’s own reads of the targets it draws and of cover. It also prints each shot the page sent its room, the newest twenty, beside where the crosshair pointed as it left, with how far the shot’s line passed that point in metres and degrees.

  • spawnite play set <subject> <path> <value> writes a trait’s value onto the entities a subject names, a dump key or a selector such as '[controlled]', so a check starts from the state it needs: spawnite play set '[controlled]' warden.health 0. The path names the trait as spawnite play dump files it, or a field of it, and in a room also as its module exports it, such as SiegeStateTrait; the value is JSON, null takes the trait off, and anything else is text. A room takes the write between two steps and records it, so spawnite replay rerun and check take it again, and a held room steps one move so its page draws it. A walker whose transform it writes stands there at once: spawnite map query --at x,z reads the ground’s height there. An entity the page keeps alone, whose dump key starts local:, such as its camera, is written on the page, so spawnite play set '[camera]' camera.fov 60 tries a lens with no edit. A replay refuses one.

  • spawnite play from-here --at x,z stands the game’s player on the ground at a point and plays from there, as the map editor’s Play from here does. She stands at the ground’s height under the point, facing the way the camera looks, out of any prop the point stands inside. On a map the scene shows she is the player who stands there; --map <name> names another, which opens in its preview, where the game’s own player spawns. The session stays held, so spawnite play step plays it. A room places its players and refuses; spawnite play set '[controlled]' transform.position '[x,y,z]' has it move her.

  • spawnite play look turns the player’s camera on the page the session shows, to --yaw and --pitch in the degrees spawnite play state reads, or by --turn degrees of yaw, and prints where it looks.

  • spawnite play screenshot shoots the page as a PNG, saves it under the game’s .spawnite/shots beside the devtools page’s shots, and prints its file: what the player sees, the HUD among it, or with --no-hud one render of the game’s camera. --until steps the world first, and --view, --angle, --around and --zoom place the camera. --around also takes an entity by its dump key, by a selector of what it is such as '[monster.kind=colossus]', or by its row’s name in the tree, framed where it stands. --eye x,y,z --target x,y,z places a free camera, --orbit <n> takes n shots round the subject, and --framing shoulder, top or wide frames the player’s character or a subject for you. --size sets the page’s size for the shot. A list such as --size 1920x1080,1280x720,960x540 shoots at each in turn in one call, with the world held between them. What the page animates on its own clock moves on a few hundred milliseconds from one size to the next.

    --out <file> writes the shot there rather than under .spawnite/shots; several shots add their place in an orbit and their size to its name, such as offer-1280x720.png. --width writes each file at most that wide. Each shot from a camera other than the player’s own prints the camera it resolved to: its lens, its eye and its target. It also prints which world direction is up and right on the image, such as screen up = -z (north), right = +x (east) from --angle top. A shot from any camera but the player’s own, and --no-hud, leaves out the screen overlays her view draws in the canvas, such as a hurt vignette, and --overlays keeps them.

  • spawnite play timeline [controls...] presses the controls, or with --clip <name> plays a clip the game registered on the player’s character and prints the registerClip line to write its mark into. It then shoots the page every --every seconds of the game’s time, 0.1 by default, for --seconds, 2 by default, from the press’s own frame. It tiles the frames into one PNG under .spawnite/shots, or at --out, each under its time. The result is a sheet a person reads to pick the frame an effect should land on, such as a cast’s release, and the second to give the ability.

    Each frame is cropped round --crop, the player’s character without it, where the page shows it on the first frame, as spawnite play see finds it; --crop none keeps the whole page, shrunk to the cell. --cell 300x420 sizes each frame’s picture and --columns 5 the row. The MCP’s timeline tool is the same for an agent, with keys or clip. The gap snaps to whole steps, 60 a second, and the labels count the gap stepped. The session is held for the sheet, so each frame is exact to its step, and a session that ran runs on after; a pausing modal that opens ends the sheet at the frames shot before it.

  • spawnite play dump prints the world: every entity’s traits, by entity id, then, as views, what each view exposes of its own state through the engine’s useInspect. --where '[monster.kind=colossus]' prints only the entities a selector picks, a trait’s field with a value or [controlled] for a trait, and takes a key or a row’s name too, as --around does. --fields transform,npc.line prints only the traits and fields it names, and the entities that hold one, as gh --json narrows a reply, with a line naming each field no entity holds.

  • spawnite play eval <expression> prints the value of a JavaScript expression on the page the session shows, as a browser’s console does, and leaves the session where it stands: a room session’s past where a seek opened one. It reads the page’s own globals, document among them. As a --until condition does, it also reads entities, count, has and player over the world and the engine’s camera, loop, cursor and room. For example, spawnite play eval "document.querySelector('[role=meter]').getAttribute('aria-valuenow')" reads a HUD’s value after a seek, and spawnite play eval "player().health" reads the world.

    A function is called and a promise awaited, for 30 s at most, or --timeout <seconds>, since every later command on the session waits for it. A string prints as it is, an element as its HTML, and any other value as JSON; --json carries it as value, left out where the expression returns undefined. It also reads views, what each view exposes through useInspect. An expression that throws exits 1 with the page’s error.

    On an engine that has it, the expression reads the engine’s whole scope, which PlayScope in @spawnite/engine/devtools types:

    • world and find(subject), for the Koota world and the entities a subject picks
    • three, for React Three Fiber’s state
    • engine(), for the engine’s module, whose traits read an entity find picks, as find('[npc]')[0].get(engine().Transform)
    • load(path), for a promise of one of the game’s modules
    • see and aim
    • the director’s input and time

    For example, spawnite play eval "async () => { await input.hold('MouseLeft'); await time.step({ seconds: 0.5 }); return input.release('MouseLeft'); }" holds the trigger. --file <path> runs a script’s default export with the scope in place of an expression, a module the game’s dev server compiles, whose imports are the game’s own.

  • spawnite play ai prints the running playtest’s AI tree in the same form, from the player or from --subject <id|name>; --json carries it as ai.

  • spawnite play wiki [query] prints the running playtest’s own wiki as JSON, { overviewPath, kinds, entries }: the parts of the game the devtools’ Wiki tab lists, such as its behaviours, traits and systems. An entry carries pagePath, its page’s path from the game’s folder, where that file exists. That is the game’s own page for its own entry, such as wiki/scene/run.md, and the page the installed engine ships for an engine entry, such as node_modules/@spawnite/engine/dist/wiki/engine/behaviours/stats.md. overviewPath is wiki/index.md where the overview exists. An entry’s properties hold its own data as its registration holds it, such as an item’s definition, and each of its links carries a relation where one says how it connects, such as changes from an item to the trait its use changes. query keeps the entries whose name, kind or description holds it, ignoring case, and --kind <kind> keeps one kind; a kind the game lacks fails, naming the game’s kinds. --json carries it as wiki.

  • spawnite play compare shoots each shot a game’s test/shots.json names, stepping the world to the shot’s step from the page’s load, and compares it with the accepted WebP in test/shots. It prints each shot’s share of pixels whose any channel differs by more than 8 of 255, pass or fail against the shot’s share, 1% without one, and the difference image it wrote to the temp folder, and exits 1 when a shot fails. --accept writes the shots as the accepted set. A game with no test/shots.json is told the JSON to write there. The MCP’s compare_shots runs the same comparison for an agent. The performance page describes the file and when the accepted set holds.

  • spawnite play stats prints what the running playtest costs to draw: the draw calls, triangles, points and lines of one frame, summed over every render call in it. It also prints the geometries and textures the renderer holds, and the mean and worst frame time over 60 drawn frames; --json carries it as stats. The page is the development build, whose dev build, devtools and replay recorder add to each frame, several times over for a busy scene, so the output says so and points to spawnite play profile, which times the production build.

  • spawnite play console prints the page’s console errors and warnings and its uncaught errors since the session started, or since the last console, each led by its level, as the MCP’s read_console reads them. A line repeated in a row is folded into one with its count. It reads a page whose scene never mounted too. --json carries the lines as console.

  • spawnite play actions lists the game’s playtest actions by name, the steps its devtools panels run, such as a quick start past the title, which a game passes to <Devtools playtestActions={...} />.

  • spawnite play action <name> runs one of the game’s playtest actions as its button does, whatever the name’s case, and prints what the action says it did, such as No damage on.. An action that refuses, such as a quick start mid-run, exits 1 with why. The world stays where it stands: step it to see what the action set going.

  • spawnite play state prints the engine’s own state on the running playtest, in this order:

    • the camera’s position, yaw, pitch, lens and how far it has turned since the loop mounted
    • the frame loop’s mode, hold, speed, steps, frames and clock
    • whether the cursor is captured
    • where the page stands with its room
    • the engine build the page loaded, and whether its dev server now serves a newer one
    • what the player holds: the controls down, the free cursor and each finger

    --json carries it as state, the engine build as engine, and what is held as input. The devtools page lists each field.

  • spawnite play profile serves the game’s build with vite preview and plays it for --seconds, 20 by default, in a headless Chromium at 1920x1080. It opens no window unless --headed takes the screen or --window places one. It walks, jumps and drags the camera as a player does. It prints the frame rate, the CPU milliseconds a frame, and each function’s inclusive milliseconds a frame, named by the source file and line the build’s source map gives. It also prints the frame times and how many took longer than --budget, 7.5 ms by default.

    --gpu with --no-vsync reads the GPU’s time from a timer query. --render counts each frame’s draws, triangles and instances per pass, and --skip <class> drops one class of draw to price it. --alloc prints the garbage a frame makes by call site, and --trace counts the garbage collections. --quality <level> plays at a quality level of its own, and --no-cpu leaves the sampler off. It also prints the step’s milliseconds by system over the recording’s last 60 steps, as spawnite simulate --profile does. Build the game with --minify=false --sourcemap=true first.

    --url profiles a page that is already served. --dev serves the game’s dev server in place of its build, and prints each warning of its replay among the notes, such as a mark that protects nothing. For a game whose package.json names a spawnite.room, the command also starts the game’s room on a free port, with the rest of the environment its env sets. It passes that address in the page’s room parameter, for the build and --dev alike. --url starts no room. --query <parameters> adds parameters to the page’s address, such as replay=off. --press <key@seconds> presses a key at a second of the recording, such as F8@30, or holds it from one second to another, such as KeyW@2-5, and marks each press on the timeline.

    Every run also keeps a timeline from the page’s first frame. It prints each frame over --hitch milliseconds, 50 by default, with what it waited on: the shader compiles by material, the draw, the script by function and the garbage collection. It prints the engine’s or the game’s performance.mark before that frame. It also prints the bytes the page fetched and the requests it made before its loading screen lifted, which is what a player waits for, and by the end of the run. With --until, on a page with no loading screen, it prints the downloads before the expression held: the wait.

    --drive idle plays nothing and --drive bot plays from what the engine knows. --map-camera <zoom> records under the map editor’s camera flown out to that zoom of the whole map’s frame, 3 its own landing and 6 twice as close. It serves the dev page and plays nothing unless --drive says so. --invulnerable spares every player’s character in the game’s room.

    Give --query more than once, or --repeat <n>, and the command sweeps: it runs each --query configuration in turn, n rounds, into a folder of its own under --out. It then compares each configuration with the first, as profile compare does. --function <name> times one function a call: a path on the page’s window, such as createImageBitmap, is wrapped and each call timed. Any other name is read from V8’s sampler, with its calls counted by V8’s coverage.

    Where the page exposes the engine’s state, the run also prints how far the camera turned over the recording and the steps and frames the loop took, and --until reads the engine’s camera, loop, cursor and room. The room the command starts logs a heartbeat a second, and the output prints its steps over the recording beside the page’s frames. It prints how many steps it ran, a step’s mean and longest milliseconds, each step timed without a recording room’s replay checkpoint, that room’s longest checkpoint, and its memory. The run’s file keeps each heartbeat. --room-env NAME=VALUE sets the room’s environment over the target’s, such as ROOM_REPLAY=0, once for each setting, with no edit to package.json. With --dev, --latency, --jitter and --stall play the page over a slow link; a build carries none.

    --until <expression> walks until the expression holds in the page and then records, within --until-seconds, 120 by default. --bots <n> plays the room’s bots in the game’s own room beside the page, as spawnite play room plays them, from before the page loads until the recording ends. --no-jump walks without pressing Space, and --scale <n> sets the headless page’s device scale. --har <path> writes every request the page made, with its URL and sizes and no response bodies, to a HAR file, which names the files behind those sums. A sweep ignores the path and writes each run’s HAR beside its run file, as <round>.har. --probe lists the first recorded frame’s draws by pass and program.

    --check profiles the build in one fixed configuration, walking and then with a still camera, prints each count beside the game’s test/profile-baseline.json, and exits 1 when a count rose past it. It also times a fixed spin before and after the runs. It appends a row of each run’s timings, over 6.94 ms or, for a run pinned to 60 fps, the frames that missed the pin, with the counts, to test/profile-history.jsonl. When a spin took more than 15% past the median of the spins there on the same CPU, it appends no row and prints Under load.

    It prints each run’s timings beside the previous row’s on the same machine, and a pinned run that held under 95% of its rate leaves garbageBytesPerFrame out as inconclusive. When a count rose, it names the first-parent merges since that row that touch the game, packages/engine or packages/assets. A check waits while another holds its lock, and gives up after ten minutes. --update-baseline writes the baseline and no history row. --update-baseline=<count>,<count> writes only the counts it names, and a baseline with no fps gets 60. The run writes itself to a JSON file. The frame page says how to read it, and Finding hitches the timeline.

  • spawnite play profile report <runs...> prints a run’s result again from its file, over --from and --to seconds or with another hitch threshold. Several runs, or a folder of them, print as a series: each reading’s median, its lowest and highest, and every run’s value. A run that started the game’s room also reads the room’s steps over the whole run: a step’s mean and longest milliseconds, as a recording room timed each step without its replay checkpoint, and its longest checkpoint. A series and a comparison also carry the bytes fetched and the requests made before the lift and by the end of the run, where the runs kept downloads. Where no run lifted and every run used --until, they carry them before the recording started. Bytes move past 1,024.

  • spawnite play profile compare <before> <after> sets two runs’ results side by side and marks each measure better or worse past the machine’s noise. Each side may be a folder of runs, read as its median, lowest and highest, and a measure moves only where the two sides’ runs do not overlap. The room’s steps sit beside the page’s frames where both sides started the game’s room, and a step’s time moves past 0.1 ms and a fifth. The bytes and requests the page fetched sit beside them, a change of under 1,024 bytes counting as the same, and a rise in the bytes before the lift is named under what got worse.

  • spawnite play room plays the game’s own room with bots and no browser, once for each bot count and seed, each run in a fresh room on a free port with the rest of the environment its spawnite.room sets. Each bot joins over the room’s WebSocket as a page does, turns to the nearest living entity with health, walks to it round walls along the room’s navmesh or strafes beside it, and walks a circle with none in sight. A scene that exports a bot beside its component names the weapon each fires and the game’s own messages each sends, such as the word that starts a run; Multiplayer says how.

    --bots takes a count from 1 to 12, a list such as 1,2,4 or a range such as 1-4, and --seed a seed, a list or a range, from which the room’s world draws its random numbers; each pair is one run. The room holds its clock until every bot has joined, then runs at --speed, up to 16 times real time, until the scene’s timeline says the run is over or --seconds of play are up: 60, or 1800 where the timeline says when a run is over. The command prints what each bot did and a table a run, with a row for each part the timeline’s parts name, such as each wave’s fight and breather. Each row holds when the part started and how long it lasted, each measure the timeline records, a step’s mean and longest milliseconds, and the bytes a page received a second. Then it prints a table over the runs of each bot count. Measuring runs says how to declare a timeline and read the tables.

    The runs go to a file as each ends, --out or spawnite/room-runs in the system’s temp folder, which keeps the newest 20, and spawnite play room report <file> prints them again. It also names the run each room recorded, which spawnite replay log, state, export and check read with --run <run> and no browser. --invulnerable spares every player’s character, and --room-env NAME=VALUE sets the room’s environment over the target’s. --latency, --jitter and --stall play each bot over a slow link, in the game’s time. The room runs unwatched, so an edit to the game’s files never ends a run.

  • spawnite play room report <file> prints the runs a file of spawnite play room holds again: each run’s table, and the tables over the runs of each bot count, summed afresh from the timeline and the heartbeats the file keeps.

  • spawnite play join [room] joins players’ pages to a room and prints what each saw: welcomed as its character, refused with the room’s close code and its reason read out, such as a full room, or no answer within --join-seconds, 90 by default. The room is the address given, ws:// or wss://, such as a room a person or a machine started. Without one, it is the game’s own room, started fresh on a free port with a heartbeat a second and no restart on an edit, and --room-env over them. For its own room, the output adds the room’s build, its last heartbeat, its peak memory and its timeline.

    The pages are the game’s build, served with vite preview, which names its build as it joins, so the line names a room’s other build beside the page’s. --dev serves the dev server’s page, which names none, and --url opens a page already served, such as the deployed game. --pages <n>, 1 to 12, opens that many, one after another, each headless in a browser context of its own so it joins as a player of its own. --press <control@seconds> lands controls on each welcomed page from the last page’s join, as step’s --press names them. --seconds plays the pages on, --until <expression> plays until the expression holds on each welcomed page within --until-seconds, and --screenshot [file] shoots every page as the command ends.

    --seat [game] joins a room the backend runs, such as production’s, which refuses a page with no seat ticket. It takes the signed-in account’s seat in a room of the game through the backend’s rooms.join. The game is named by its id or by a link to it. An unlisted game’s link carries its key, which join needs from any account but the game’s own. Without one, it takes the game the project’s package.json names, as the app does. It joins the room join answered, so it takes no room address beside it, and adds the ticket to the page’s join message. It opens one page, since an account holds one seat in a game. Nothing holds the room: the pages play live. It exits 1 when --until did not hold. Joining a room says how to read it.

  • spawnite play list lists the playtests running for the games under the folder, or the game --project names. For each, it shows the game’s address, a room game’s room, what started it and so what stops it, and when it started and was last used. What started it is spawnite play start, which spawnite play stop ends, or an MCP server’s start_playtest, which its stop_playtest ends. spawnite play start names the others running under the folder too, so an agent notices one it forgot to stop. --json carries them as playtests.

  • spawnite play stop closes the playtest’s browser, its dev server and its room. Every other command ends the Vite, room, bots and browser it starts when it ends, killed or not; the processes page says how.

  • spawnite symbolicate [file] names the engine’s frames in a stack, read from the file or from stdin. The engine’s core is minified, so its frames carry no names. The command sends each engine frame, and nothing else of the stack, to the platform. The platform resolves it from the release’s private frame index to the public engine call it ran in, never to a source line. The command prints the stack with those frames named, each run of them ending in the call the game made, such as The game called startCooldown, which failed inside the engine, from: at cool (src/systems/dash.ts:12:9).

    It finds an engine frame as follows:

    • a frame of the engine’s library build, by @spawnite/engine/dist/ in its path, at the version the game installed or --release <version>
    • a frame of a published page’s release, by its address
    • a frame of the dev server’s prebundled engine, through the map Vite wrote beside it

    Each engine frame prints as [engine, in <export>], a frame inside that public export, whoever called it, or [engine, its own work]; the summary line is the one that names the call the game made. The game’s own frames stay as they are. What it sends of each engine frame is the release, the file’s name inside the engine’s package, and the line and column. - reads stdin, --project names the game, and --json answers calls, each the game’s call and the frame it came from, and frames. It needs spawnite login, within a limit of 60 stacks an hour.

  • spawnite map place --map <name> --props <json> writes named props into src/maps/<name>.json. Each one is upserted, and null removes it. { "asset": id } downloads a shared asset, as spawnite add asset does, stands it as a model, and prints the lines to paste that spawnite add asset would print, such as the import of the src/models.ts a download wrote. It changes only the props key, through the writer the map editor uses, so a stroke written at the same moment stays. The whole map must parse before the file is written, and a refusal names the prop and the field, such as props.tower.size. It prints where the engine stands each placed prop, then what spawnite map check found, which now also lists a prop off the map and a model path that is no file in the game’s public/ folder; a registered model name is not checked. --assets works as it does for spawnite add asset.
  • spawnite wiki list lists the wiki’s guide pages by folder: each one’s address for wiki read, its title, what it covers, and what a creator asks for that it answers, from the page’s useCases. The API reference, one page per export, is not listed; wiki search finds it. --json gives each page’s page, title, description and useCases under pages.
  • spawnite wiki search <query> finds the pages that answer the query, best first, at most 10, each with the address of the section that matched best and a line from it. A page titled exactly the query comes first, its guide page before its reference page. --scope guides, reference or all, the default, says which pages to search. --json gives each match’s address, title, heading and line under matches.
  • spawnite wiki read <page> prints one page’s markdown, or one section of it when the address ends in #anchor, as wiki search gives them; the hosted wiki’s full address works too. A page or section past 60,000 characters reads in parts, --part 2 for the second, and the reply says which part it is and how many there are. An unknown page refuses with the guide pages, an unknown anchor with the page’s sections, and --json carries page, title, heading, part, parts, markdown and sections.
  • The three are what the MCP’s list_wiki, search_wiki and read_wiki tools do, over the same code, so an agent without the MCP reads the wiki the same way. SPAWNITE_WIKI points them at another wiki, such as a folder holding pages.json.

The playtest the MCP server drives, a game’s own Vite dev server and a headless Chromium page on it, lives here too, as an export the server calls rather than a command.

The registry is the JSON the wiki serves under /r. Each game template and each feature is one item, and each item names the JSON Schema the wiki serves at /schema/registry-item.json, built from the registry’s shapes in @spawnite/schema. --registry points a command at another base URL, or at a folder of that JSON, and SPAWNITE_REGISTRY sets its default.