@solidrt/cli 0.0.51 → 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 +68 -38
- package/README.md +86 -15
- package/agents/assets.md +19 -5
- package/agents/debugging.md +169 -17
- package/dist/console.srtapp +94123 -73
- package/dist/server.js +3936 -0
- package/package.json +10 -11
- package/src/android/docs.md +21 -0
- package/src/android/main.ts +286 -0
- package/src/{bundler.ts → bundle/bundler.ts} +67 -55
- package/src/bundle/docs.md +12 -0
- package/src/{commands/bundle.ts → bundle/main.ts} +67 -25
- package/src/check/docs.md +10 -0
- package/src/check/main.ts +85 -0
- package/src/{commands/check.ts → check/typecheck.ts} +11 -41
- 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} +21 -15
- 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/{commands/pack.ts → pack/main.ts} +28 -17
- package/src/{packer.ts → pack/trailer.ts} +32 -42
- package/src/render/docs.md +19 -0
- package/src/{commands/render.ts → render/main.ts} +17 -15
- 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 +239 -81
- 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/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 -185
- 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 -76
- package/server/state.ts +0 -94
- package/src/args.ts +0 -210
- package/src/bundle-cli.ts +0 -13
- package/src/commands/client.ts +0 -34
- package/src/commands/mcp.ts +0 -617
- 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/repl.ts +0 -249
- package/src/util.ts +0 -122
- package/src/watcher.ts +0 -73
- /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/{server → src/server}/remap.ts +0 -0
package/server/main.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
// The srt dev server as a flux script. srt (Bun) spawns this with one JSON
|
|
2
|
-
// config argument; bundling, file watching, and the repl stay in srt, which
|
|
3
|
-
// drives this process over the loopback-only /__internal__/ routes. The
|
|
4
|
-
// shutdown-when-empty policy also lives in srt (it polls /__internal__/clients);
|
|
5
|
-
// this process runs until srt kills it.
|
|
6
|
-
|
|
7
|
-
import { serve } from "flux:http"
|
|
8
|
-
import type { FluxRequest, Server } from "flux:http"
|
|
9
|
-
import { file } from "flux:fs"
|
|
10
|
-
import { argv } from "flux:process"
|
|
11
|
-
import { resolveWithin } from "flux:path"
|
|
12
|
-
import { state, type Config } from "./state"
|
|
13
|
-
import * as cache from "./cache"
|
|
14
|
-
import { handleProxy } from "./proxy"
|
|
15
|
-
import { appendLog, clientList, handleControl, resolveQuery } from "./control"
|
|
16
|
-
import { printQr } from "./qr"
|
|
17
|
-
import { createTunnelEndpoint, TUNNEL_PROTOCOL } from "./tunnel"
|
|
18
|
-
|
|
19
|
-
let config: Config = JSON.parse(argv[0]!)
|
|
20
|
-
state.config = config
|
|
21
|
-
state.sourceDir = config.sourceDir
|
|
22
|
-
state.projectDir = config.projectDir
|
|
23
|
-
state.stats = config.stats
|
|
24
|
-
state.serverUrl = `${config.address}:${config.port}`
|
|
25
|
-
|
|
26
|
-
if (config.cache) {
|
|
27
|
-
await cache.initCache({ dir: config.cacheDir })
|
|
28
|
-
console.log("[cli] HTTP cache enabled")
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (config.capture) {
|
|
32
|
-
// Start each capture from an empty file: appends would otherwise tack onto
|
|
33
|
-
// whatever a previous run left behind.
|
|
34
|
-
await file(config.capture).write("")
|
|
35
|
-
state.captureStartMs = Date.now()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Split an origin-form request URL ("/path?a=1&b=2") into its decoded path and
|
|
39
|
-
// query parameters. flux has no URL global; this covers what the routes need.
|
|
40
|
-
function splitQuery(url: string): { path: string; query: Map<string, string> } {
|
|
41
|
-
let i = url.indexOf("?")
|
|
42
|
-
let path = i < 0 ? url : url.slice(0, i)
|
|
43
|
-
let query = new Map<string, string>()
|
|
44
|
-
if (i >= 0) {
|
|
45
|
-
for (let pair of url.slice(i + 1).split("&")) {
|
|
46
|
-
if (!pair) continue
|
|
47
|
-
let j = pair.indexOf("=")
|
|
48
|
-
let k = j < 0 ? pair : pair.slice(0, j)
|
|
49
|
-
let v = j < 0 ? "" : pair.slice(j + 1)
|
|
50
|
-
query.set(decodeURIComponent(k), decodeURIComponent(v.replace(/\+/g, " ")))
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { path: decodeURIComponent(path), query }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Send `text` to the clients with the given ids, or to every client when
|
|
57
|
-
// `ids` is omitted.
|
|
58
|
-
function sendTo(ids: number[] | undefined, text: string) {
|
|
59
|
-
for (let [ws, info] of state.clients) {
|
|
60
|
-
if (!ids || ids.includes(info.id)) ws.send(text)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// The srt -> server IPC under /__internal__/: only the srt process on this
|
|
65
|
-
// machine may drive it, so reject any peer that is not loopback.
|
|
66
|
-
async function handleInternal(req: FluxRequest, server: Server, path: string): Promise<Response> {
|
|
67
|
-
let ip = server.requestIP(req)
|
|
68
|
-
let loopback = ip && (ip.address === "127.0.0.1" || ip.address === "::1" || ip.address === "::ffff:127.0.0.1")
|
|
69
|
-
if (!loopback) return new Response("Forbidden", { status: 403 })
|
|
70
|
-
|
|
71
|
-
if (path === "/__internal__/clients") return Response.json(clientList(true))
|
|
72
|
-
if (path === "/__internal__/watch" && req.method === "GET") return Response.json({ enabled: state.watch })
|
|
73
|
-
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 })
|
|
74
|
-
|
|
75
|
-
switch (path) {
|
|
76
|
-
case "/__internal__/reload": {
|
|
77
|
-
// { message, clients?, latch?, sourceDir?, maps? }: send `message` (a
|
|
78
|
-
// full client-protocol message, built by srt) to the listed client ids,
|
|
79
|
-
// or to all when omitted. `latch` keeps it for late-joining clients
|
|
80
|
-
// (code reloads latch, one-shot bytecode loads do not); `sourceDir`
|
|
81
|
-
// moves the file-serving root (repl `load`; the project root - and with
|
|
82
|
-
// it the /assets/ root - is fixed for the life of the run); `maps` is
|
|
83
|
-
// the bundle's sourcemaps keyed by module name for log remapping,
|
|
84
|
-
// replaced on every reload (absent means none).
|
|
85
|
-
let body = await req.json()
|
|
86
|
-
if (typeof body.sourceDir === "string") state.sourceDir = body.sourceDir
|
|
87
|
-
// Keep the rebuild entry in sync when `load` moves it, so a later MCP
|
|
88
|
-
// reload bundles the newly loaded file, not the launch-time one.
|
|
89
|
-
if (typeof body.entry === "string") state.config.entry = body.entry
|
|
90
|
-
state.currentMaps = body.maps && typeof body.maps === "object" ? body.maps : null
|
|
91
|
-
let text = JSON.stringify(body.message)
|
|
92
|
-
if (body.latch) state.currentReload = text
|
|
93
|
-
sendTo(body.clients, text)
|
|
94
|
-
return new Response("", { status: 204 })
|
|
95
|
-
}
|
|
96
|
-
case "/__internal__/stop": {
|
|
97
|
-
let body = await req.json()
|
|
98
|
-
// A broadcast stop also forgets the latched reload, so a client that
|
|
99
|
-
// connects afterwards starts clean.
|
|
100
|
-
if (!body.clients) {
|
|
101
|
-
state.currentReload = null
|
|
102
|
-
state.currentMaps = null
|
|
103
|
-
}
|
|
104
|
-
sendTo(body.clients, JSON.stringify({ type: "stop" }))
|
|
105
|
-
return new Response("", { status: 204 })
|
|
106
|
-
}
|
|
107
|
-
case "/__internal__/watch": {
|
|
108
|
-
// The repl's `watch on|off`; agents use /__control__/watch instead.
|
|
109
|
-
let body = await req.json()
|
|
110
|
-
state.watch = !!body.enabled
|
|
111
|
-
return new Response("", { status: 204 })
|
|
112
|
-
}
|
|
113
|
-
case "/__internal__/stats": {
|
|
114
|
-
let body = await req.json()
|
|
115
|
-
state.stats = !!body.stats
|
|
116
|
-
sendTo(undefined, JSON.stringify({ type: "stats", stats: state.stats }))
|
|
117
|
-
return new Response("", { status: 204 })
|
|
118
|
-
}
|
|
119
|
-
default:
|
|
120
|
-
return Response.json({ error: "Unknown internal endpoint" }, { status: 404 })
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// The file routes: GET file with single-range 206 support. All paths are
|
|
125
|
-
// contained in `root` (the source directory, or the project dir for the
|
|
126
|
-
// /assets/ convention route).
|
|
127
|
-
async function handleFiles(req: FluxRequest, path: string, root: string): Promise<Response> {
|
|
128
|
-
let filePath = resolveWithin(root, "." + path)
|
|
129
|
-
if (!filePath) {
|
|
130
|
-
return new Response("Forbidden", { status: 403 })
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
console.log("[cli] get " + path)
|
|
134
|
-
|
|
135
|
-
let stat
|
|
136
|
-
try {
|
|
137
|
-
stat = await file(filePath).stat()
|
|
138
|
-
} catch {
|
|
139
|
-
console.log(`[cli] file not found ${path}`)
|
|
140
|
-
return new Response("Not found", { status: 404 })
|
|
141
|
-
}
|
|
142
|
-
if (stat.type === "directory") {
|
|
143
|
-
return new Response("Not found", { status: 404 })
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let baseHeaders: Record<string, string> = { "Accept-Ranges": "bytes" }
|
|
147
|
-
|
|
148
|
-
// Honor a single byte-range request (e.g. streaming audio decoding on the
|
|
149
|
-
// client, which seeks and reads on demand). Only the common "bytes=a-b" /
|
|
150
|
-
// "bytes=a-" / "bytes=-n" forms; anything else falls through to the whole
|
|
151
|
-
// file. Range makes proxied streaming viable without pulling the whole
|
|
152
|
-
// track over the wire.
|
|
153
|
-
let range = req.headers.get("range")
|
|
154
|
-
let match = range ? /^bytes=(\d*)-(\d*)$/.exec(range.trim()) : null
|
|
155
|
-
if (match) {
|
|
156
|
-
let size = stat.size
|
|
157
|
-
let start: number
|
|
158
|
-
let end: number
|
|
159
|
-
if (match[1] === "") {
|
|
160
|
-
// Suffix range: the last N bytes.
|
|
161
|
-
let n = parseInt(match[2]!, 10)
|
|
162
|
-
start = isNaN(n) ? 0 : Math.max(0, size - n)
|
|
163
|
-
end = size - 1
|
|
164
|
-
} else {
|
|
165
|
-
start = parseInt(match[1]!, 10)
|
|
166
|
-
end = match[2] === "" ? size - 1 : Math.min(parseInt(match[2]!, 10), size - 1)
|
|
167
|
-
}
|
|
168
|
-
if (start > end || start >= size) {
|
|
169
|
-
return new Response("Range not satisfiable", {
|
|
170
|
-
status: 416,
|
|
171
|
-
headers: { ...baseHeaders, "Content-Range": `bytes */${size}` },
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
return new Response(await file(filePath).read(start, end - start + 1), {
|
|
175
|
-
status: 206,
|
|
176
|
-
headers: {
|
|
177
|
-
...baseHeaders,
|
|
178
|
-
"Content-Range": `bytes ${start}-${end}/${size}`,
|
|
179
|
-
"Content-Length": String(end - start + 1),
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return new Response(await file(filePath).bytes(), { headers: baseHeaders })
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Ticket-paired clients connect through this endpoint; serve() accepts its
|
|
188
|
-
// connections directly alongside the TCP listener.
|
|
189
|
-
let tunnel = config.tunnel ? await createTunnelEndpoint(config.port, config.keyDir) : null
|
|
190
|
-
|
|
191
|
-
serve({
|
|
192
|
-
port: config.port,
|
|
193
|
-
p2p: tunnel ? { endpoint: tunnel, protocol: TUNNEL_PROTOCOL } : undefined,
|
|
194
|
-
async fetch(req, server) {
|
|
195
|
-
if (server.upgrade(req)) return
|
|
196
|
-
|
|
197
|
-
let { path, query } = splitQuery(req.url)
|
|
198
|
-
|
|
199
|
-
if (path === "/__proxy__") {
|
|
200
|
-
return handleProxy(req)
|
|
201
|
-
}
|
|
202
|
-
if (path.startsWith("/__control__/")) {
|
|
203
|
-
return handleControl(req, path, query)
|
|
204
|
-
}
|
|
205
|
-
if (path.startsWith("/__internal__/")) {
|
|
206
|
-
return handleInternal(req, server, path)
|
|
207
|
-
}
|
|
208
|
-
// The assets/ convention roots at the project dir (package.json), which is
|
|
209
|
-
// not necessarily the entry's dir the file routes serve; clients fetch
|
|
210
|
-
// manifest asset paths here (live proxy reads and store installs alike).
|
|
211
|
-
if (path === "/assets" || path.startsWith("/assets/")) {
|
|
212
|
-
return handleFiles(req, path, state.projectDir)
|
|
213
|
-
}
|
|
214
|
-
// Isolate bundles are build outputs, not project files: srt (and the
|
|
215
|
-
// rebuild here) write them under .srt-data/isolates/, and the manifest
|
|
216
|
-
// lists them as isolates/<id>.js.
|
|
217
|
-
if (path.startsWith("/isolates/")) {
|
|
218
|
-
return handleFiles(req, path, config.cacheDir)
|
|
219
|
-
}
|
|
220
|
-
return handleFiles(req, path, state.sourceDir)
|
|
221
|
-
},
|
|
222
|
-
websocket: {
|
|
223
|
-
open(ws) {
|
|
224
|
-
let id = state.nextClientId++
|
|
225
|
-
state.clients.set(ws, {
|
|
226
|
-
platform: "unknown",
|
|
227
|
-
version: "unknown",
|
|
228
|
-
profile: "unknown",
|
|
229
|
-
id,
|
|
230
|
-
capabilities: [],
|
|
231
|
-
queries: [],
|
|
232
|
-
})
|
|
233
|
-
console.log(`[cli] Client connected ${ws.remoteAddr ?? "unknown"}`)
|
|
234
|
-
// Advertise our real LAN address so clients dialed over a loopback hop
|
|
235
|
-
// can show/remember the directly reachable address (see connection.rs).
|
|
236
|
-
ws.send(
|
|
237
|
-
JSON.stringify({ type: "welcome", address: state.serverUrl, stats: state.stats, capture: !!config.capture }),
|
|
238
|
-
)
|
|
239
|
-
if (state.currentReload) {
|
|
240
|
-
ws.send(state.currentReload)
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
close(ws) {
|
|
244
|
-
let info = state.clients.get(ws)
|
|
245
|
-
state.clients.delete(ws)
|
|
246
|
-
console.log(`[cli] Client disconnected: ${info?.platform ?? "unknown"}`)
|
|
247
|
-
},
|
|
248
|
-
message(ws, msg) {
|
|
249
|
-
try {
|
|
250
|
-
let data = JSON.parse(typeof msg === "string" ? msg : new TextDecoder().decode(msg))
|
|
251
|
-
if (data.type === "info") {
|
|
252
|
-
let existing = state.clients.get(ws)
|
|
253
|
-
state.clients.set(ws, {
|
|
254
|
-
platform: data.platform ?? "unknown",
|
|
255
|
-
version: data.version ?? "unknown",
|
|
256
|
-
profile: data.profile ?? "unknown",
|
|
257
|
-
id: existing?.id ?? state.nextClientId++,
|
|
258
|
-
capabilities: Array.isArray(data.capabilities) ? data.capabilities.map(String) : [],
|
|
259
|
-
queries: Array.isArray(data.queries) ? data.queries.map(String) : [],
|
|
260
|
-
})
|
|
261
|
-
console.log(`[cli] Client info ${ws.remoteAddr ?? "unknown"} ${data.platform} (${data.version})`)
|
|
262
|
-
} else if (data.type === "log") {
|
|
263
|
-
// Forwarded console output / runtime errors from the client's
|
|
264
|
-
// engine logger, buffered for the control API (see control.ts).
|
|
265
|
-
// Not printed here: the local client already writes to this
|
|
266
|
-
// terminal, so echoing would duplicate every line.
|
|
267
|
-
let device = state.clients.get(ws)?.id ?? -1
|
|
268
|
-
appendLog(device, String(data.level ?? "log"), String(data.text ?? ""))
|
|
269
|
-
} else if (data.type === "result") {
|
|
270
|
-
// Reply to a query the control API forwarded to this client.
|
|
271
|
-
resolveQuery(data)
|
|
272
|
-
} else if (data.type === "capture" && config.capture) {
|
|
273
|
-
let device = state.clients.get(ws)?.id ?? -1
|
|
274
|
-
// Milliseconds, integer: Date.now() is already integer ms, so the
|
|
275
|
-
// delta needs no rounding.
|
|
276
|
-
let at = Date.now() - state.captureStartMs
|
|
277
|
-
let after = at - state.captureLastAt
|
|
278
|
-
state.captureLastAt = at
|
|
279
|
-
// JSON Lines: one event object per line, streamed to disk as it
|
|
280
|
-
// arrives rather than buffered - no in-memory growth for a long
|
|
281
|
-
// capture, and the file is always complete on disk mid-session.
|
|
282
|
-
// Appends are chained so events land in arrival order.
|
|
283
|
-
let line = JSON.stringify({ after, type: data.kind, key: data.key, device }) + "\n"
|
|
284
|
-
state.captureChain = state.captureChain.then(() => file(config.capture!).append(line))
|
|
285
|
-
}
|
|
286
|
-
} catch {}
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
// One QR on screen: with the tunnel on, the ticket QR (printed by
|
|
292
|
-
// createTunnelEndpoint) is the pairing story and the address stays text-only;
|
|
293
|
-
// without it, the address QR is the scan target as before.
|
|
294
|
-
if (!config.tunnel) {
|
|
295
|
-
console.log("")
|
|
296
|
-
printQr(state.serverUrl)
|
|
297
|
-
console.log("")
|
|
298
|
-
}
|
|
299
|
-
console.log(`[cli] WebSocket server on ws://${state.serverUrl}`)
|
|
300
|
-
// mDNS advertise is intentionally not implemented here: the p2p ticket is the
|
|
301
|
-
// cross-device connect story (see okf/backlog/mdns-discovery.md).
|
|
302
|
-
|
|
303
|
-
// Keepalive
|
|
304
|
-
setInterval(() => {
|
|
305
|
-
for (let ws of state.clients.keys()) {
|
|
306
|
-
ws.ping()
|
|
307
|
-
}
|
|
308
|
-
}, 5000)
|
package/server/rebuild.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { command } from "flux:subprocess"
|
|
2
|
-
import { dir, file } from "flux:fs"
|
|
3
|
-
import { state } from "./state"
|
|
4
|
-
|
|
5
|
-
// Server-owned "rebuild and push": the single place the running app is rebuilt
|
|
6
|
-
// from source on demand (an MCP reload). The srt repl still bundles in-process
|
|
7
|
-
// for its own keystroke reloads, but both routes call the same bundle-cli, so
|
|
8
|
-
// the bundling logic cannot drift. Making the server the rebuild authority is
|
|
9
|
-
// the interim step toward folding the whole CLI into flux (see
|
|
10
|
-
// okf/backlog/cli-flux-migration.md).
|
|
11
|
-
|
|
12
|
-
// Build the reload message the same way srt's buildReload does, so a
|
|
13
|
-
// server-triggered reload is indistinguishable from a repl-triggered one to
|
|
14
|
-
// clients. proxyHttp is a message flag, not a build input.
|
|
15
|
-
function buildReload(code: string, manifest?: string) {
|
|
16
|
-
let config = state.config
|
|
17
|
-
return {
|
|
18
|
-
type: "reload",
|
|
19
|
-
proxyHttp: config.proxyHttp,
|
|
20
|
-
args: config.args,
|
|
21
|
-
...(manifest ? { manifest } : {}),
|
|
22
|
-
code,
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Rebuild from state.config.entry via the external bundle-cli subprocess, then
|
|
27
|
-
// latch (for late-joining clients) and broadcast the reload to every connected
|
|
28
|
-
// client. Resolves with an error message on failure (no entry configured, or a
|
|
29
|
-
// build error), or null on success.
|
|
30
|
-
export async function rebuildAndBroadcast(): Promise<string | null> {
|
|
31
|
-
let config = state.config
|
|
32
|
-
if (!config.entry) {
|
|
33
|
-
return "No app entry to rebuild. Start srt with a source file (srt run src/index.tsx)."
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let params = JSON.stringify({
|
|
37
|
-
entry: config.entry,
|
|
38
|
-
devBase: state.serverUrl,
|
|
39
|
-
dev: true,
|
|
40
|
-
minify: config.minify,
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
let result = await command(config.bundlerCmd[0]!, [...config.bundlerCmd.slice(1), params]).output()
|
|
44
|
-
if (!result.success) {
|
|
45
|
-
let stderr = typeof result.stderr === "string" ? result.stderr : ""
|
|
46
|
-
return `Rebuild failed:\n${stderr.trim()}`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// bundle-cli writes one JSON object { code, map, manifest, isolates } to stdout.
|
|
50
|
-
let bundle: {
|
|
51
|
-
code?: string
|
|
52
|
-
map?: string | null
|
|
53
|
-
manifest?: string
|
|
54
|
-
isolates?: { id: string; code: string; map?: string | null }[]
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
bundle = JSON.parse(typeof result.stdout === "string" ? result.stdout : "")
|
|
58
|
-
} catch {
|
|
59
|
-
return "Rebuild failed: unreadable bundler output"
|
|
60
|
-
}
|
|
61
|
-
// Isolate bundles are manifest assets clients fetch from our /isolates/
|
|
62
|
-
// route (served from cacheDir), so they must be on disk before the push.
|
|
63
|
-
let maps: Record<string, string> = {}
|
|
64
|
-
if (bundle.map) maps.main = bundle.map
|
|
65
|
-
for (let isolate of bundle.isolates ?? []) {
|
|
66
|
-
let path = `${config.cacheDir}/isolates/${isolate.id}.js`
|
|
67
|
-
await dir(path.slice(0, path.lastIndexOf("/"))).create()
|
|
68
|
-
await file(path).write(isolate.code)
|
|
69
|
-
if (isolate.map) maps[isolate.id] = isolate.map
|
|
70
|
-
}
|
|
71
|
-
state.currentMaps = Object.keys(maps).length ? maps : null
|
|
72
|
-
let text = JSON.stringify(buildReload(bundle.code ?? "", bundle.manifest))
|
|
73
|
-
state.currentReload = text
|
|
74
|
-
for (let ws of state.clients.keys()) ws.send(text)
|
|
75
|
-
return null
|
|
76
|
-
}
|
package/server/state.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// Server-side state shared by the route handlers. The srt process keeps the
|
|
2
|
-
// bundler, watcher, and repl; this state is only what the protocol needs.
|
|
3
|
-
|
|
4
|
-
import type { ServerWebSocket } from "flux:http"
|
|
5
|
-
|
|
6
|
-
export type Config = {
|
|
7
|
-
port: number
|
|
8
|
-
/** Directory served by the file routes (updatable via /__internal__/reload). */
|
|
9
|
-
sourceDir: string
|
|
10
|
-
/** Project root whose assets/ folder the /assets/ route serves (updatable
|
|
11
|
-
* via /__internal__/reload, moves with the repl `load`). */
|
|
12
|
-
projectDir: string
|
|
13
|
-
/** The address clients can reach this machine on (LAN IP or 127.0.0.1). */
|
|
14
|
-
address: string
|
|
15
|
-
proxyHttp: boolean
|
|
16
|
-
/** The app entry (absolute .tsx/.jsx path) the server rebuilds on an
|
|
17
|
-
* MCP-triggered reload, or undefined when srt was started without a source.
|
|
18
|
-
* Moved by the repl `load` command via /__internal__/reload. */
|
|
19
|
-
entry?: string
|
|
20
|
-
/** The session's app arguments (the srt command-line tail after a bare
|
|
21
|
-
* "--"), included in every reload push as flux:process argv. */
|
|
22
|
-
args: string[]
|
|
23
|
-
/** Minify the rebuild output, mirroring the srt --minify flag. */
|
|
24
|
-
minify: boolean
|
|
25
|
-
/** How the server invokes the external bundler: [bunPath, bundleCliPath],
|
|
26
|
-
* spawned with a JSON params argument appended (see rebuild.ts). */
|
|
27
|
-
bundlerCmd: string[]
|
|
28
|
-
/** Enable the sqlite-backed proxy cache. */
|
|
29
|
-
cache: boolean
|
|
30
|
-
/** Directory holding the proxy cache db (the project-local .srt-data). */
|
|
31
|
-
cacheDir: string
|
|
32
|
-
/** Directory holding tunnel.key (the server's ~/.solidrt/servers/<port>/ folder). */
|
|
33
|
-
keyDir: string
|
|
34
|
-
/** Destination for captured key events, or unset when off. */
|
|
35
|
-
capture?: string
|
|
36
|
-
stats: boolean
|
|
37
|
-
/** Accept ticket-paired clients through the p2p tunnel. */
|
|
38
|
-
tunnel: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type ClientInfo = {
|
|
42
|
-
platform: string
|
|
43
|
-
version: string
|
|
44
|
-
profile: string
|
|
45
|
-
id: number
|
|
46
|
-
capabilities: string[]
|
|
47
|
-
/** Query kinds this client's runtime answers (empty on runtimes that predate
|
|
48
|
-
* the advertisement); dev tools plan their verification surface from it. */
|
|
49
|
-
queries: string[]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export let state = {
|
|
53
|
-
config: undefined as unknown as Config,
|
|
54
|
-
clients: new Map<ServerWebSocket, ClientInfo>(),
|
|
55
|
-
nextClientId: 0,
|
|
56
|
-
/**
|
|
57
|
-
* Identity of this server run, included in control responses that carry
|
|
58
|
-
* cross-call state (client ids, log seq cursors). Both reset on restart, so
|
|
59
|
-
* a consumer that sees the generation change knows its ids and cursors are
|
|
60
|
-
* from a dead server and must be re-fetched.
|
|
61
|
-
*/
|
|
62
|
-
generation: Date.now(),
|
|
63
|
-
/**
|
|
64
|
-
* The latched reload message (JSON text), replayed to late-joining clients.
|
|
65
|
-
* Set by /__internal__/reload posts with `latch`, cleared by a broadcast stop.
|
|
66
|
-
*/
|
|
67
|
-
currentReload: null as string | null,
|
|
68
|
-
/**
|
|
69
|
-
* The running bundle's sourcemaps (JSON text, bundle -> .tsx sources),
|
|
70
|
-
* keyed by the module name stack frames cite ("main" for the app, the
|
|
71
|
-
* isolate id for each isolate), used to remap stack traces in forwarded
|
|
72
|
-
* client logs (see control.ts). Replaced on every reload; a reload without
|
|
73
|
-
* maps clears them so frames are never remapped against a stale map.
|
|
74
|
-
*/
|
|
75
|
-
currentMaps: null as Record<string, string> | null,
|
|
76
|
-
sourceDir: "",
|
|
77
|
-
projectDir: "",
|
|
78
|
-
serverUrl: "",
|
|
79
|
-
stats: false,
|
|
80
|
-
/**
|
|
81
|
-
* Whether srt's file watcher may auto-reload on source changes. Agents
|
|
82
|
-
* pause it (MCP watch tool -> /__control__/watch) while creating or
|
|
83
|
-
* editing files; a successful /reload or /load re-enables it. srt reads
|
|
84
|
-
* it via /__internal__/watch before acting on a change event.
|
|
85
|
-
*/
|
|
86
|
-
watch: true,
|
|
87
|
-
// Capture events from all connected clients share one clock (captureStartMs,
|
|
88
|
-
// integer milliseconds) so they merge into one coherent timeline, tagged by
|
|
89
|
-
// `device`. Streamed to disk as JSON Lines - see main.ts's "capture" handling.
|
|
90
|
-
captureStartMs: 0,
|
|
91
|
-
captureLastAt: 0, // ms, same clock as captureStartMs
|
|
92
|
-
/** Serializes capture appends so events land on disk in arrival order. */
|
|
93
|
-
captureChain: Promise.resolve(),
|
|
94
|
-
}
|