A batteries-included project template for building RhostMUSH softcode systems. Provides a compiler, Docker-based test runner, and CLAUDE.md conventions so every new RhostMUSH project starts with the same structure.
- Node.js 20+
- npm
- Docker (for
npm test) - Claude Code with mush-skills installed
# 1. Install Claude Code skills (once per machine)
git clone https://github.com/lcanady/mush-skills ~/.claude/skills/mush-skills-repo
cd ~/.claude/skills/mush-skills-repo && ./install.sh
# Restart Claude Code
# 2. Use this template on GitHub → "Use this template" button
# or copy locally:
cp -r rhost-template my-new-game
cd my-new-game
# 3. Install Node dependencies
npm install
# 4. Configure environment
cp .env.example .env
# Edit .env with your server details (only needed for manual installs)
# 5. Build the installer
npm run build
# → writes dist/installer.txt
# 6. Run tests (requires Docker)
npm testmy-project/
├── src/ # Softcode source (.mush files)
│ └── system/ # Core system files
│ ├── config.mush # Object creation — ALWAYS runs first
│ └── udfs-core.mush # Core UDFs
├── showcases/ # Showcase JSON files (one per feature demo)
│ └── example.json # Copy this to add a new showcase section
├── tests/ # TypeScript test suites (@rhost/testkit)
│ ├── run.ts # Master test runner (Docker + wave execution)
│ ├── helpers.ts # Shared helpers (createThing, createRoom, etc.)
│ └── example.test.ts # Starter suite — copy to add new tests
├── tools/ # Build tooling
│ ├── build.ts # Compiles .mush → dist/installer.txt
│ ├── showcase.ts # Interactive showcase runner
│ ├── header.js # Installer header (think/ansi lines)
│ └── post.js # Installer footer
├── dist/ # Compiled output (gitignored)
│ └── installer.txt # Paste this into RhostMUSH as Wizard
├── db/ # SQLite databases if needed (gitignored)
├── scripts/ # Shell helpers
├── resources/ # Reference PDFs/rulebooks (gitignored)
├── .env # Local credentials (never commit)
├── .env.example # Committed env template
├── CLAUDE.md # Claude Code instructions for this project
├── mush.json # Softcode package manifest
├── package.json
└── tsconfig.json
This template is registry-ready. mush.json is the softcode equivalent of
package.json — it declares your package's identity, namespace, and dependencies
on other softcode packages.
{
"name": "@mushpkg/my-project",
"version": "1.0.0",
"main": "dist/installer.txt",
"namespace": "cg",
"server": "rhostmush",
"dependencies": {
"@mushpkg/bbs": "^1.3.0",
"@mushpkg/jobs": "github:lcanady/mush-jobs@v2.0.0"
},
"registry": "https://registry.mushpkg.dev"
}npm run mush:installFetches each entry in mush.json → dependencies into deps/ and writes
mush-lock.json with pinned versions and SHA-256 checksums. Commit mush-lock.json;
deps/ is gitignored (re-fetch after a fresh clone).
Supported version specifiers:
| Format | Example |
|---|---|
| Semver (registry) | "^1.3.0" |
| GitHub release | "github:owner/repo@v2.0.0" |
| Local path | "file:../local-pkg" |
npm run mush:install # fetch deps first (if any)
npm run buildCompiles all .mush files listed in mush.json → installers[].manifest into a single
dist/installer.txt. Multiple named installers are supported — use --only <name> to
build one:
npm run build -- --only main
npm run build -- --list # print installer namesDependency installers are automatically prepended before your own source files.
The compiler:
- Collapses indented continuation lines into single-line commands
- Strips blank lines
- Passes
@@comments andthinklines through as-is - Executes
.js/.tshelper scripts and appends their stdout
- Create
src/<system>/my-feature.mush - Add
'<system>/my-feature.mush'(or'<system>/'for all files in the dir) to themanifestarray of the appropriate installer inmush.json - Run
npm run build
The showcase runner lets you demo your softcode interactively against a live server — no test fixtures, no assertions, just real output. Useful for demos, debugging visual output, and end-to-end walkthroughs.
npm run showcase # interactive menu
npm run showcase -- --list # list available sections (no connection)
npm run showcase -- my-section # run one section, then menu
npm run showcase -- --spin --deploy # spin Docker, deploy, then menuEach showcase section is a JSON file in showcases/. Register files in mush.json:
{
"showcases": [
{ "file": "showcases/hello-world.json" }
]
}{
"key": "hello-world",
"label": "Hello world — basic UDF output",
"vars": {
"sys": "search(name=My System <tmpl.sys>)"
},
"steps": [
{ "sub": "Basic eval" },
{ "eval": "u({{sys}}/F.HELLO,{{player}})", "label": "+hello" },
{ "sub": "Command with stored result" },
{ "eval": "num(me)", "store": "wiz_dbref" },
{ "command": "think {{wiz_dbref}}", "label": "wiz dbref" },
{ "sub": "Reset player state" },
{ "reset": true }
]
}| Field | Purpose |
|---|---|
{ "sub": "text" } |
Print a sub-heading |
{ "cmd": "text" } |
Print an informational line |
{ "eval": "expr", "label": "..." } |
Run wiz.eval(expr) and show result |
{ "eval": "expr", "store": "key" } |
Eval and store result in vars (silent) |
{ "command": "cmd", "label": "..." } |
Run wiz.command(cmd) and show output |
{ "reset": true } |
Wipe all _* attrs on the showcase player |
{ "set_stats": "INT:6 REF:7 ..." } |
Set _CG_STAT_* attrs (chargen systems) |
All eval, command, and label strings support {{token}} interpolation:
| Token | Value |
|---|---|
{{player}} |
ShowcasePlayer dbref |
{{key}} |
Any key declared in the file's vars block |
{{key}} |
Any key stored by a prior store step |
vars values are MUSH expressions evaluated at startup (e.g. search(name=...)).
The result's dbref is extracted automatically; the full result is kept if no dbref
is found (useful for non-dbref values).
| Flag | Purpose |
|---|---|
--spin |
Start a fresh Docker container |
--deploy |
Deploy the first installer before running |
--no-deploy |
Skip deploy even with --spin |
--installer NAME |
Deploy a specific named installer |
--host HOST |
MUSH host (default: $RHOST_HOST or localhost) |
--port PORT |
MUSH port (default: $RHOST_PORT or 4201) |
--pass PASS |
Wizard password (default: $RHOST_PASS or changeme) |
--list |
Print sections and exit, no connection needed |
npm testThe test runner (tests/run.ts):
- Pulls
lcanady/rhostmush:latestand starts a throwaway container - Logs in as Wizard, deploys
dist/installer.txtin batches of 20 lines - Creates one MUSH account per parallel test slot (WIZARD + ROYALTY + SIDEFX)
- Gives each account a private room (prevents connect/disconnect message bleed)
- Runs test waves in order; suites within a wave run in parallel
- Stops the container on pass or fail
Edit WAVES in tests/run.ts to control grouping and parallelism:
const WAVES: string[][] = [
// Wave 1 — fast unit tests (run in parallel)
['feature-a.test.ts', 'feature-b.test.ts'],
// Wave 2 — integration tests (run after Wave 1 completes)
['workflow.test.ts'],
// Wave 3 — heavy/queue-sensitive (run alone)
['e2e.test.ts'],
];Copy tests/example.test.ts and adapt:
import { RhostRunner } from '@rhost/testkit';
import { createThing } from './helpers';
const runner = new RhostRunner();
let sysObj: string;
runner.describe('My Feature', ({ it, beforeAll }) => {
beforeAll(async ({ client }) => {
// Always look up by name — never hardcode dbrefs
sysObj = (await client.eval('search(name=My System <sys>)')).trim();
});
it('F.MY.FUNC returns expected value', async ({ expect }) => {
await expect(`u(${sysObj}/F.MY.FUNC,arg)`).toBe('expected');
});
});
runner.run({
host: process.env.RHOST_HOST || 'localhost',
port: parseInt(process.env.RHOST_PORT || '4201', 10),
username: process.env.RHOST_USER || 'Wizard',
password: process.env.RHOST_PASS || '',
}).then(r => process.exit(r.failed > 0 ? 1 : 0))
.catch(err => { console.error(err); process.exit(1); });Set a namespace in mush.json. Objects use <ns.tag> format:
@@ mush.json: { "namespace": "cg" }
@create Chargen System <cg.sys>
@create Chargen Data <cg.data>
Look up in tests with: search(name=Chargen System <cg.sys>)
The <ns.tag> convention prevents name collisions between packages on the same server.
| Pattern | Purpose |
|---|---|
F.VERB.NOUN |
User-defined functions |
CMD.VERB |
Command handlers ($+cmd) |
_COR_* |
Hidden internal state (wiz-only via _ prefix) |
_<SYS>_* |
System-specific hidden state |
_ prefix = wiz-only hidden
In RhostMUSH, a _ prefix makes an attribute invisible to non-wizard players.
Use it for all internal state that players should not read or set directly.
@@at line start = MUSH comment (safe anywhere)/*is only a comment when it appears at^\s*/\*(start of line)- Never use
/*mid-line or inside attribute values
npm run build
# Paste contents of dist/installer.txt into your RhostMUSH session as Wizard
# Then: @shutdown/reboot| Variable | Default | Purpose |
|---|---|---|
RHOST_HOST |
localhost |
Server hostname |
RHOST_PORT |
4201 |
Server port |
RHOST_USER |
Wizard |
Login name |
RHOST_PASS |
changeme |
Login password |
RHOST_IMAGE |
lcanady/rhostmush:latest |
Docker image for tests |
Copy .env.example → .env and fill in values. Never commit .env.
| Skill | Use for |
|---|---|
/mush-build |
Writing new softcode |
/mush-test |
Writing @rhost/testkit tests |
/mush-lint |
Static analysis before build |
/mush-review |
Code review (logic, patterns) |
/mush-security |
Security audit |
/mush-coverage |
Find untested attributes |
/mush-simulate |
Trace execution without a server |
/mush-efficiency |
Optimize speed and attribute count |
/mush-explain |
Explain what softcode does |
/mush-docs |
Generate in-game help text |
/mush-deps |
Map attribute dependencies |
/mush-readme |
Regenerate this README |
MIT