@solidrt/cli 0.0.25 → 0.0.27
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/package.json +13 -8
- package/scaffold/AGENTS.md +60 -0
- package/scaffold/package.json +5 -5
- package/{src → server}/cache.ts +23 -51
- package/server/control.ts +187 -0
- package/server/main.ts +301 -0
- package/server/proxy.ts +72 -0
- package/server/qr.ts +35 -0
- package/server/rebuild.ts +54 -0
- package/server/remap.ts +47 -0
- package/server/state.ts +62 -0
- package/server/tsconfig.json +11 -0
- package/server/tunnel.ts +53 -0
- package/src/args.ts +2 -0
- package/src/bundle-cli.ts +12 -0
- package/src/bundler.ts +69 -23
- package/src/commands/bundle.ts +4 -7
- package/src/commands/mcp.ts +151 -43
- package/src/commands/server.ts +16 -20
- package/src/dev-client.ts +10 -21
- package/src/dev-server.ts +153 -300
- package/src/repl.ts +67 -45
- package/src/util.ts +30 -16
- package/src/watcher.ts +6 -8
- package/src/control.ts +0 -119
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
|
+
"server/",
|
|
13
14
|
"scaffold/",
|
|
14
15
|
"AGENTS.md"
|
|
15
16
|
],
|
|
@@ -17,21 +18,25 @@
|
|
|
17
18
|
"@babel/core": "^7.0.0",
|
|
18
19
|
"@babel/plugin-syntax-jsx": "^7.0.0",
|
|
19
20
|
"@babel/preset-typescript": "^7.0.0",
|
|
21
|
+
"@jridgewell/remapping": "^2.3.0",
|
|
22
|
+
"@jridgewell/trace-mapping": "^0.3.25",
|
|
20
23
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
21
|
-
"babel-preset-solid": "2.0.0-beta.
|
|
24
|
+
"babel-preset-solid": "2.0.0-beta.17",
|
|
22
25
|
"bonjour-service": "^1.4.0",
|
|
23
|
-
"qrcode-generator": "^2.0.4"
|
|
26
|
+
"qrcode-generator": "^2.0.4",
|
|
27
|
+
"zod": "^4.4.3"
|
|
24
28
|
},
|
|
25
29
|
"optionalDependencies": {
|
|
26
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
27
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
28
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
30
|
+
"@solidrt/darwin-arm64": "0.0.27",
|
|
31
|
+
"@solidrt/linux-x64-gnu": "0.0.27",
|
|
32
|
+
"@solidrt/win32-x64-msvc": "0.0.27"
|
|
29
33
|
},
|
|
30
34
|
"peerDependencies": {
|
|
31
|
-
"@solidrt/core": "0.0.
|
|
32
|
-
"typescript": "^
|
|
35
|
+
"@solidrt/core": "0.0.27",
|
|
36
|
+
"typescript": "^7"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
39
|
+
"@solidrt/flux-types": "0.0.27",
|
|
35
40
|
"@types/bun": "latest"
|
|
36
41
|
}
|
|
37
42
|
}
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -97,6 +97,66 @@ its tools over guessing at runtime state:
|
|
|
97
97
|
- get_render_tree: what the app actually rendered - node kinds, text, and
|
|
98
98
|
window-relative boxes
|
|
99
99
|
- get_stats: fps, CPU/memory, frame phase timings, setProperty rate
|
|
100
|
+
- get_snapshot: PNG capture of any render-tree node's pixels (get node ids
|
|
101
|
+
from get_render_tree; the window node captures everything)
|
|
102
|
+
- get_gpu_resources: inventory of GPU state - textures (size, render target
|
|
103
|
+
or not), vertex buffers (byteLength), pipelines (draw count, attribute
|
|
104
|
+
layout, bound textures, last-applied uniform values)
|
|
105
|
+
- get_texture: any GPU texture read back as a PNG by id - atlases, data
|
|
106
|
+
textures, and shader/pipeline render targets alike (a render target is
|
|
107
|
+
"what this pipeline last drew", no frame or snapshot needed); crop with
|
|
108
|
+
x/y/width/height
|
|
109
|
+
- get_buffer: a vertex-buffer range decoded to numbers (f32/u16/u8, 64 KiB
|
|
110
|
+
per call) - verify geometry after a writeBuffer instead of inferring it
|
|
111
|
+
from pixels
|
|
112
|
+
- reload: rebuild from source and push to every client - THE dev loop is
|
|
113
|
+
edit -> reload -> get_logs -> get_snapshot. reload surfaces build errors
|
|
114
|
+
but not type errors; run the typecheck separately.
|
|
100
115
|
|
|
101
116
|
The tools need a running app: if list_clients is empty, ask the user to start
|
|
102
117
|
`bunx srt run src/index.tsx`.
|
|
118
|
+
|
|
119
|
+
## Debugging a running app (lessons that cost real time)
|
|
120
|
+
|
|
121
|
+
- console.log + get_logs is your primary probe into runtime state. For state
|
|
122
|
+
you will want repeatedly (a pose, a mode, a counter), bind a debug key that
|
|
123
|
+
logs it and read it back via get_logs.
|
|
124
|
+
- Key events are delivered ONLY to the focused node (no bubbling): call
|
|
125
|
+
setFocus(node.id) from the window's ref or onKeyDown never fires. This
|
|
126
|
+
runtime names arrow keys "Left"/"Right"/"Up"/"Down", not "ArrowLeft".
|
|
127
|
+
- Idle frames skip work: shaders/pipelines only re-render when their params
|
|
128
|
+
change, so measure performance while uniforms are actually changing, and
|
|
129
|
+
a get_snapshot of an idle client can time out - retry, make the app
|
|
130
|
+
produce a frame, or use get_texture on the pipeline's render target, which
|
|
131
|
+
reads the last-drawn frame without needing a new one.
|
|
132
|
+
- When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
|
|
133
|
+
in it before investigating, so you agree on the symptom. If you cannot see
|
|
134
|
+
the problem in the capture, say that instead of guessing.
|
|
135
|
+
- GPU/geometry bugs: inspect the actual GPU data FIRST - get_gpu_resources
|
|
136
|
+
for draw counts/uniforms/sizes, get_texture for atlas or data-texture
|
|
137
|
+
contents ("is this tile blank?" is a ten-second question), get_buffer for
|
|
138
|
+
vertex data. The pixels only tell you THAT something is wrong; the
|
|
139
|
+
resources tell you WHERE the data stops being right. In a one-big-pipeline
|
|
140
|
+
app the render tree is a single <texture> leaf and tells you nothing -
|
|
141
|
+
these tools are the visibility layer behind it. Only when the GPU data is
|
|
142
|
+
all correct (so the bug is in producing it, or in the shader), reproduce
|
|
143
|
+
the math CPU-side in a scratch bun script against the app's real data and
|
|
144
|
+
print values.
|
|
145
|
+
- Validate assets at load time and log anomalies (missing lumps/files,
|
|
146
|
+
fully-transparent composites, zero-sized images). Silent fallbacks hide
|
|
147
|
+
bugs for days; a one-line warning surfaces them the first run.
|
|
148
|
+
- After every reload the app restarts from its initial state. If reaching
|
|
149
|
+
the bug site takes navigation, add a dev shortcut (teleport key, noclip,
|
|
150
|
+
initial-state override) before iterating - the round trips add up fast.
|
|
151
|
+
- Clamp onFrame time deltas to [0, cap], not just capped: across a hot
|
|
152
|
+
reload the runtime's tick counter resets AFTER the new instance's first
|
|
153
|
+
frame, so the second frame computes a hugely NEGATIVE delta.
|
|
154
|
+
Math.min(dt, cap) lets it through, and one bad frame can corrupt anything
|
|
155
|
+
integrated from dt (positions fly off, accumulators go so negative they
|
|
156
|
+
never recover). Math.max(0, Math.min(dt, cap)) costs nothing.
|
|
157
|
+
- Frames are demand-gated: JS frame callbacks only run when the previous
|
|
158
|
+
frame changed something (input, signal write, GPU upload). An app whose
|
|
159
|
+
onFrame returns early without side effects on its first frame never gets
|
|
160
|
+
a second one - self-running animation (game clocks, shader-driven
|
|
161
|
+
effects) must make one state change at startup to prime the loop; after
|
|
162
|
+
that its own writes keep it awake.
|
package/scaffold/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"android": "srt client --android"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@solidrt/core": "0.0.
|
|
13
|
-
"@solidrt/components": "0.0.
|
|
12
|
+
"@solidrt/core": "0.0.27",
|
|
13
|
+
"@solidrt/components": "0.0.27"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@solidrt/cli": "0.0.
|
|
17
|
-
"@solidrt/flux-types": "0.0.
|
|
18
|
-
"typescript": "^
|
|
16
|
+
"@solidrt/cli": "0.0.27",
|
|
17
|
+
"@solidrt/flux-types": "0.0.27",
|
|
18
|
+
"typescript": "^7"
|
|
19
19
|
}
|
|
20
20
|
}
|
package/{src → server}/cache.ts
RENAMED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// SQLite-backed HTTP response cache for the dev server's /__proxy__ endpoint.
|
|
2
2
|
//
|
|
3
|
-
// Project-local: stored at <
|
|
4
|
-
//
|
|
3
|
+
// Project-local: stored at <dir>/.srt-cache.db. Entries live forever; delete
|
|
4
|
+
// .srt-cache.db to drop them.
|
|
5
5
|
//
|
|
6
6
|
// Cached: GET (and HEAD) 2xx responses with no Authorization on the request
|
|
7
7
|
// and no Cache-Control: no-store on either side. The cache key is
|
|
8
|
-
//
|
|
8
|
+
// method + "\n" + url, stored raw; headers are intentionally not part of the
|
|
9
|
+
// key. (The Bun predecessor hashed the key with sha256, which was cosmetic:
|
|
10
|
+
// old hashed entries simply miss and re-populate.)
|
|
9
11
|
|
|
10
|
-
import { Database } from "
|
|
11
|
-
import {
|
|
12
|
-
import { createHash } from "node:crypto"
|
|
12
|
+
import { Database } from "flux:sqlite"
|
|
13
|
+
import { join } from "flux:path"
|
|
13
14
|
|
|
14
15
|
const CACHE_FILE = ".srt-cache.db"
|
|
15
16
|
|
|
@@ -27,9 +28,9 @@ export type Entry = {
|
|
|
27
28
|
let db: Database | null = null
|
|
28
29
|
let enabled = false
|
|
29
30
|
|
|
30
|
-
export function initCache(opts: { dir: string }) {
|
|
31
|
-
let d =
|
|
32
|
-
d.
|
|
31
|
+
export async function initCache(opts: { dir: string }) {
|
|
32
|
+
let d = await Database.connect(join(opts.dir, CACHE_FILE), "rw+")
|
|
33
|
+
await d.exec(`CREATE TABLE IF NOT EXISTS entries (
|
|
33
34
|
key TEXT PRIMARY KEY,
|
|
34
35
|
method TEXT NOT NULL,
|
|
35
36
|
url TEXT NOT NULL,
|
|
@@ -46,28 +47,8 @@ export function isEnabled(): boolean {
|
|
|
46
47
|
return enabled
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
// Strings returned by bun:sqlite carry an internal representation that
|
|
50
|
-
// Headers.set() rejects, even when value-identical to an acceptable string
|
|
51
|
-
// (Bun bug oven-sh/bun#28266, present through 1.3.14). Rebuilding each char
|
|
52
|
-
// yields a clean string.
|
|
53
|
-
function reflatten(s: string): string {
|
|
54
|
-
return Array.from(s, (c) => String.fromCharCode(c.charCodeAt(0))).join("")
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function reflattenHeaders(obj: Record<string, string>): Record<string, string> {
|
|
58
|
-
let out: Record<string, string> = {}
|
|
59
|
-
for (let key in obj) {
|
|
60
|
-
out[reflatten(key)] = reflatten(obj[key]!)
|
|
61
|
-
}
|
|
62
|
-
return out
|
|
63
|
-
}
|
|
64
|
-
|
|
65
50
|
function keyFor(method: string, url: string): string {
|
|
66
|
-
|
|
67
|
-
h.update(method)
|
|
68
|
-
h.update("\n")
|
|
69
|
-
h.update(url)
|
|
70
|
-
return h.digest("hex")
|
|
51
|
+
return method + "\n" + url
|
|
71
52
|
}
|
|
72
53
|
|
|
73
54
|
function cacheableMethod(method: string): boolean {
|
|
@@ -98,32 +79,23 @@ export function isBypass(reqHeaders: Headers): boolean {
|
|
|
98
79
|
return false
|
|
99
80
|
}
|
|
100
81
|
|
|
101
|
-
export function get(method: string, url: string): Entry | null {
|
|
82
|
+
export async function get(method: string, url: string): Promise<Entry | null> {
|
|
102
83
|
if (!db || !enabled) return null
|
|
103
|
-
let row = db
|
|
84
|
+
let row = await db
|
|
104
85
|
.query("SELECT method, url, status, headers, body, cached_at FROM entries WHERE key = ?")
|
|
105
|
-
.get(keyFor(method, url))
|
|
106
|
-
| {
|
|
107
|
-
method: string
|
|
108
|
-
url: string
|
|
109
|
-
status: number
|
|
110
|
-
headers: string
|
|
111
|
-
body: Uint8Array
|
|
112
|
-
cached_at: number
|
|
113
|
-
}
|
|
114
|
-
| null
|
|
86
|
+
.get([keyFor(method, url)])
|
|
115
87
|
if (!row) return null
|
|
116
88
|
return {
|
|
117
|
-
method: row.method,
|
|
118
|
-
url: row.url,
|
|
119
|
-
status: row.status,
|
|
120
|
-
headers:
|
|
121
|
-
body: row.body,
|
|
122
|
-
cachedAt: row.cached_at,
|
|
89
|
+
method: row.method as string,
|
|
90
|
+
url: row.url as string,
|
|
91
|
+
status: row.status as number,
|
|
92
|
+
headers: JSON.parse(row.headers as string),
|
|
93
|
+
body: row.body as Uint8Array,
|
|
94
|
+
cachedAt: row.cached_at as number,
|
|
123
95
|
}
|
|
124
96
|
}
|
|
125
97
|
|
|
126
|
-
export function put(
|
|
98
|
+
export async function put(
|
|
127
99
|
method: string,
|
|
128
100
|
url: string,
|
|
129
101
|
status: number,
|
|
@@ -133,10 +105,10 @@ export function put(
|
|
|
133
105
|
if (!db || !enabled) return
|
|
134
106
|
if (status < 200 || status >= 300) return
|
|
135
107
|
if (hasNoStore(headers["cache-control"] ?? null)) return
|
|
136
|
-
db.run(
|
|
108
|
+
await db.run(
|
|
137
109
|
`INSERT OR REPLACE INTO entries
|
|
138
110
|
(key, method, url, status, headers, body, cached_at)
|
|
139
111
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
140
112
|
[keyFor(method, url), method, url, status, JSON.stringify(headers), body, Date.now()],
|
|
141
113
|
)
|
|
142
|
-
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { state } from "./state"
|
|
2
|
+
import { rebuildAndBroadcast } from "./rebuild"
|
|
3
|
+
import { remapPositions } from "./remap"
|
|
4
|
+
import type { ServerWebSocket } from "flux:http"
|
|
5
|
+
|
|
6
|
+
// The control API under /__control__/: read-only introspection of connected
|
|
7
|
+
// app clients, served next to the file routes. The MCP bridge (srt mcp) is the
|
|
8
|
+
// primary consumer. Two shapes: server-held data answered directly (clients,
|
|
9
|
+
// logs) and queries forwarded to a client over its websocket and correlated
|
|
10
|
+
// back by id (tree, stats).
|
|
11
|
+
|
|
12
|
+
export type LogEntry = { seq: number; at: number; client: number; level: string; text: string }
|
|
13
|
+
|
|
14
|
+
// Ring buffer of forwarded client logs. Capped so a chatty app cannot grow the
|
|
15
|
+
// server without bound; readers page through it with the `since` cursor.
|
|
16
|
+
const LOG_CAP = 2000
|
|
17
|
+
const QUERY_TIMEOUT_MS = 5000
|
|
18
|
+
const MAX_WAIT_MS = 30000
|
|
19
|
+
|
|
20
|
+
let logs: LogEntry[] = []
|
|
21
|
+
let logSeq = 0
|
|
22
|
+
// Pending long-poll wakeups (see handleLogs). Flushed on every append; a
|
|
23
|
+
// waiter that already timed out resolves again harmlessly.
|
|
24
|
+
let logWaiters: Array<() => void> = []
|
|
25
|
+
|
|
26
|
+
let nextQueryId = 1
|
|
27
|
+
let pendingQueries = new Map<number, (msg: any) => void>()
|
|
28
|
+
|
|
29
|
+
function sleep(ms: number): Promise<void> {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// A `log` message arrived from a client: buffer it and wake long-polls.
|
|
34
|
+
/// Bundle positions in stack traces are remapped to .tsx sources on the way in.
|
|
35
|
+
export function appendLog(client: number, level: string, text: string) {
|
|
36
|
+
logs.push({ seq: ++logSeq, at: Date.now(), client, level, text: remapPositions(text, state.currentMap) })
|
|
37
|
+
if (logs.length > LOG_CAP) logs.splice(0, logs.length - LOG_CAP)
|
|
38
|
+
let waiters = logWaiters
|
|
39
|
+
logWaiters = []
|
|
40
|
+
for (let wake of waiters) wake()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// A `result` message arrived from a client: hand it to the awaiting query.
|
|
44
|
+
export function resolveQuery(msg: { id?: number }) {
|
|
45
|
+
if (typeof msg.id !== "number") return
|
|
46
|
+
let resolve = pendingQueries.get(msg.id)
|
|
47
|
+
if (resolve) {
|
|
48
|
+
pendingQueries.delete(msg.id)
|
|
49
|
+
resolve(msg)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The connected-client list. `withAddress` adds each socket's peer address for
|
|
54
|
+
// the internal API (the repl `list` display); the public control shape stays
|
|
55
|
+
// without it.
|
|
56
|
+
export function clientList(withAddress = false) {
|
|
57
|
+
return [...state.clients.entries()].map(([ws, info]) => ({
|
|
58
|
+
id: info.id,
|
|
59
|
+
platform: info.platform,
|
|
60
|
+
version: info.version,
|
|
61
|
+
profile: info.profile,
|
|
62
|
+
capabilities: info.capabilities,
|
|
63
|
+
...(withAddress ? { address: ws.remoteAddress ?? null } : {}),
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Resolve the target client for a query: an explicit ?client=<id>, or the only
|
|
68
|
+
// connected client when the parameter is omitted.
|
|
69
|
+
function findClient(param: string | undefined): { ws: ServerWebSocket } | { error: Response } {
|
|
70
|
+
let entries = [...state.clients.entries()]
|
|
71
|
+
if (param === undefined) {
|
|
72
|
+
if (entries.length === 1) return { ws: entries[0]![0] }
|
|
73
|
+
if (entries.length === 0) return { error: Response.json({ error: "No connected clients" }, { status: 503 }) }
|
|
74
|
+
return { error: Response.json({ error: "Multiple clients connected; pass ?client=<id>" }, { status: 400 }) }
|
|
75
|
+
}
|
|
76
|
+
let id = parseInt(param, 10)
|
|
77
|
+
let entry = entries.find(([, info]) => info.id === id)
|
|
78
|
+
if (!entry) return { error: Response.json({ error: `No client with id ${param}` }, { status: 404 }) }
|
|
79
|
+
return { ws: entry[0] }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleQuery(query: Map<string, string>, kind: string, extra?: Record<string, unknown>): Promise<Response> {
|
|
83
|
+
let target = findClient(query.get("client"))
|
|
84
|
+
if ("error" in target) return target.error
|
|
85
|
+
let id = nextQueryId++
|
|
86
|
+
let reply = new Promise<any>((resolve) => {
|
|
87
|
+
pendingQueries.set(id, resolve)
|
|
88
|
+
})
|
|
89
|
+
target.ws.send(JSON.stringify({ type: "query", kind, id, ...extra }))
|
|
90
|
+
let msg = await Promise.race([reply, sleep(QUERY_TIMEOUT_MS)])
|
|
91
|
+
pendingQueries.delete(id)
|
|
92
|
+
if (!msg) return Response.json({ error: "Query timed out" }, { status: 504 })
|
|
93
|
+
// Error strings may carry stack traces (e.g. a debug command threw); remap
|
|
94
|
+
// bundle positions to .tsx sources like appendLog does for forwarded logs.
|
|
95
|
+
if (msg.error) return Response.json({ error: remapPositions(String(msg.error), state.currentMap) }, { status: 502 })
|
|
96
|
+
return Response.json(msg.data)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// GET /__control__/logs?since=N&wait=MS: entries with seq > since, plus the
|
|
100
|
+
// latest seq as the next cursor. With `wait`, holds the response until a new
|
|
101
|
+
// entry arrives or the timeout passes (long-poll), so a caller can follow the
|
|
102
|
+
// stream without tight polling.
|
|
103
|
+
async function handleLogs(query: Map<string, string>): Promise<Response> {
|
|
104
|
+
let since = parseInt(query.get("since") ?? "0", 10) || 0
|
|
105
|
+
let wait = Math.min(parseInt(query.get("wait") ?? "0", 10) || 0, MAX_WAIT_MS)
|
|
106
|
+
let entries = logs.filter((e) => e.seq > since)
|
|
107
|
+
if (entries.length === 0 && wait > 0) {
|
|
108
|
+
await new Promise<void>((resolve) => {
|
|
109
|
+
let timer = setTimeout(resolve, wait)
|
|
110
|
+
logWaiters.push(() => {
|
|
111
|
+
clearTimeout(timer)
|
|
112
|
+
resolve()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
entries = logs.filter((e) => e.seq > since)
|
|
116
|
+
}
|
|
117
|
+
return Response.json({ entries, latest: logSeq })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function handleControl(req: Request, path: string, query: Map<string, string>): Promise<Response> {
|
|
121
|
+
switch (path) {
|
|
122
|
+
case "/__control__/clients":
|
|
123
|
+
return Response.json(clientList())
|
|
124
|
+
case "/__control__/logs":
|
|
125
|
+
return handleLogs(query)
|
|
126
|
+
case "/__control__/tree":
|
|
127
|
+
return handleQuery(query, "tree")
|
|
128
|
+
case "/__control__/stats":
|
|
129
|
+
return handleQuery(query, "stats")
|
|
130
|
+
case "/__control__/snapshot": {
|
|
131
|
+
let nodeId = parseInt(query.get("node") ?? "", 10)
|
|
132
|
+
if (!Number.isFinite(nodeId)) return Response.json({ error: "Snapshot requires ?node=<id>" }, { status: 400 })
|
|
133
|
+
return handleQuery(query, "snapshot", { nodeId })
|
|
134
|
+
}
|
|
135
|
+
case "/__control__/gpu":
|
|
136
|
+
return handleQuery(query, "gpu")
|
|
137
|
+
case "/__control__/debug": {
|
|
138
|
+
// GET lists the app's registered debug commands; POST calls one, with
|
|
139
|
+
// an optional JSON body as its args.
|
|
140
|
+
if (req.method !== "POST") return handleQuery(query, "debug_list")
|
|
141
|
+
let name = query.get("name")
|
|
142
|
+
if (!name) return Response.json({ error: "Debug call requires ?name=<command>" }, { status: 400 })
|
|
143
|
+
let args: unknown = null
|
|
144
|
+
try {
|
|
145
|
+
args = await req.json()
|
|
146
|
+
} catch {}
|
|
147
|
+
return handleQuery(query, "debug_call", { name, args })
|
|
148
|
+
}
|
|
149
|
+
case "/__control__/texture": {
|
|
150
|
+
let textureId = parseInt(query.get("id") ?? "", 10)
|
|
151
|
+
if (!Number.isFinite(textureId)) return Response.json({ error: "Texture requires ?id=<textureId>" }, { status: 400 })
|
|
152
|
+
// Optional crop: all four of x/y/width/height, in texture pixels.
|
|
153
|
+
let rectParams = ["x", "y", "width", "height"].map((k) => query.get(k))
|
|
154
|
+
let extra: Record<string, unknown> = { textureId }
|
|
155
|
+
if (rectParams.some((v) => v !== undefined)) {
|
|
156
|
+
let [x, y, width, height] = rectParams.map((v) => parseInt(v ?? "", 10))
|
|
157
|
+
if (![x, y, width, height].every(Number.isFinite))
|
|
158
|
+
return Response.json({ error: "Texture rect requires all of x, y, width, height" }, { status: 400 })
|
|
159
|
+
extra.rect = { x, y, width, height }
|
|
160
|
+
}
|
|
161
|
+
return handleQuery(query, "texture", extra)
|
|
162
|
+
}
|
|
163
|
+
case "/__control__/buffer": {
|
|
164
|
+
let bufferId = parseInt(query.get("id") ?? "", 10)
|
|
165
|
+
if (!Number.isFinite(bufferId)) return Response.json({ error: "Buffer requires ?id=<bufferId>" }, { status: 400 })
|
|
166
|
+
let extra: Record<string, unknown> = { bufferId }
|
|
167
|
+
let byteOffset = parseInt(query.get("offset") ?? "", 10)
|
|
168
|
+
if (Number.isFinite(byteOffset)) extra.byteOffset = byteOffset
|
|
169
|
+
let length = parseInt(query.get("length") ?? "", 10)
|
|
170
|
+
if (Number.isFinite(length)) extra.length = length
|
|
171
|
+
let as = query.get("as")
|
|
172
|
+
if (as !== undefined) extra.as = as
|
|
173
|
+
return handleQuery(query, "buffer", extra)
|
|
174
|
+
}
|
|
175
|
+
case "/__control__/reload": {
|
|
176
|
+
// Explicit rebuild-and-push, the primary way a coding agent applies its
|
|
177
|
+
// edits (srt mcp's reload tool). Unlike the repl's file watcher this is
|
|
178
|
+
// on demand, so a burst of edits collapses into one reload.
|
|
179
|
+
if (req.method !== "POST") return Response.json({ error: "Reload requires POST" }, { status: 405 })
|
|
180
|
+
let error = await rebuildAndBroadcast()
|
|
181
|
+
if (error) return Response.json({ error }, { status: 502 })
|
|
182
|
+
return Response.json({ ok: true, clients: state.clients.size })
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
return Response.json({ error: "Unknown control endpoint" }, { status: 404 })
|
|
186
|
+
}
|
|
187
|
+
}
|