@solidrt/cli 0.0.23 → 0.0.24

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 CHANGED
@@ -18,21 +18,25 @@ bundled `flux` runtime, not on Bun. Invoke via `bunx srt <command>`.
18
18
  - `bunx srt bundle src/index.tsx` - transpile to `<file>.srt.js`. With
19
19
  `--compile`, emits `.srt.bin` bytecode. `--minify`, `--dev`, `--stdout`,
20
20
  `--output` also available.
21
- - `bunx srt record src/index.tsx [flags]` - render OFFSCREEN to PNG frames.
21
+ - `bunx srt render src/index.tsx [flags]` - render OFFSCREEN to PNG frames,
22
+ optionally replaying a `--script` file recorded via `--capture`.
22
23
  - `bunx srt server [file]` / `bunx srt client` - the two halves of `run`
23
24
  separately (server distributes code; clients on other devices connect to it).
25
+ - `bunx srt run src/index.tsx --capture out.script.json` - records keydown/keyup
26
+ from every connected client into one script file (written on client
27
+ disconnect), for replaying later with `render --script`.
24
28
 
25
29
  ## Verifying without a display (headless / CI / agent box)
26
30
 
27
31
  Two reliable checks that need no GUI:
28
32
 
29
33
  1. `bunx srt bundle src/index.tsx` - exit 0 means the app compiles. Fast.
30
- 2. `bunx srt record src/index.tsx --size 480x640 --duration 1 --fps 2` -
34
+ 2. `bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2` -
31
35
  renders offscreen via EGL/wgpu and writes `frame-NNNNNN.png`. This actually
32
36
  proves the app renders. Combine with `--fps`/`--duration` (defaults
33
37
  1280x720, 60fps, 1s).
34
38
 
35
- `record` gotchas:
39
+ `render` gotchas:
36
40
  - Frames are written to the RUNTIME's working dir (`~/.local/share/SolidRT/go/`),
37
41
  NOT the directory you ran the command from. Look there for the PNGs.
38
42
  - The recording includes a debug overlay (FPS/REQ/MiB/CPU) in a corner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/cli",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -22,12 +22,12 @@
22
22
  "qrcode-generator": "^2.0.4"
23
23
  },
24
24
  "optionalDependencies": {
25
- "@solidrt/darwin-arm64": "0.0.23",
26
- "@solidrt/linux-x64-gnu": "0.0.23",
27
- "@solidrt/win32-x64-msvc": "0.0.23"
25
+ "@solidrt/darwin-arm64": "0.0.24",
26
+ "@solidrt/linux-x64-gnu": "0.0.24",
27
+ "@solidrt/win32-x64-msvc": "0.0.24"
28
28
  },
29
29
  "peerDependencies": {
30
- "@solidrt/core": "0.0.23",
30
+ "@solidrt/core": "0.0.24",
31
31
  "typescript": "^6"
32
32
  },
33
33
  "devDependencies": {
@@ -81,6 +81,6 @@ Authoritative references ship inside the installed packages - read them:
81
81
 
82
82
  - bunx srt run src/index.tsx - dev server + window (needs a display)
83
83
  - bunx srt bundle src/index.tsx - exit 0 means it compiles
84
- - bunx srt record src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
84
+ - bunx srt render src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
85
85
  render to PNG frames (proves it renders; see the cli AGENTS.md for where the
86
86
  frames land)
@@ -9,12 +9,12 @@
9
9
  "android": "srt client --android"
10
10
  },
11
11
  "dependencies": {
12
- "@solidrt/core": "0.0.23",
13
- "@solidrt/components": "0.0.23"
12
+ "@solidrt/core": "0.0.24",
13
+ "@solidrt/components": "0.0.24"
14
14
  },
15
15
  "devDependencies": {
16
- "@solidrt/cli": "0.0.23",
17
- "@solidrt/flux-types": "0.0.23",
16
+ "@solidrt/cli": "0.0.24",
17
+ "@solidrt/flux-types": "0.0.24",
18
18
  "typescript": "^6"
19
19
  }
20
20
  }
@@ -0,0 +1,11 @@
1
+ import { render } from "@solidrt/core"
2
+
3
+ function App() {
4
+ return (
5
+ <window>
6
+ <text>Hello, World!</text>
7
+ </window>
8
+ )
9
+ }
10
+
11
+ render(() => <App />)
package/src/args.ts CHANGED
@@ -13,6 +13,8 @@ export let { values, positionals } = parseArgs({
13
13
  fps: { type: "string" },
14
14
  duration: { type: "string" },
15
15
  size: { type: "string" },
16
+ script: { type: "string" },
17
+ capture: { type: "string" },
16
18
  stats: { type: "boolean", default: false },
17
19
  android: { type: "boolean", default: false },
18
20
  device: { type: "string" },
@@ -43,8 +45,8 @@ export function validateArgs() {
43
45
  usage("srt bundle [options] <entry.[tsx|jsx|ts|js|srt.js|srt.bin]>")
44
46
  }
45
47
  break
46
- case "record":
47
- if (!source || !isTsx) usage("srt record <entry.[tsx|jsx]>")
48
+ case "render":
49
+ if (!source || !isTsx) usage("srt render <entry.[tsx|jsx]>")
48
50
  break
49
51
  case "pack":
50
52
  if (values.flux) {
@@ -71,7 +73,7 @@ Commands:
71
73
  server [file] Start dev server only
72
74
  client Start solidrt-go client only
73
75
  bundle <file> Transpile TS/JS/TSX/JSX to JS or bytecode
74
- record <file.tsx|jsx> Capture frames for video generation
76
+ render <file.tsx|jsx> Replay a script (optional) and render frames for video generation
75
77
  pack <file> Bundle + compile to a standalone executable (experimental)
76
78
 
77
79
  init options:
@@ -80,6 +82,7 @@ init options:
80
82
  run/server options:
81
83
  --proxy-files Route file/dir access through the dev server
82
84
  --proxy-http Route fetch calls through the dev server (HTTP cache enabled)
85
+ --capture <file> Record connected clients' key events to a script file
83
86
 
84
87
  run/client options:
85
88
  --size <WxH> Window size (default: 1280x720)
@@ -102,7 +105,8 @@ pack options:
102
105
  -m, --minify Minify the output
103
106
  -o, --output <name> Output filename
104
107
 
105
- record options:
108
+ render options:
109
+ --script <file> Script file to replay (default: no scripted input)
106
110
  --fps <N> Frames per second (default: 60)
107
111
  --duration <N> Duration in seconds (default: 1)
108
112
  --size <WxH> Frame size (default: 1280x720)`)
@@ -0,0 +1,18 @@
1
+ import { source, values } from "../args"
2
+ import { requireBinary, run } from "../util"
3
+ import { bundleTo } from "../bundler"
4
+ import { resolve } from "path"
5
+
6
+ export async function runRenderCommand() {
7
+ let jsOutfile = source!.replace(/\.[jt]sx$/, "") + ".srt.js"
8
+ await bundleTo(jsOutfile)
9
+ let runner = requireBinary("solidrt-go")
10
+ let playbackArgs = ["--playback"]
11
+ if (values.fps) playbackArgs.push("--fps", values.fps)
12
+ if (values.duration) playbackArgs.push("--duration", values.duration)
13
+ if (values.size) playbackArgs.push("--size", values.size)
14
+ if (values.script) playbackArgs.push("--script", resolve(values.script))
15
+ playbackArgs.push(resolve(jsOutfile))
16
+ let exit = await run(runner, playbackArgs)
17
+ process.exit(exit)
18
+ }
@@ -7,6 +7,7 @@ import { startRepl } from "../repl"
7
7
  import { startWatcher } from "../watcher"
8
8
  import * as cache from "../cache"
9
9
  import { resolve, dirname } from "path"
10
+ import { writeFileSync } from "node:fs"
10
11
 
11
12
  // Brings up the dev server (HTTP/WS + initial bundle + repl + watcher). The
12
13
  // `run` command spawns a local client on top of this from main.ts.
@@ -15,6 +16,11 @@ export async function runServerCommand() {
15
16
  state.source = source
16
17
  state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
17
18
  state.stats = values.stats
19
+ 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, "")
18
24
 
19
25
  if (values["proxy-http"]) {
20
26
  cache.initCache({ dir: process.cwd() })
package/src/dev-server.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolve } from "path"
2
2
  import { stat as fsStat, readdir } from "node:fs/promises"
3
+ import { appendFileSync } from "node:fs"
3
4
  import { networkInterfaces } from "node:os"
4
5
  import { Bonjour } from "bonjour-service"
5
6
  import qrcode from "qrcode-generator"
@@ -172,11 +173,14 @@ export function startServer() {
172
173
  },
173
174
  websocket: {
174
175
  open(ws) {
175
- state.clients.set(ws, { platform: "unknown", version: "unknown" })
176
+ let id = state.nextClientId++
177
+ state.clients.set(ws, { platform: "unknown", version: "unknown", id })
176
178
  print(`[cli] Client connected ${ws.remoteAddress}`)
177
179
  // Advertise our real LAN address so clients dialed over the adb loopback
178
180
  // can show/remember the directly reachable address (see connection.rs).
179
- ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl, stats: state.stats }))
181
+ ws.send(
182
+ JSON.stringify({ type: "welcome", address: state.serverUrl, stats: state.stats, capture: !!state.capture }),
183
+ )
180
184
  if (state.currentCode) {
181
185
  ws.send(buildReload({ code: state.currentCode }))
182
186
  }
@@ -195,11 +199,25 @@ export function startServer() {
195
199
  try {
196
200
  let data = JSON.parse(typeof msg === "string" ? msg : Buffer.from(msg).toString())
197
201
  if (data.type === "info") {
202
+ let existing = state.clients.get(ws)
198
203
  state.clients.set(ws, {
199
204
  platform: data.platform ?? "unknown",
200
205
  version: data.version ?? "unknown",
206
+ id: existing?.id ?? state.nextClientId++,
201
207
  })
202
208
  print(`[cli] Client info ${ws.remoteAddress} ${data.platform} (${data.version})`)
209
+ } else if (data.type === "capture" && state.capture) {
210
+ let device = state.clients.get(ws)?.id ?? -1
211
+ // Milliseconds, integer: Date.now() is already integer ms, so the
212
+ // delta needs no rounding.
213
+ let at = Date.now() - state.captureStartMs
214
+ let after = at - state.captureLastAt
215
+ state.captureLastAt = at
216
+ // JSON Lines: one event object per line, streamed to disk as it
217
+ // arrives rather than buffered - no in-memory growth for a long
218
+ // capture, and the file is always complete on disk mid-session.
219
+ let line = JSON.stringify({ after, type: data.kind, key: data.key, device }) + "\n"
220
+ appendFileSync(state.capture, line)
203
221
  }
204
222
  } catch {}
205
223
  },
package/src/main.ts CHANGED
@@ -4,7 +4,7 @@ import { values, command, validateArgs, printUsage } from "./args"
4
4
  import { runInitCommand } from "./commands/init"
5
5
  import { runBundleCommand } from "./commands/bundle"
6
6
  import { runPackCommand } from "./commands/pack"
7
- import { runRecordCommand } from "./commands/record"
7
+ import { runRenderCommand } from "./commands/render"
8
8
  import { runServerCommand } from "./commands/server"
9
9
  import { runClientCommand } from "./commands/client"
10
10
  import { spawnClient } from "./dev-client"
@@ -22,7 +22,7 @@ validateArgs()
22
22
  // the auto-activation by setting NODE_ENV=production. Since that is read at startup,
23
23
  // we re-exec rather than mutate process.env. Assumes srt runs via bun (argv is
24
24
  // [bun, script, ...]); would need rework if ever shipped as a compiled binary.
25
- let isProdBuild = (command === "bundle" || command === "record" || command === "pack") && !values.dev
25
+ let isProdBuild = (command === "bundle" || command === "render" || command === "pack") && !values.dev
26
26
  if (isProdBuild && process.env.NODE_ENV !== "production") {
27
27
  let proc = Bun.spawnSync({
28
28
  cmd: [process.execPath, ...process.argv.slice(1)],
@@ -42,8 +42,8 @@ if (command === "init") {
42
42
  await runBundleCommand()
43
43
  } else if (command === "pack") {
44
44
  await runPackCommand()
45
- } else if (command === "record") {
46
- await runRecordCommand()
45
+ } else if (command === "render") {
46
+ await runRenderCommand()
47
47
  } else if (command === "client") {
48
48
  await runClientCommand()
49
49
  } else if (command === "server") {
package/src/util.ts CHANGED
@@ -6,7 +6,8 @@ import type { Server as BunServer } from "bun"
6
6
  import type { Bonjour } from "bonjour-service"
7
7
 
8
8
  export let state = {
9
- clients: new Map<any, { platform: string; version: string }>(),
9
+ clients: new Map<any, { platform: string; version: string; id: number }>(),
10
+ nextClientId: 0,
10
11
  currentCode: null as string | null,
11
12
  source: undefined as string | undefined,
12
13
  sourceDir: process.cwd(),
@@ -16,6 +17,16 @@ export let state = {
16
17
  rl: null as ReadlineInterface | null,
17
18
  bonjour: null as Bonjour | null,
18
19
  stats: false,
20
+ // --capture <file>: destination for captured key events, or undefined when
21
+ // off. Clients only report kind/key; the server stamps `after` itself (one
22
+ // shared clock from captureStartMs, integer milliseconds) so events from
23
+ // several connected clients merge into one coherent timeline, tagged by
24
+ // `device` (see dev-server.ts) so they can be told apart. Streamed to disk
25
+ // as JSON Lines (one event object per line) as each arrives - see
26
+ // dev-server.ts's "capture" message handling.
27
+ capture: undefined as string | undefined,
28
+ captureStartMs: 0,
29
+ captureLastAt: 0, // ms, same clock as captureStartMs
19
30
  }
20
31
 
21
32
  // Build target per binary, for the "not found" hint. Run from the repo root.
@@ -1,16 +0,0 @@
1
- import { source, values } from "../args"
2
- import { requireBinary, run } from "../util"
3
- import { bundleTo } from "../bundler"
4
- import { resolve } from "path"
5
-
6
- export async function runRecordCommand() {
7
- let jsOutfile = source!.replace(/\.[jt]sx$/, "") + ".srt.js"
8
- await bundleTo(jsOutfile)
9
- let runner = requireBinary("solidrt-go")
10
- let recordArgs = ["--record", resolve(jsOutfile)]
11
- if (values.fps) recordArgs.push("--fps", values.fps)
12
- if (values.duration) recordArgs.push("--duration", values.duration)
13
- if (values.size) recordArgs.push("--size", values.size)
14
- let exit = await run(runner, recordArgs)
15
- process.exit(exit)
16
- }