@solidrt/cli 0.0.50 → 0.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +79 -36
- package/README.md +86 -15
- package/agents/assets.md +46 -0
- package/agents/debugging.md +294 -0
- package/dist/console.srtapp +94123 -73
- package/dist/server.js +3936 -0
- package/package.json +11 -10
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +163 -76
- package/src/bundle/docs.md +12 -0
- package/src/bundle/main.ts +203 -0
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -34
- package/src/client/docs.md +9 -0
- package/src/client/main.ts +33 -0
- package/src/console/docs.md +14 -0
- package/src/console/main.ts +21 -0
- package/src/demo/docs.md +27 -0
- package/src/demo/main.ts +67 -0
- package/src/init/docs.md +11 -0
- package/src/{commands/init.ts → init/main.ts} +31 -25
- package/src/init/scaffold/AGENTS.md +98 -0
- package/src/init/scaffold/package.json +23 -0
- package/{scaffold → src/init/scaffold}/templates/components/index.tsx +2 -3
- package/{scaffold → src/init/scaffold}/templates/default/index.tsx +2 -3
- package/src/lib/args.ts +194 -0
- package/src/{artifacts.ts → lib/artifacts.ts} +41 -1
- package/src/{dev-dir.ts → lib/dev-dir.ts} +10 -8
- package/src/{fonts.ts → lib/fonts.ts} +12 -26
- package/src/lib/mode.ts +77 -0
- package/src/{project.ts → lib/project.ts} +109 -61
- package/src/lib/registry.ts +120 -0
- package/src/lib/server-bundle.ts +24 -0
- package/src/lib/usage.ts +117 -0
- package/src/lib/util.ts +36 -0
- package/src/main.ts +109 -31
- package/src/mcp/docs.md +22 -0
- package/src/mcp/main.ts +719 -0
- package/src/pack/docs.md +18 -0
- package/src/{pack-folder.ts → pack/layout.ts} +13 -22
- package/src/pack/main.ts +86 -0
- package/src/pack/trailer.ts +97 -0
- package/src/render/docs.md +19 -0
- package/src/render/main.ts +49 -0
- package/src/server/args.ts +126 -0
- package/src/server/binaries.ts +47 -0
- package/src/server/config.ts +54 -0
- package/{server → src/server}/control.ts +246 -78
- package/src/server/docs.md +51 -0
- package/src/server/line-editor.ts +200 -0
- package/src/server/main.ts +473 -0
- package/src/server/mode.ts +92 -0
- package/src/server/rebuild.ts +90 -0
- package/src/server/registry.ts +138 -0
- package/src/server/remap.ts +60 -0
- package/src/server/repl.ts +223 -0
- package/src/server/state.ts +54 -0
- package/{server → src/server}/tsconfig.json +1 -1
- package/{server → src/server}/tunnel.ts +6 -6
- package/src/server/watcher.ts +121 -0
- package/src/tool/main.ts +70 -0
- package/src/types/bundle.d.ts +24 -0
- package/src/types/control.d.ts +90 -0
- package/src/types/registry.d.ts +16 -0
- package/scaffold/AGENTS.md +0 -630
- package/scaffold/package.json +0 -22
- package/scaffold/templates/components/icon.tsx +0 -48
- package/scaffold/templates/default/icon.tsx +0 -48
- package/server/main.ts +0 -308
- package/server/rebuild.ts +0 -68
- package/server/remap.ts +0 -47
- package/server/state.ts +0 -93
- package/src/args.ts +0 -212
- package/src/bundle-cli.ts +0 -13
- package/src/commands/bundle.ts +0 -76
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -603
- package/src/commands/pack.ts +0 -65
- package/src/commands/render.ts +0 -24
- package/src/commands/server.ts +0 -73
- package/src/dev-android.ts +0 -176
- package/src/dev-client.ts +0 -29
- package/src/dev-server.ts +0 -302
- package/src/packer.ts +0 -103
- package/src/repl.ts +0 -233
- package/src/util.ts +0 -121
- package/src/watcher.ts +0 -69
- /package/src/{untyped-deps.d.ts → bundle/untyped-deps.d.ts} +0 -0
- /package/src/{prompt.ts → init/prompt.ts} +0 -0
- /package/{scaffold → src/init/scaffold}/gitignore +0 -0
- /package/{scaffold → src/init/scaffold}/icon.svg +0 -0
- /package/{scaffold → src/init/scaffold}/mcp.json +0 -0
- /package/{scaffold → src/init/scaffold}/tsconfig.json +0 -0
- /package/{server → src/server}/cache.ts +0 -0
- /package/{server → src/server}/proxy.ts +0 -0
- /package/{server → src/server}/qr.ts +0 -0
package/AGENTS.md
CHANGED
|
@@ -1,41 +1,78 @@
|
|
|
1
1
|
# @solidrt/cli - agent notes
|
|
2
2
|
|
|
3
|
-
Dense, self-contained facts for running and verifying a SolidRT app.
|
|
4
|
-
|
|
5
|
-
reactivity), see @solidrt/core (its
|
|
3
|
+
Dense, self-contained facts for running and verifying a SolidRT app. The
|
|
4
|
+
prose lives in README.md and src/<command>/docs.md (also the website). For
|
|
5
|
+
the authoring model (elements, props, reactivity), see @solidrt/core (its
|
|
6
|
+
AGENTS.md).
|
|
6
7
|
|
|
7
8
|
`srt` is the dev tool. Bun is a dev prerequisite only; SolidRT apps run on the
|
|
8
9
|
bundled `flux` runtime, not on Bun. Invoke via `bunx srt <command>`.
|
|
9
10
|
|
|
11
|
+
The dev loop against a running app is pause_watch -> edit -> reload ->
|
|
12
|
+
resume_watch -> get_logs -> get_snapshot, with mute_user_input while you
|
|
13
|
+
measure or test and unmute_user_input after; agents/debugging.md has the
|
|
14
|
+
why of each hold. `reload` surfaces build errors but not type errors:
|
|
15
|
+
`bunx srt check` is for those.
|
|
16
|
+
|
|
17
|
+
agents/ carries the depth this one leaves out; read the one that matches
|
|
18
|
+
the work before starting it:
|
|
19
|
+
- agents/debugging.md - driving a running app over MCP, pointing an agent
|
|
20
|
+
client at the `srt mcp` server, what the dev server serves and its control
|
|
21
|
+
API without MCP (a shell, a CI step), and the debugging lessons that cost
|
|
22
|
+
real time. Read before investigating a bug, verifying a change against the
|
|
23
|
+
live app, or scripting against a server.
|
|
24
|
+
- agents/assets.md - the assets/ folder, inlined imports and the bundle
|
|
25
|
+
output; fonts, the `solidrt` package.json identity key, and distribution
|
|
26
|
+
builds. Read before adding an asset or a font, or building to distribute.
|
|
27
|
+
|
|
10
28
|
## Commands
|
|
11
29
|
|
|
12
30
|
- `bunx srt init <dir>` - scaffold a new SolidRT project into a new (empty)
|
|
13
31
|
folder: package.json, tsconfig.json, AGENTS.md, a starter src/index.tsx, an
|
|
14
32
|
empty assets/ (everything in it ships with the app), then installs deps. Greenfield shortcut (no install needed first):
|
|
15
33
|
`bun create solidrt <dir>`.
|
|
16
|
-
- `bunx srt run
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
- `bunx srt run` - dev server + a local client window, from the project root
|
|
35
|
+
(entry `solidrt.entry` in package.json, default src/index.tsx); `bunx srt run
|
|
36
|
+
<file>` serves a single file outside a project. NEEDS A DISPLAY (opens a GUI
|
|
37
|
+
window). Not usable headless. Reloads on save (the bundle's inputs and
|
|
38
|
+
`assets/`); an agent pauses that with `pause_watch` and pushes its edits
|
|
39
|
+
with the MCP `reload` tool.
|
|
40
|
+
- `bunx srt tool` - list the build-time tools the installed `@solidrt/*`
|
|
41
|
+
packages ship (`<package>/tools/<name>.ts`, named `<package>/<name>`);
|
|
42
|
+
`bunx srt tool <package>/<name> [arguments]` runs one under bun in the
|
|
43
|
+
project, everything after the name passed through as the tool's own
|
|
44
|
+
arguments (each tool prints its own usage on `--help`). What a tool does
|
|
45
|
+
is the package's business (e.g. `3d/model` bakes a glTF into a model
|
|
46
|
+
file); srt only finds and runs them.
|
|
47
|
+
- `bunx srt check [file|dir]` - build in memory and typecheck, no output
|
|
48
|
+
and no reload. With a folder it covers every entry under it (src/index.tsx
|
|
49
|
+
and examples/*), so `bunx srt check .` answers "did I break any example"
|
|
50
|
+
in one call. The dev server runs it once at startup without gating on it.
|
|
51
|
+
- `bunx srt bundle` - bundle the project into `dist/bundle/` (or
|
|
52
|
+
`--output <dir>`): `<name>.srt.js` plus the app's isolate modules as
|
|
53
|
+
`isolates/<id>.js`. With `--compile`, bytecode (`.srt.bin`/`.bin`)
|
|
54
|
+
instead. Move the dir, not the bare file - a bundle loaded without its
|
|
55
|
+
isolates/ dir loses them (`--stdout` cannot carry them at all).
|
|
56
|
+
`--minify`, `--dev` also available.
|
|
57
|
+
- `bunx srt render [flags]` - render the project OFFSCREEN to PNG frames,
|
|
22
58
|
optionally replaying a `--script` file recorded via `--capture`.
|
|
23
59
|
- `bunx srt server [file]` / `bunx srt client` - the two halves of `run`
|
|
24
60
|
separately (server distributes code; clients on other devices connect to it).
|
|
25
|
-
- `bunx srt run
|
|
61
|
+
- `bunx srt run --capture out.script.json` - records keydown/keyup
|
|
26
62
|
from every connected client into one script file (written on client
|
|
27
63
|
disconnect), for replaying later with `render --script`. The file is JSON
|
|
28
|
-
Lines and hand-authorable
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
|
|
64
|
+
Lines and hand-authorable: one object per line,
|
|
65
|
+
`{"after": <ms since previous event>, "type": "keydown" | "keyup",
|
|
66
|
+
"key": <W3C KeyboardEvent.key>, "device": <client id>}`. For probing app
|
|
67
|
+
state without a display, `-- <args...>` reaches the app as `flux:process`
|
|
68
|
+
argv, which is often simpler than scripting input.
|
|
32
69
|
|
|
33
70
|
## Verifying without a display (headless / CI / agent box)
|
|
34
71
|
|
|
35
72
|
Two reliable checks that need no GUI:
|
|
36
73
|
|
|
37
|
-
1. `bunx srt bundle
|
|
38
|
-
2. `bunx srt render
|
|
74
|
+
1. `bunx srt bundle` - exit 0 means the app compiles. Fast.
|
|
75
|
+
2. `bunx srt render --size 480x640 --duration 1 --fps 2` -
|
|
39
76
|
renders offscreen via EGL and writes `frame-NNNNNN.png`. This actually
|
|
40
77
|
proves the app renders. Combine with `--fps`/`--duration` (defaults
|
|
41
78
|
1280x720, 60fps, 1s). No display needed: rendering uses SDL's offscreen
|
|
@@ -63,29 +100,35 @@ behavior in isolation.
|
|
|
63
100
|
(Wayland) and the pbuffer path on Windows from a desktop session; a
|
|
64
101
|
non-interactive Windows session and macOS are unverified.
|
|
65
102
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
## Servers (what is served, and finding it again)
|
|
104
|
+
|
|
105
|
+
- A dev server serves a project (started in its root: the cwd must hold the
|
|
106
|
+
package.json) or a single file (`srt run <file>` outside a project). Both
|
|
107
|
+
in one place is ambiguous, so `srt run <file>` in a project root needs
|
|
108
|
+
`--project` (the project, with this entry) or `--file` (the file alone).
|
|
109
|
+
Nothing searches upward for a package.json.
|
|
110
|
+
- One server per project or file. The server binds the port it had last
|
|
111
|
+
time, else the first free one from 34884 up, and prints it; `--port <N>`
|
|
112
|
+
pins it. Loopback only
|
|
113
|
+
unless `--lan`, which is what phones and other devices (and `srt android`
|
|
114
|
+
on a real device) need; `--tunnel` works without it.
|
|
115
|
+
- `-c <N>` / `--client <N>` picks the client data tree (default 0). Storage
|
|
116
|
+
is per app inside a tree, so two projects share client 0; only two clients
|
|
117
|
+
of the same app need distinct slots.
|
|
118
|
+
- Dev state lives in `~/.solidrt/`: `servers/<key hash>/` holds each server's
|
|
119
|
+
`live.json` (the registry record, written by the server and removed at
|
|
120
|
+
exit), its remembered `port` and tunnel key; `clients/client<M>/` the
|
|
121
|
+
client trees (srt passes `--data-root ~/.solidrt/clients` to every locally
|
|
122
|
+
spawned client). A record left behind by a crash is pruned when the next
|
|
123
|
+
server starts, and `srt client`, `srt mcp` and `srt android` confirm a
|
|
124
|
+
record against the server (its control API names the key it serves)
|
|
125
|
+
before using it.
|
|
126
|
+
- `srt client` and `srt mcp` need no port: run from the project root (or the
|
|
127
|
+
directory of a served file) they resolve the server from the registry;
|
|
128
|
+
`--port` pins one.
|
|
82
129
|
|
|
83
130
|
## Dev server proxies (when clients on other devices need your machine's data)
|
|
84
131
|
|
|
85
132
|
- `--proxy-http` - route `fetch` through the dev server; responses cached in
|
|
86
133
|
`.srt-data/http-cache.db` in the project root (delete the file to clear).
|
|
87
134
|
|
|
88
|
-
## REPL (opened by `run`/`server`)
|
|
89
|
-
|
|
90
|
-
`load <file>`, `reload [n]`, `stop [n]`, `list`, `!<cmd>`, `quit`/`exit`.
|
|
91
|
-
`load` is bound to the project root the server was started in.
|
package/README.md
CHANGED
|
@@ -1,26 +1,97 @@
|
|
|
1
1
|
# @solidrt/cli
|
|
2
2
|
|
|
3
|
-
Developer tooling for SolidRT
|
|
3
|
+
Developer tooling for SolidRT: `srt`, one command-line tool covering the
|
|
4
|
+
whole cycle of a `@solidrt/core` application: scaffold, develop, inspect,
|
|
5
|
+
ship. A GUI inspector, itself a SolidRT app, is in progress alongside it.
|
|
4
6
|
|
|
5
|
-
> LLM agents:
|
|
7
|
+
> LLM agents: `AGENTS.md` in this package is a dense, self-contained quickstart.
|
|
8
|
+
|
|
9
|
+
Bun is a dev prerequisite only; apps run on the bundled `flux` runtime.
|
|
10
|
+
Invoke via `bunx srt <command>`. A scaffolded project wires the common
|
|
11
|
+
commands into scripts, so day-to-day work is `bun run dev`, `bun run
|
|
12
|
+
android` (and `server`, `client`, `pack` for the rest).
|
|
6
13
|
|
|
7
14
|
## Commands
|
|
8
15
|
|
|
16
|
+
| Command | |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| [`srt init <dir>`](src/init/docs.md) | scaffold a new project into a new (empty) folder |
|
|
19
|
+
| [`srt run [file]`](src/server/docs.md) | dev server + local client window |
|
|
20
|
+
| [`srt server [file]`](src/server/docs.md) | dev server only |
|
|
21
|
+
| [`srt client`](src/client/docs.md) | client only, attached to the project's dev server |
|
|
22
|
+
| [`srt android`](src/android/docs.md) | install and launch the client on a connected Android device |
|
|
23
|
+
| [`srt demo [n]`](src/demo/docs.md) | list the demos the installed packages ship, or run one |
|
|
24
|
+
| [`srt console`](src/console/docs.md) | start the dev console: the dev servers on this machine and their clients |
|
|
25
|
+
| [`srt check [file]`](src/check/docs.md) | build and typecheck, writing nothing |
|
|
26
|
+
| [`srt bundle [file]`](src/bundle/docs.md) | transpile to JS or bytecode (dist/bundle/) |
|
|
27
|
+
| [`srt render [file]`](src/render/docs.md) | render frames offscreen, optionally replaying a script |
|
|
28
|
+
| [`srt pack [file]`](src/pack/docs.md) | bundle + compile to a standalone executable (experimental) |
|
|
29
|
+
| [`srt mcp`](src/mcp/docs.md) | MCP server (stdio) exposing the running dev server to agents |
|
|
30
|
+
|
|
31
|
+
`srt --help` lists every command and option; `srt --version` prints the
|
|
32
|
+
version. Run from the project root to work on the project (its entry is
|
|
33
|
+
`solidrt.entry` in package.json, default `src/index.tsx`); pass a file to
|
|
34
|
+
work on that file on its own.
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
srt run
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Starts the dev server and a local client window against it. The server
|
|
43
|
+
pushes the bundle to every connected client, so one server can drive a
|
|
44
|
+
desktop window and a phone at the same time; edits reach them on an
|
|
45
|
+
explicit reload (the MCP `reload` tool) or on save.
|
|
46
|
+
|
|
47
|
+
Split them when you need to:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
srt server # server only
|
|
51
|
+
srt client # client only, the project's server (from its root)
|
|
52
|
+
srt client --server 192.168.1.5:34884 # client only, pointed at that address
|
|
53
|
+
srt android # install and launch on a connected device
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Inspect
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
srt mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Exposes the running app to a coding agent: logs, stats, the live render
|
|
63
|
+
tree, screenshots, GPU resources, input injection, a virtual-time transport,
|
|
64
|
+
reload, and the app's own debug commands. A scaffolded project ships an
|
|
65
|
+
`.mcp.json`, so Claude Code attaches with no setup.
|
|
66
|
+
|
|
67
|
+
## Record and replay
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
srt run --capture session.json
|
|
71
|
+
srt render --script session.json --fps 60 --duration 5
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--capture` records key events from connected clients to a script; `render`
|
|
75
|
+
replays it headlessly and writes frames, which makes bug reports
|
|
76
|
+
reproducible and turns an interaction into a video.
|
|
77
|
+
|
|
78
|
+
## Ship
|
|
79
|
+
|
|
9
80
|
```sh
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
81
|
+
srt check . # build and typecheck every entry, no build output
|
|
82
|
+
srt bundle # transpile to JS or bytecode
|
|
83
|
+
srt pack # standalone executable (experimental)
|
|
13
84
|
```
|
|
14
85
|
|
|
15
|
-
|
|
86
|
+
`srt bundle --flux` and `srt pack --flux` target the bare Flux runtime
|
|
87
|
+
instead of a SolidRT app, for scripts and servers with no UI.
|
|
16
88
|
|
|
17
|
-
|
|
89
|
+
## Layout
|
|
18
90
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| `quit` / `exit` | exit the dev server |
|
|
91
|
+
One folder per command under `src/`, named for what it does; the first line
|
|
92
|
+
of each says its runtime. `src/server/` is the dev server, a flux script
|
|
93
|
+
(`bun`-free at runtime, its own tsconfig); every other command runs on bun.
|
|
94
|
+
`src/lib/` is what the bun commands share, `src/types/` the type-only
|
|
95
|
+
contracts between the two runtimes. Each command folder carries its
|
|
96
|
+
`docs.md` (this site); the depth agents need lives in `agents/` at the
|
|
97
|
+
package root, as in the other packages.
|
package/agents/assets.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Assets, fonts, identity and distribution
|
|
2
|
+
|
|
3
|
+
Read this before adding an asset or a font, setting the app's identity, or
|
|
4
|
+
preparing a build for distribution.
|
|
5
|
+
|
|
6
|
+
## Assets and inlined imports
|
|
7
|
+
|
|
8
|
+
- Everything under `assets/` ships with the app: the folder is collected
|
|
9
|
+
wholesale into each build's version manifest (no bundler analysis, no
|
|
10
|
+
registration step). Reference assets by path - `file("assets/sounds/x.ogg")`
|
|
11
|
+
from `flux:fs` - and treat them as read-only at runtime; writes belong in
|
|
12
|
+
plain relative paths, which land in the app's private data dir.
|
|
13
|
+
- Small text-like assets (SVG documents, shaders) can instead be inlined via
|
|
14
|
+
imports. An import attribute picks the form and works on any extension:
|
|
15
|
+
`import src from "./effect.glsl" with { type: "text" }` yields the file's
|
|
16
|
+
contents as a string, `with { type: "binary" }` yields a Uint8Array. `.svg`
|
|
17
|
+
is text-loaded with no attribute needed. Shader sources (`.glsl`/`.vert`/
|
|
18
|
+
`.frag`) are declared as text modules out of the box, so they typecheck
|
|
19
|
+
without setup. Inlining trades update granularity for zero I/O - keep big or
|
|
20
|
+
streamable files (audio, images) in `assets/`.
|
|
21
|
+
- `bunx srt bundle` writes `dist/bundle/` (or `--output <dir>`):
|
|
22
|
+
`<name>.srt.js` plus the app's isolate modules as `isolates/<id>.js`; with
|
|
23
|
+
`--compile`, bytecode (`.srt.bin`/`.bin`) instead. Move the dir, not the
|
|
24
|
+
bare file - a bundle loaded without its isolates/ dir loses them
|
|
25
|
+
(`--stdout` cannot carry them at all). Isolates (`"use isolate"` modules)
|
|
26
|
+
exist in projects only, not for a file served on its own.
|
|
27
|
+
- `bunx srt bundle --json` is the dev server's rebuild contract: one JSON
|
|
28
|
+
object (code, sourcemap, manifest, isolates) on stdout. Not for humans.
|
|
29
|
+
|
|
30
|
+
## Fonts, identity and distribution
|
|
31
|
+
|
|
32
|
+
- Custom fonts go in `assets/fonts/` and are declared in the `solidrt.fonts`
|
|
33
|
+
map in package.json (alias -> file path; role aliases `sans`/`serif`/`mono`
|
|
34
|
+
replace the built-in defaults, `false` drops one, other keys add fonts
|
|
35
|
+
selectable via fontFamily). A newly added font shows after restarting the
|
|
36
|
+
client.
|
|
37
|
+
- The `solidrt` key in package.json is the app's identity: set a stable
|
|
38
|
+
reverse-DNS `appId` before distributing - it keys the app's storage
|
|
39
|
+
folder, defaults from the package name in dev, and `srt pack` warns
|
|
40
|
+
while defaulted. `org` and `displayName` are optional display metadata
|
|
41
|
+
(future launcher/window naming) with no storage meaning.
|
|
42
|
+
- `bunx srt pack` builds a single-file executable (the runner with the
|
|
43
|
+
bytecode, manifest, assets and fonts appended as a trailer);
|
|
44
|
+
`bunx srt pack --folder` writes the flat app folder
|
|
45
|
+
(runner + manifest.json + bundle + assets/, plus the runner's GL
|
|
46
|
+
libraries on Windows and macOS) to `dist/pack/`.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Debugging a running app
|
|
2
|
+
|
|
3
|
+
Read this before investigating a bug in a running app, or before driving the
|
|
4
|
+
app to verify a change: over MCP, or through the same control API from a
|
|
5
|
+
shell or a CI step ("The control API without MCP" below).
|
|
6
|
+
|
|
7
|
+
## Driving the app over MCP
|
|
8
|
+
|
|
9
|
+
The project ships an MCP server (`srt mcp`) that talks to the dev server
|
|
10
|
+
`bunx srt run` starts. If your client lists no `solidrt` tools, it has not
|
|
11
|
+
been pointed at the server yet - see "Wiring up an agent client" below. Each
|
|
12
|
+
tool documents itself in full - read the tool description rather than guessing
|
|
13
|
+
at its arguments. What the individual descriptions cannot tell you:
|
|
14
|
+
|
|
15
|
+
- If `list_clients` is empty, no app is running: ask the user to start
|
|
16
|
+
`bunx srt run` rather than starting a second one yourself.
|
|
17
|
+
The bridge needs no port: it resolves the server currently serving this
|
|
18
|
+
project, whatever `--port` it was started with, and re-resolves when
|
|
19
|
+
that server goes away or a different project's server takes its port.
|
|
20
|
+
Passing the flag to `srt mcp` pins the bridge to that one server instead.
|
|
21
|
+
- Several clients may be attached at once (desktop window, phone, tablet)
|
|
22
|
+
with different sizes, display scales and safe areas. `reload` pushes to all
|
|
23
|
+
of them, but `call_debug` / `send_input` / `get_snapshot` are per client
|
|
24
|
+
(`get_logs` takes a `client` filter), and interactive state does NOT sync
|
|
25
|
+
- a flow driven on one client leaves the others sitting on the initial
|
|
26
|
+
screen, which reads as a crash to a human holding that device. So: when
|
|
27
|
+
driving state via `call_debug`, send the same call to every client (or say
|
|
28
|
+
which client you are using); and before calling a visual change done,
|
|
29
|
+
snapshot each distinct form factor at least once - a layout that fits one
|
|
30
|
+
window can clip or overflow another.
|
|
31
|
+
- Measuring or testing? `mute_user_input` first: it mutes the user's own
|
|
32
|
+
input on every client (a stray click or keypress mid-measurement corrupts
|
|
33
|
+
the result); `send_input` still works. `unmute_user_input` the moment you
|
|
34
|
+
are done, or whenever you need the human to press something: they see an
|
|
35
|
+
unresponsive client meanwhile. The bridge unmutes when it exits, the
|
|
36
|
+
server when it stops, but neither is a reason to leave a mute on.
|
|
37
|
+
- Editing? `pause_watch` first: the server reloads on save, and a
|
|
38
|
+
half-finished save would land on the user's screens as a build error or
|
|
39
|
+
a broken app. Edit, `reload`, then `resume_watch`. The two holds are
|
|
40
|
+
separate on purpose: the mute keeps the human out while you measure, the
|
|
41
|
+
pause keeps your own saves out while you edit, and a human editing next
|
|
42
|
+
to you keeps auto-reload unless you hold it. The bridge resumes when it
|
|
43
|
+
exits, but do not rely on that.
|
|
44
|
+
- A `shader` on `<window>` runs on the finished frame past the point every
|
|
45
|
+
capture reads: `get_snapshot` returns the UNSHADED content (window node
|
|
46
|
+
included), `get_texture` has no id for the shaded layer, and
|
|
47
|
+
`get_gpu_resources` reports only that the pass exists. `bunx srt render` is
|
|
48
|
+
the only programmatic view of what a window shader produces.
|
|
49
|
+
- Permission prompts: agents typically ask approval per MCP tool. All of
|
|
50
|
+
these tools only talk to the local dev server the user started with
|
|
51
|
+
`bunx srt run` - nothing leaves the machine - so approving the server as a
|
|
52
|
+
whole is a reasonable default. If repeated prompts get in the way, do not
|
|
53
|
+
work around them; tell the user they can pre-approve the server in their
|
|
54
|
+
agent's settings (most agents have a per-server trust or allowlist setting
|
|
55
|
+
- in Claude Code, add "mcp__solidrt" to `permissions.allow` in
|
|
56
|
+
~/.claude/settings.json to cover every solidrt project). This is the user's
|
|
57
|
+
call to make, once, in their own tooling.
|
|
58
|
+
|
|
59
|
+
## Wiring up an agent client
|
|
60
|
+
|
|
61
|
+
`.mcp.json` in the project root is Claude Code's convention, and a scaffolded
|
|
62
|
+
app ships one. MCP standardizes the protocol, not how a client discovers
|
|
63
|
+
servers, so every other client reads its own file. The entry is the same
|
|
64
|
+
everywhere, in the client's syntax:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "command": "bun", "args": ["node_modules/@solidrt/cli/bin/srt", "mcp"] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Where it goes (these locations move between client releases; check the
|
|
71
|
+
client's own docs if it does not match):
|
|
72
|
+
|
|
73
|
+
- Claude Code: `.mcp.json` in the project root, under `mcpServers`. Claude
|
|
74
|
+
Code asks once, per project, before it will launch a server from this
|
|
75
|
+
file; until that is approved the `solidrt` tools are absent.
|
|
76
|
+
- Cursor: `.cursor/mcp.json`, under `mcpServers`
|
|
77
|
+
- VS Code / Copilot: `.vscode/mcp.json`, under `servers`
|
|
78
|
+
- Gemini CLI: `.gemini/settings.json`, under `mcpServers`
|
|
79
|
+
- Codex CLI: `~/.codex/config.toml`, under `[mcp_servers.solidrt]`
|
|
80
|
+
|
|
81
|
+
Several of these also ship a command that writes the entry for you
|
|
82
|
+
(`claude mcp add`, `codex mcp add`); prefer it over hand-editing.
|
|
83
|
+
|
|
84
|
+
The one constraint: the bridge must run with the project as its working
|
|
85
|
+
directory. It finds the dev server through the nearest package.json above
|
|
86
|
+
its cwd, and the `args` path is relative to the project root. A per-project
|
|
87
|
+
file gives that for free. A user-level entry works too, but only while the
|
|
88
|
+
client itself was launched from the project root (or the client supports a
|
|
89
|
+
`cwd` field and it is set); launched from elsewhere, every tool call answers
|
|
90
|
+
"No dev server for ..." while one is running.
|
|
91
|
+
|
|
92
|
+
## The dev server
|
|
93
|
+
|
|
94
|
+
What the dev server (`srt run` / `srt server`) is and what it serves.
|
|
95
|
+
|
|
96
|
+
- A dev server serves a project (started in its root: the cwd must hold the
|
|
97
|
+
package.json) or a single file (`srt run <file>` outside a project). Both
|
|
98
|
+
in one place is ambiguous, so `srt run <file>` in a project root needs
|
|
99
|
+
`--project` (the project, with this entry) or `--file` (the file alone).
|
|
100
|
+
Nothing searches upward for a package.json.
|
|
101
|
+
- One server per project or file. The server binds the port it had last
|
|
102
|
+
time, else the first free one from 34884 up, and prints it; `--port <N>`
|
|
103
|
+
pins it. Loopback only unless `--lan`.
|
|
104
|
+
- Dev state lives in `~/.solidrt/`: `servers/<key hash>/` holds each server's
|
|
105
|
+
`live.json` (the registry record, written by the server and removed at
|
|
106
|
+
exit), its remembered `port` and tunnel key; `clients/client<N>/` the
|
|
107
|
+
client trees. `srt client` and `srt mcp` resolve the server from the
|
|
108
|
+
registry by the project (or served file) at their cwd; `--port` pins one.
|
|
109
|
+
A record whose process died is pruned when the next server starts, and
|
|
110
|
+
resolution confirms a record against the server before using it.
|
|
111
|
+
- The server is one flux process, complete on its own:
|
|
112
|
+
`flux dist/server.js [file] [--project|--file] [--port N] [--lan]
|
|
113
|
+
[--proxy-http] [--capture f] [--tunnel] [--stats] [--client N ...] [-- args]`
|
|
114
|
+
(src/server/args.ts). It finds the platform binaries, bun and srt through
|
|
115
|
+
`SRT_PLATFORM_DIR`, `SRT_BUN` and `SRT_CLI`, which `srt` sets; started by
|
|
116
|
+
hand in a checkout it needs only `SRT_HOME`.
|
|
117
|
+
- Reload-on-save watches the bundle's inputs (every file the running
|
|
118
|
+
bundle was built from, dependencies included) and the `assets/` tree, not
|
|
119
|
+
a directory: a file the app does not import never triggers a rebuild.
|
|
120
|
+
While the last build failed, the source tree is watched as a whole until
|
|
121
|
+
a build succeeds. `POST /__control__/watch?active=false` pauses it (the
|
|
122
|
+
MCP `pause_watch` tool) while an agent edits; `POST /__control__/reload`
|
|
123
|
+
(the `reload` tool) is the agent's push.
|
|
124
|
+
|
|
125
|
+
## The control API without MCP
|
|
126
|
+
|
|
127
|
+
Every MCP tool is a thin wrapper over the dev server's HTTP control API, so
|
|
128
|
+
a shell script, a CI step, or an agent with no MCP bridge can drive the app
|
|
129
|
+
the same way. Base: `http://127.0.0.1:<port>/__control__/`, where `<port>`
|
|
130
|
+
is the one `srt run` printed at startup (also in the server's
|
|
131
|
+
`~/.solidrt/servers/*/live.json` record). GET unless noted;
|
|
132
|
+
every endpoint answers JSON and an error is `{ "error": "..." }` with a
|
|
133
|
+
4xx/5xx status, and every response carries `x-solidrt-project` (the key
|
|
134
|
+
served) and `x-solidrt-generation` (the server run) headers, so a caller can
|
|
135
|
+
confirm it reached the server it meant and notice a restart. Endpoints that
|
|
136
|
+
talk to a client take `?client=<id>` (from `/clients`); it may be omitted
|
|
137
|
+
when exactly one client is connected.
|
|
138
|
+
|
|
139
|
+
- `/clients` - `{ generation, key, mode, entry, projectDir, userInputMuted,
|
|
140
|
+
watchPaused, clients: [{ id, ... }] }`. Check `key` (the project root or
|
|
141
|
+
single file served) is the app you mean.
|
|
142
|
+
- `/logs?since=<seq>&level=<lvl>&contains=<text>&wait=<ms>&client=<id>` -
|
|
143
|
+
`{ entries: [{ seq, at, client, level, text, repeats? }], latest,
|
|
144
|
+
generation }`. Pass the previous `latest` as `since` to read only new
|
|
145
|
+
output; `client` keeps one client's entries (all clients without); a
|
|
146
|
+
changed `generation` means the server restarted and cursors and client
|
|
147
|
+
ids are stale.
|
|
148
|
+
- `/tree?query=<text>&root=<id>&depth=<n>&props=true` - `{ limit, matches:
|
|
149
|
+
[{ id, kind, path, x, y, width, height }] }` for a query, the nested tree
|
|
150
|
+
otherwise. Node ids are per client and change on reload; re-query after
|
|
151
|
+
`/reload`.
|
|
152
|
+
- `/snapshot?node=<id>` - `{ width, height, pngBase64 }`, display-scaled;
|
|
153
|
+
add `&format=raw` for `rgbaBase64` (RGBA8 bytes, no decoder needed for
|
|
154
|
+
pixel assertions), `&x=&y=&width=&height=` (all four) to crop,
|
|
155
|
+
`&scale=<1-8>`. Snapshot the smallest node that shows the change, not the
|
|
156
|
+
window root.
|
|
157
|
+
- `/texture?id=<textureId>` - same shape and options as `/snapshot`, at the
|
|
158
|
+
texture's native size (a scene or shader target behind a `<texture>` leaf).
|
|
159
|
+
- `/gpu?label=<text>` - the GPU resource inventory; `label` keeps only the
|
|
160
|
+
resources created with exactly that label (ids change on reload, labels
|
|
161
|
+
do not).
|
|
162
|
+
- `/buffer?id=<bufferId>&offset=<n>&length=<n>&as=<f32|u16|u8>` - vertex
|
|
163
|
+
buffer contents (default f32; reads cap at 64 KiB).
|
|
164
|
+
- `/stats?window=<ms>` - the performance statistics. POST
|
|
165
|
+
`/stats?active=true|false` switches the on-screen stats overlay instead
|
|
166
|
+
(the `set_stats_overlay` tool): one client with `&client=<id>`, every
|
|
167
|
+
client (and the setting new clients join with) without; `/clients`
|
|
168
|
+
reports each client's `stats`.
|
|
169
|
+
- `/debug` - the app's registered debug commands; POST
|
|
170
|
+
`/debug?name=<cmd>` with a JSON body as its args to call one.
|
|
171
|
+
- POST `/input` with `{ "events": [...] }` - synthetic input through the
|
|
172
|
+
real pipeline, same event shape as the `send_input` tool (tap real
|
|
173
|
+
coordinates read from `/tree`).
|
|
174
|
+
- POST `/clock?scale=<x>` (0 pauses) / `?step=<n>` frames while paused;
|
|
175
|
+
`{ scale, pendingSteps }` back. `/clients` reports each client's `timeScale`,
|
|
176
|
+
reset to 1 by every push.
|
|
177
|
+
- POST `/reload` - rebuild and push to every client; `{ ok, clients }` or
|
|
178
|
+
the build error.
|
|
179
|
+
- POST `/load` with `{ "entry": "<path>" }` - switch the entry and push it;
|
|
180
|
+
`{ ok, entry, clients }` or the build error. Relative paths resolve
|
|
181
|
+
against the project root (file mode: the served file's directory). A
|
|
182
|
+
project server only loads files inside its project.
|
|
183
|
+
- POST `/mute?active=true|false` - mute/unmute the user's own input on
|
|
184
|
+
every client, gamepads included (synthetic `/input` still goes through;
|
|
185
|
+
resize and close cannot be muted). `{ ok, active, clients }`. The mute
|
|
186
|
+
lifts when the server stops; unmute yourself when done.
|
|
187
|
+
- POST `/watch?active=true|false` - resume/pause reload-on-save (`active`
|
|
188
|
+
is whether it watches). `{ ok, active }`. Paused, saves push nothing
|
|
189
|
+
until `/reload`; changes made meanwhile are not replayed on resume. The
|
|
190
|
+
pause lifts when the server stops; resume yourself when done.
|
|
191
|
+
|
|
192
|
+
The loop is the same as over MCP: `/reload`, then `/logs?since=`, then
|
|
193
|
+
`/tree` for coordinates and `/snapshot` of the smallest relevant node.
|
|
194
|
+
|
|
195
|
+
## Lessons that cost real time
|
|
196
|
+
|
|
197
|
+
- console.log + get_logs is your primary probe into runtime state. For state
|
|
198
|
+
you will want repeatedly (a pose, a mode, a counter), bind a debug key that
|
|
199
|
+
logs it and read it back via get_logs.
|
|
200
|
+
- Better than debug keys when driving the app over MCP: register debug
|
|
201
|
+
COMMANDS - `registerDebug(name, fn)` from `srt:dev`, invoked via the
|
|
202
|
+
list_debug/call_debug tools. Use them to SET UP state (jump to a level,
|
|
203
|
+
force a mode, seed a scenario); then the runtime-level tools take over -
|
|
204
|
+
set_time_scale 0 freezes the result for as many snapshots as you need,
|
|
205
|
+
and step_frames walks it forward deterministically. Set state, pause,
|
|
206
|
+
snapshot. Registrations reset on hot reload, so register at module init;
|
|
207
|
+
sync return values only - and note a signal you just wrote flushes on a
|
|
208
|
+
microtask, so returning a signal read straight after setting it returns
|
|
209
|
+
the OLD value.
|
|
210
|
+
- call_debug sets state directly, skipping focus, key routing, and
|
|
211
|
+
TextInput - fine for SETUP, but "the interaction works" is only shown by
|
|
212
|
+
the real pipeline: verify clicks, typing, and drags with send_input,
|
|
213
|
+
which enters events where SDL input does.
|
|
214
|
+
- Key events start at the focused node and bubble to the window root; with
|
|
215
|
+
nothing focused they go to the window root alone. So a debug key bound via
|
|
216
|
+
`<window onKeyDown>` always fires (unless a focused component consumes the
|
|
217
|
+
key with stopPropagation, as TextInput does for editing keys). `key` and
|
|
218
|
+
`code` are W3C KeyboardEvent values, so arrow keys arrive as "ArrowLeft"/
|
|
219
|
+
"ArrowRight"/"ArrowUp"/"ArrowDown" (not "Left"), alongside "Enter",
|
|
220
|
+
"Escape", "a".
|
|
221
|
+
- Idle frames skip work: shaders/pipelines only re-render when an input
|
|
222
|
+
changes - their own params/geometry, or a sampled texture (a data upload,
|
|
223
|
+
or a sampled target re-rendering; chains propagate automatically). Measure
|
|
224
|
+
performance while inputs are actually changing. get_snapshot works on an
|
|
225
|
+
idle client (it requests its own frame); a timeout means the JS thread is
|
|
226
|
+
busy or wedged. get_texture on a pipeline's render target reads the
|
|
227
|
+
current output, pending writes included, without needing a new frame.
|
|
228
|
+
- When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
|
|
229
|
+
in it before investigating, so you agree on the symptom. If you cannot see
|
|
230
|
+
the problem in the capture, say that instead of guessing.
|
|
231
|
+
- Snapshots are downscaled by the time you see them, so a full-window capture
|
|
232
|
+
cannot show you a defect a few pixels across. Whenever you hand-author
|
|
233
|
+
geometry - a `d-path` from raw path math, a `radius` where two shapes meet,
|
|
234
|
+
a stroke join - inspect it MAGNIFIED once, when you write it: get_snapshot
|
|
235
|
+
with a tight crop at scale 4-8 shows the actual rendered pixels enlarged,
|
|
236
|
+
in one call, on the real app. Verifying that a shape is in the right place
|
|
237
|
+
is not the same check as verifying it is drawn right - and get_render_tree
|
|
238
|
+
props answers the third question, whether the value you set is the value
|
|
239
|
+
the renderer holds.
|
|
240
|
+
- GPU/geometry bugs: inspect the actual GPU data FIRST - get_gpu_resources
|
|
241
|
+
for draw counts/uniforms/sizes, get_texture for atlas or data-texture
|
|
242
|
+
contents ("is this tile blank?" is a ten-second question), get_buffer for
|
|
243
|
+
vertex data. The pixels only tell you THAT something is wrong; the
|
|
244
|
+
resources tell you WHERE the data stops being right. In a one-big-pipeline
|
|
245
|
+
app the render tree is a single <texture> leaf and tells you nothing -
|
|
246
|
+
these tools are the visibility layer behind it. Only when the GPU data is
|
|
247
|
+
all correct (so the bug is in producing it, or in the shader), reproduce
|
|
248
|
+
the math CPU-side in a scratch bun script against the app's real data and
|
|
249
|
+
print values.
|
|
250
|
+
- Validate assets at load time and log anomalies (missing lumps/files,
|
|
251
|
+
fully-transparent composites, zero-sized images). Silent fallbacks hide
|
|
252
|
+
bugs for days; a one-line warning surfaces them the first run.
|
|
253
|
+
- After every reload the app restarts from its initial state. If reaching
|
|
254
|
+
the bug site takes navigation, add a dev shortcut (teleport key, noclip,
|
|
255
|
+
initial-state override) before iterating - the round trips add up fast.
|
|
256
|
+
- Clamp onFrame time deltas to [0, cap], not just capped: across a hot
|
|
257
|
+
reload the runtime's tick counter resets AFTER the new instance's first
|
|
258
|
+
frame, so the second frame computes a hugely NEGATIVE delta.
|
|
259
|
+
Math.min(dt, cap) lets it through, and one bad frame can corrupt anything
|
|
260
|
+
integrated from dt (positions fly off, accumulators go so negative they
|
|
261
|
+
never recover). Math.max(0, Math.min(dt, cap)) costs nothing.
|
|
262
|
+
- A registered onFrame is a standing request, not demand-gated: it re-requests
|
|
263
|
+
the next frame every time it runs, so the runtime keeps calling it - and
|
|
264
|
+
presents - every frame at the refresh rate until you deregister it (fps
|
|
265
|
+
stays at the refresh rate on an idle screen; that is the 60fps burn called
|
|
266
|
+
out in @solidrt/core agents/performance.md, rule 4). The upside is that a
|
|
267
|
+
self-running loop - a game clock, a shader driver, a stepped VM doing silent
|
|
268
|
+
CPU work with no console output - keeps advancing on its own: it does NOT
|
|
269
|
+
stall when the body changes nothing and needs no startup "prime" write.
|
|
270
|
+
Deregister onFrame (return its cleanup, or let onCleanup fire) whenever
|
|
271
|
+
there is nothing left to advance.
|
|
272
|
+
- Layout is incremental: a change re-solves only the dirty path, and clean
|
|
273
|
+
subtrees answer from a per-node cache, so long lists no longer cap layout
|
|
274
|
+
(a thousand-node tree relays out in well under a millisecond). If layoutMs
|
|
275
|
+
still grows with tree size, read the get_stats counters - a low
|
|
276
|
+
cacheHits/cacheGets ratio means the layout cache is being defeated, high
|
|
277
|
+
paraShapes means text is actually reshaping. Paint is viewport-culled:
|
|
278
|
+
under an `overflow="hidden"` scroller only the subtrees that can reach the
|
|
279
|
+
visible box are painted, so paintMs tracks what is on screen, not what is
|
|
280
|
+
mounted (nodesPainted in get_stats shows the count). Very long lists still
|
|
281
|
+
pay for the initial mount and for memory, so windowing stays sensible at
|
|
282
|
+
the thousands-of-rows scale.
|
|
283
|
+
- Remote images: createImage (and Image) dedupes repeated URLs, caches the
|
|
284
|
+
bytes on disk, and the runtime rate-limits concurrent asset fetches per
|
|
285
|
+
host - do not build your own promise cache around it. Images are fetched
|
|
286
|
+
with no freshness check (an already-cached URL is never re-checked), so
|
|
287
|
+
use versioned URLs for content that changes. Use Image's `fallback` prop
|
|
288
|
+
(an image source) for the broken-image case instead of catching errors
|
|
289
|
+
yourself.
|
|
290
|
+
- fetch() never caches by default and ignores server cache headers. Caching
|
|
291
|
+
is explicit and per call: `fetch(url, { cache: "force-cache" })` for
|
|
292
|
+
assets (serve from disk or fetch-and-store, no freshness),
|
|
293
|
+
`{ cache: "reload" }` to refresh an entry. Image/createImage already do
|
|
294
|
+
this for you.
|