The sbs CLI
sbs (the sbs.pyz tool) builds, runs, and serves missions from the command
line. Run sbs <command> --help for full options.
| Command | What it does |
|---|---|
sbs create <name> |
Start a new mission from a boilerplate template |
sbs templates |
List the templates available, per release line |
sbs debug <mission> |
Run a mission in a browser mock GUI (3D cinematic + 2D radar) |
sbs debug <mission> --map 0 |
Auto-start a map instead of the picker; --no-gui for headless |
sbs overnight <mission> |
Long soak test under autoplay |
sbs web <mission> |
Serve the mission's web pages to browsers |
sbs web-static <mission> <page> |
Render one web page to a standalone HTML file |
sbs lib <folder> |
Build a .sbslib / .mastlib library |
sbs compile <mission> |
Compile-check the MAST |
sbs lint <mission> |
Validate the mission's .amd files (headings, references, signals) |
sbs fmt <mission> |
Format the mission's .amd files (canonical, prose-safe) |
sbs docs <mission> |
Print the mission's .amd as a paper-ready HTML document (four lenses) |
sbs docs <mission> --pdf |
...and a PDF, via a headless browser or the weasyprint CLI |
sbs doctor |
Check the setup - tools, libraries, mission health (details) |
sbs deps install <pkg> |
Optional Python libraries, on an interpreter that cannot pip (why) |
sbs swap <name> |
Switch which missions_* set Cosmos loads |
sbs fetch / sbs update |
Fetch missions / update the tool |
Starting a mission
sbs templates # what's on offer, per release line
sbs create MyMission # pick from a list
sbs create MyMission -t sandbox # pick up front
sbs create MyMission --title "My Mission"
create lays the template down, rewrites the mission's name into
description.yaml, and fetches everything its story.json pins. It refuses to write
into a folder that already has anything in it.
Templates come from the
mast_starter repo, which keeps a
templates.json catalog on each branch — so a new template is a commit there,
not a release of this tool.
Release lines
A branch per line (v1.3.0, v1.4.0), and every dependency of a mission comes from
one line. That is not just a version string: a template can only use what its line
has — provides/requires are v1.4.0+, and pickup_spawn / scatter_box don't
exist in the v1.3.0 library at all — so the branch decides which templates exist.
Resolution order is: what you asked for, then a line this install already has an sbslib for, capped by your Cosmos version. Deliberately not in that list: the newest branch on GitHub. That's the upper bound, not a default.
sbs create MyMission -l v1.4.0 # pin the line
sbs create MyMission -b v1.4.0_dev # a specific branch (pre-releases are opt-in)
sbs create MyMission --retarget # re-pin a template's deps to the resolved line
Running a mission
sbs debug . # browser GUI, map picker
sbs debug . --map 0 # auto-start map 0
sbs debug . --no-gui --map 0 --test 30 # headless, play ~30s, pass/fail verdict
Handy flags: --use-working-tree (test local library edits against the packaged
mission), --seed N (reproducible runs), and settings overrides that don't touch
settings.yaml (--auto-start, --players N, --set KEY=VALUE).
Launching the real game
sbs debug runs a mission in the browser; sbs run opens the actual game — a server and
as many console clients as you ask for, arranged on screen.
sbs run server + a full test bridge
sbs run comms,weapons just those two consoles
sbs run -m LM_TestRange a different mission (default: LegendaryMissions)
sbs run comms --ip 192.168.1.50 clients pointed at another machine
sbs run --dry-run show the command lines, launch nothing
Nothing needs clicking: the server starts itself, clients connect, and each lands on its
console. --no-auto restores the old launcher-menu behavior.
The mission is passed as defaultmission=, so preferences.json is never edited.
Anything you add on the end reaches the mission, which is how you start a particular map or set of settings:
sbs run -m LM_TestRange map=sandbox profile=soak var.DIFFICULTY=3
sbs run -m LegendaryMissions test=60 a pass/fail verdict from the real engine
sbs run -m LegendaryMissions record=session transcribe what you click
See Command-line arguments for the full list and how settings merge.
Switching mission sets
Cosmos loads exactly one data/missions folder. Keep several sets beside it as
data/missions_<name> and let data/missions be a link to whichever you want:
sbs swap # current target + available sets
sbs swap amd # point data/missions at data/missions_amd
sbs swap cos # back to the stock missions
The prefix is optional (amd and missions_amd are the same). Any
missions_<name> folder is a target, so adding a set is just creating the folder.
A real (non-link) data/missions is renamed to missions_cos on the first swap
rather than deleted — nothing but the link is ever removed. Run it from anywhere
under the install; it finds the data folder by walking up, or pass --data.
Back up data/missions before your first swap
The command rearranges the folder that holds all your missions. It will not delete a mission folder, but take a backup anyway if you have edits that aren't in source control. And close Cosmos first — a running client holds files open under the link, and the swap will refuse rather than half-finish.
Validating AMD
compile checks the MAST and exits non-zero when it fails, so it can gate a build.
What compile cannot see
A { } literal split across lines. MAST parses line by line, so the first line is
an unclosed {; the parser desyncs for the rest of the file, the story's main task
can end up empty, and the compiler still reports zero errors. The mission then
runs and does nothing. Keep dict literals on one line or inside ~~ … ~~, and if a
mission mysteriously does nothing, suspect this first. A headless
--test run catches it (labels 0/N); a compile never will.
lint checks the .amd content (quests, dialogue, cast, maps). AMD fails silently — a typo'd # [Display](key) heading becomes body
text and its node vanishes — so lint re-scans a mission's .amd and surfaces it.
sbs lint . # errors + warnings, exit 0/1
sbs lint . --strict # warnings fail too (CI)
sbs lint . --no-cross # skip cross-file (signal->route, reach->landmark) checks
Errors (fail the run): broken/vanishing headings, unclosed --- fences,
heading-level jumps. Warnings: dangling choice / Scene: / Then: reveal /
Parent: targets, an emitted signal X with no //signal/X route, a quest
Starts when: signal X that nothing emits, a reach i,j with no landmark At:, and
non-ASCII author text (the engine renders ASCII only). Backed by sbs_utils.procedural.amd_lint — also callable
directly on a single file: python -m sbs_utils.procedural.amd_lint <file.amd>.
lint also reads the mission's .mast for two signal problems that are easy to write and
hard to see. First, work that runs once per console when it should run once on the
server (signal-side-effect-* — a //signal route that spawns, rewards, saves, counts or
rolls random). Second, setup that can run more than once because its signal gets
emitted more than once:
| Code | Fires on |
|---|---|
signal-init-unkeyed-spawn |
a //shared/signal/create_* route that spawns without a key and isn't marked once |
signal-emit-in-loop |
a setup signal_emit inside a for / while |
signal-multi-emit |
such a signal emitted from more than one place in the mission |
All are warnings — the fix is usually a keyed create (player_ensure) or a once route.
See Signal routes. Skip them with
--no-signals.
References resolve across all of the mission's .amd files and against MAST
== labels ==, so a Scene: / choice / reveal that targets a node in a sibling
file (or a MAST handler label) isn't wrongly flagged. To vouch for a signal the
linter can't see statically — a dynamic or computed signal_emit — add an optional
emits: [name, …] (or handles: [name]) line to a metadata: block; it needs no
new syntax (MAST just treats the key as an unused variable).
--format compact emits file:line:col: lines for editor problem-matchers;
--format json emits structured findings (with exact ranges) for tools/CI.
Formatting
sbs fmt . # canonically format this mission's .amd (writes in place)
sbs fmt . --check # report + exit 1 if any file isn't formatted (CI)
Normalizes trailing whitespace, heading spacing, --- fences, and blank-line runs.
It is prose-safe and idempotent — it never reflows prose and is guaranteed not to
change the parsed model. Backed by sbs_utils.procedural.amd_fmt (single file:
python -m sbs_utils.procedural.amd_fmt --write <file.amd>), and exposed as the LSP
formatting provider below (format-on-save).
In your editor (language server)
sbs lint --lsp # AMD language server over stdio
Point any LSP client at that command for live diagnostics as you type — VSCode,
Neovim, Emacs, Sublime, JetBrains. A ready-to-build VSCode extension (syntax
highlighting + a thin client for this server) lives in the sbs_cli repo under
editors/vscode/. It's the same checks over the same amd_core
model (sbs_utils.procedural.amd_lsp), dependency-free. Beyond diagnostics it also
provides go-to-definition (a reveal / choice / Scene: target → its node),
find-references and rename (a node key + every reference to it), a
document outline, hover, completion, quick-fixes (did-you-mean /
create-node), a reference CodeLens, color swatches for #rrggbb, inlay
hints (a reference's display name inline), and formatting — all from the one
model, and all resolved across the whole mission.
Serving web pages
sbs web . # serve this mission's //web pages
sbs web --engine a=missionA --engine b=missionB # one server, many engines
sbs web-static . scores -o scores.html --query title=Standings
See Serving web pages and the Web pages cookbook.