@debuffjp/cli
The debuff command: dev server, production builds with size reports, asset manifests, browser scenarios, fuzz playtesting and project inspection. Built on Vite.
npm install -D @debuffjp/cli
npx debuff --help
| Command | What it does |
|---|---|
debuff create [dir] | Scaffold a game (same as npm create debuff) |
debuff dev [--port] [--host] | Dev server with hot reload; regenerates the asset manifest on change |
debuff build [--sourcemap] | Production build, then a raw/gzip/brotli report. Fails if dev-only code leaks |
debuff report | Size report for an existing dist/ |
debuff assets | Run the asset pipeline: list every asset, its importer and size, plus unused and generated files |
debuff test [files] [-t text] [--changed] [--queue name] [--accept-saves] [--json] | Run logic tests (tests/**/*.test.ts) with keiko: the real game in Node, no browser, in milliseconds. From 0.32, every run records what each test used, and --changed runs only the tests whose code or files changed since they last passed, plus new and failed ones; --queue push keeps a separate record for a pre-push hook or CI (how it decides). From 0.29.3 it also compares the saves in src/ with saves.json and prints what changed, the same check as debuff build, and --accept-saves updates saves.json. It only reports: debuff build still fails on a save error. Before 0.29.3 only debuff build checked saves, and debuff test --accept-saves failed with "Unknown option" |
debuff scenario [files] [--update] [--strict] | Run scenarios/*.json in headless Chrome/Edge |
debuff fuzz [--seconds] [--runs] [--seed] [--keys] [--ignore] | Play headless with random but plausible input. Reports explained errors, stuck states and loops (the scene, or two state fields, flipping between the same two values), each saved as a scenario in .debuff/fuzz/ that reproduces it. --keys takes key codes, PointerRight / PointerMiddle, and the gestures Wheel (a scroll) and Touch (a finger tap or drag), e.g. --keys Touch,Wheel,Space for a touch game. --ignore state.shots skips noisy fields. Exits 1 on errors |
debuff telemetry [on|off|status] | Anonymous build registration: after a successful build, debuff build sends a random project id, the package name and the debuff version to debuff.jp, so we can find games made with debuff. Off in CI, or with debuff telemetry off / DEBUFF_TELEMETRY=0. What's sent |
debuff papercut <title> [--minutes] [--repo] [--area] [--kind] [--workaround] [--hit <id>] [--force] | Report tooling friction (a slow or flaky tool, a missing API, confusing docs) to the public board at papercuts.debuff.jp. It lists close matches instead of filing a duplicate: rerun with --hit <id> to add your minutes to one, or --force. With --hit, the title or --body is the note and --workaround fills the papercut's workaround when it has none (from 0.24.1; 0.24.0 dropped it). Sends only what you typed and debuff-cli <version>. DEBUFF_PAPERCUTS_TOKEN makes new entries show at once; without it they wait for review. Added in 0.24 |
debuff papercuts [on|off|status] | Automatic papercuts: the CLI and MCP server report a setup failure seen twice in a week, or a command far slower than it should be, to papercuts.debuff.jp. They send a fixed title, an error code, minutes and the version, never messages, paths or project details. From 0.26.2 also the OS, CPU count, memory, Node version and what was checked (which browsers were missing). Once a day per fingerprint at most; off in CI or with DEBUFF_PAPERCUTS=0. What's sent. Added in 0.24 |
debuff inspect | JSON summary of the project |
debuff upgrade [--to] [--dry-run] | Move every debuff package to the latest release (or --to <version>) together, keeping each range's ^, ~ or exact style, install with the project's package manager, then run the new version's doctor --fix, which also rewrites the agent skills. Never downgrades without --to. If an earlier run updated package.json but its install failed or was interrupted, running it again installs and runs doctor (before 0.31.1 it said "already on" and stopped; --json lists the packages as stale). Added in 0.21 |
debuff doctor [--live] [--fix] | Check the setup in one pass: Node version, debuff package versions, node_modules, debuff.config.ts, assets, saves.json, agent files and the browser scenarios use. Each problem comes with a fix. --fix applies the safe ones (install, dedupe, add @debuffjp/dev, write a missing .mcp.json, rewrite the agent skills for the installed version, and from 0.23 write the agent hook files that are missing) in the project's package manager, then checks again. --live also starts the dev server and checks the game exposes its dev hooks. From 0.21 it also says when a newer debuff is out. Exits 2 when a required check fails (1 before 0.20). Other commands that fail on setup point here |
Add --json for machine-readable output and --root <dir> to target another project.
From 0.29.1, size reports use Brotli quality 11 for code and HTML, and quality 4 for assets, source maps and other files. This avoids spending minutes recompressing large assets just to report their size. Before 0.29.1, every file used quality 11. This applies to debuff build, debuff report, debuff inspect, the MCP build tool and buildReport(). Raw and gzip measurements keep their previous settings. Reports measure compressed sizes without writing compressed files; your host chooses how to compress downloads.
Once a day, at the end of a command, the CLI prints one line on stderr when a newer debuff is out (from 0.21). From 0.24, debuff dev prints it under its banner instead, since it never exits. The hint is never printed in --json mode, and it's off in CI and with DEBUFF_UPDATE_CHECK=0. Agents get the same news from the agent hook at session start (below).
Exit codes and errors
From 0.20. Before that, every failure exited 1, doctor exited 1 when a required check failed, unknown options were ignored, and --json errors were printed as text.
The exit code says what to do next, so scripts and agents can act without reading the output:
| Exit | Meaning | What to do |
|---|---|---|
| 0 | Success | Carry on |
| 1 | The run reported a failure (a test, scenario, fuzz finding or leak), or something unexpected broke | Read the report or the error |
| 2 | The project or machine needs fixing first (no browser, invalid config, missing dev hooks, a busy port, a failing doctor) | Run npx debuff doctor and apply its fixes. A retry won't help |
| 3 | Bad input: an unknown command or option, or a folder that isn't there | Fix the command line |
With --json (and always for inspect), stdout carries exactly one JSON document: the result, or this when the command can't run:
{ "error": { "code": "browser_not_found", "message": "…", "fix": "Install Chrome or Edge, or set DEBUFF_BROWSER…", "transient": false } }
code is one of unknown_command, bad_option, not_a_project, nothing_to_run (from 0.21: no scenarios, or no build to report on), config_invalid, browser_not_found, dev_hooks_missing, game_hung (from 0.28: one call into the game took over 60 s), port_in_use, network and unexpected, and keeps its meaning between releases. transient: true means the same command may work if run again (a network blip). Logs go to stderr. The MCP server's tools add the same code and fix to their errors.
Agent hook
From 0.23. New projects ship .claude/hooks/debuff.mjs, wired into Claude Code by .claude/settings.json and into Codex by .codex/hooks.json. Around each edit and shell command, it:
- asks the human before screenshot baselines or saves change;
- refuses edits to
.debuff/,dist/andnode_modules/, a foreground dev server, and hand-changed@debuffjp/*versions; - warns about new
Math.random,Date.nowand real-timer lines insrc/; - once per stop, asks for
debuff testwhensrc/changed since the last run; - from 0.24, at session start and at most once a day, tells the agent when a newer debuff is out, so it can ask the human about
npx debuff upgrade.
The shim imports @debuffjp/cli/hook, so the rules come from the installed CLI. It does nothing before npm install, and DEBUFF_HOOK=0 turns it off. // debuff-hook-ignore <rule> at the end of a line skips that line. Rules and details: Working with AI agents.
Scenarios
{
"$schema": "../node_modules/@debuffjp/cli/schema/scenario.schema.json",
"steps": [
{ "press": "ArrowRight" },
{ "wait": 30 },
{ "expect": { "state.player.x": { "gt": 200 } } },
{ "set": { "player.speed": 400 } },
{ "capture": "after-move" }
]
}
Time advances only through wait and until steps, so runs are deterministic. Every step renders in a real browser, so long waits are slow (a minute of game time can take a few minutes): check timers and other long timings with logic tests, and keep scenarios short. until steps one tick at a time until its conditions hold, and fails if they don't within that many steps (default 600), so a scenario says "until the player lands" instead of guessing a wait. Paths read the dev state: scene, tick, state.* (your inspect()), and events.<name>, the number of times the game called game.emit("<name>") (0 until the first). For example { "until": { "events.coin": { "gte": 1 } }, "within": 60 }. Replays saved from the dev panel (or the MCP game_replay tool) are scenarios too. From 0.31 they hold a checkpoint step every 60 ticks, a hash of each part of the game's state; when one stops matching, the report warns once with the tick and the parts that differ (state.player, events), and the scenario still passes. ### Accessibility checks
Scenarios can screen what the game actually draws:
{ "flashes": 240 }plays 240 steps and fails on flashing that could trigger photosensitive seizures: more than three flashes a second over a quarter of a 10° field. It approximates WCAG 2.3.1's general flash threshold, taking the 10° field as a third of the screen each way.{ "contrast": [x, y, w, h], "min": 4.5 }fails if the light and dark parts of a region (say, your HUD text) contrast less thanmin(default 4.5:1, WCAG AA for text)."stepTimeout": 120at the top of a scenario changes how many seconds one step may take before the game counts as hung. Default 60, from 0.28."reducedMotion": trueat the top of a scenario runs it with the browser asking for reduced motion, so you can check that mode too.
These are screening checks on one run, not a certification: a pass means nothing obviously unsafe happened in the steps you played. Red flashes aren't checked yet.
Captures are compared against per-platform baselines in scenarios/__baselines__/. A missing baseline is recorded unless you pass --strict. Set DEBUFF_BROWSER to use a specific Chromium build.
Programmatic use
import { buildReport, compileAssets, runScenarios } from "@debuffjp/cli";
Assets and plugins
Put source assets in assets/. The pipeline builds an in-memory manifest (/assets/manifest.json). Only files the manifest references are served in dev and emitted in builds, and sources are never modified.
The pipeline warns about a PNG wider or taller than 4096 pixels, because a GPU with a smaller texture limit draws it black. Split a long sprite strip into several sheets. Before 0.29.3, there was no warning.
Add tooling plugins in debuff.config.ts:
import { defineConfig } from "@debuffjp/cli";
export default defineConfig({
plugins: [
{
name: "maps",
apiVersion: 1,
// Any language: SourceFile JSON on stdin, ImportResult JSON on stdout.
assets: [{ name: "maps", extensions: [".map"], command: ["python", "tools/import_map.py"] }],
},
],
});
Aseprite
The first-party aseprite() plugin runs Aseprite for you, so .aseprite and .ase files in assets/ load like PNGs with no export step:
import { aseprite, defineConfig } from "@debuffjp/cli";
export default defineConfig({ plugins: [aseprite()] });
hero.asepritewith several frames becomes a sprite sheet:assets.frames("hero")returns the frames in order.[email protected]flattens to one image with a 6x9 grid, ready forBitmapFont.- Hidden layers are left out, so guide layers stay in the source.
It looks for Aseprite in $ASEPRITE, then on PATH, then in the usual install folders (including Steam). You can also pass aseprite({ binary }). If it can't find Aseprite, the build warns and skips those files. Results are cached by file contents, so Aseprite only runs when a file changes.
aseprite/new-font.lua creates a blank font sheet with the grid set up:
aseprite -b --script-param w=6 --script-param h=9 --script-param out=assets/[email protected] --script node_modules/@debuffjp/cli/aseprite/new-font.lua
tenten
tenten is a headless pixel-art tool for coding agents: an MCP server draws sprites and animations and saves them as .tt, a JSON file with the pixels as text grids that diffs in git. .tt files in assets/ become PNGs and sprite sheets with no configuration, the same way aseprite() does for .aseprite. Add the tenten() plugin only to change how tenten is run:
import { defineConfig, tenten } from "@debuffjp/cli";
export default defineConfig({ plugins: [tenten({ command: ["node", "../tenten/bin/tenten.js"] })] });
slime.ttwith several frames becomes a sprite sheet:assets.frames("slime")returns the frames in order.[email protected]flattens to one image with a 6x9 grid.
debuff runs tenten from $TENTEN (its bin/tenten.js), then the copy installed in the project, and otherwise fetches it with npx -y -p @debuffjp/tenten tenten the first time a .tt file is seen. tenten needs Node 24. Results are cached by file contents.