@onceworlds/cli
v0.14.0
Published
Deploy games to Onceworlds.
Readme
@onceworlds/cli
Publishes browser games to Onceworlds, where every game gets multiplayer, saves, player accounts and a shared overlay (sound, chat, players) with no server code.
Deploy from GitHub Actions
Deploys run from GitHub Actions: GitHub vouches for the repo, so there are no API
keys. The easiest setup is to import the repo at https://onceworlds.com/upload?new=github,
which adds onceworlds.json and .github/workflows/onceworlds.yml for you (that
workflow also builds with npm, pnpm, yarn or bun when there's a build script). To write
the same files into a folder before it is on GitHub, run npx @onceworlds/cli init there.
By hand, the minimum is:
name: Deploy to Onceworlds
on:
push:
branches: ['**']
permissions:
contents: read
id-token: write # proves which repo is deploying; no API keys needed
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- run: npx --yes @onceworlds/cli@latest deployPushes to the production branch (main unless you change it in the dashboard or
onceworlds.json) go live at onceworlds.com/play/<slug>; other branches get a
preview URL in the job log. Log in to https://onceworlds.com with a GitHub
account that has write access to the repo before the first deploy. Install the
Onceworlds GitHub App on that repo to use the dashboard and open the game while
private. If a deploy fails, the dashboard shows why.
New games start private: only accounts with write access to the repo can play them. To share one, open it in the dashboard and pick Unlisted (anyone with the link) or Public (also listed on Onceworlds).
Project file
onceworlds.json at the root of the repo:
{
"slug": "kart-rush",
"name": "Kart Rush",
"description": "Race friends around twisty tracks.",
"dir": ".",
"genre": "Racing",
"icon": "store/icon.png",
"thumbnails": ["store/thumb-1.png", "store/thumb-2.png"],
"badges": [{ "id": "first-win", "name": "Top Step", "description": "Win a race." }]
}engine ("1", "1.2" or an exact "1.2.3") makes a game run on the engine the platform hosts: it adds an import map
so import { createGame } from '@onceworlds/engine' works in a page with no build step. "1" is the newest 1.x, which gets
fixes without a redeploy; an exact version never changes. A deploy fails, saying what is available, for a version that doesn't exist.
dir is the folder to publish (for example dist after a build); it must contain
index.html. productionBranch (default main) picks the branch that goes live;
the dashboard can change it too, and the file wins again only when this value changes.
controls ([{ "keys": ["W", "A", "S", "D"], "action": "Move" }], up to 12) fill "How to play" in the platform
menu, folded until a player opens it (help is still accepted but no longer shown). The rest fills in the game page: genre, a square icon (at least 128×128),
16:9 thumbnails (at least 480 wide; the first is the cover) and badges (id,
name, description and an optional square icon). A game page holds at most 5
thumbnails and videos together, and a game at most 50 badges. Images are PNG, JPEG, WebP
or GIF up to 5 MB, with paths relative to onceworlds.json. Live deploys apply
these fields only when they change, so edits made in the dashboard last until then.
Make the art before the first deploy: players see the icon and thumbnails before the game, and a game
without them gets a plain colored placeholder. The icon is a simple, wordless 512×512 square; the thumbnails
are three to five 1280×720 pictures of the game being played, the best one first (it is the cover). Capture them
from the running game and commit them under store/. onceworlds doctor says which fields are missing.
Commands
onceworlds new [folder]starts a game on the web platform and the SDK:index.html,game.json the stack you pick (--stack canvas, the default with no library,three,pixiorphaser, loaded from a CDN by an import map),rules.jswith tests that run in Node (npm test:node --test, nothing to install), anonceworlds.jsonfor your game (--name,--slug,--description; the folder's name by default), the platform guide asAGENTS.mdandCLAUDE.md, aDESIGN.mdto fill in first (the hook and signature mechanic, an originality pass, the first minute, playing together, the art direction, the feel, the UI, the stack and a pre-build check), a.gitignoreand the deploy workflow. Each starting point is tiny on purpose: the platform wired in (a room joined as the page loads, you and everyone in your server, the keys and the phone's thumbstick, a title with one Play button) and nothing that decides the game.--enginemakes a blank project on the Onceworlds engine instead, an optional library (--space 3dfor 3D), with the engine guide as itsAGENTS.md.--kit <name>starts from an engine kit: a complete game of one kind (--listshows them), a reference for its plumbing (rooms, netcode, Flow, bots, tests) whose idea, look and screens are never yours; it adds its own notes asKIT.mdand its fingerprints as.onceworlds/kit.json, socheckcan note what is still the kit's.--template partyorstarterstarts from a complete game on the SDK, and--template <name>from a scene project of Onceworlds Build.onceworlds check [dir]is what to run before a deploy, for any game (canvas, three.js, PixiJS, Phaser, the DOM, the engine, a Godot or Ren'Py web export): doctor's findings, plus the hosts the platform's CSP won't let the game reach, WebSockets of its own, text too small for a phone, a second countdown under the platform's lobby, shared state written fromMath.random()or the device's clock, and aDESIGN.mdsilent on originality and the first minute. Then it plays the game on the local platform (serve's) in headless Chrome: it loads with no errors, draws, joins a room (pressing Play as a player does), starts its match from the platform's lobby, answers input, takes a second player, survives its host reloading and then leaving, fits a phone (nothing of its text under the platform's menu or touch controls, text big enough, no sideways scrolling), and keeps its own work per frame and its memory in hand: one ✓, ! or ✗ line each, with the fix. A ✗ exits with an error (--strict: a ! too);--quickskips the browser. For an engine game it also runs the game's own tests (npm test) and simulated matches played with no browser, with a reloaded host, a dropped player, the host changing (also while paused), a late joiner and garbage messages. A failing test or a match with errors exits with an error. The game's config is read fromgame.config.js(orsrc/game.config.js, or--config <file>):export const config = {...}, andexport const actionslets check compare "How to play" with the action map exactly. Needsnpm installin the game folder. Simulated players only ready up, so a game with long rounds or turn timers may not reach its results in the time check has:export const quick = ...from the same file (the game with one short round and short timers) and check plays that. A process that outlives its time limit is killed with everything it started.onceworlds posters [dir]draws eachgame.poster()scene of your latest draft (?poster=<name>) in a browser on this computer and saves the pictures intostore/(a poster name is letters, digits, dashes, underscores and dots, as in the engine), naming them inonceworlds.jsonwhen it has no icon or thumbnails.--only cover,iconpicks some.onceworlds init [dir]sets a game folder up, with the files the dashboard's import commits. It writesonceworlds.json(the link and name come frompackage.json's name or the folder's;diris your build's output folder whenpackage.jsonhas a build script, else.or the folder with yourindex.html),.github/workflows/onceworlds.yml, a.gitignore(node_modules/and the picturesshotandshot --storysave; to one you have it adds only those lines, once), and the guide for AI coding tools asAGENTS.mdwith aCLAUDE.mdthat imports it (next to aAGENTS.mdof your own it writesONCEWORLDS.mdand adds a pointer to yours). It never overwrites a file you have:--forcereplaces only what init writes, never your ownAGENTS.md,CLAUDE.mdor.gitignore.onceworlds newwrites the same workflow and.gitignoreinto every game it makes. Then commit, push, and import the repo at https://onceworlds.com/upload?new=github.onceworlds deploy [dir]uploads only the files that changed and publishes the game (the workflow runs it). It lists every file it skipped because Onceworlds can't serve it. A game's tests never go up:*.test.js,vitest.config.*and thetest/folder at the top of the game.onceworlds serve [dir]plays any game in your own browser on this computer, offline and with no login, on a local platform at http://127.0.0.1:8788/ (--port): the platform's play page (its bridge, room connection, lobby, touch controls and the corner the menu takes), its rooms (matchmaking, private servers with&server=<code>, the host role, held seats, state, private values, ready, the countdown and the match, as onceworlds.com runs them) and the real SDK with its helpers (/__onceworlds/v1/match.js,host.js,bots.js), under the platform's CSP. Saves, badges, leaderboards and game data ("data") are kept in memory while it runs. Each browser is a player, and?player=2to9are more in the same one. A game with a build script is built first and itsdirserved. Ranked ratings, parties into matchmaking,onceworlds.fetchand chat moderation need onceworlds.com. An engine game uses the installed engine (else the hosted copy this CLI ships with) and also gets its page on its own, the engine's stand-in for the platform, with--bots 3(the default) bots; any other game gets?bots=Npassed on.onceworlds dev [dir]deploys to a local Onceworlds platform (for platform development) and redeploys on every save.--players 3also prints links for players 2 and 3 (?player=N): on a local platform each is another player, so one person can fill a lobby.onceworlds doctor [dir]looks through the game's files for what games often ship with: no viewport tag, text fields under 16px (Safari zooms the page when one is focused),confirm()and other dialogs (blocked in the game frame), localStorage (gone on reload), and, for games with rooms,Date.now()for shared time and no use of the platform's lobby. For an engine game it also reads the action map and offers thecontrolsit implies:--fixwrites them intoonceworlds.jsonwhen it has none. For an installed engine game the list is the engine's own (controlsFromActionson theactionsthatgame.config.jsexports); otherwise it is read from the source text (Input.axis2d,Input.button) and may differ. It checksonceworlds.jsonby every rule a deploy applies (lengths, genres, badges, How to play, the pictures it names), in the deploy's words and with the fix, and fails as the deploy would; the rest only suggests (--strictexits with an error on likely bugs, for CI).checkdoes the same. For an engine game it also says whenDESIGN.mdis missing or has parts left empty (the originality pass included), and notes a game still close to the kit it started from (scripts unchanged, the kit's numbers, its screens). For a game on Flow it says what still looks like a kit (Flow's placeholder screens, the default theme), a 3-2-1 of the game's own beside Flow's, and a first round no different from the rest. Taste is the game's: these are warnings and notes, never a failure.onceworlds api <name or words>looks the engine up in a few lines instead of the whole reference: a signature, what it does, a component's fields or an options object's keys, and an example (api Shape2D,api feel.shake,api "camera follow";--fullfor every field with its doc).onceworlds recipe [name]prints short, tested code for a kind of game or a feature (tycoon, survival, racing, platformer, shooter, party, rpg, idle, sandbox, chase-ai, inventory, shop, dialogue, day-night), andonceworlds guide [topic]the engine guide's topics or any heading of the guide or the reference. They read the index of the engine installed in the game'snode_modules(its own version), else the platform's.onceworlds shot [dir]saves a picture of any game to look at: a moment of play (--at 20), the title screen (--phase title) or the lobby (--phase lobby), or a poster (--poster cover), at--size 1280x720by default (--size allmakes one each of 390x844, 844x390, 1280x720 and 1920x1080). A game off the engine is played on the local platform in Chrome: the title as it loads, the lobby once it joins (a Play button pressed when it waits on one), or play after the platform's Start (more local players join when its match needs them), moving about, with the platform's menu (and on a phone its touch controls) over it as players see it; the caption names errors, files the CSP refused, fonts that never loaded, symbols and text too small. A poster is the game's own?poster=page when it saysdata-ready. An engine game in 2D is drawn with no browser, from the frame the engine draws, with its text in the fonts the game's files name (its own font files, or Google Fonts, downloaded once and kept) and Sora for the rest; the caption says which it couldn't have, and that the HTML overlay (ui.overlay) needs--browser. A 3D game (or--browser) is drawn by Chrome, Chromium or Edge from a local copy of the game.--outpicks the file (defaultstore/shot.png).--storyplays the first minute as a new player lives it (the title, the lobby, the countdown, the round's goal, play at +1, +5 and +12 s, the first result), saves each frame intostore/story/and lays them out on one contact sheet,store/story.png, with the questions to ask of it.
Running onceworlds mcp gives an AI tool the same as tools: api_lookup, recipe, read_guide, search_assets (the engine's
asset library), check, screenshot (the picture comes back to the model), serve (an address for the user to play it at),
new_game, deploy_preview, playtest (local: true for the folder on the local platform) and game_status.
onceworlds playtest [dir]plays the game as several guests in headless Chrome (join, a match, reloads, drops, the host leaving, a late arrival, a phone): your latest draft, or--localthe folder on the local platform, with no login.
Games talk to the platform through the injected window.onceworlds SDK. The
starter template's AGENTS.md documents it for AI coding tools.
