# create-excalibur
## Scaffolding for ExcaliburJS projects
With NPM:
```
npx create-excalibur@latest
```

## The `ex` CLI
Installing the package globally also gives you the `ex` command:
```
npm i -g create-excalibur # installs `ex` (and an `excalibur` alias)
ex # interactive menu (same as create-excalibur)
ex create # scaffold a game from a template
ex sample # scaffold a sample project
ex inspect # download a showcase game
ex docs # search the Excalibur docs & API
ex generate # generate an actor, label, scene, resource, engine settings, material, spritesheet, or animation — or update an actor's options (alias: ex g)
ex doctor # type-aware diagnostics: actors never added to a scene, unnamed actors
ex upgrade # migrate your game to a newer Excalibur version, codemod-style (alias: ex up)
ex mcp # MCP server over stdio (docs + codegen tools for AI agents)
```
### `ex docs` — search the docs from your terminal
```
ex docs # type-as-you-search prompt
ex docs actor collision # search, pick a result, read it in the terminal
ex docs Vector.distance -1 # open the top result immediately (no picker)
ex docs actor collision --list # just print the matches + links (pipe friendly)
ex docs vector --json # machine-readable results
ex docs --help # all options
```
Search is powered by the excaliburjs.com DocSearch (Algolia) index. Pages are rendered as
markdown in the terminal with links back to the online docs; long pages open in `$PAGER`/`less`.
**Version aware:** when run inside a project, `ex docs` detects the installed `excalibur`
version (from `node_modules` or `package.json`) and renders pages from that release's docs
(`--ref v0.32.0` to override; `--ref main` for the latest).
**Offline:**
```
ex docs offline # download the docs for your Excalibur version (~1 MB) + plugin READMEs + build a local index
ex docs actor --offline # search the downloaded docs only
ex docs offline --status # what's cached and where (~/.excalibur/docs, or $EXCALIBUR_HOME)
ex docs offline --clear # remove the cache
```
When the network is unavailable, `ex docs` falls back to the offline index automatically.
**Plugins:** `ex docs offline` also indexes the `@excaliburjs/plugin-*` READMEs (Tiled, Aseprite,
LDtk, perlin, …) from npm, so plugin usage is searchable too — filter with `--kind plugin`.
### `ex doctor` — check your game for common mistakes
```bash
ex doctor # human-readable report, exits 1 when problems are found
ex doctor --json # machine-readable findings (CI friendly)
```
Type-aware diagnostics powered by your project's own TypeScript and excalibur's type
declarations (run `npm install` first). Twelve rules, each grounded in bugs found in shipped
games:
- `actor-not-added` — an Actor-derived `new` that never reaches `.add`/`.addChild`
- `unnamed-actor` — an Actor constructed without a name, harder to spot in debugging tools
- `dont-shadow-excalibur-internals` — a field like `isActive` on an Entity subclass shadows
engine state and silently kills the entity (tip: set `"noImplicitOverride": true`)
- `leaked-subscription` — `.on()` to an engine-lifetime emitter with no cleanup; handlers
compound across scene restarts
- `dead-collision-hooks` — collision handlers while the Engine has `physics: false`
- `dont-mutate-shared-graphics` — writes to cached `getAnimation()`/`getSpriteSheet()` results;
`.clone()` first
- `unknown-scene-key` — `goToScene` typos checked against the `scenes:` map
- `dont-call-lifecycle-hooks` — calling an engine lifecycle hook directly instead of letting
the engine invoke it
- `camera-pos-aliasing` — `camera.pos = actor.pos` writes through to the live vector
- `no-reserved-tags` — engine-owned `ex.*` tags added via `addTag`/`removeTag`
- `no-reserved-uniforms` — a Material/ScreenShader source declares a built-in like `u_time_ms`
or `v_uv` with a conflicting GLSL type; the engine sets it by name at draw time, so it
silently reads as zeros or fails to link
- `prefer-seeded-random` — `Math.random()`, unseeded `new Random()`, and duplicate seeds that
correlate streams
Run `ex doctor --help` for the list. Doctor scans `src/` plus any `example/`, `examples/`,
`demo/`, `sample/` or `sandbox/` directory that exists (plugins keep their public-API usage
there), or the package root when there is no `src/`; `--scope
` (repeatable) overrides.
`prefer-seeded-random` is also the cheapest pre-check before recording screenshot baselines —
unseeded randomness is what makes them flaky.
Ignore a finding case-by-case with eslint-style comments — after a report, an interactive
prompt offers to insert them for you:
```ts
// ex-doctor-ignore-next-line actor-not-added
new OffscreenHelper();
new Cursor(); // ex-doctor-ignore-line unnamed-actor
```
Omit the rule list to ignore every rule on that line.
### `ex upgrade` — codemod-style version migrations
```bash
ex upgrade --dry-run # preview the full migration plan, write nothing
ex upgrade # plan preview + one confirm, then apply + bump package.json
ex upgrade --to next # target v1 (the `next` prerelease); default is latest
ex upgrade --migrate-only # rewrite code but leave package.json alone
```
Chained migrations (v0.29.3 onward, ng-update style) rewrite your source with
formatting-preserving splices, classified against your project's *installed* excalibur types —
so run it **before** installing the new version. Every migration is one of three types:
**Automated** — rewritten for you, no review needed:
- `ex.Input.*` namespace flattened into `ex.*`
- event `.delta` → `.elapsed`
- `Engine.goto(...)` → `goToScene(...)`
- `GraphicsComponent.show(...)` → `use(...)`
- `Vector.size` → `magnitude`
- `getGlobalPos()/getGlobalRotation()/getGlobalScale()` → the equivalent accessors
- `EventDispatcher` → `EventEmitter`
- `Engine.get/setAntialiasing()` → `engine.screen.antialiasing`
- `Particle`/`ParticleEmitter` option renames (emitters gained a nested particle config)
- `easeTo`/`easeBy` actions → `moveTo`/`moveBy` with easing
- `Timer` now only takes the option-bag constructor
- `ScreenShader` `v_texcoord` → `v_uv`
- `BoundingBox.draw(...)` → `debug(...)`
- `TileMap`'s default `compositeStrategy` changed to `'separate'` — pins the old default
**Manual** — needs human judgment, so `// ex-upgrade(): …` breadcrumb comments are inserted
at each site with a link and recipe:
- `ex.Physics.*` statics were removed — configure physics in the Engine constructor
- collision events now target `Collider` (was sometimes `Entity`)
- `System.priority` is now static
- Trigger API changed (action signature, target vs filter)
- legacy `EasingFunctions.*` are deprecated — use the simple `(t) => number` forms
- screen space is now rooted at the content area (v1's `worldToScreenCoordinates`/`contentArea`
change)
**Notification** — no code changes, just a heads-up about a behavior change:
- `Vector.normalize()` on a zero vector now returns `(0,0)` (was `(0,1)`)
- realistic physics bodies now sleep by default
- Font/Text render slightly differently in v1
Requires a clean git tree (your undo) unless `--allow-dirty`; never runs `npm install` for you.
### `ex mcp` — MCP server for AI agents
Exposes the CLI's capabilities as [Model Context Protocol](https://modelcontextprotocol.io) tools
over stdio, so agents like Claude Code and OpenCode can search the Excalibur docs, scaffold
projects, and generate code in your project.
Claude Code (add `-s user` to register it globally instead of per-project):
```
claude mcp add excalibur -- npx -y create-excalibur mcp
```
OpenCode — add to `opencode.json` in your project (or `~/.config/opencode/opencode.json`):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"excalibur": {
"type": "local",
"command": ["npx", "-y", "create-excalibur", "mcp"],
"enabled": true
}
}
}
```
```
ex mcp # serve, tools operate on the current directory by default
ex mcp --project # point the tools at a specific project
ex mcp --help
```
**Docs** — also cover the `@excaliburjs/plugin-*` READMEs (`kind: "plugin"`, `/plugins/`
slugs):
- `docs_search` — search guides, API reference, and plugin READMEs; live by default, falls
back to the offline cache
- `docs_get_page` — fetch a docs page or plugin README (or one section) as markdown
- `docs_sync` — download docs + plugin READMEs into the local cache for offline/version-pinned
search
**Generate** — scaffold new code into the project (accept `dryRun` to preview):
- `analyze_project` — inspect scenes, actors, resources, spritesheets, installed version/plugins
- `generate_actor` — new Actor class, optionally wired into a scene
- `generate_label` — new Label (text) class, optionally wired into a scene
- `generate_scene` — new Scene class, registered in the scenes map by default
- `generate_resource` — register an image/sound/font/other asset in the resource loader
- `generate_material` — new WebGL shader Material, optionally assigned to an actor
- `generate_spritesheet` — slice a sheet image into an `ex.SpriteSheet`
- `generate_animation` — build an `ex.Animation` from an existing spritesheet
**Update** — edit existing code in place, preserving untouched options and comments:
- `update_actor` — change an Actor's `super({ ... })` ActorArgs
- `update_engine` — change the project's `new Engine(...)` options
**Project lifecycle:**
- `list_templates` — list templates and sample projects usable with `create_project`
- `create_project` — scaffold a new game from a template (skips `npm install`/`git init` unless asked)
**Diagnostics:**
- `doctor` — type-aware lint for common Excalibur mistakes (14 rules); `scope` narrows the scan
- `preflight` — read-only readiness check before `upgrade`: git state, where excalibur is
declared (incl. `peerDependencies`), whether its types resolve, scan roots, what `to`
resolves to, and how many migrations are in the path
- `upgrade` — chained codemod migrations to a newer Excalibur version. `to: "next"` resolves
the dist-tag to an exact version and pins it (a floating tag makes installs
non-reproducible); libraries get `peerDependencies.excalibur` bumped too; the result's
`backlog` lists older migrations the code still matches (re-run with `from`); `include`
runs a single migration id, `scope` narrows the scan
Errors come back with actionable hints so agents can self-correct.
> Note: `ex` shadows the rarely-used system `ex` (vi's line-editor mode) while the
> npm global bin dir is first on your PATH. Use the `excalibur` alias if that bothers you.
## Architecture
How the pieces fit together. Everything is TypeScript ESM; each command is a "flow"
registered in `src/constants.ts` and dispatched from `index.ts`. Development runs the
sources directly (Node's type stripping — no build step in the dev loop); publishing
compiles to `dist/` via `tsc`, which is what the bins run on end-user machines.
### Command dispatch
Both bins point at the compiled `dist/index.js`. Dispatch is persona-aware: the create persona treats a bare
positional as a project name, while `ex`/`excalibur` stay strict so a typo never scaffolds.
```mermaid
flowchart LR
A["npm create excalibur my-game"] --> D
B["create-excalibur bin"] --> D
C["ex / excalibur bins"] --> D
D["resolveInvocation
src/dispatch.ts"]
D -->|"no args"| MENU["interactive menu
FLOW_CHOICES"]
D -->|"known command"| FLOWS["FLOWS lookup
src/constants.ts"]
D -->|"create persona + positional"| CREATE["create flow
name pre-filled"]
D -->|"ex persona + unknown"| ERR["error: unknown command"]
MENU --> FLOWS
FLOWS --> F1["create / sample / inspect"]
FLOWS --> F2["docs"]
FLOWS --> F3["generate"]
FLOWS --> F4["doctor"]
FLOWS --> F5["mcp
dynamic import, stdout = protocol only"]
F5 -.->|"16 tools reuse the same cores:
search, apply, doctor, scaffold"| F2
```
### `ex docs` — search and the offline index
Searches hit the site's Algolia index first and fall back to a locally built index; `ex docs
offline` builds that index straight from the Excalibur repo's docs source, pinned to your
installed version.
```mermaid
flowchart TD
Q["ex docs query"] --> RS["runDocsSearch
src/docs/search.ts"]
RS -->|"online"| ALG["Algolia DocSearch
public search-only key"]
RS -->|"--offline"| LOCAL["MiniSearch index
one doc per page section"]
ALG -->|"network error"| LOCAL
ALG --> MERGE["merge plugin README hits
up to 3 tail slots"]
LOCAL --> MERGE
MERGE --> RENDER["markdown to ANSI renderer
pager for long pages"]
subgraph SYNC["ex docs offline — sync and indexing"]
V["detect installed excalibur
node_modules or package.json"] --> REF["pick ref: release tag v0.32.0
or main for old/no version"]
REF --> TREE["GitHub trees API, one call
list site/docs/**"]
TREE --> RAW["fetch raw files by commit sha
raw.githubusercontent.com"]
RAW --> MDX["mdx.ts: frontmatter slugs, admonitions,
playground embeds, wiki links"]
MDX --> IDX["cache ~/.excalibur/docs/ref:
index.json + slugs.json + manifest"]
NPM["npm registry:
@excaliburjs/plugin-* readmes"] --> PIDX["plugin index
sibling plugins/ cache"]
ALG2["Algolia symbol sweep"] --> SYM["api-symbols.json
resolves wiki links"]
end
IDX --> LOCAL
PIDX --> MERGE
```
### `ex generate` — what it looks for in your TypeScript
Generation is a wizard/apply split: the wizard only builds an option model, and `apply*()` does
the edits. Edits are minimal text splices validated by re-parsing — never a full AST reprint, so
your formatting and comments survive. It uses **your project's own TypeScript** (never bundled;
TypeScript 7 removed the compiler API, so it asks for 5.x/6.x).
```mermaid
flowchart TD
G["ex generate actor / label / scene / resource /
engine / material / update-actor / spritesheet / animation"] --> AP["analyzeProject"]
AP --> TSL["load the project's TypeScript
from its node_modules"]
AP --> SCAN
subgraph SCAN["syntactic AST scan of src/**/*.ts"]
S1["new Engine and its
scenes map keys"]
S2["Resources literal:
keys + asset paths"]
S3["classes extending Scene"]
S4["classes extending
Actor / Label / ScreenElement"]
S5["SpriteSheet consts:
grid, spacing, image key"]
S6["package.json:
@excaliburjs/* plugins"]
end
SCAN --> W["wizard prompts
build an option model"]
W --> APPLY["apply: minimal text splices
insert option property, add import,
add to a scene's onInitialize"]
APPLY --> VAL["re-parse: zero syntax errors
or the edit is abandoned"]
VAL -->|"ok"| WRITE["write files"]
VAL -->|"seam not found"| MANUAL["print a manual snippet
instead of guessing"]
```
### `ex doctor` — type-aware diagnostics
Doctor is the one place a full `ts.Program` + TypeChecker is used (generate stays syntactic):
the checker is what catches `class Boss extends Monster extends Actor`. Rules are kind-keyed
listeners over a single AST walk per file, the same shape typescript-eslint uses.
```mermaid
flowchart LR
D["ex doctor --json"] --> AP["analyzeProject"]
AP --> PROG["ts.createProgram + TypeChecker
tsconfig.json or vite-flavored defaults"]
PROG --> PROBE["probe: excalibur .d.ts resolvable?
no: run npm install"]
PROBE --> WALK["one AST walk per src file
dispatch to rule listeners"]
WALK --> R1["actor-not-added:
Actor-derived new never traced
to add or addChild"]
WALK --> R2["unnamed-actor:
super options or new Actor
without a name"]
R1 --> OUT["findings: rule, file:line, message, hint
exit 1 when any are found"]
R2 --> OUT
```
## Running this project locally
Run `npm run dev`, or `node index.ts docs ` (Node 22.18+/24 — the dev loop runs
the TypeScript sources directly via type stripping; end users only ever run compiled JS).
Tests: `npm test` · Typecheck: `npm run typecheck` · Build: `npm run build` ·
Publish smoke test: `npm run smoke:pack`