@solidrt/cli 0.0.27 → 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 +7 -6
- package/scaffold/AGENTS.md +120 -15
- package/scaffold/package.json +4 -4
- package/scaffold/templates/components/index.tsx +15 -0
- package/scaffold/templates/components/template.json +4 -0
- package/scaffold/templates/default/index.tsx +7 -9
- package/scaffold/templates/default/template.json +4 -0
- package/scaffold/templates/gallery/template.json +4 -0
- package/scaffold/templates/minimal/index.tsx +5 -2
- package/scaffold/templates/minimal/template.json +4 -0
- package/scaffold/tsconfig.json +1 -0
- package/server/control.ts +113 -14
- package/server/main.ts +7 -0
- package/server/state.ts +14 -0
- package/src/args.ts +4 -0
- package/src/artifacts.ts +2 -0
- package/src/bundler.ts +3 -1
- package/src/commands/check.ts +94 -0
- package/src/commands/init.ts +53 -14
- package/src/commands/mcp.ts +129 -15
- package/src/dev-server.ts +20 -0
- package/src/main.ts +3 -0
- package/src/prompt.ts +19 -11
- package/src/repl.ts +8 -3
- package/src/watcher.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
31
|
-
"@solidrt/linux-
|
|
32
|
-
"@solidrt/
|
|
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.
|
|
36
|
+
"@solidrt/core": "0.0.29",
|
|
36
37
|
"typescript": "^7"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@solidrt/flux-types": "0.0.
|
|
40
|
+
"@solidrt/flux-types": "0.0.29",
|
|
40
41
|
"@types/bun": "latest"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -4,6 +4,23 @@ This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
|
|
|
4
4
|
runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
|
|
5
5
|
before writing or editing code here.
|
|
6
6
|
|
|
7
|
+
## Levels: core, and frameworks on top
|
|
8
|
+
|
|
9
|
+
- @solidrt/core is the low-level foundation: host intrinsics (`<window>`,
|
|
10
|
+
`<view>`, `<text>`, the detached `d-*` drawing primitives) with flat props
|
|
11
|
+
that feed the layout and paint engine directly. An app can be written
|
|
12
|
+
entirely at this level.
|
|
13
|
+
- Higher-level component frameworks build on core. @solidrt/components is
|
|
14
|
+
the first-party one: themed widgets (Window, View, Text, Button,
|
|
15
|
+
ScrollView, SafeArea, ...) with the `layout={{...}}`/`style={{...}}` prop
|
|
16
|
+
split. It is not privileged - a framework is just functions returning core
|
|
17
|
+
JSX, and an app can use a third-party one or grow its own.
|
|
18
|
+
|
|
19
|
+
Match the level the code you are editing already uses. package.json shows
|
|
20
|
+
the choice this app made: if no component framework is among the
|
|
21
|
+
dependencies, the app is core-only - do not add one for a change core
|
|
22
|
+
covers.
|
|
23
|
+
|
|
7
24
|
Authoritative references ship inside the installed packages - read them:
|
|
8
25
|
- node_modules/solid-js/CHEATSHEET.md - SolidJS 2.0 reactivity/control-flow model
|
|
9
26
|
- node_modules/@solidrt/components/AGENTS.md - the component vocabulary; build UI from these
|
|
@@ -24,10 +41,10 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
24
41
|
|
|
25
42
|
1. This is SolidJS 2.0 (see CHEATSHEET.md for the reactivity/control-flow
|
|
26
43
|
model), rendering through a custom Rust runtime instead of the DOM. Build
|
|
27
|
-
UI
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
UI at the level the app uses (see "Levels" above): core intrinsics
|
|
45
|
+
directly, or a component framework such as @solidrt/components (Window,
|
|
46
|
+
View, Text, Image, TextInput, ScrollView, Pressable, Button, SafeArea,
|
|
47
|
+
theme/setTheme) - the first-party, batteries-included one.
|
|
31
48
|
2. Most components split their props into two objects: `layout={{...}}` for
|
|
32
49
|
anything that feeds the layout engine (flex/grid, sizing, padding/margin,
|
|
33
50
|
position; font fields for Text) and `style={{...}}` for paint-only
|
|
@@ -52,7 +69,8 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
52
69
|
(runtime-paced, auto-cleans), re-exported from @solidrt/core.
|
|
53
70
|
requestAnimationFrame(t => {}) exists as a web-standard one-shot but is
|
|
54
71
|
not the preferred animation driver.
|
|
55
|
-
8.
|
|
72
|
+
8. In a components-based app, reach for @solidrt/core directly only for
|
|
73
|
+
what components doesn't wrap:
|
|
56
74
|
raw host intrinsics and the `d-` (detached, non-layout) primitives like
|
|
57
75
|
`d-rect`/`d-path`/`d-oval` for vector art or perf-sensitive positioned
|
|
58
76
|
drawing, device/GPU subpath imports (@solidrt/core/camera, /microphone,
|
|
@@ -76,11 +94,37 @@ Authoritative references ship inside the installed packages - read them:
|
|
|
76
94
|
`compute` is the tracked read phase; `apply(value, prev)` runs untracked
|
|
77
95
|
and is where side effects/DOM-equivalent writes belong. The old
|
|
78
96
|
single-arg `createEffect(fn)` form is gone - using it is an error.
|
|
97
|
+
13. A scroll container (ScrollView, or anything on createScroll) needs an
|
|
98
|
+
explicit main-axis size - a height, or flex inside a sized parent. With
|
|
99
|
+
neither it resolves to 0 and its content silently vanishes; maxHeight
|
|
100
|
+
alone does not size it (the auto size it would clamp is already 0). The
|
|
101
|
+
runtime warns when this happens.
|
|
102
|
+
14. Text `lineHeight` is a MULTIPLIER of fontSize (the theme uses 1.3-1.6),
|
|
103
|
+
not pixels. A CSS-reflex value like 22 makes each line box 22x the font
|
|
104
|
+
size: the text becomes blank space and the parent balloons.
|
|
105
|
+
15. Signal writes flush on a microtask: a handler that sets a signal and
|
|
106
|
+
immediately reads it back gets the OLD value. Read the new value in an
|
|
107
|
+
effect, or call `flush()` (from @solidjs/signals) to force it through.
|
|
108
|
+
16. Portals cannot mount during the app's initial render: a Modal (or any
|
|
109
|
+
createPortal content) that is visible at first mount throws "no mount
|
|
110
|
+
target". Gate it behind a signal that starts false and open it after
|
|
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.
|
|
79
120
|
|
|
80
121
|
## Run / verify
|
|
81
122
|
|
|
82
123
|
- bunx srt run src/index.tsx - dev server + window (needs a display)
|
|
83
|
-
- bunx srt
|
|
124
|
+
- bunx srt check src/index.tsx - exit 0 means it compiles and the app's
|
|
125
|
+
types hold (dependency-internal type errors are hidden). Builds in memory:
|
|
126
|
+
writes nothing and never triggers a dev-server reload, so use this while
|
|
127
|
+
iterating - `srt bundle` writes output files and reloads connected clients
|
|
84
128
|
- bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
|
|
85
129
|
render to PNG frames (proves it renders; see the cli AGENTS.md for where the
|
|
86
130
|
frames land)
|
|
@@ -93,12 +137,25 @@ its tools over guessing at runtime state:
|
|
|
93
137
|
|
|
94
138
|
- list_clients: connected app clients, their platform and runtime capabilities
|
|
95
139
|
- get_logs: console output and runtime errors (seq cursor; `wait_ms` long-poll
|
|
96
|
-
to catch output right after a reload
|
|
140
|
+
to catch output right after a reload; `level`/`contains` filters; repeated
|
|
141
|
+
lines collapse into one entry with a `repeats` count)
|
|
97
142
|
- get_render_tree: what the app actually rendered - node kinds, text, and
|
|
98
|
-
window-relative boxes
|
|
99
|
-
|
|
143
|
+
window-relative boxes. Whole trees get large: `query` finds nodes by
|
|
144
|
+
kind/text, then `root` + `depth` inspect just that region
|
|
145
|
+
- client ids and log cursors die with the dev server: list_clients and
|
|
146
|
+
get_logs responses carry `generation`, and a changed generation means
|
|
147
|
+
re-fetch ids and restart cursors
|
|
148
|
+
- get_stats: fps, CPU/memory, frame phase timings, setProperty rate, plus
|
|
149
|
+
layout-activity counters for the last rebuild (nodes, measureCalls,
|
|
150
|
+
paraShapes, dirtiedNodes, cacheGets/cacheHits) - when layoutMs looks
|
|
151
|
+
wrong, these say whether the cost is text shaping, invalidation breadth,
|
|
152
|
+
or a defeated layout cache (healthy incremental rebuilds show a near-100%
|
|
153
|
+
cacheHits rate)
|
|
100
154
|
- get_snapshot: PNG capture of any render-tree node's pixels (get node ids
|
|
101
|
-
from get_render_tree; the window node captures everything)
|
|
155
|
+
from get_render_tree; the window node captures everything). Pass `save_to`
|
|
156
|
+
on get_snapshot or get_texture to also write the PNG to a file - the image
|
|
157
|
+
in the tool result cannot be saved afterwards, so decide before capturing
|
|
158
|
+
(e.g. keep a before/after pair to diff)
|
|
102
159
|
- get_gpu_resources: inventory of GPU state - textures (size, render target
|
|
103
160
|
or not), vertex buffers (byteLength), pipelines (draw count, attribute
|
|
104
161
|
layout, bound textures, last-applied uniform values)
|
|
@@ -111,11 +168,39 @@ its tools over guessing at runtime state:
|
|
|
111
168
|
from pixels
|
|
112
169
|
- reload: rebuild from source and push to every client - THE dev loop is
|
|
113
170
|
edit -> reload -> get_logs -> get_snapshot. reload surfaces build errors
|
|
114
|
-
but not type errors; run
|
|
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.
|
|
115
180
|
|
|
116
181
|
The tools need a running app: if list_clients is empty, ask the user to start
|
|
117
182
|
`bunx srt run src/index.tsx`.
|
|
118
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
|
+
|
|
119
204
|
## Debugging a running app (lessons that cost real time)
|
|
120
205
|
|
|
121
206
|
- console.log + get_logs is your primary probe into runtime state. For state
|
|
@@ -125,10 +210,10 @@ The tools need a running app: if list_clients is empty, ask the user to start
|
|
|
125
210
|
setFocus(node.id) from the window's ref or onKeyDown never fires. This
|
|
126
211
|
runtime names arrow keys "Left"/"Right"/"Up"/"Down", not "ArrowLeft".
|
|
127
212
|
- Idle frames skip work: shaders/pipelines only re-render when their params
|
|
128
|
-
change, so measure performance while uniforms are actually changing
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
reads the last-drawn frame without needing a new one.
|
|
213
|
+
change, so measure performance while uniforms are actually changing.
|
|
214
|
+
get_snapshot works on an idle client (it requests its own frame); a
|
|
215
|
+
timeout means the JS thread is busy or wedged. get_texture on a pipeline's
|
|
216
|
+
render target reads the last-drawn frame without needing a new one.
|
|
132
217
|
- When a human reports a visual bug: capture a snapshot and SAY WHAT YOU SEE
|
|
133
218
|
in it before investigating, so you agree on the symptom. If you cannot see
|
|
134
219
|
the problem in the capture, say that instead of guessing.
|
|
@@ -160,3 +245,23 @@ The tools need a running app: if list_clients is empty, ask the user to start
|
|
|
160
245
|
a second one - self-running animation (game clocks, shader-driven
|
|
161
246
|
effects) must make one state change at startup to prime the loop; after
|
|
162
247
|
that its own writes keep it awake.
|
|
248
|
+
- Layout is incremental: a change re-solves only the dirty path, and clean
|
|
249
|
+
subtrees answer from a per-node cache, so long lists no longer cap layout
|
|
250
|
+
(a thousand-node tree relays out in well under a millisecond). If layoutMs
|
|
251
|
+
still grows with tree size, read the get_stats counters - a low
|
|
252
|
+
cacheHits/cacheGets ratio means the layout cache is being defeated, high
|
|
253
|
+
paraShapes means text is actually reshaping. Very long lists still pay
|
|
254
|
+
for the initial mount and for memory, so windowing stays sensible at the
|
|
255
|
+
thousands-of-rows scale.
|
|
256
|
+
- Remote images: createImage (and Image) dedupes repeated URLs, caches the
|
|
257
|
+
bytes on disk, and the runtime rate-limits concurrent asset fetches per
|
|
258
|
+
host - do not build your own promise cache around it. Images are fetched
|
|
259
|
+
with no freshness check (an already-cached URL is never re-checked), so
|
|
260
|
+
use versioned URLs for content that changes. Use Image's `fallback` prop
|
|
261
|
+
(an image source) for the broken-image case instead of catching errors
|
|
262
|
+
yourself.
|
|
263
|
+
- fetch() never caches by default and ignores server cache headers. Caching
|
|
264
|
+
is explicit and per call: `fetch(url, { cache: "force-cache" })` for
|
|
265
|
+
assets (serve from disk or fetch-and-store, no freshness),
|
|
266
|
+
`{ cache: "reload" }` to refresh an entry. Image/createImage already do
|
|
267
|
+
this for you.
|
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.29",
|
|
13
|
+
"@solidrt/components": "0.0.29"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@solidrt/cli": "0.0.
|
|
17
|
-
"@solidrt/flux-types": "0.0.
|
|
16
|
+
"@solidrt/cli": "0.0.29",
|
|
17
|
+
"@solidrt/flux-types": "0.0.29",
|
|
18
18
|
"typescript": "^7"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Components template: built with the @solidrt/components framework.
|
|
2
|
+
import { render } from "@solidrt/core"
|
|
3
|
+
import { Window, SafeArea, Text } from "@solidrt/components"
|
|
4
|
+
|
|
5
|
+
function App() {
|
|
6
|
+
return (
|
|
7
|
+
<Window>
|
|
8
|
+
<SafeArea>
|
|
9
|
+
<Text>Hello, World!</Text>
|
|
10
|
+
</SafeArea>
|
|
11
|
+
</Window>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render(() => <App />)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
// Core-only template: built from @solidrt/core primitives.
|
|
1
2
|
import { render, For, onFrame, createSignal, createLinearGradient } from "@solidrt/core"
|
|
2
|
-
import { View, Window } from "@solidrt/components"
|
|
3
3
|
|
|
4
4
|
const FADE = 360
|
|
5
5
|
|
|
@@ -13,7 +13,7 @@ let SEGMENTS = [
|
|
|
13
13
|
{ base: 540, light: "#7ea9ea", dark: "#5681c1", d: "M100.000 50.000 L75.000 75.000 L75.000 65.830 C73.810 65.830 72.711 65.195 72.116 64.165 C71.521 63.135 71.521 61.865 72.116 60.835 C72.711 59.805 73.810 59.170 75.000 59.170 L75.000 50.000 L75.000 40.830 C73.810 40.830 72.711 40.195 72.116 39.165 C71.521 38.135 71.521 36.865 72.116 35.835 C72.711 34.805 73.810 34.170 75.000 34.170 L75.000 25.000 L100.000 50.000 Z" },
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
-
let LAST = SEGMENTS[SEGMENTS.length - 1]
|
|
16
|
+
let LAST = SEGMENTS[SEGMENTS.length - 1]!.base
|
|
17
17
|
let IN_DONE = LAST + FADE
|
|
18
18
|
let CYCLE = IN_DONE + LAST + FADE
|
|
19
19
|
|
|
@@ -32,7 +32,7 @@ function Icon() {
|
|
|
32
32
|
let start = -1
|
|
33
33
|
onFrame((t) => (start < 0 && (start = t), setClock((t - start) % CYCLE)))
|
|
34
34
|
return (
|
|
35
|
-
<
|
|
35
|
+
<view width={100} height={100} scale={3}>
|
|
36
36
|
<For each={SEGMENTS}>
|
|
37
37
|
{(seg) => {
|
|
38
38
|
let a = () => {
|
|
@@ -43,7 +43,7 @@ function Icon() {
|
|
|
43
43
|
return <d-path d={seg.d} color={fill(seg, a())} />
|
|
44
44
|
}}
|
|
45
45
|
</For>
|
|
46
|
-
</
|
|
46
|
+
</view>
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -54,12 +54,10 @@ function App() {
|
|
|
54
54
|
])
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
style={{ backgroundColor }}
|
|
60
|
-
>
|
|
57
|
+
<window alignItems="center" justifyContent="center">
|
|
58
|
+
<d-rect color={backgroundColor} />
|
|
61
59
|
<Icon />
|
|
62
|
-
</
|
|
60
|
+
</window>
|
|
63
61
|
)
|
|
64
62
|
}
|
|
65
63
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// Core-only template: built from @solidrt/core primitives.
|
|
2
|
+
import { render, safeArea } from "@solidrt/core"
|
|
2
3
|
|
|
3
4
|
function App() {
|
|
4
5
|
return (
|
|
5
6
|
<window>
|
|
6
|
-
<
|
|
7
|
+
<view flex={1} paddingTop={safeArea().top} paddingBottom={safeArea().bottom}>
|
|
8
|
+
<text>Hello, World!</text>
|
|
9
|
+
</view>
|
|
7
10
|
</window>
|
|
8
11
|
)
|
|
9
12
|
}
|
package/scaffold/tsconfig.json
CHANGED
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"
|
|
@@ -75,7 +76,19 @@ function findClient(param: string | undefined): { ws: ServerWebSocket } | { erro
|
|
|
75
76
|
}
|
|
76
77
|
let id = parseInt(param, 10)
|
|
77
78
|
let entry = entries.find(([, info]) => info.id === id)
|
|
78
|
-
if (!entry)
|
|
79
|
+
if (!entry) {
|
|
80
|
+
let ids = entries.map(([, info]) => info.id)
|
|
81
|
+
return {
|
|
82
|
+
error: Response.json(
|
|
83
|
+
{
|
|
84
|
+
error:
|
|
85
|
+
`Client ${param} is gone (connected ids: ${ids.length ? ids.join(", ") : "none"}). ` +
|
|
86
|
+
"Ids reset when the dev server restarts; call list_clients for current ones.",
|
|
87
|
+
},
|
|
88
|
+
{ status: 404 },
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
79
92
|
return { ws: entry[0] }
|
|
80
93
|
}
|
|
81
94
|
|
|
@@ -89,42 +102,91 @@ async function handleQuery(query: Map<string, string>, kind: string, extra?: Rec
|
|
|
89
102
|
target.ws.send(JSON.stringify({ type: "query", kind, id, ...extra }))
|
|
90
103
|
let msg = await Promise.race([reply, sleep(QUERY_TIMEOUT_MS)])
|
|
91
104
|
pendingQueries.delete(id)
|
|
92
|
-
if (!msg)
|
|
105
|
+
if (!msg)
|
|
106
|
+
return Response.json(
|
|
107
|
+
{ error: "Query timed out: the client is connected but did not answer (JS thread busy or app wedged?)" },
|
|
108
|
+
{ status: 504 },
|
|
109
|
+
)
|
|
93
110
|
// Error strings may carry stack traces (e.g. a debug command threw); remap
|
|
94
111
|
// bundle positions to .tsx sources like appendLog does for forwarded logs.
|
|
95
112
|
if (msg.error) return Response.json({ error: remapPositions(String(msg.error), state.currentMap) }, { status: 502 })
|
|
96
113
|
return Response.json(msg.data)
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
116
|
+
// Merge runs of consecutive identical entries (same client, level, text) into
|
|
117
|
+
// one entry carrying `repeats` and the run's last seq/at, so 176 copies of one
|
|
118
|
+
// error read as a single line and a `since` cursor still skips the whole run.
|
|
119
|
+
function collapseRepeats(entries: LogEntry[]): (LogEntry & { repeats?: number })[] {
|
|
120
|
+
let out: (LogEntry & { repeats?: number })[] = []
|
|
121
|
+
for (let e of entries) {
|
|
122
|
+
let last = out[out.length - 1]
|
|
123
|
+
if (last && last.client === e.client && last.level === e.level && last.text === e.text) {
|
|
124
|
+
last.repeats = (last.repeats ?? 1) + 1
|
|
125
|
+
last.seq = e.seq
|
|
126
|
+
last.at = e.at
|
|
127
|
+
} else {
|
|
128
|
+
out.push({ ...e })
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// GET /__control__/logs?since=N&wait=MS&level=L1,L2&contains=TEXT: entries with
|
|
135
|
+
// seq > since, plus the latest seq as the next cursor and the server
|
|
136
|
+
// generation. `level` keeps only the listed levels; `contains` keeps entries
|
|
137
|
+
// whose text has the substring (case-insensitive). Consecutive identical
|
|
138
|
+
// entries come back collapsed with a `repeats` count. With `wait`, holds the
|
|
139
|
+
// response until an entry passes the filters or the timeout expires
|
|
140
|
+
// (long-poll), so a caller can follow the stream without tight polling.
|
|
103
141
|
async function handleLogs(query: Map<string, string>): Promise<Response> {
|
|
104
142
|
let since = parseInt(query.get("since") ?? "0", 10) || 0
|
|
105
143
|
let wait = Math.min(parseInt(query.get("wait") ?? "0", 10) || 0, MAX_WAIT_MS)
|
|
106
|
-
let
|
|
107
|
-
|
|
144
|
+
let levels = query
|
|
145
|
+
.get("level")
|
|
146
|
+
?.split(",")
|
|
147
|
+
.map((l) => l.trim())
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
let contains = query.get("contains")?.toLowerCase()
|
|
150
|
+
let select = () =>
|
|
151
|
+
logs.filter(
|
|
152
|
+
(e) =>
|
|
153
|
+
e.seq > since &&
|
|
154
|
+
(!levels || levels.length === 0 || levels.includes(e.level)) &&
|
|
155
|
+
(!contains || e.text.toLowerCase().includes(contains)),
|
|
156
|
+
)
|
|
157
|
+
let entries = select()
|
|
158
|
+
// Filtered long-poll: an append may not pass the filters, so keep waiting
|
|
159
|
+
// until one does or the deadline runs out.
|
|
160
|
+
let deadline = Date.now() + wait
|
|
161
|
+
while (entries.length === 0 && Date.now() < deadline) {
|
|
108
162
|
await new Promise<void>((resolve) => {
|
|
109
|
-
let timer = setTimeout(resolve,
|
|
163
|
+
let timer = setTimeout(resolve, deadline - Date.now())
|
|
110
164
|
logWaiters.push(() => {
|
|
111
165
|
clearTimeout(timer)
|
|
112
166
|
resolve()
|
|
113
167
|
})
|
|
114
168
|
})
|
|
115
|
-
entries =
|
|
169
|
+
entries = select()
|
|
116
170
|
}
|
|
117
|
-
return Response.json({ entries, latest: logSeq })
|
|
171
|
+
return Response.json({ entries: collapseRepeats(entries), latest: logSeq, generation: state.generation })
|
|
118
172
|
}
|
|
119
173
|
|
|
120
174
|
export async function handleControl(req: Request, path: string, query: Map<string, string>): Promise<Response> {
|
|
121
175
|
switch (path) {
|
|
122
176
|
case "/__control__/clients":
|
|
123
|
-
return Response.json(clientList())
|
|
177
|
+
return Response.json({ generation: state.generation, clients: clientList() })
|
|
124
178
|
case "/__control__/logs":
|
|
125
179
|
return handleLogs(query)
|
|
126
|
-
case "/__control__/tree":
|
|
127
|
-
|
|
180
|
+
case "/__control__/tree": {
|
|
181
|
+
let extra: Record<string, unknown> = {}
|
|
182
|
+
let root = parseInt(query.get("root") ?? "", 10)
|
|
183
|
+
if (Number.isFinite(root)) extra.root = root
|
|
184
|
+
let depth = parseInt(query.get("depth") ?? "", 10)
|
|
185
|
+
if (Number.isFinite(depth)) extra.depth = depth
|
|
186
|
+
let q = query.get("query")
|
|
187
|
+
if (q) extra.query = q
|
|
188
|
+
return handleQuery(query, "tree", extra)
|
|
189
|
+
}
|
|
128
190
|
case "/__control__/stats":
|
|
129
191
|
return handleQuery(query, "stats")
|
|
130
192
|
case "/__control__/snapshot": {
|
|
@@ -179,8 +241,45 @@ export async function handleControl(req: Request, path: string, query: Map<strin
|
|
|
179
241
|
if (req.method !== "POST") return Response.json({ error: "Reload requires POST" }, { status: 405 })
|
|
180
242
|
let error = await rebuildAndBroadcast()
|
|
181
243
|
if (error) return Response.json({ error }, { status: 502 })
|
|
244
|
+
state.watch = true
|
|
182
245
|
return Response.json({ ok: true, clients: state.clients.size })
|
|
183
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
|
+
}
|
|
184
283
|
default:
|
|
185
284
|
return Response.json({ error: "Unknown control endpoint" }, { status: 404 })
|
|
186
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
|
@@ -37,6 +37,13 @@ export let state = {
|
|
|
37
37
|
config: undefined as unknown as Config,
|
|
38
38
|
clients: new Map<ServerWebSocket, ClientInfo>(),
|
|
39
39
|
nextClientId: 0,
|
|
40
|
+
/**
|
|
41
|
+
* Identity of this server run, included in control responses that carry
|
|
42
|
+
* cross-call state (client ids, log seq cursors). Both reset on restart, so
|
|
43
|
+
* a consumer that sees the generation change knows its ids and cursors are
|
|
44
|
+
* from a dead server and must be re-fetched.
|
|
45
|
+
*/
|
|
46
|
+
generation: Date.now(),
|
|
40
47
|
/**
|
|
41
48
|
* The latched reload message (JSON text), replayed to late-joining clients.
|
|
42
49
|
* Set by /__internal__/reload posts with `latch`, cleared by a broadcast stop.
|
|
@@ -52,6 +59,13 @@ export let state = {
|
|
|
52
59
|
sourceDir: "",
|
|
53
60
|
serverUrl: "",
|
|
54
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,
|
|
55
69
|
// Capture events from all connected clients share one clock (captureStartMs,
|
|
56
70
|
// integer milliseconds) so they merge into one coherent timeline, tagged by
|
|
57
71
|
// `device`. Streamed to disk as JSON Lines - see main.ts's "capture" handling.
|
package/src/args.ts
CHANGED
|
@@ -46,6 +46,9 @@ export function validateArgs() {
|
|
|
46
46
|
usage("srt bundle [options] <entry.[tsx|jsx|ts|js|srt.js|srt.bin]>")
|
|
47
47
|
}
|
|
48
48
|
break
|
|
49
|
+
case "check":
|
|
50
|
+
if (!source || !isSource) usage("srt check <entry.[tsx|jsx|ts|js]>")
|
|
51
|
+
break
|
|
49
52
|
case "render":
|
|
50
53
|
if (!source || !isTsx) usage("srt render <entry.[tsx|jsx]>")
|
|
51
54
|
break
|
|
@@ -74,6 +77,7 @@ Commands:
|
|
|
74
77
|
server [file] Start dev server only
|
|
75
78
|
client Start solidrt-go client only
|
|
76
79
|
bundle <file> Transpile TS/JS/TSX/JSX to JS or bytecode
|
|
80
|
+
check <file> Verify the app builds and typechecks, without writing anything
|
|
77
81
|
render <file.tsx|jsx> Replay a script (optional) and render frames for video generation
|
|
78
82
|
pack <file> Bundle + compile to a standalone executable (experimental)
|
|
79
83
|
mcp MCP server (stdio) exposing the running dev server to coding agents
|
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
|
|