@gwigz/slua

CLI

Commands, addressing, flags, and JSON output for slua-viewer

The package installs a slua-viewer bin. Examples use that name, which resolves inside a package script or after a devDependency install. From a bare shell, prefix them with bunx @gwigz/slua-viewer-client (or npx, pnpm dlx).

Publishing an object

Nothing can be read or written until the viewer publishes the object, and selecting it does not do that. Open the object's Build window, go to Content and press "Explore in IDE".

The button only works when an editor client is already connected. Press it with nothing connected and it launches your external editor instead, which is why it can look like it does nothing here. So connect first:

slua-viewer push dist/main.slua --wait

--wait holds the connection open until the object arrives, saying what it is waiting for. Every command that resolves or lists a published object takes it. Addressing an object by UUID skips all of this, since the viewer publishes those on demand. Names and description keys it cannot look up.

Stale inventory

The published inventory goes briefly stale right after a save. The item, or the whole object, can drop out of the listing, or a save comes back as Item not found in prim inventory with the item still there under the same id. Commands re-read and retry a few times before giving up.

Commands

CommandWhat it does
objectsList objects the viewer has published
pull <object>/<item> [out]Fetch script or notecard content
push [file] [object]/[item]Upload and compile, non-zero exit on failure
link <name>Pair a target with the published object
reset <object>/<item>Reset a script
set-running on|off <object>/<item>Start or stop a script
logsStream runtime output from published objects
connectHold a session: watch, push and tail in one
statusWhat the running session is doing
waitBlock until the next push settles
mcpServe the session to an agent over MCP
syntax [defs.lsl|defs.lua]Dump language definitions

objects

Every published object, its child prims and their inventory, each script marked running or stopped, with the object's description, where link keys live.

slua-viewer objects

push

Uploads a file and compiles it in place, preserving the script's running state.

slua-viewer push dist/main.slua 4f2b0c1e-.../Main

The destination can also come from slua.json or a source header, in which case the file alone is enough, with --target and --all for multi-script projects. See Targets. The VM comes from --vm, then the extension (.slua, .luau and .lua are luau, .lsl is mono), then whatever the item already is.

A save can succeed while the compile fails, and the source is stored either way. Either failure exits non-zero, and with --all the targets behind it still run.

A save also restarts the script, so push listens for 1.5s afterwards and prints what that object and item said under the compile result:

compiled Main.luau in My Rezzer
say My Rezzer/Main  ready

--tail 10s widens that window, bare --tail listens until you interrupt it, and --no-tail skips it. The result carries a cursor, the point everything the push caused comes after. Anything the script says later, from a touch or a timer, needs a connection that is still open, so connect or logs --follow.

pull

Writes the item's content to out, or to stdout when no path is given. Content the viewer base64-encodes is decoded for you.

slua-viewer pull "My Rezzer/Main" src/main.recovered.luau

logs

Streams runtime.debug and runtime.error output, stack frames included.

slua-viewer logs --object 4f2b0c1e-... --follow

Output arrives live or not at all. The viewer buffers nothing and cannot replay, so a line spoken while nothing was connected is gone. --follow at least reconnects with backoff when the viewer restarts. Run it that way unless you are scripting against it.

Only published objects forward output, so pass --object with a UUID to have one published on demand, or --wait to publish it from the viewer. Every published object then reaches every connection, so --object also narrows the stream to one object, matching each prim in its linkset, and --targets narrows it to the items your slua.json deploys to.

Lines are labelled object/item, llOwnerSay output is tagged say rather than debug, and --targets has something to filter on. All three need a viewer advertising unifiedDiagnostics, which sends an item reference with each event. Against an older one logs warns once and shows everything.

Runtime positions point at the generated Lua (lua_script:5), so each line is annotated with the TypeScript it maps to, using the source maps of the targets in your slua.json.

--since takes a cursor, the number each record carries in --json, or a duration like 5m. It reads from a running session, or from .slua/logs.jsonl when there is none.

connect

push and logs do one thing and let go. connect stays, which is what the viewer expects. It hears the llOwnerSay from a touch you make ten minutes later, lets you press "Explore in IDE" on a new object whenever you like, watches the built outputs your slua.json names, and pushes when they change.

slua-viewer connect --exec "bun run build:watch"

--exec runs your build inside the session and prefixes its output, so the whole loop is one terminal. The scaffolds wire this up as bun run dev. --no-watch still holds the connection, streams output and serves the control socket, which is what an agent driving pushes explicitly wants.

Every push restarts the script and costs an asset upload through the simulator, so the watcher waits for a burst of edits to finish rather than reacting fast:

  • A 3s trailing debounce (--debounce, --edge leading)
  • Changes during a push collapsed into one follow-up, not a queue
  • A minimum interval per item
  • A rebuild that produced identical bytes ignored outright

A failed compile says what it left behind. The save succeeded, so the item holds source that never compiled, while the previously compiled version keeps running.

While a session is running, the other commands go through it rather than opening their own connection. --direct opts out.

status, wait and mcp

status reports what the session is doing, wait blocks until a push newer than a given cursor settles and then reports its results with the output that followed, and mcp serves both to an agent over stdio MCP. See Driving it from an agent.

syntax

Without an argument, prints the viewer's syntax id and the definition files it has cached. With defs.lsl or defs.lua, dumps that definition set as JSON.

Addressing

<object>/<item>, or <object>/<link>/<item> for an item in a child prim. The --object, --link and --item flags are equivalent.

slua-viewer push dist/main.slua 4f2b0c1e-.../Main
slua-viewer push dist/main.slua "desc:slua:my-project/Main"
slua-viewer push dist/main.slua "My Rezzer/Panel/Main"

The object segment takes a UUID, a name, or an explicit id: / name: / desc: prefix. Items match with or without the display extension, so Main and Main.luau are the same item. Child prims match on link name, Name (2) when siblings share a name, link number, or UUID.

Only published objects can be addressed, and only a UUID publishes one on demand, see Publishing an object.

A session's state

connect keeps .slua/ beside your slua.json. session.json holds the pid, socket path, viewer port and CLI version, and goes away on a clean exit. logs.jsonl holds every runtime record and push result as one JSON object per line, survives the session crashing, and rotates at 5MB keeping one old file.

Ignore it

Add .slua/ to your .gitignore. Projects scaffolded with @gwigz/slua-create already do.

Driving it from an agent

Three tiers, in order of how little integration they need.

Read the file. tail -n 100 .slua/logs.jsonl needs nothing from us, and still answers after the session has gone.

Run the CLI. status, wait and logs --since are thin wrappers over the session, and everything speaks --json:

slua-viewer status --json
slua-viewer wait --since 412 --for 20s
slua-viewer logs --since 5m

wait takes the cursor you last saw, not "the current push". Usually nothing is in flight, so that form would hand back the previous run's results.

Speak MCP. slua-viewer mcp is a stdio MCP server over the session's control socket, exposing slua_status, slua_push, slua_wait and slua_logs. slua_push pushes and waits in one call, returning the compile result per target and the output the restarted script produced.

{
  "mcpServers": {
    "slua": { "command": "slua-viewer", "args": ["mcp"], "cwd": "/path/to/project" },
  },
}

The control socket

A unix socket, or a named pipe on Windows, at mode 0600 in your temp directory, keyed by a hash of the project root. The path is recorded in .slua/session.json. It speaks the viewer's own JSON-RPC, so a ViewerClient connects to a session exactly as it connects to the viewer, with only the control.* namespace new. Viewer calls forward upstream, viewer notifications broadcast back down.

Local processes

Any local process that can reach that socket can push scripts to your viewer. That is already true of port 9020 itself, so it is not a new exposure, but it is worth knowing.

Options

FlagApplies toMeaning
--object <id|name>mostTarget object, same grammar as the object segment
--item <id|name>mostTarget inventory item
--link <id|name>mostChild prim within the linkset
--vm <vm>pushCompile target: luau, mono, lsl2
--target <name>pushDeploy a named target from slua.json
--allpushDeploy every target in slua.json
--save-backpushDerez back into the source prim after a good compile
--file <path>push, linkFile to push, or to record when linking
--key <key>linkDescription key to pair on, default slua:<name>
-f, --followlogsKeep streaming, reconnecting if the viewer restarts
--targetslogsOnly output from items your slua.json targets
--tail [duration]pushKeep listening after the push, 5s, or until ctrl-c
--no-tailpushPush without waiting for output at all
--watch/--no-watchconnectPush when a target's output changes, on by default
--debounce <ms>connectHow long a target must be quiet first, default 3000
--edge <edge>connecttrailing (default) or leading
--exec <command>connectRun a build alongside the session
--since <cursor|duration>logs, waitOutput after a cursor, or from the last 5m
--for <duration>waitHow long to block, default 30s
--directallTalk to the viewer even with a session running
--waitmostHold the viewer connection open until it publishes
--port <port>allViewer websocket port, default 9020
--timeout <ms>allRequest timeout
--jsonallMachine-readable output on stdout

Compile errors

push looks for a source map beside the file it uploads (dist/main.slua.map) and translates the viewer's Lua line numbers back to your source, printing the offending line with each error. Enable it with "sourceMap": true in your tsconfig. Without a map, errors point at the generated output and the CLI says so. Mapping is line accurate, not column accurate. LSL compile errors carry real column numbers, which the Luau compiler does not report.

logs maps too. A viewer that names the item its output came from narrows that to the target deploying it. Without one, a line covered by more than one target's map is shown against every candidate, with its target name.

Bundled output

If you bundle with @gwigz/tstl-bundle-flatten, you need 1.2.0 or newer. Earlier versions rewrote the emitted Lua without updating the map, leaving it describing the unflattened bundle.

JSON output

--json puts exactly one JSON document on stdout and nothing else, with progress and errors kept on stderr. A failure still emits a document ({"ok": false, "error": "..."}), so empty stdout always means something went badly wrong.

slua-viewer push dist/main.slua --json | jq .compiled

Streams are the exception and emit one JSON object per line: logs --json, and a push --json --tail given no duration to stop at, which prints its document first and then streams. A bounded push --tail keeps the single-document contract by attaching what it drained as logs.

Keys are camelCase (objectId, primId, itemId, savedBack), matching the library rather than the viewer's wire format. See Naming.

On this page