@solidrt/cli 0.0.28 → 0.0.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/cli",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -27,16 +27,17 @@
27
27
  "zod": "^4.4.3"
28
28
  },
29
29
  "optionalDependencies": {
30
- "@solidrt/darwin-arm64": "0.0.28",
31
- "@solidrt/linux-x64-gnu": "0.0.28",
32
- "@solidrt/win32-x64-msvc": "0.0.28"
30
+ "@solidrt/darwin-arm64": "0.0.29",
31
+ "@solidrt/linux-arm64-gnu": "0.0.29",
32
+ "@solidrt/linux-x64-gnu": "0.0.29",
33
+ "@solidrt/win32-x64-msvc": "0.0.29"
33
34
  },
34
35
  "peerDependencies": {
35
- "@solidrt/core": "0.0.28",
36
+ "@solidrt/core": "0.0.29",
36
37
  "typescript": "^7"
37
38
  },
38
39
  "devDependencies": {
39
- "@solidrt/flux-types": "0.0.28",
40
+ "@solidrt/flux-types": "0.0.29",
40
41
  "@types/bun": "latest"
41
42
  }
42
43
  }
@@ -109,6 +109,14 @@ Authoritative references ship inside the installed packages - read them:
109
109
  createPortal content) that is visible at first mount throws "no mount
110
110
  target". Gate it behind a signal that starts false and open it after
111
111
  startup - overlay content is opened, not born open.
112
+ 17. An element-valued prop (children, a content/icon slot) compiles to a
113
+ getter that builds a fresh native subtree on EVERY read, and a subtree
114
+ that is never inserted is never freed - native nodes are not garbage
115
+ collected, so what is only wasted work in DOM Solid is a permanent
116
+ memory leak here. Read such props exactly once, at the place they are
117
+ mounted. To inspect children (a typeof probe, counting), resolve them
118
+ first with the children() helper (re-exported from @solidrt/core) and
119
+ probe the resolved memo - never `typeof props.children` on the raw prop.
112
120
 
113
121
  ## Run / verify
114
122
 
@@ -161,10 +169,38 @@ its tools over guessing at runtime state:
161
169
  - reload: rebuild from source and push to every client - THE dev loop is
162
170
  edit -> reload -> get_logs -> get_snapshot. reload surfaces build errors
163
171
  but not type errors; run `bunx srt check` for those.
172
+ - load: bundle a given source file and push it to every client, replacing
173
+ the running app; later reloads rebuild that entry. Use it when the dev
174
+ server has no app loaded yet, or to switch apps without restarting srt.
175
+ - watch: pause (enabled: false) or resume the automatic reload-on-save.
176
+ Pause BEFORE creating or editing source files so half-finished work is
177
+ not pushed to the user's screens mid-burst; a successful reload or load
178
+ resumes it, so pause again before the next burst. Never leave it paused
179
+ when you stop working - the user's own saves rely on it.
164
180
 
165
181
  The tools need a running app: if list_clients is empty, ask the user to start
166
182
  `bunx srt run src/index.tsx`.
167
183
 
184
+ - Permission prompts: agents typically ask approval per MCP tool. All of
185
+ these tools only talk to the local dev server the user started with
186
+ `bunx srt run` - nothing leaves the machine - so approving the server as
187
+ a whole is a reasonable default. If repeated prompts get in the way, do
188
+ not work around them; tell the user they can pre-approve the server in
189
+ their agent's settings (most agents have a per-server trust or allowlist
190
+ setting - in Claude Code, add "mcp__solidrt" to `permissions.allow` in
191
+ ~/.claude/settings.json to cover every solidrt project). This is the
192
+ user's call to make, once, in their own tooling.
193
+ - Multiple clients: several clients may be attached (desktop window,
194
+ phone, tablet) with different sizes, display scales, and safe areas.
195
+ reload pushes to all of them, but call_debug / get_snapshot / log
196
+ cursors are per client, and interactive state does NOT sync - a flow
197
+ driven on one client leaves the others sitting on the initial screen,
198
+ which reads as a crash to a human holding that device. So: when driving
199
+ state via call_debug, send the same call to every client (or say which
200
+ client you are using); and before calling a visual change done,
201
+ snapshot each distinct form factor at least once - a layout that fits
202
+ one window can clip or overflow another.
203
+
168
204
  ## Debugging a running app (lessons that cost real time)
169
205
 
170
206
  - console.log + get_logs is your primary probe into runtime state. For state
@@ -9,12 +9,12 @@
9
9
  "android": "srt client --android"
10
10
  },
11
11
  "dependencies": {
12
- "@solidrt/core": "0.0.28",
13
- "@solidrt/components": "0.0.28"
12
+ "@solidrt/core": "0.0.29",
13
+ "@solidrt/components": "0.0.29"
14
14
  },
15
15
  "devDependencies": {
16
- "@solidrt/cli": "0.0.28",
17
- "@solidrt/flux-types": "0.0.28",
16
+ "@solidrt/cli": "0.0.29",
17
+ "@solidrt/flux-types": "0.0.29",
18
18
  "typescript": "^7"
19
19
  }
20
20
  }
package/server/control.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { file } from "flux:fs"
1
2
  import { state } from "./state"
2
3
  import { rebuildAndBroadcast } from "./rebuild"
3
4
  import { remapPositions } from "./remap"
@@ -240,8 +241,45 @@ export async function handleControl(req: Request, path: string, query: Map<strin
240
241
  if (req.method !== "POST") return Response.json({ error: "Reload requires POST" }, { status: 405 })
241
242
  let error = await rebuildAndBroadcast()
242
243
  if (error) return Response.json({ error }, { status: 502 })
244
+ state.watch = true
243
245
  return Response.json({ ok: true, clients: state.clients.size })
244
246
  }
247
+ case "/__control__/load": {
248
+ // Load (or switch) the app entry and push it: srt mcp's load tool.
249
+ // Moves the rebuild entry and the file-serving root like the repl's
250
+ // `load` command, then reuses the reload path, so a later /reload
251
+ // rebuilds the newly loaded file. The srt process is not told: a
252
+ // watcher started on the launch-time source keeps watching that file.
253
+ if (req.method !== "POST") return Response.json({ error: "Load requires POST" }, { status: 405 })
254
+ let entry = (await req.json().catch(() => null))?.entry
255
+ if (typeof entry !== "string" || !entry) {
256
+ return Response.json({ error: "Load requires { entry: <absolute source path> }" }, { status: 400 })
257
+ }
258
+ if (!(await file(entry).exists())) {
259
+ return Response.json({ error: `Entry not found: ${entry}` }, { status: 400 })
260
+ }
261
+ state.config.entry = entry
262
+ let cut = Math.max(entry.lastIndexOf("/"), entry.lastIndexOf("\\"))
263
+ if (cut > 0) state.sourceDir = entry.slice(0, cut)
264
+ let error = await rebuildAndBroadcast()
265
+ if (error) return Response.json({ error }, { status: 502 })
266
+ state.watch = true
267
+ return Response.json({ ok: true, entry, clients: state.clients.size })
268
+ }
269
+ case "/__control__/watch": {
270
+ // Pause/resume srt's auto-reload-on-save: the MCP watch tool. Latched
271
+ // here because the watcher lives in the srt process; it reads the flag
272
+ // via /__internal__/watch before acting on a change event. An agent
273
+ // pauses while creating or editing files so half-finished work is not
274
+ // pushed; a successful /reload or /load turns it back on.
275
+ if (req.method !== "POST") return Response.json({ error: "Watch requires POST" }, { status: 405 })
276
+ let enabled = (await req.json().catch(() => null))?.enabled
277
+ if (typeof enabled !== "boolean") {
278
+ return Response.json({ error: "Watch requires { enabled: <boolean> }" }, { status: 400 })
279
+ }
280
+ state.watch = enabled
281
+ return Response.json({ ok: true, enabled })
282
+ }
245
283
  default:
246
284
  return Response.json({ error: "Unknown control endpoint" }, { status: 404 })
247
285
  }
package/server/main.ts CHANGED
@@ -69,6 +69,7 @@ async function handleInternal(req: FluxRequest, server: Server, path: string): P
69
69
  if (!loopback) return new Response("Forbidden", { status: 403 })
70
70
 
71
71
  if (path === "/__internal__/clients") return Response.json(clientList(true))
72
+ if (path === "/__internal__/watch" && req.method === "GET") return Response.json({ enabled: state.watch })
72
73
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 })
73
74
 
74
75
  switch (path) {
@@ -101,6 +102,12 @@ async function handleInternal(req: FluxRequest, server: Server, path: string): P
101
102
  sendTo(body.clients, JSON.stringify({ type: "stop" }))
102
103
  return new Response("", { status: 204 })
103
104
  }
105
+ case "/__internal__/watch": {
106
+ // The repl's `watch on|off`; agents use /__control__/watch instead.
107
+ let body = await req.json()
108
+ state.watch = !!body.enabled
109
+ return new Response("", { status: 204 })
110
+ }
104
111
  case "/__internal__/stats": {
105
112
  let body = await req.json()
106
113
  state.stats = !!body.stats
package/server/state.ts CHANGED
@@ -59,6 +59,13 @@ export let state = {
59
59
  sourceDir: "",
60
60
  serverUrl: "",
61
61
  stats: false,
62
+ /**
63
+ * Whether srt's file watcher may auto-reload on source changes. Agents
64
+ * pause it (MCP watch tool -> /__control__/watch) while creating or
65
+ * editing files; a successful /reload or /load re-enables it. srt reads
66
+ * it via /__internal__/watch before acting on a change event.
67
+ */
68
+ watch: true,
62
69
  // Capture events from all connected clients share one clock (captureStartMs,
63
70
  // integer milliseconds) so they merge into one coherent timeline, tagged by
64
71
  // `device`. Streamed to disk as JSON Lines - see main.ts's "capture" handling.
package/src/artifacts.ts CHANGED
@@ -7,12 +7,14 @@ let require = createRequire(import.meta.url)
7
7
 
8
8
  let TRIPLE_MAP: Record<string, string> = {
9
9
  "linux-x64": "linux-x64-gnu",
10
+ "linux-arm64": "linux-arm64-gnu",
10
11
  "darwin-arm64": "darwin-arm64",
11
12
  "win32-x64": "win32-x64-msvc",
12
13
  }
13
14
 
14
15
  let PKG_MAP: Record<string, string> = {
15
16
  "linux-x64": "@solidrt/linux-x64-gnu",
17
+ "linux-arm64": "@solidrt/linux-arm64-gnu",
16
18
  "darwin-arm64": "@solidrt/darwin-arm64",
17
19
  "win32-x64": "@solidrt/win32-x64-msvc",
18
20
  }
package/src/bundler.ts CHANGED
@@ -103,8 +103,10 @@ export type BundleResult = {
103
103
  // spawns. It never touches the ambient args/state singletons and never prints
104
104
  // progress (callers own that), so its stdout stays clean for subprocess use.
105
105
  export async function bundleWith(opts: BundleOptions): Promise<BundleResult | null> {
106
+ // Define values are parsed as expressions, so string values need embedded
107
+ // quotes - a bare word substitutes as an identifier and crashes at runtime.
106
108
  let define: Record<string, string> = {
107
- "process.env.NODE_ENV": opts.dev ? "development" : "production",
109
+ "process.env.NODE_ENV": opts.dev ? '"development"' : '"production"',
108
110
  }
109
111
  if (opts.devBase) define.__SRT_DEV_BASE__ = opts.devBase
110
112
 
@@ -52,15 +52,30 @@ let SAVE_TO_ARG = z
52
52
  )
53
53
  .optional()
54
54
 
55
- let TOOLS: { name: string; description: string; inputSchema: Record<string, z.ZodTypeAny> }[] = [
55
+ // readOnly marks tools that only inspect state; it is surfaced as the
56
+ // MCP-standard readOnlyHint annotation so agent harnesses that honor it can
57
+ // auto-approve the inspection majority. load, reload, and call_debug mutate
58
+ // the running app and keep the default hints (destructive, not idempotent);
59
+ // `annotations` overrides those defaults where a mutating tool is benign
60
+ // (watch: a reversible, idempotent toggle). Every tool gets
61
+ // openWorldHint: false - the bridge only ever talks to the local dev server.
62
+ let TOOLS: {
63
+ name: string
64
+ description: string
65
+ inputSchema: Record<string, z.ZodTypeAny>
66
+ readOnly?: boolean
67
+ annotations?: { destructiveHint?: boolean; idempotentHint?: boolean }
68
+ }[] = [
56
69
  {
57
70
  name: "list_clients",
71
+ readOnly: true,
58
72
  description:
59
73
  "List the app clients connected to the SolidRT dev server. Returns `generation` (identity of this server run: client ids and log cursors are only valid within one generation, so if it changed since your last call, re-fetch ids and cursors) and `clients`. 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.",
60
74
  inputSchema: {},
61
75
  },
62
76
  {
63
77
  name: "get_logs",
78
+ readOnly: true,
64
79
  description:
65
80
  "Read console output and runtime errors from connected app clients. Returns entries (seq, at, client, level, text; consecutive identical entries are collapsed into one with a `repeats` count and the run's last seq), plus `latest` (the newest seq) and `generation` (identity of this server run; if it changed since your last call, your seq cursor and client ids are stale - start over from since 0). 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; pass `level`/`contains` to filter, e.g. level \"error\" to skip chatty output.",
66
81
  inputSchema: {
@@ -86,12 +101,14 @@ let TOOLS: { name: string; description: string; inputSchema: Record<string, z.Zo
86
101
  },
87
102
  {
88
103
  name: "get_stats",
104
+ readOnly: true,
89
105
  description:
90
- "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. Layout-activity counters cover the last full rebuild, raw: nodes (live node count), measureCalls (text measures; mostly cache hits, cheap), paraShapes (paragraphs actually shaped; the expensive signal - high layoutMs with near-zero paraShapes means the cost is not text shaping), dirtiedNodes (layout caches cleared by property writes since the previous rebuild; how much of the tree a write burst invalidated), cacheGets/cacheHits (layout-cache lookups during the rebuild; a hit on a container skips its whole subtree, so a healthy incremental rebuild shows a near-100% hit rate - a low rate at scale means the layout cache is being defeated).",
106
+ "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. Layout-activity counters cover the last full rebuild, raw: nodes (live node count, mounted AND detached), mountedNodes/orphanNodes (live at query time: nodes reachable from the root vs not - orphans growing at a stable tree shape mean an unmount leak; absent when no engine is running), measureCalls (text measures; mostly cache hits, cheap), paraShapes (paragraphs actually shaped; the expensive signal - high layoutMs with near-zero paraShapes means the cost is not text shaping), dirtiedNodes (layout caches cleared by property writes since the previous rebuild; how much of the tree a write burst invalidated), cacheGets/cacheHits (layout-cache lookups during the rebuild; a hit on a container skips its whole subtree, so a healthy incremental rebuild shows a near-100% hit rate - a low rate at scale means the layout cache is being defeated).",
91
107
  inputSchema: { client: CLIENT_ARG },
92
108
  },
93
109
  {
94
110
  name: "get_render_tree",
111
+ readOnly: true,
95
112
  description:
96
113
  "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. Whole trees get large: prefer `query` to find nodes by kind or text first, then `root` + `depth` to inspect the region around a match. A node whose children were cut off by `depth` carries `childCount`; descend into it with root=<its id>.",
97
114
  inputSchema: {
@@ -118,6 +135,7 @@ let TOOLS: { name: string; description: string; inputSchema: Record<string, z.Zo
118
135
  },
119
136
  {
120
137
  name: "get_snapshot",
138
+ readOnly: true,
121
139
  description:
122
140
  "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. Works on an idle client (the capture requests its own frame); a timeout means the client's JS thread is busy or wedged, not that the app is idle.",
123
141
  inputSchema: {
@@ -128,12 +146,14 @@ let TOOLS: { name: string; description: string; inputSchema: Record<string, z.Zo
128
146
  },
129
147
  {
130
148
  name: "get_gpu_resources",
149
+ readOnly: true,
131
150
  description:
132
151
  "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.",
133
152
  inputSchema: { client: CLIENT_ARG },
134
153
  },
135
154
  {
136
155
  name: "get_texture",
156
+ readOnly: true,
137
157
  description:
138
158
  "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.",
139
159
  inputSchema: {
@@ -148,6 +168,7 @@ let TOOLS: { name: string; description: string; inputSchema: Record<string, z.Zo
148
168
  },
149
169
  {
150
170
  name: "get_buffer",
171
+ readOnly: true,
151
172
  description:
152
173
  "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.",
153
174
  inputSchema: {
@@ -160,6 +181,7 @@ let TOOLS: { name: string; description: string; inputSchema: Record<string, z.Zo
160
181
  },
161
182
  {
162
183
  name: "list_debug",
184
+ readOnly: true,
163
185
  description:
164
186
  "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.",
165
187
  inputSchema: { client: CLIENT_ARG },
@@ -177,9 +199,26 @@ let TOOLS: { name: string; description: string; inputSchema: Record<string, z.Zo
177
199
  {
178
200
  name: "reload",
179
201
  description:
180
- "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.",
202
+ "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. A successful reload re-enables the file watcher if you paused it with the watch tool. Follow with get_logs to see runtime output from the reloaded app.",
181
203
  inputSchema: {},
182
204
  },
205
+ {
206
+ name: "load",
207
+ description:
208
+ "Load an app entry: bundle the given .tsx/.jsx source file and push it to every connected client, replacing whatever is running. Use it when the dev server has no app loaded yet, or to switch to a different app; later reload calls rebuild this entry. Returns the number of clients loaded, or a build error if the source failed to compile. A successful load re-enables the file watcher if you paused it with the watch tool.",
209
+ inputSchema: {
210
+ entry: z.string().describe("App entry source file to load (relative paths resolve against the project root)"),
211
+ },
212
+ },
213
+ {
214
+ name: "watch",
215
+ annotations: { destructiveHint: false, idempotentHint: true },
216
+ description:
217
+ "Pause or resume the dev server's automatic reload-on-save. The srt file watcher pushes a rebuild whenever app source changes on disk; call watch with enabled: false BEFORE creating or editing source files so your half-finished work is not pushed to the user's screens mid-burst, then apply everything with one explicit reload (a successful reload or load re-enables the watcher, so pause again before the next burst of file changes). The human's own saves auto-reload only while the watcher is enabled, so do not leave it paused when you stop working.",
218
+ inputSchema: {
219
+ enabled: z.boolean().describe("false pauses auto-reload-on-save, true resumes it"),
220
+ },
221
+ },
183
222
  ]
184
223
 
185
224
  function clientParam(args: any): string {
@@ -212,6 +251,16 @@ async function callTool(name: string, args: any): Promise<ControlResult> {
212
251
  }
213
252
  case "reload":
214
253
  return control("/reload", "POST")
254
+ case "load": {
255
+ if (typeof args?.entry !== "string" || !args.entry) return { ok: false, message: "load requires an entry path" }
256
+ // Resolved here in the bridge: this process runs at the project root,
257
+ // the dev server may not.
258
+ return control("/load", "POST", { entry: resolve(args.entry) })
259
+ }
260
+ case "watch": {
261
+ if (typeof args?.enabled !== "boolean") return { ok: false, message: "watch requires enabled: true or false" }
262
+ return control("/watch", "POST", { enabled: args.enabled })
263
+ }
215
264
  case "get_snapshot": {
216
265
  if (typeof args?.nodeId !== "number") return { ok: false, message: "get_snapshot requires a numeric nodeId" }
217
266
  let params = new URLSearchParams({ node: String(args.nodeId) })
@@ -286,7 +335,11 @@ export async function runMcpCommand() {
286
335
  for (let tool of TOOLS) {
287
336
  server.registerTool(
288
337
  tool.name,
289
- { description: tool.description, inputSchema: tool.inputSchema },
338
+ {
339
+ description: tool.description,
340
+ inputSchema: tool.inputSchema,
341
+ annotations: { readOnlyHint: !!tool.readOnly, openWorldHint: false, ...tool.annotations },
342
+ },
290
343
  async (args: any) => toContent(tool.name, await callTool(tool.name, args ?? {}), args),
291
344
  )
292
345
  }
package/src/dev-server.ts CHANGED
@@ -51,6 +51,26 @@ export async function sendStats(stats: boolean) {
51
51
  await post("/stats", { stats })
52
52
  }
53
53
 
54
+ /** Latch the auto-reload flag on the server (repl `watch on|off`). */
55
+ export async function sendWatch(enabled: boolean) {
56
+ await post("/watch", { enabled })
57
+ }
58
+
59
+ /**
60
+ * Whether the watcher may auto-reload: agents pause it via the MCP watch
61
+ * tool, latched on the server. Fails open so an unreachable server surfaces
62
+ * as a reload error, not a silently ignored change.
63
+ */
64
+ export async function watchAllowed(): Promise<boolean> {
65
+ try {
66
+ let resp = await fetch(`${INTERNAL_BASE}/watch`)
67
+ if (!resp.ok) return true
68
+ return (await resp.json()).enabled !== false
69
+ } catch {
70
+ return true
71
+ }
72
+ }
73
+
54
74
  export type ClientEntry = {
55
75
  id: number
56
76
  platform: string
package/src/repl.ts CHANGED
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline"
2
2
  import { resolve, dirname } from "path"
3
3
  import { readdirSync } from "node:fs"
4
4
  import { state, print, printErr, shutdown } from "./util"
5
- import { buildReload, getClients, sendReload, sendStop, sendStats, showBuildFailure } from "./dev-server"
5
+ import { buildReload, getClients, sendReload, sendStop, sendStats, sendWatch, showBuildFailure } from "./dev-server"
6
6
  import { bundle } from "./bundler"
7
7
  import { startWatcher, stopWatcher } from "./watcher"
8
8
 
@@ -133,7 +133,7 @@ async function cmdLoad(file: string) {
133
133
  print(`[cli] Loaded ${file}`)
134
134
  }
135
135
 
136
- let COMMANDS = ["load ", "stop", "reload", "list", "stats", "quit", "exit", "help"]
136
+ let COMMANDS = ["load ", "stop", "reload", "list", "stats", "watch ", "quit", "exit", "help"]
137
137
  let LOAD_EXTENSIONS = [".tsx", ".srt.js", ".srt.bin"]
138
138
 
139
139
  function completer(line: string): [string[], string] {
@@ -186,6 +186,11 @@ export function startRepl() {
186
186
  guard(cmdList())
187
187
  } else if (cmd === "stats" || cmd.startsWith("stats ")) {
188
188
  guard(cmdStats(cmd.slice(6).trim()))
189
+ } else if (cmd === "watch on" || cmd === "watch off") {
190
+ // Manual override for the agent-latched auto-reload pause (an agent
191
+ // that died mid-edit leaves it off; its reload normally restores it).
192
+ let enabled = cmd === "watch on"
193
+ guard(sendWatch(enabled).then(() => print(`[cli] Auto-reload on change ${enabled ? "on" : "off"}`)))
189
194
  } else if (cmd === "quit" || cmd === "exit") {
190
195
  shutdown()
191
196
  } else if (cmd.startsWith("!")) {
@@ -201,7 +206,7 @@ export function startRepl() {
201
206
  )
202
207
  }
203
208
  } else if (cmd === "help") {
204
- print("Commands: load, stop, reload, list, stats, !<cmd>, quit, help")
209
+ print("Commands: load, stop, reload, list, stats, watch on|off, !<cmd>, quit, help")
205
210
  } else if (cmd) {
206
211
  print(`Unknown command: ${cmd}`)
207
212
  }
package/src/watcher.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { watch } from "node:fs"
2
2
  import { resolve, dirname } from "path"
3
3
  import { state, print, printErr } from "./util"
4
- import { buildReload, sendReload, showBuildFailure } from "./dev-server"
4
+ import { buildReload, sendReload, showBuildFailure, watchAllowed } from "./dev-server"
5
5
  import { bundle } from "./bundler"
6
6
 
7
7
  let currentWatcher: ReturnType<typeof watch> | null = null
@@ -24,6 +24,11 @@ export function startWatcher() {
24
24
  if (!filename) return
25
25
  if (!/\.(tsx?|jsx?)$/.test(filename)) return
26
26
 
27
+ if (!(await watchAllowed())) {
28
+ print(`[cli] Change detected: ${filename} (auto-reload paused by agent; "watch on" resumes)`)
29
+ return
30
+ }
31
+
27
32
  print(`[cli] Change detected: ${filename}`)
28
33
  let result = await bundle(state.source)
29
34
  if (!result) {