@solidrt/cli 0.0.10 → 0.0.13

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 ADDED
@@ -0,0 +1,50 @@
1
+ # @solidrt/cli - agent notes
2
+
3
+ Dense, self-contained facts for running and verifying a SolidRT app. Full docs
4
+ live in docs/ (and the website). For the authoring model (elements, props,
5
+ reactivity), see @solidrt/core (its AGENTS.md).
6
+
7
+ `srt` is the dev tool. Bun is a dev prerequisite only; SolidRT apps run on the
8
+ bundled `flux` runtime, not on Bun. Invoke via `bunx srt <command>`.
9
+
10
+ ## Commands
11
+
12
+ - `bunx srt init <dir>` - scaffold a new SolidRT project into a new (empty)
13
+ folder: package.json, tsconfig.json, AGENTS.md, a starter src/index.tsx, then
14
+ installs deps. Greenfield shortcut (no install needed first):
15
+ `bun create solidrt <dir>`.
16
+ - `bunx srt run src/index.tsx` - dev server + a local client window, watches and
17
+ hot-reloads. NEEDS A DISPLAY (opens a GUI window). Not usable headless.
18
+ - `bunx srt bundle src/index.tsx` - transpile to `<file>.srt.js`. With
19
+ `--compile`, emits `.srt.bin` bytecode. `--minify`, `--dev`, `--stdout`,
20
+ `--output` also available.
21
+ - `bunx srt record src/index.tsx [flags]` - render OFFSCREEN to PNG frames.
22
+ - `bunx srt server [file]` / `bunx srt client` - the two halves of `run`
23
+ separately (server distributes code; clients on other devices connect to it).
24
+
25
+ ## Verifying without a display (headless / CI / agent box)
26
+
27
+ Two reliable checks that need no GUI:
28
+
29
+ 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` -
31
+ renders offscreen via EGL/wgpu and writes `frame-NNNNNN.png`. This actually
32
+ proves the app renders. Combine with `--fps`/`--duration` (defaults
33
+ 1280x720, 60fps, 1s).
34
+
35
+ `record` gotchas:
36
+ - Frames are written to the RUNTIME's working dir (`~/.local/share/SolidRT/go/`),
37
+ NOT the directory you ran the command from. Look there for the PNGs.
38
+ - The recording includes a debug overlay (FPS/REQ/MiB/CPU) in a corner.
39
+ - Run from the project directory. There is no `bunx --cwd` flag.
40
+
41
+ ## Dev server proxies (when clients on other devices need your machine's data)
42
+
43
+ - `--proxy-http` - route `fetch` through the dev server; responses cached in
44
+ `.srt-cache.db` (delete the file to clear).
45
+ - `--proxy-files` - route flux:fs (`file`/`dir`/`write`) through the dev server.
46
+ Exposes your dev machine's files to all clients; use with care.
47
+
48
+ ## REPL (opened by `run`/`server`)
49
+
50
+ `load <file>`, `reload [n]`, `stop [n]`, `list`, `!<cmd>`, `quit`/`exit`.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Developer tooling for SolidRT. Provides a development environment for `@solidrt/core` applications.
4
4
 
5
+ > LLM agents: see [AGENTS.md](./AGENTS.md) for a dense, self-contained quickstart.
6
+
5
7
  ## Commands
6
8
 
7
9
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.13",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "bin/",
12
12
  "src/",
13
- "LICENSE"
13
+ "scaffold/",
14
+ "AGENTS.md"
14
15
  ],
15
16
  "dependencies": {
16
17
  "@babel/core": "^7.28.5",
@@ -20,13 +21,13 @@
20
21
  "qrcode-generator": "^2.0.4"
21
22
  },
22
23
  "optionalDependencies": {
23
- "@solidrt/darwin-arm64": "0.0.10",
24
- "@solidrt/linux-x64-gnu": "0.0.10",
25
- "@solidrt/win32-x64-msvc": "0.0.10"
24
+ "@solidrt/darwin-arm64": "0.0.13",
25
+ "@solidrt/linux-x64-gnu": "0.0.13",
26
+ "@solidrt/win32-x64-msvc": "0.0.13"
26
27
  },
27
28
  "peerDependencies": {
28
- "@solidrt/core": "0.0.10",
29
- "typescript": "^5"
29
+ "@solidrt/core": "0.0.13",
30
+ "typescript": "^6"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/bun": "latest"
@@ -0,0 +1,48 @@
1
+ # SolidRT app - agent notes
2
+
3
+ This project uses SolidRT: a custom SolidJS renderer that paints through a Rust
4
+ runtime. No DOM, no HTML, no CSS cascade. If you are an AI assistant, read this
5
+ before writing or editing code here.
6
+
7
+ Authoritative references ship inside the installed packages - read them:
8
+ - node_modules/@solidrt/core/AGENTS.md - element/prop/reactivity model
9
+ - node_modules/@solidrt/cli/AGENTS.md - running, bundling, headless verify
10
+ - node_modules/@solidrt/core/src/types.d.ts and jsx-runtime.d.ts - source of truth
11
+
12
+ <!-- Claude Code auto-imports these; other tools read the paths above. -->
13
+ @./node_modules/@solidrt/core/AGENTS.md
14
+ @./node_modules/@solidrt/cli/AGENTS.md
15
+
16
+ ## The things assistants get wrong (this is not React/DOM)
17
+
18
+ 1. SolidJS 2.0, not React (and not Solid 1.x). Primitives come from
19
+ @solidjs/signals; no hooks, no virtual DOM, the component body runs once.
20
+ createEffect has the 2.0 two-function shape - a TRACKED compute that reads
21
+ signals and returns a value, then an UNTRACKED effect that receives it:
22
+ createEffect(() => count(), (c) => console.log(c))
23
+ The Solid 1.x single-callback form, createEffect(() => console.log(count())),
24
+ does NOT track in 2.0.
25
+ 2. Host elements are lowercase intrinsics: window, view, text, rect, oval, path,
26
+ texture, audio (+ d- variants). No div/span/img/button.
27
+ 3. render(() => <App/>) once, top level. The root MUST be <window> or it throws.
28
+ 4. Containers (window, view) DO NOT PAINT. Background = a draw primitive child
29
+ placed behind the content, e.g. <d-rect color="..." />.
30
+ 5. Paint color is the color prop (a CSS color string). No fill/stroke/background
31
+ prop. Outline: drawStyle="stroke" + strokeWidth.
32
+ 6. No onClick/onPress. A button is a <view>/<rect> with onPointerDown.
33
+ 7. d- prefix = detached from layout: a plain element is laid out by Taffy, a
34
+ d-element you position with x/y (omit to fill the parent = how backgrounds
35
+ work).
36
+ 8. Per-frame work: onFrame((tick, frame) => {}), or the standard
37
+ requestAnimationFrame(t => {}) for animations. Also onResize, onLayout.
38
+ 9. Device/GPU via subpath imports: @solidrt/core/camera, /microphone, /gpu.
39
+ 10. tsconfig needs jsx:"preserve" + jsxImportSource:"@solidrt/core". Solid peer
40
+ deps are pinned betas - do not bump them casually.
41
+
42
+ ## Run / verify
43
+
44
+ - bunx srt run src/index.tsx - dev server + window (needs a display)
45
+ - bunx srt bundle src/index.tsx - exit 0 means it compiles
46
+ - bunx srt record src/index.tsx --size 480x640 --duration 1 --fps 2 - headless
47
+ render to PNG frames (proves it renders; see the cli AGENTS.md for where the
48
+ frames land)
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ *.srt.js
3
+ *.srt.bin
4
+ .srt-cache.db
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "solidrt-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "srt run src/index.tsx",
7
+ "bundle": "srt bundle src/index.tsx"
8
+ },
9
+ "dependencies": {
10
+ "@solidrt/core": "latest"
11
+ },
12
+ "devDependencies": {
13
+ "@solidrt/cli": "latest",
14
+ "@solidrt/flux-types": "latest",
15
+ "typescript": "^6"
16
+ }
17
+ }
@@ -0,0 +1,25 @@
1
+ import { render } from "@solidrt/core"
2
+
3
+ function Icon() {
4
+ return (
5
+ <view width={100} height={100} scale={3}>
6
+ <d-path d="M50 50L0 50L50 0Z" color="#1a3380" />
7
+ <d-path d="M50 50L50 100L0 50Z" color="#3366b3" />
8
+ <d-path d="M50 25L50 0L75 25Z" color="#6699e6" />
9
+ <d-path d="M50 25L75 25L75 50L50 50Z" color="#3366b3" />
10
+ <d-path d="M50 50L75 50L50 75Z" color="#6699e6" />
11
+ <d-path d="M75 50L75 75L50 100L50 75Z" color="#1a3380" />
12
+ <d-path d="M100 50L75 75L75 25Z" color="#6699e6" />
13
+ </view>
14
+ )
15
+ }
16
+
17
+ function App() {
18
+ return (
19
+ <window alignItems="center" justifyContent="center">
20
+ <Icon />
21
+ </window>
22
+ )
23
+ }
24
+
25
+ render(() => <App />)
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "preserve",
4
+ "jsxImportSource": "@solidrt/core",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "types": ["@solidrt/flux-types"]
8
+ },
9
+ "include": ["src"]
10
+ }
package/src/args.ts CHANGED
@@ -13,6 +13,7 @@ export let { values, positionals } = parseArgs({
13
13
  fps: { type: "string" },
14
14
  duration: { type: "string" },
15
15
  size: { type: "string" },
16
+ stats: { type: "boolean", default: false },
16
17
  android: { type: "boolean", default: false },
17
18
  device: { type: "string" },
18
19
  },
@@ -34,6 +35,9 @@ function usage(line: string): never {
34
35
  // Per-command argument requirements. Called once before dispatch.
35
36
  export function validateArgs() {
36
37
  switch (command) {
38
+ case "init":
39
+ if (!source) usage("srt init <dir> (the target folder is required)")
40
+ break
37
41
  case "bundle":
38
42
  if (values.flux) {
39
43
  if (!source || !isTs) usage("srt bundle --flux [options] <entry.[ts|js]>")
@@ -64,6 +68,7 @@ export function printUsage() {
64
68
  console.error(`Usage: srt <command> [options] [file]
65
69
 
66
70
  Commands:
71
+ init <dir> Scaffold a new SolidRT project into a new (empty) folder
67
72
  run [file] Start dev server + local solidrt-go client
68
73
  server [file] Start dev server only
69
74
  client Start solidrt-go client only
@@ -77,6 +82,7 @@ run/server options:
77
82
 
78
83
  run/client options:
79
84
  --size <WxH> Window size (default: 1280x720)
85
+ --stats Show the debug stats overlay (FPS, memory, frame timings)
80
86
 
81
87
  client options:
82
88
  --android Install and launch the client on a connected Android device
package/src/artifacts.ts CHANGED
@@ -44,27 +44,35 @@ export function resolveBinary(name: string) {
44
44
  return null
45
45
  }
46
46
 
47
- // The Android client APK is per-ABI (it bundles native .so for one architecture)
48
- // and host-independent, so it lives under dist/android/<abi>/ rather than the
49
- // host triple map. Only arm64-v8a is supported for now.
50
- let ANDROID_ABI = "arm64-v8a"
51
- let ANDROID_PKG = "@solidrt/android-arm64-v8a"
47
+ // The Android client APK is host-independent (it bundles native .so for the
48
+ // device, not the host), so it lives under dist/android/<abi>/ rather than the
49
+ // host triple map. arm64-v8a (a fat APK: arm64-v8a + the x86_64 emulator) and
50
+ // armeabi-v7a (32-bit) ship published npm packages; other ABIs (e.g. x86) only
51
+ // resolve via the SRT_HOME contributor path.
52
+ let DEFAULT_ANDROID_ABI = "arm64-v8a"
53
+ export let ANDROID_PKG_MAP: Record<string, string> = {
54
+ "arm64-v8a": "@solidrt/android-arm64-v8a",
55
+ "armeabi-v7a": "@solidrt/android-armeabi-v7a",
56
+ }
52
57
 
53
- export function resolveApk() {
58
+ export function resolveApk(abi: string = DEFAULT_ANDROID_ABI) {
54
59
  // 1. SRT_HOME: contributor checkout, where `make dist-android` stages the APK
55
60
  // under dist/android/<abi>/.
56
61
  let srtRoot = process.env.SRT_HOME
57
62
  if (srtRoot) {
58
- let apk = resolve(srtRoot, "dist/android", ANDROID_ABI, "solidrt-go.apk")
63
+ let apk = resolve(srtRoot, "dist/android", abi, "solidrt-go.apk")
59
64
  if (existsSync(apk)) return apk
60
65
  }
61
66
 
62
67
  // 2. Platform npm package
63
- try {
64
- let pkgDir = dirname(require.resolve(`${ANDROID_PKG}/package.json`))
65
- let apk = resolve(pkgDir, "solidrt-go.apk")
66
- if (existsSync(apk)) return apk
67
- } catch {}
68
+ let pkg = ANDROID_PKG_MAP[abi]
69
+ if (pkg) {
70
+ try {
71
+ let pkgDir = dirname(require.resolve(`${pkg}/package.json`))
72
+ let apk = resolve(pkgDir, "solidrt-go.apk")
73
+ if (existsSync(apk)) return apk
74
+ } catch {}
75
+ }
68
76
 
69
77
  return null
70
78
  }
package/src/cache.ts CHANGED
@@ -48,7 +48,8 @@ export function isEnabled(): boolean {
48
48
 
49
49
  // Strings returned by bun:sqlite carry an internal representation that
50
50
  // Headers.set() rejects, even when value-identical to an acceptable string
51
- // (Bun bug, present through 1.3.14). Rebuilding each char yields a clean string.
51
+ // (Bun bug oven-sh/bun#28266, present through 1.3.14). Rebuilding each char
52
+ // yields a clean string.
52
53
  function reflatten(s: string): string {
53
54
  return Array.from(s, (c) => String.fromCharCode(c.charCodeAt(0))).join("")
54
55
  }
@@ -0,0 +1,67 @@
1
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
2
+ import { basename, dirname, join, resolve } from "node:path"
3
+ import { source } from "../args"
4
+
5
+ const SCAFFOLD_DIR = join(import.meta.dir, "../../scaffold")
6
+
7
+ // What gets written into a new project. Sources live in cli/scaffold/. The
8
+ // .gitignore is stored there as `gitignore` because npm strips files literally
9
+ // named `.gitignore` from published packages, so it is renamed on the way out.
10
+ const TEMPLATE_FILES: Array<{ from: string; to: string }> = [
11
+ { from: "package.json", to: "package.json" },
12
+ { from: "tsconfig.json", to: "tsconfig.json" },
13
+ { from: "gitignore", to: ".gitignore" },
14
+ { from: "AGENTS.md", to: "AGENTS.md" },
15
+ { from: "src/index.tsx", to: "src/index.tsx" },
16
+ ]
17
+
18
+ // A valid npm package name derived from the target directory.
19
+ function packageName(dir: string): string {
20
+ let name = basename(resolve(dir))
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9-_.]/g, "-")
23
+ return name || "solidrt-app"
24
+ }
25
+
26
+ export async function runInitCommand() {
27
+ // The target folder is required (validateArgs enforces it) and must be empty
28
+ // or absent, so init can never overwrite files in an existing project.
29
+ let dir = source!
30
+ let existing = await readdir(dir).catch(() => null)
31
+ if (existing && existing.length > 0) {
32
+ console.error(`!! ${resolve(dir)} already exists and is not empty; choose a new folder name`)
33
+ process.exit(1)
34
+ }
35
+
36
+ console.log(`>> scaffolding SolidRT project in ${resolve(dir)}`)
37
+ for (let { from, to } of TEMPLATE_FILES) {
38
+ let dest = join(dir, to)
39
+ await mkdir(dirname(dest), { recursive: true })
40
+ await writeFile(dest, await readFile(join(SCAFFOLD_DIR, from)))
41
+ console.log(` write ${to}`)
42
+ }
43
+
44
+ // The scaffold package.json carries a placeholder name; set it from the
45
+ // target folder.
46
+ let pkgPath = join(dir, "package.json")
47
+ let pkg = JSON.parse(await readFile(pkgPath, "utf8"))
48
+ pkg.name = packageName(dir)
49
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n")
50
+
51
+ // Deps are declared in scaffold/package.json (Solid peers resolve via
52
+ // @solidrt/core's peerDependencies), so a plain install is enough.
53
+ console.log("\n>> installing dependencies")
54
+ let install = Bun.spawnSync(["bun", "install"], {
55
+ cwd: dir,
56
+ stdout: "inherit",
57
+ stderr: "inherit",
58
+ })
59
+ if (install.exitCode !== 0) {
60
+ console.error("\n!! dependency install failed; retry with `bun install` in the project")
61
+ process.exit(1)
62
+ }
63
+
64
+ let prefix = dir === "." ? "" : `cd ${dir} && `
65
+ console.log(`\n>> done. next:\n ${prefix}bunx srt run src/index.tsx\n`)
66
+ process.exit()
67
+ }
@@ -14,6 +14,7 @@ export async function runServerCommand() {
14
14
  // Initialize state from args
15
15
  state.source = source
16
16
  state.sourceDir = source ? dirname(resolve(source)) : process.cwd()
17
+ state.stats = values.stats
17
18
 
18
19
  if (values["proxy-http"]) {
19
20
  cache.initCache({ dir: process.cwd() })
@@ -1,26 +1,54 @@
1
+ import { networkInterfaces } from "node:os"
1
2
  import { print, requireAdb } from "./util"
2
- import { resolveApk } from "./artifacts"
3
+ import { resolveApk, ANDROID_PKG_MAP } from "./artifacts"
3
4
  import { values } from "./args"
4
5
  import { DEV_PORT } from "./dev-server"
5
6
 
6
7
  // Launch component of the "go" dev-client flavor (see lattice/Makefile.android).
7
8
  let PACKAGE_ACTIVITY = "com.solidrt.go/com.solidrt.app.MainActivity"
8
9
 
9
- // Forward the device's loopback DEV_PORT to the host dev server, so the client
10
- // reaches it at 127.0.0.1:DEV_PORT (see lattice/src/go/connection.rs). This is
11
- // the adb-reverse path: it works for the emulator (behind NAT, cannot reach the
12
- // host via LAN mDNS discovery) and for USB-tethered devices alike, and is
13
- // harmless on any adb connection. (Android only uses this path; the client's
14
- // mDNS discovery is desktop-only -- see lattice/src/go/connection.rs.)
15
- function setupAdbReverse(adb: string, target: string) {
16
- print(`[cli] Forwarding 127.0.0.1:${DEV_PORT} on ${target} to host dev server`)
17
- let res = Bun.spawnSync([adb, "-s", target, "reverse", `tcp:${DEV_PORT}`, `tcp:${DEV_PORT}`], {
10
+ let ipToInt = (ip: string) => ip.split(".").reduce((acc, o) => (acc << 8) + (parseInt(o, 10) & 255), 0) >>> 0
11
+
12
+ // This host's IPv4 on the same subnet as `deviceIp`: the interface whose
13
+ // (address & netmask) matches the device's. Pure computation over what the OS
14
+ // reports (cross-platform, no routing socket, no hardcoded ranges).
15
+ function hostIpFor(deviceIp: string): string | null {
16
+ let d = ipToInt(deviceIp)
17
+ for (let addrs of Object.values(networkInterfaces())) {
18
+ for (let a of addrs ?? []) {
19
+ if (a.family === "IPv4" && !a.internal && (ipToInt(a.address) & ipToInt(a.netmask)) === (d & ipToInt(a.netmask))) {
20
+ return a.address
21
+ }
22
+ }
23
+ }
24
+ return null
25
+ }
26
+
27
+ // The device's own IPv4 on its primary network: from the serial for wireless adb
28
+ // (ip:port), else queried over adb. `ip route get` reports the source IP of the
29
+ // route to a public address, i.e. the device's main interface IP.
30
+ function deviceIp(adb: string, target: string): string | null {
31
+ let m = target.match(/^(\d+\.\d+\.\d+\.\d+):\d+$/)
32
+ if (m) return m[1] ?? null
33
+ let res = Bun.spawnSync([adb, "-s", target, "shell", "ip", "route", "get", "1.1.1.1"], {
18
34
  stdout: "pipe",
19
35
  stderr: "pipe",
20
36
  })
21
- if (res.exitCode !== 0) {
22
- print(`[cli] adb reverse failed (client will fall back to discovery):\n${res.stderr.toString()}`)
23
- }
37
+ return res.stdout.toString().match(/src (\d+\.\d+\.\d+\.\d+)/)?.[1] ?? null
38
+ }
39
+
40
+ // The host:port the client on `target` should dial to reach this machine's dev
41
+ // server, by adb transport: the emulator reaches the host through its NAT alias
42
+ // 10.0.2.2; every other transport shares a LAN with the host, so match the
43
+ // device's IP to the host interface on its subnet. Replaces the old adb-reverse
44
+ // loopback tunnel, which never worked over wireless adb. Returns null when the
45
+ // address cannot be resolved (the client then falls back to QR/recents).
46
+ function devServerAddress(adb: string, target: string): string | null {
47
+ if (target.startsWith("emulator-")) return `10.0.2.2:${DEV_PORT}`
48
+ let dip = deviceIp(adb, target)
49
+ if (!dip) return null
50
+ let host = hostIpFor(dip)
51
+ return host ? `${host}:${DEV_PORT}` : null
24
52
  }
25
53
 
26
54
  // Serials of connected, authorized devices (excludes offline/unauthorized).
@@ -70,22 +98,37 @@ function resolveTarget(adb: string): string {
70
98
  return only
71
99
  }
72
100
 
73
- // Install + launch the Android client on a connected device over adb, forwarding
74
- // its loopback to the host dev server so the client connects at 127.0.0.1 (see
75
- // setupAdbReverse). Fire-and-forget: the client's lifecycle is tracked via WS
76
- // connect/disconnect in dev-server.ts, not as a child process here.
101
+ // Primary ABI of the connected device (e.g. "arm64-v8a", "armeabi-v7a"), used
102
+ // to pick the matching APK -- a fat (multi-ABI) APK reports its 64-bit ABI
103
+ // here and still installs fine, so this only matters for single-ABI builds.
104
+ function deviceAbi(adb: string, target: string): string {
105
+ let res = Bun.spawnSync([adb, "-s", target, "shell", "getprop", "ro.product.cpu.abi"], {
106
+ stdout: "pipe",
107
+ stderr: "pipe",
108
+ })
109
+ return res.stdout.toString().trim()
110
+ }
111
+
112
+ // Install + launch the Android client on a connected device over adb, passing it
113
+ // the dev-server address to dial as a launch-intent extra (see devServerAddress).
114
+ // Fire-and-forget: the client's lifecycle is tracked via WS connect/disconnect in
115
+ // dev-server.ts, not as a child process here.
77
116
  export async function spawnAndroidClient() {
78
117
  let adb = requireAdb()
79
118
 
80
- let apk = resolveApk()
119
+ let target = resolveTarget(adb)
120
+ let abi = deviceAbi(adb, target)
121
+
122
+ let apk = resolveApk(abi)
81
123
  if (!apk) {
82
- console.error("Could not find the SolidRT-Go APK.")
83
- console.error("Add it with: bun add -d @solidrt/android-arm64-v8a")
124
+ console.error(`Could not find a SolidRT-Go APK for ABI "${abi}".`)
125
+ let pkg = ANDROID_PKG_MAP[abi]
126
+ if (pkg) console.error(`Add it with: bun add -d ${pkg}`)
127
+ let target = abi === "armeabi-v7a" ? "make dist-android-armeabi-v7a" : "make dist-android"
128
+ console.error(`Or build one locally (from lattice/): ${target}`)
84
129
  process.exit(1)
85
130
  }
86
131
 
87
- let target = resolveTarget(adb)
88
-
89
132
  print(`[cli] Installing SolidRT-Go on ${target}`)
90
133
  let install = Bun.spawn([adb, "-s", target, "install", "-r", apk], { stdout: "pipe", stderr: "pipe" })
91
134
  if ((await install.exited) !== 0) {
@@ -93,12 +136,19 @@ export async function spawnAndroidClient() {
93
136
  process.exit(1)
94
137
  }
95
138
 
96
- setupAdbReverse(adb, target)
139
+ // Hand the client the dev-server address to dial, as a launch-intent extra that
140
+ // MainActivity forwards to native argv (--dev-server); the client auto-connects
141
+ // to it. Replaces adb reverse, which never worked over wireless adb.
142
+ let devServer = devServerAddress(adb, target)
143
+ let launchArgs = [adb, "-s", target, "shell", "am", "start", "-n", PACKAGE_ACTIVITY]
144
+ if (devServer) {
145
+ print(`[cli] Client will dial dev server at ${devServer}`)
146
+ launchArgs.push("--es", "srt_dev_server", devServer)
147
+ } else {
148
+ print("[cli] Could not resolve a host address for the device; client will need a manual/QR connect")
149
+ }
97
150
 
98
- let start = Bun.spawn([adb, "-s", target, "shell", "am", "start", "-n", PACKAGE_ACTIVITY], {
99
- stdout: "pipe",
100
- stderr: "pipe",
101
- })
151
+ let start = Bun.spawn(launchArgs, { stdout: "pipe", stderr: "pipe" })
102
152
  if ((await start.exited) !== 0) {
103
153
  console.error("adb start failed:\n" + (await new Response(start.stderr).text()))
104
154
  process.exit(1)
package/src/dev-server.ts CHANGED
@@ -8,16 +8,17 @@ import { values } from "./args"
8
8
  import * as cache from "./cache"
9
9
 
10
10
  export const DEV_HOST = "127.0.0.1"
11
- export const DEV_PORT = 15194
11
+ export const DEV_PORT = 0x8844
12
12
 
13
- // Dev-server WS protocol helpers: the reload message shape and the stop broadcast.
13
+ // Dev-server WS protocol helpers: the reload message shape and a broadcast to all clients.
14
14
  export function buildReload(payload: { code?: string | null; bytecode?: string }) {
15
15
  return JSON.stringify({ type: "reload", proxyFiles: values["proxy-files"], proxyHttp: values["proxy-http"], ...payload })
16
16
  }
17
17
 
18
- export function broadcastStop() {
18
+ export function broadcast(msg: object) {
19
+ let text = JSON.stringify(msg)
19
20
  for (let ws of state.clients.keys()) {
20
- ws.send(JSON.stringify({ type: "stop" }))
21
+ ws.send(text)
21
22
  }
22
23
  }
23
24
 
@@ -157,7 +158,7 @@ export function startServer() {
157
158
  print(`[cli] Client connected ${ws.remoteAddress}`)
158
159
  // Advertise our real LAN address so clients dialed over the adb loopback
159
160
  // can show/remember the directly reachable address (see connection.rs).
160
- ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl }))
161
+ ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl, stats: state.stats }))
161
162
  if (state.currentCode) {
162
163
  ws.send(buildReload({ code: state.currentCode }))
163
164
  }
@@ -245,4 +246,4 @@ export function startServer() {
245
246
  ws.ping()
246
247
  }
247
248
  }, 5000)
248
- }
249
+ }
package/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { values, command, validateArgs, printUsage } from "./args"
4
+ import { runInitCommand } from "./commands/init"
4
5
  import { runBundleCommand } from "./commands/bundle"
5
6
  import { runPackCommand } from "./commands/pack"
6
7
  import { runRecordCommand } from "./commands/record"
@@ -35,7 +36,9 @@ if (isProdBuild && process.env.NODE_ENV !== "production") {
35
36
 
36
37
  // -- Dispatch --
37
38
 
38
- if (command === "bundle") {
39
+ if (command === "init") {
40
+ await runInitCommand()
41
+ } else if (command === "bundle") {
39
42
  await runBundleCommand()
40
43
  } else if (command === "pack") {
41
44
  await runPackCommand()
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, broadcastStop } from "./dev-server"
5
+ import { buildReload, broadcast } from "./dev-server"
6
6
  import { bundle } from "./bundler"
7
7
  import { startWatcher, stopWatcher } from "./watcher"
8
8
 
@@ -11,7 +11,7 @@ function cmdStop(args: string) {
11
11
  stopWatcher()
12
12
  state.currentCode = null
13
13
  state.source = undefined
14
- broadcastStop()
14
+ broadcast({ type: "stop" })
15
15
  print("[cli] Sent stop to all clients")
16
16
  return
17
17
  }
@@ -56,6 +56,21 @@ async function cmdReload(args: string) {
56
56
  }
57
57
  }
58
58
 
59
+ function cmdStats(args: string) {
60
+ if (args === "on") {
61
+ state.stats = true
62
+ } else if (args === "off") {
63
+ state.stats = false
64
+ } else if (!args) {
65
+ state.stats = !state.stats
66
+ } else {
67
+ print("Usage: stats [on|off]")
68
+ return
69
+ }
70
+ broadcast({ type: "stats", stats: state.stats })
71
+ print(`[cli] Stats overlay ${state.stats ? "on" : "off"}`)
72
+ }
73
+
59
74
  function cmdList() {
60
75
  if (state.clients.size === 0) {
61
76
  print("No connected clients")
@@ -105,7 +120,7 @@ async function cmdLoad(file: string) {
105
120
  print(`[cli] Loaded ${file}`)
106
121
  }
107
122
 
108
- let COMMANDS = ["load ", "stop", "reload", "list", "quit", "exit", "help"]
123
+ let COMMANDS = ["load ", "stop", "reload", "list", "stats", "quit", "exit", "help"]
109
124
  let LOAD_EXTENSIONS = [".tsx", ".srt.js", ".srt.bin"]
110
125
 
111
126
  function completer(line: string): [string[], string] {
@@ -150,6 +165,8 @@ export function startRepl() {
150
165
  cmdLoad(cmd.slice(5).trim())
151
166
  } else if (cmd === "list") {
152
167
  cmdList()
168
+ } else if (cmd === "stats" || cmd.startsWith("stats ")) {
169
+ cmdStats(cmd.slice(6).trim())
153
170
  } else if (cmd === "quit" || cmd === "exit") {
154
171
  shutdown()
155
172
  } else if (cmd.startsWith("!")) {
@@ -165,7 +182,7 @@ export function startRepl() {
165
182
  )
166
183
  }
167
184
  } else if (cmd === "help") {
168
- print("Commands: load, stop, reload, list, !<cmd>, quit, help")
185
+ print("Commands: load, stop, reload, list, stats, !<cmd>, quit, help")
169
186
  } else if (cmd) {
170
187
  print(`Unknown command: ${cmd}`)
171
188
  }
package/src/util.ts CHANGED
@@ -15,6 +15,7 @@ export let state = {
15
15
  serverUrl: null as string | null,
16
16
  rl: null as ReadlineInterface | null,
17
17
  bonjour: null as Bonjour | null,
18
+ stats: false,
18
19
  }
19
20
 
20
21
  // Build target per binary, for the "not found" hint. Run from the repo root.