The live REPL
A running RUSM node isn't a black box you watch from the outside — it's a system you can reach into and drive. rusm attach opens a JavaScript shell into the live node: type real code against real processes while the node keeps serving. It's the BEAM's iex --remsh, brought to WebAssembly — and it's how you inspect, debug, and operate a node without redeploying it.
Why this matters
Most runtimes hand you logs and a metrics dashboard. When something misbehaves you add more logging and ship again. RUSM gives you a prompt inside the process table: ask the live system what it's doing, poke it, and watch it respond. Nothing to wire up — the node you started with rusm node start already exposes it.
rusm node start
rusm attachdetail, help, and quit are shell commands; every other line is JavaScript, evaluated against the node. Bindings persist across lines, so you build up context as you go.
The whole Process API, at a prompt
Everything a component can do, you can do live — the Process global is in scope:
> Process.list().length // how many processes are alive
1284
> p = Process.whereis("store") // find a service by name (the binding persists)
43
> Process.isAlive(p)
true
> Process.whereisTag("room:general").length // who's in a chat room right now
12Call a running service — and get the reply
connect(name) hands you a typed client over a resident service; call its methods and await the result, straight from the shell. (spawn(name) does the same over a freshly-spawned component.)
> await connect("store").list()
[{"id":1,"text":"ship the docs"}]
> await connect("store").add({ text: "write the REPL page" })
{"id":2,"text":"write the REPL page"}That's a real request/reply over the actor wire — the same typed client your components use, pointed at the live node from your terminal.
Operate the live system
Message a process, schedule a delayed send, disconnect a whole group, stop a runaway:
> Process.send(p, JSON.stringify({ op: "flush" })) // fire-and-forget
> Process.sendAfter(p, 5000, "reminder") // deliver in 5s → a timer handle
1
> Process.killTag("room:general") // disconnect everyone in the room
12
> Process.kill(stuck) // stop a wedged process
trueOne prompt, every language
whereis / send / kill speak the actor wire, not a language — so the same shell inspects and drives Rust, TypeScript, and Go processes identically. A Go WebSocket connection, a Rust store service, a TypeScript worker: from here they're all just pids.
Stateful, async, forgiving
- Bindings persist.
const p = Process.whereis("store")on one line, usepon the next —const/let/var/function/classand bare assignments all carry over. - Top-level
awaitjust works.const ok = await connect("api").healthy(). console.*is echoed back. Output your guest logs to your prompt withconsole.log.- Errors don't end your session. A throw — or a typo — is reported; your bindings survive, and the next line runs.
How it works
Each connection gets its own sandboxed REPL process — a Trusted JavaScript worker on the shared rquickjs runner, which is RUSM's dynamic-JS machinery pointed at you. Your lines run in that one process's persistent scope; it inspects and messages the rest of the node over the ordinary actor API, and it dies with your connection. A line that blocks forever (awaiting a message that never comes) is bounded by a timeout that resets the session, so the shell can never wedge.
Local-only — for now
Eval is accepted only from a loopback client; the node refuses it from a remote attach. That is the deliberate interim boundary: a JavaScript shell into a node is, by design, as powerful as iex --remsh, so until the attach channel is authenticated (Phase 12) it stays on the machine that started the node. A remote rusm attach still gets full read-only observe.
See observe a running node for the watch side, and live attach for the protocol underneath.