@runmu.sh/dev
v0.5.0
Published
The μClient extension dev server: watch, build with esbuild, serve on localhost with CORS, push hot reloads over Server-Sent Events and a headless test host that replays .murec recordings (@runmu.sh/dev/test).
Maintainers
Readme
@runmu.sh/dev
The μClient extension dev server (R-EXT-DEV).
node clients/extensions/dev/serve.mjs path/to/my-ext # in this repo
npx @runmu.sh/dev path/to/my-ext --port 5199 # from npmThen in μClient: Extensions → Developer → load from dev server (or open μClient with
?ext-dev=http://localhost:5199/). Every save rebuilds the extension and hot-reloads it in place.
- Builds
muclient.source(defaultsrc/index.ts) intoexports(e.g.dist/index.js) with esbuild: ESM, es2022,vueand@muclient/*external. - Serves the folder on 127.0.0.1 with CORS and
Access-Control-Allow-Private-Network: true. GET /__mu/events(Server-Sent Events):hello,reload {n, ms, entry},build-error {n, message}.GET /__mu/statusreturns JSON.- Options:
--port <n>(0 = free port),--host 127.0.0.1|localhost|::1,--quiet.
Implement snapshot() / restore() on a panel to keep its state across reloads.
Recordings (.murec)
Record a real game in μClient: open the GMCP inspector, press record, play, then save .murec. The file
is a local download, never uploaded. It holds the session's output, GMCP in and out (MSDP as MSDP.<VAR>), and on
MCP worlds the MCP messages and negotiation, with timings. Your character's name becomes {char}. Char.Login* and
credential-like keys (password, token, secret, auth*key, …) are dropped, and the MCP authentication key
is never in the client to record. Play one into the headless host below with host.play().
Headless tests (@runmu.sh/dev/test)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHost } from '@runmu.sh/dev/test';
test('follows the room', async () => {
const host = createHost(); // one session s1 in world w1, connected
const ext = await host.load('src/index.ts'); // built with esbuild like the dev server, or host.load(def)
await host.play('test/fixtures/session.murec'); // or host.gmcp('Room.Info', {...}), host.line('text')
assert.equal(host.widget('room')?.title, 'A cellar');
assert.deepEqual(host.sends('command'), []);
await host.unload(); // deactivate + dispose, as when disabled
assert.deepEqual(host.live(), []); // nothing left registered
});- Feed:
host.gmcp([sid,] pkg, data),host.msdp([sid,] var, value),host.mcp([sid,] message, args),host.line([sid,] text),host.setState(sid, state),host.open(session),host.close(sid),host.switchTo(sid),await host.play(src, { sid, realtime }),host.route(text, { sid })(runs themu.lines.routerouters over a line with the host's matcher; returns{ delivered, moved }). - Held lines (0.4.0): every line
host.lineandhost.routefeed is kept per session unless it was gagged or moved, andmu.lines.query(sid, { rules?, limit?, before? })(SDK 1.15) answers from them as μClient does: oldest first,{ line, targets, move }, only lines with a target whenrulesis given (echo lines never match). The spans are plain ([{ text }]);host.close(sid)drops the session's lines. - Tips and the games directory (0.5.0, SDK 1.16):
l.tip(range, spec)in atransformstage is checked as μClient checks it (a bad spec or another phase is an error inhost.errors) and each match is in the result ofhost.line(...)astips: [{ text, start, end, tip }].mu.gamesanswers fromcreateHost({ games: { links: { w1: 'slug' }, lore: { slug: [LoreTip, …] } } }):linked(worldId),lore(slug)(a copy,[]when unset;host.setLore(slug, tips)changes it) andwatchLinked(worldId, cb), whichhost.link(worldId, slug | null)fires. - Observe:
host.sent/host.sends(kind, sid)(command,gmcp,mcp,msdp),host.widgets(sid),host.widget(id),host.scene(sid),host.channels(sid),host.setting(key),host.panels,host.commands,host.routers,host.toasts,host.logs,host.errors(a throwing handler is contained and logged, as in μClient),host.live(), andhost.calls(anymumember the stub does not model, by path). readMurec(src)checks a recording, andrecordedSends(src)lists what the recorded client sent, for comparing withhost.sent.- Matching follows μClient:
gmcp.onexact or prefix, any case;mcp.onexact or a prefix ending in-;msdp.onby variable or a prefix ending in_.
npm create @runmu.sh/extension scaffolds test/index.test.mjs and test/fixtures/session.murec. npm test runs them.
