@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/src/bundler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { transformAsync } from "@babel/core"
|
|
2
2
|
import jsx from "@babel/plugin-syntax-jsx"
|
|
3
3
|
import ts from "@babel/preset-typescript"
|
|
4
|
+
import remapping from "@jridgewell/remapping"
|
|
4
5
|
import solid from "babel-preset-solid"
|
|
5
6
|
import { type BunPlugin, type BuildArtifact } from "bun"
|
|
6
7
|
import { readFileSync } from "node:fs"
|
|
@@ -53,7 +54,7 @@ function binaryImport({ types: t }: { types: any }) {
|
|
|
53
54
|
// build, skipping emitted asset outputs. Bun's file loader emits binary assets
|
|
54
55
|
// as extra outputs; the callers that flatten outputs into a single code string
|
|
55
56
|
// must not glue those raw bytes onto the program.
|
|
56
|
-
|
|
57
|
+
async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string> {
|
|
57
58
|
let code = ""
|
|
58
59
|
for (let o of outputs) {
|
|
59
60
|
if (o.kind === "entry-point" || o.kind === "chunk") code += await o.text()
|
|
@@ -62,61 +63,106 @@ export async function codeFromOutputs(outputs: BuildArtifact[]): Promise<string>
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// Bun build plugin that runs JSX/TSX through babel-preset-solid (universal
|
|
65
|
-
// generate, targeting @solidrt/core) plus the TS preset.
|
|
66
|
-
|
|
66
|
+
// generate, targeting @solidrt/core) plus the TS preset. Plain .js/.ts app
|
|
67
|
+
// modules take the same path (solid is a no-op without JSX) so binaryImport
|
|
68
|
+
// can rewrite their `with { type: "binary" }` imports too; dependency code
|
|
69
|
+
// (node_modules) skips the babel detour and keeps Bun's native loaders.
|
|
70
|
+
// With `babelMaps`, each file's transform map (original -> babel output) is
|
|
71
|
+
// collected there, keyed by absolute path, for sourcemap composition later.
|
|
72
|
+
function solidPlugin(babelMaps?: Map<string, object>): BunPlugin {
|
|
67
73
|
return {
|
|
68
74
|
name: "bun-plugin-solid",
|
|
69
75
|
setup: (build) => {
|
|
70
|
-
build.onLoad({ filter: /\.(js|ts)x
|
|
76
|
+
build.onLoad({ filter: /\.(js|ts)x?$/ }, async (args) => {
|
|
77
|
+
if (!/\.(js|ts)x$/.test(args.path) && args.path.includes("node_modules")) return
|
|
71
78
|
let file = Bun.file(args.path)
|
|
72
79
|
let code = await file.text()
|
|
73
80
|
let transforms = await transformAsync(code, {
|
|
74
81
|
filename: args.path,
|
|
82
|
+
sourceMaps: !!babelMaps,
|
|
75
83
|
presets: [[solid, { moduleName: "@solidrt/core", generate: "universal" }], [ts]],
|
|
76
84
|
plugins: [jsx, binaryImport],
|
|
77
85
|
})
|
|
86
|
+
if (babelMaps && transforms?.map) babelMaps.set(args.path, transforms.map)
|
|
78
87
|
return { contents: transforms?.code ?? "", loader: "js" }
|
|
79
88
|
})
|
|
80
89
|
},
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
export
|
|
85
|
-
let result = null
|
|
93
|
+
export type BundleOptions = { entry: string; devBase?: string; dev: boolean; minify: boolean }
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
export type BundleResult = {
|
|
96
|
+
code: string
|
|
97
|
+
/** Composed sourcemap JSON (bundle -> original .tsx sources), dev builds only. */
|
|
98
|
+
map: string | null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The pure bundle: every input is explicit, so it runs identically in the srt
|
|
102
|
+
// (Bun) process and in the standalone bundle-cli subprocess the dev server
|
|
103
|
+
// spawns. It never touches the ambient args/state singletons and never prints
|
|
104
|
+
// progress (callers own that), so its stdout stays clean for subprocess use.
|
|
105
|
+
export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
|
|
91
106
|
let define: Record<string, string> = {
|
|
92
|
-
"process.env.NODE_ENV": dev ? "development" : "production",
|
|
107
|
+
"process.env.NODE_ENV": opts.dev ? "development" : "production",
|
|
93
108
|
}
|
|
94
|
-
if (devBase) define.__SRT_DEV_BASE__ = devBase
|
|
109
|
+
if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
|
|
95
110
|
|
|
111
|
+
let babelMaps = opts.dev ? new Map<string, object>() : undefined
|
|
112
|
+
let result = null
|
|
96
113
|
try {
|
|
97
114
|
result = await Bun.build({
|
|
98
|
-
entrypoints: [entry
|
|
115
|
+
entrypoints: [opts.entry],
|
|
99
116
|
target: "browser",
|
|
100
117
|
format: "esm",
|
|
101
|
-
minify:
|
|
118
|
+
minify: opts.minify,
|
|
102
119
|
external: ["flux:*", "srt:*"],
|
|
103
120
|
define,
|
|
104
121
|
loader: { ".svg": "text" },
|
|
105
|
-
|
|
122
|
+
sourcemap: opts.dev ? "external" : "none",
|
|
123
|
+
plugins: [solidPlugin(babelMaps)],
|
|
106
124
|
})
|
|
107
125
|
} catch (e) {
|
|
108
126
|
console.error("[cli] compile error:\n", e)
|
|
109
127
|
return null
|
|
110
128
|
}
|
|
111
129
|
|
|
112
|
-
if (result
|
|
113
|
-
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
for (let msg of result.logs) console.error(msg)
|
|
132
|
+
return null
|
|
114
133
|
}
|
|
115
134
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
return { code: await codeFromOutputs(result.outputs), map: await composeMap(result.outputs, babelMaps) }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Compose Bun's bundle map (babel output -> bundle) with the per-file Babel
|
|
139
|
+
// maps (original source -> babel output) so positions point at the .tsx
|
|
140
|
+
// sources. Bun ignores sourcemaps in plugin onLoad contents, so this second
|
|
141
|
+
// hop has to happen here. Only a single-artifact build gets a map: code
|
|
142
|
+
// splitting is off, and concatenated artifacts would invalidate offsets.
|
|
143
|
+
async function composeMap(outputs: BuildArtifact[], babelMaps?: Map<string, object>): Promise<string | null> {
|
|
144
|
+
if (!babelMaps) return null
|
|
145
|
+
let js = outputs.filter((o) => o.kind === "entry-point" || o.kind === "chunk")
|
|
146
|
+
if (js.length !== 1 || !js[0]!.sourcemap) return null
|
|
147
|
+
let bunMap = JSON.parse(await js[0]!.sourcemap.text())
|
|
148
|
+
let composed = remapping(bunMap, (file: string) => {
|
|
149
|
+
// Bun writes cwd-relative source paths; the babel maps are keyed by the
|
|
150
|
+
// absolute path. Serve each map exactly once: remapping asks again for a
|
|
151
|
+
// served map's own original source, and that lookup must return null.
|
|
152
|
+
let abs = resolvePath(file)
|
|
153
|
+
let map = babelMaps.get(abs)
|
|
154
|
+
babelMaps.delete(abs)
|
|
155
|
+
return (map as any) ?? null
|
|
156
|
+
})
|
|
157
|
+
return composed.toString()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function bundle(entry = source) {
|
|
161
|
+
let devBase = state.serverUrl ?? undefined
|
|
162
|
+
let dev = !!devBase || values.dev
|
|
163
|
+
// Keep stdout clean when the bundle itself is written to stdout.
|
|
164
|
+
if (!values.stdout) print(`[cli] Bundling (${dev ? "development" : "production"})`)
|
|
165
|
+
return bundleWith({ entry: entry!, devBase, dev, minify: values.minify })
|
|
120
166
|
}
|
|
121
167
|
|
|
122
168
|
export async function bundleTo(outfile: string) {
|
|
@@ -125,7 +171,7 @@ export async function bundleTo(outfile: string) {
|
|
|
125
171
|
console.error("Build failed")
|
|
126
172
|
process.exit(1)
|
|
127
173
|
}
|
|
128
|
-
await Bun.write(outfile,
|
|
174
|
+
await Bun.write(outfile, result.code)
|
|
129
175
|
return result
|
|
130
176
|
}
|
|
131
177
|
|
|
@@ -153,7 +199,7 @@ export async function bundleSolid(): Promise<string> {
|
|
|
153
199
|
console.error("Build failed")
|
|
154
200
|
process.exit(1)
|
|
155
201
|
}
|
|
156
|
-
return
|
|
202
|
+
return result.code
|
|
157
203
|
}
|
|
158
204
|
|
|
159
205
|
// Compile JS source to QuickJS bytecode via the fluxc binary.
|
package/src/commands/bundle.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { values, source, isPrebuilt } from "../args"
|
|
2
|
-
import { bundle, bundleTo, bundleFlux, compileToBytecode
|
|
2
|
+
import { bundle, bundleTo, bundleFlux, compileToBytecode } from "../bundler"
|
|
3
3
|
import { resolve } from "path"
|
|
4
4
|
|
|
5
5
|
// Write to stdout and resolve only once the whole payload is flushed.
|
|
@@ -55,7 +55,7 @@ export async function runBundleCommand() {
|
|
|
55
55
|
console.error("Build failed")
|
|
56
56
|
process.exit(1)
|
|
57
57
|
}
|
|
58
|
-
await writeStdout(
|
|
58
|
+
await writeStdout(result.code)
|
|
59
59
|
process.exit()
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -65,15 +65,12 @@ export async function runBundleCommand() {
|
|
|
65
65
|
console.error("Build failed")
|
|
66
66
|
process.exit(1)
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
await writeBytecode(jsCode, baseName + ".srt.bin")
|
|
68
|
+
await writeBytecode(result.code, baseName + ".srt.bin")
|
|
70
69
|
process.exit()
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
let jsOutfile = baseName + ".srt.js"
|
|
74
73
|
let result = await bundleTo(jsOutfile)
|
|
75
|
-
|
|
76
|
-
console.log(`>> wrote ${output.size} bytes to ${jsOutfile}`)
|
|
77
|
-
}
|
|
74
|
+
console.log(`>> wrote ${result.code.length} bytes to ${jsOutfile}`)
|
|
78
75
|
process.exit()
|
|
79
76
|
}
|
package/src/commands/mcp.ts
CHANGED
|
@@ -5,16 +5,25 @@
|
|
|
5
5
|
//
|
|
6
6
|
// stdout is the JSON-RPC channel; nothing here may print to it.
|
|
7
7
|
|
|
8
|
+
import { z } from "zod"
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
11
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"
|
|
8
12
|
import { DEV_PORT } from "../dev-server"
|
|
9
13
|
|
|
10
14
|
const CONTROL_BASE = `http://127.0.0.1:${DEV_PORT}/__control__`
|
|
11
15
|
|
|
12
16
|
type ControlResult = { ok: true; body: any } | { ok: false; message: string }
|
|
13
17
|
|
|
14
|
-
async function control(path: string): Promise<ControlResult> {
|
|
18
|
+
async function control(path: string, method: "GET" | "POST" = "GET", payload?: unknown): Promise<ControlResult> {
|
|
15
19
|
let resp
|
|
16
20
|
try {
|
|
17
|
-
|
|
21
|
+
let init: RequestInit = { method }
|
|
22
|
+
if (payload !== undefined) {
|
|
23
|
+
init.headers = { "content-type": "application/json" }
|
|
24
|
+
init.body = JSON.stringify(payload)
|
|
25
|
+
}
|
|
26
|
+
resp = await fetch(CONTROL_BASE + path, init)
|
|
18
27
|
} catch {
|
|
19
28
|
return {
|
|
20
29
|
ok: false,
|
|
@@ -29,56 +38,110 @@ async function control(path: string): Promise<ControlResult> {
|
|
|
29
38
|
return { ok: true, body }
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
let
|
|
41
|
+
let CLIENT_ARG = z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.describe("Client id from list_clients (default: the only connected client)")
|
|
45
|
+
.optional()
|
|
46
|
+
|
|
47
|
+
let TOOLS: { name: string; description: string; inputSchema: Record<string, z.ZodTypeAny> }[] = [
|
|
33
48
|
{
|
|
34
49
|
name: "list_clients",
|
|
35
50
|
description:
|
|
36
|
-
"List the app clients connected to the SolidRT dev server. Each entry has id (pass it as `client` to the other tools), platform, runtime version, and the capability names compiled into that client's runtime.",
|
|
37
|
-
inputSchema: {
|
|
51
|
+
"List the app clients connected to the SolidRT dev server. Each entry has id (pass it as `client` to the other tools), platform, runtime version (git describe; a -dirty suffix means the binary was built from uncommitted engine changes), build profile (debug/release), and the capability names compiled into that client's runtime. Use version/profile to check whether a connected binary contains a given engine change before debugging against it.",
|
|
52
|
+
inputSchema: {},
|
|
38
53
|
},
|
|
39
54
|
{
|
|
40
55
|
name: "get_logs",
|
|
41
56
|
description:
|
|
42
57
|
"Read console output and runtime errors from connected app clients. Returns entries (seq, at, client, level, text) plus `latest`, the newest seq. Pass `since` (a seq or `latest` from a previous call) to only get newer entries; pass `wait_ms` to hold the call until new output arrives, e.g. right after triggering a reload.",
|
|
43
58
|
inputSchema: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
additionalProperties: false,
|
|
59
|
+
since: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.describe("Only return entries with seq greater than this (default 0: the whole buffer)")
|
|
63
|
+
.optional(),
|
|
64
|
+
wait_ms: z
|
|
65
|
+
.number()
|
|
66
|
+
.int()
|
|
67
|
+
.describe("If nothing is newer than `since`, wait up to this many milliseconds for new output (max 30000)")
|
|
68
|
+
.optional(),
|
|
56
69
|
},
|
|
57
70
|
},
|
|
58
71
|
{
|
|
59
72
|
name: "get_stats",
|
|
60
73
|
description:
|
|
61
74
|
"Performance statistics from a running app client: fps, CPU%, memory, smoothed JS/layout/paint/hover frame times (ms), setProperty writes per frame, demand-gate reuse/skip counts per second, and live texture count.",
|
|
62
|
-
inputSchema: {
|
|
63
|
-
type: "object",
|
|
64
|
-
properties: {
|
|
65
|
-
client: { type: "integer", description: "Client id from list_clients (default: the only connected client)" },
|
|
66
|
-
},
|
|
67
|
-
additionalProperties: false,
|
|
68
|
-
},
|
|
75
|
+
inputSchema: { client: CLIENT_ARG },
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
name: "get_render_tree",
|
|
72
79
|
description:
|
|
73
80
|
"Snapshot of a running app client's render tree: node id, kind, window-relative box (x, y, width, height), text content, and children. Use it to verify what the app actually rendered and where.",
|
|
81
|
+
inputSchema: { client: CLIENT_ARG },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "get_snapshot",
|
|
85
|
+
description:
|
|
86
|
+
"Capture a PNG image of any node in a running app client's render tree, by node id (get ids from get_render_tree). Returns the rendered pixels of that node's subtree, so you can see what the app actually drew. The node must be currently mounted and have a non-zero layout box.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
nodeId: z.number().int().describe("Id of the node to capture, from get_render_tree"),
|
|
89
|
+
client: CLIENT_ARG,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "get_gpu_resources",
|
|
94
|
+
description:
|
|
95
|
+
"Inventory of a running app client's GPU resources: textures (id, size, whether a shader renders into it), vertex buffers (id, byteLength), and shader/pipeline targets (output textureId, kind, bufferId, topology, drawCount, depth, attribute layout, bound sampler texture ids, last-applied uniform values). Use it when the render tree is just a <texture> leaf and the interesting state lives behind it; follow up with get_texture or get_buffer to see contents.",
|
|
96
|
+
inputSchema: { client: CLIENT_ARG },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "get_texture",
|
|
100
|
+
description:
|
|
101
|
+
"Read back any GPU texture from a running app client as a PNG, by texture id (from get_gpu_resources, or the id returned by createImage/createShader/createPipeline in app code). Works on sampled textures (atlases, data textures) and shader/pipeline render targets alike, without needing a frame. Pass x/y/width/height to crop, e.g. one tile of an atlas.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
id: z.number().int().describe("Texture id, from get_gpu_resources"),
|
|
104
|
+
x: z.number().int().describe("Crop rect left edge in texture pixels (requires y, width, height)").optional(),
|
|
105
|
+
y: z.number().int().describe("Crop rect top edge in texture pixels").optional(),
|
|
106
|
+
width: z.number().int().describe("Crop rect width in texture pixels").optional(),
|
|
107
|
+
height: z.number().int().describe("Crop rect height in texture pixels").optional(),
|
|
108
|
+
client: CLIENT_ARG,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "get_buffer",
|
|
113
|
+
description:
|
|
114
|
+
"Read back part of a GPU vertex buffer from a running app client, decoded to numbers. Returns values plus byteOffset/byteLength actually read and bufferByteLength. Reads are capped at 64 KiB per call; page through larger buffers with offset. Use it to verify geometry after a writeBuffer, e.g. the dynamic sprite tail of a vertex buffer.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
id: z.number().int().describe("Buffer id, from get_gpu_resources"),
|
|
117
|
+
offset: z.number().int().describe("Byte offset to start reading at (default 0)").optional(),
|
|
118
|
+
length: z.number().int().describe("Number of values to read (default: the rest of the buffer, capped)").optional(),
|
|
119
|
+
as: z.enum(["f32", "u16", "u8"]).describe("How to decode the bytes (default f32)").optional(),
|
|
120
|
+
client: CLIENT_ARG,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "list_debug",
|
|
125
|
+
description:
|
|
126
|
+
"List the debug commands the running app registered via registerDebug from srt:dev. Returns the command names; call one with call_debug. Empty when the app registered none.",
|
|
127
|
+
inputSchema: { client: CLIENT_ARG },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "call_debug",
|
|
131
|
+
description:
|
|
132
|
+
"Call a debug command the running app registered via registerDebug from srt:dev, by name (from list_debug). `args` is passed to the command's function as its single argument; the command's return value comes back JSON-serialized (undefined as null). Commands run synchronously on the app's JS thread - use them to query app state (positions, counters, internal flags) or trigger app behavior (toggle a mode, open a door) without touching its real input handling.",
|
|
74
133
|
inputSchema: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
79
|
-
additionalProperties: false,
|
|
134
|
+
name: z.string().describe("Debug command name, from list_debug"),
|
|
135
|
+
args: z.record(z.string(), z.any()).describe("Argument object passed to the command (default: none)").optional(),
|
|
136
|
+
client: CLIENT_ARG,
|
|
80
137
|
},
|
|
81
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: "reload",
|
|
141
|
+
description:
|
|
142
|
+
"Rebuild the app from source and push it to every connected client. Call this after editing the app's .tsx/.jsx source to apply the changes: it bundles once and reloads all clients, so a burst of edits becomes a single explicit reload. Returns the number of clients reloaded, or a build error if the source failed to compile. Follow with get_logs to see runtime output from the reloaded app.",
|
|
143
|
+
inputSchema: {},
|
|
144
|
+
},
|
|
82
145
|
]
|
|
83
146
|
|
|
84
147
|
function clientParam(args: any): string {
|
|
@@ -100,27 +163,72 @@ async function callTool(name: string, args: any): Promise<ControlResult> {
|
|
|
100
163
|
return control(`/stats${clientParam(args)}`)
|
|
101
164
|
case "get_render_tree":
|
|
102
165
|
return control(`/tree${clientParam(args)}`)
|
|
166
|
+
case "reload":
|
|
167
|
+
return control("/reload", "POST")
|
|
168
|
+
case "get_snapshot": {
|
|
169
|
+
if (typeof args?.nodeId !== "number") return { ok: false, message: "get_snapshot requires a numeric nodeId" }
|
|
170
|
+
let params = new URLSearchParams({ node: String(args.nodeId) })
|
|
171
|
+
if (typeof args?.client === "number") params.set("client", String(args.client))
|
|
172
|
+
return control(`/snapshot?${params.toString()}`)
|
|
173
|
+
}
|
|
174
|
+
case "get_gpu_resources":
|
|
175
|
+
return control(`/gpu${clientParam(args)}`)
|
|
176
|
+
case "list_debug":
|
|
177
|
+
return control(`/debug${clientParam(args)}`)
|
|
178
|
+
case "call_debug": {
|
|
179
|
+
if (typeof args?.name !== "string") return { ok: false, message: "call_debug requires a command name" }
|
|
180
|
+
let params = new URLSearchParams({ name: args.name })
|
|
181
|
+
if (typeof args?.client === "number") params.set("client", String(args.client))
|
|
182
|
+
return control(`/debug?${params.toString()}`, "POST", args?.args)
|
|
183
|
+
}
|
|
184
|
+
case "get_texture": {
|
|
185
|
+
if (typeof args?.id !== "number") return { ok: false, message: "get_texture requires a numeric id" }
|
|
186
|
+
let params = new URLSearchParams({ id: String(args.id) })
|
|
187
|
+
for (let key of ["x", "y", "width", "height"]) {
|
|
188
|
+
if (typeof args?.[key] === "number") params.set(key, String(args[key]))
|
|
189
|
+
}
|
|
190
|
+
if (typeof args?.client === "number") params.set("client", String(args.client))
|
|
191
|
+
return control(`/texture?${params.toString()}`)
|
|
192
|
+
}
|
|
193
|
+
case "get_buffer": {
|
|
194
|
+
if (typeof args?.id !== "number") return { ok: false, message: "get_buffer requires a numeric id" }
|
|
195
|
+
let params = new URLSearchParams({ id: String(args.id) })
|
|
196
|
+
if (typeof args?.offset === "number") params.set("offset", String(args.offset))
|
|
197
|
+
if (typeof args?.length === "number") params.set("length", String(args.length))
|
|
198
|
+
if (typeof args?.as === "string") params.set("as", args.as)
|
|
199
|
+
if (typeof args?.client === "number") params.set("client", String(args.client))
|
|
200
|
+
return control(`/buffer?${params.toString()}`)
|
|
201
|
+
}
|
|
103
202
|
default:
|
|
104
203
|
return { ok: false, message: `Unknown tool: ${name}` }
|
|
105
204
|
}
|
|
106
205
|
}
|
|
107
206
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
207
|
+
function toContent(name: string, result: ControlResult): CallToolResult {
|
|
208
|
+
if (!result.ok) return { content: [{ type: "text", text: result.message }], isError: true }
|
|
209
|
+
if (name === "get_snapshot" || name === "get_texture") {
|
|
210
|
+
let { pngBase64, width, height } = result.body
|
|
211
|
+
let label = name === "get_snapshot" ? "Captured node snapshot" : "Texture contents"
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{ type: "image", data: pngBase64, mimeType: "image/png" },
|
|
215
|
+
{ type: "text", text: `${label}: ${width}x${height} px` },
|
|
216
|
+
],
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { content: [{ type: "text", text: JSON.stringify(result.body, null, 2) }] }
|
|
220
|
+
}
|
|
114
221
|
|
|
115
|
-
|
|
222
|
+
export async function runMcpCommand() {
|
|
223
|
+
let server = new McpServer({ name: "solidrt", version: "0.0.0" })
|
|
116
224
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
225
|
+
for (let tool of TOOLS) {
|
|
226
|
+
server.registerTool(
|
|
227
|
+
tool.name,
|
|
228
|
+
{ description: tool.description, inputSchema: tool.inputSchema },
|
|
229
|
+
async (args: any) => toContent(tool.name, await callTool(tool.name, args ?? {})),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
124
232
|
|
|
125
233
|
// The stdin read keeps the process alive; it exits when the agent host
|
|
126
234
|
// closes the pipe.
|
package/src/commands/server.ts
CHANGED
|
@@ -1,45 +1,41 @@
|
|
|
1
1
|
import pkg from "../../package.json"
|
|
2
2
|
import { source, isSource, isPrebuilt, values } from "../args"
|
|
3
3
|
import { state, shutdown } from "../util"
|
|
4
|
-
import { bundle
|
|
5
|
-
import { startServer, showBuildFailure } from "../dev-server"
|
|
4
|
+
import { bundle } from "../bundler"
|
|
5
|
+
import { startServer, buildReload, sendReload, showBuildFailure } from "../dev-server"
|
|
6
6
|
import { startRepl } from "../repl"
|
|
7
7
|
import { startWatcher } from "../watcher"
|
|
8
|
-
import * as cache from "../cache"
|
|
9
8
|
import { resolve, dirname } from "path"
|
|
10
|
-
import { writeFileSync } from "node:fs"
|
|
11
9
|
|
|
12
|
-
// Brings up the dev server (
|
|
13
|
-
//
|
|
10
|
+
// Brings up the dev server (a spawned flux script serving HTTP/WS) plus the
|
|
11
|
+
// initial bundle, repl, and watcher in this process. The `run` command spawns
|
|
12
|
+
// a local client on top of this from main.ts.
|
|
14
13
|
export async function runServerCommand() {
|
|
15
14
|
// Initialize state from args
|
|
16
15
|
state.source = source
|
|
17
16
|
state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
|
|
18
17
|
state.stats = values.stats
|
|
19
18
|
state.capture = values.capture ? resolve(values.capture) : undefined
|
|
20
|
-
state.captureStartMs = Date.now()
|
|
21
|
-
// Start each capture from an empty file: appendFileSync (dev-server.ts)
|
|
22
|
-
// would otherwise tack onto whatever a previous run left behind.
|
|
23
|
-
if (state.capture) writeFileSync(state.capture, "")
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
startServer()
|
|
20
|
+
// Spawns the server process and waits until it answers; it owns the QR and
|
|
21
|
+
// address announcements, the capture file, and the proxy cache.
|
|
22
|
+
await startServer()
|
|
31
23
|
|
|
32
24
|
// Bundle initial code if source file given (after server start so the
|
|
33
|
-
// dev base URL is available to the bundler)
|
|
25
|
+
// dev base URL is available to the bundler), and latch it on the server
|
|
26
|
+
// for the clients about to connect.
|
|
34
27
|
if (source && isSource) {
|
|
35
28
|
let initialResult = await bundle()
|
|
36
29
|
if (initialResult) {
|
|
37
|
-
state.currentCode =
|
|
30
|
+
state.currentCode = initialResult.code
|
|
31
|
+
state.currentMap = initialResult.map
|
|
32
|
+
await sendReload(buildReload({ code: state.currentCode }), { latch: true, map: state.currentMap })
|
|
38
33
|
} else {
|
|
39
|
-
showBuildFailure()
|
|
34
|
+
await showBuildFailure()
|
|
40
35
|
}
|
|
41
36
|
} else if (source && isPrebuilt && source.endsWith(".srt.js")) {
|
|
42
37
|
state.currentCode = await Bun.file(resolve(source)).text()
|
|
38
|
+
await sendReload(buildReload({ code: state.currentCode }), { latch: true })
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
process.on("SIGINT", shutdown)
|
|
@@ -49,4 +45,4 @@ export async function runServerCommand() {
|
|
|
49
45
|
console.log(`[cli] Welcome to SolidRT${version}!`)
|
|
50
46
|
startRepl()
|
|
51
47
|
startWatcher()
|
|
52
|
-
}
|
|
48
|
+
}
|
package/src/dev-client.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
import { state, print, requireBinary } from "./util"
|
|
2
|
-
import { DEV_HOST, DEV_PORT } from "./dev-server"
|
|
1
|
+
import { state, print, requireBinary, pipeAbovePrompt, shutdown } from "./util"
|
|
2
|
+
import { DEV_HOST, DEV_PORT, getClients, shutdownWhenEmpty } from "./dev-server"
|
|
3
3
|
import { values } from "./args"
|
|
4
4
|
|
|
5
|
-
function pipeAbovePrompt(stream: ReadableStream<Uint8Array>, out: NodeJS.WriteStream) {
|
|
6
|
-
let reader = stream.getReader()
|
|
7
|
-
;(async () => {
|
|
8
|
-
while (true) {
|
|
9
|
-
let { done, value } = await reader.read()
|
|
10
|
-
if (done || !value) break
|
|
11
|
-
process.stdout.write("\r\x1b[K")
|
|
12
|
-
out.write(value)
|
|
13
|
-
state.rl?.prompt(true)
|
|
14
|
-
}
|
|
15
|
-
})()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
5
|
export function spawnClient() {
|
|
19
6
|
let runner = requireBinary("solidrt-go")
|
|
20
7
|
// The local client and dev server share this machine, so connect straight to
|
|
@@ -30,11 +17,13 @@ export function spawnClient() {
|
|
|
30
17
|
if (state.child.stderr && typeof state.child.stderr !== "number")
|
|
31
18
|
pipeAbovePrompt(state.child.stderr, process.stderr)
|
|
32
19
|
|
|
33
|
-
state.child.exited.then(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
state.child.exited.then(async () => {
|
|
21
|
+
let clients = await getClients().catch(() => [])
|
|
22
|
+
if (clients.length === 0) {
|
|
23
|
+
shutdown()
|
|
37
24
|
}
|
|
38
|
-
print(`[cli] Local client exited, ${
|
|
25
|
+
print(`[cli] Local client exited, ${clients.length} remote client(s) still connected`)
|
|
26
|
+
// From here, exit once the last remote client disconnects.
|
|
27
|
+
shutdownWhenEmpty()
|
|
39
28
|
})
|
|
40
|
-
}
|
|
29
|
+
}
|