@solidrt/cli 0.0.9 → 0.0.11
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 +46 -0
- package/README.md +2 -0
- package/package.json +7 -6
- package/src/args.ts +10 -4
- package/src/bundler.ts +1 -1
- package/src/cache.ts +17 -1
- package/src/commands/bundle.ts +27 -2
- package/src/dev-android.ts +74 -35
- package/src/dev-server.ts +34 -20
- package/src/util.ts +4 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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 run src/index.tsx` - dev server + a local client window, watches and
|
|
13
|
+
hot-reloads. NEEDS A DISPLAY (opens a GUI window). Not usable headless.
|
|
14
|
+
- `bunx srt bundle src/index.tsx` - transpile to `<file>.srt.js`. With
|
|
15
|
+
`--compile`, emits `.srt.bin` bytecode. `--minify`, `--dev`, `--stdout`,
|
|
16
|
+
`--output` also available.
|
|
17
|
+
- `bunx srt record src/index.tsx [flags]` - render OFFSCREEN to PNG frames.
|
|
18
|
+
- `bunx srt server [file]` / `bunx srt client` - the two halves of `run`
|
|
19
|
+
separately (server distributes code; clients on other devices connect to it).
|
|
20
|
+
|
|
21
|
+
## Verifying without a display (headless / CI / agent box)
|
|
22
|
+
|
|
23
|
+
Two reliable checks that need no GUI:
|
|
24
|
+
|
|
25
|
+
1. `bunx srt bundle src/index.tsx` - exit 0 means the app compiles. Fast.
|
|
26
|
+
2. `bunx srt record src/index.tsx --size 480x640 --duration 1 --fps 2` -
|
|
27
|
+
renders offscreen via EGL/wgpu and writes `frame-NNNNNN.png`. This actually
|
|
28
|
+
proves the app renders. Combine with `--fps`/`--duration` (defaults
|
|
29
|
+
1280x720, 60fps, 1s).
|
|
30
|
+
|
|
31
|
+
`record` gotchas:
|
|
32
|
+
- Frames are written to the RUNTIME's working dir (`~/.local/share/SolidRT/go/`),
|
|
33
|
+
NOT the directory you ran the command from. Look there for the PNGs.
|
|
34
|
+
- The recording includes a debug overlay (FPS/REQ/MiB/CPU) in a corner.
|
|
35
|
+
- Run from the project directory. There is no `bunx --cwd` flag.
|
|
36
|
+
|
|
37
|
+
## Dev server proxies (when clients on other devices need your machine's data)
|
|
38
|
+
|
|
39
|
+
- `--proxy-http` - route `fetch` through the dev server; responses cached in
|
|
40
|
+
`.srt-cache.db` (delete the file to clear).
|
|
41
|
+
- `--proxy-files` - route flux:fs (`file`/`dir`/`write`) through the dev server.
|
|
42
|
+
Exposes your dev machine's files to all clients; use with care.
|
|
43
|
+
|
|
44
|
+
## REPL (opened by `run`/`server`)
|
|
45
|
+
|
|
46
|
+
`load <file>`, `reload [n]`, `stop [n]`, `list`, `!<cmd>`, `quit`/`exit`.
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -10,21 +10,22 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
|
-
"
|
|
13
|
+
"AGENTS.md"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@babel/core": "^7.28.5",
|
|
17
17
|
"@babel/preset-typescript": "^7.28.5",
|
|
18
18
|
"babel-preset-solid": "2.0.0-beta.14",
|
|
19
|
+
"bonjour-service": "^1.4.0",
|
|
19
20
|
"qrcode-generator": "^2.0.4"
|
|
20
21
|
},
|
|
21
22
|
"optionalDependencies": {
|
|
22
|
-
"@solidrt/darwin-arm64": "0.0.
|
|
23
|
-
"@solidrt/linux-x64-gnu": "0.0.
|
|
24
|
-
"@solidrt/win32-x64-msvc": "0.0.
|
|
23
|
+
"@solidrt/darwin-arm64": "0.0.11",
|
|
24
|
+
"@solidrt/linux-x64-gnu": "0.0.11",
|
|
25
|
+
"@solidrt/win32-x64-msvc": "0.0.11"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
|
-
"@solidrt/core": "0.0.
|
|
28
|
+
"@solidrt/core": "0.0.11",
|
|
28
29
|
"typescript": "^5"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
package/src/args.ts
CHANGED
|
@@ -5,7 +5,7 @@ export let { values, positionals } = parseArgs({
|
|
|
5
5
|
dev: { type: "boolean", short: "d", default: false },
|
|
6
6
|
minify: { type: "boolean", short: "m", default: false },
|
|
7
7
|
compile: { type: "boolean", short: "c", default: false },
|
|
8
|
-
flux: { type: "boolean", default: false },
|
|
8
|
+
flux: { type: "boolean", short: "f", default: false },
|
|
9
9
|
stdout: { type: "boolean", default: false },
|
|
10
10
|
output: { type: "string", short: "o" },
|
|
11
11
|
"proxy-files": { type: "boolean", default: false },
|
|
@@ -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
|
},
|
|
@@ -35,8 +36,11 @@ function usage(line: string): never {
|
|
|
35
36
|
export function validateArgs() {
|
|
36
37
|
switch (command) {
|
|
37
38
|
case "bundle":
|
|
38
|
-
if (
|
|
39
|
+
if (values.flux) {
|
|
40
|
+
if (!source || !isTs) usage("srt bundle --flux [options] <entry.[ts|js]>")
|
|
41
|
+
} else if (!source || (!isSource && !isPrebuilt)) {
|
|
39
42
|
usage("srt bundle [options] <entry.[tsx|jsx|ts|js|srt.js|srt.bin]>")
|
|
43
|
+
}
|
|
40
44
|
break
|
|
41
45
|
case "record":
|
|
42
46
|
if (!source || !isTsx) usage("srt record <entry.[tsx|jsx]>")
|
|
@@ -74,12 +78,14 @@ run/server options:
|
|
|
74
78
|
|
|
75
79
|
run/client options:
|
|
76
80
|
--size <WxH> Window size (default: 1280x720)
|
|
81
|
+
--stats Show the debug stats overlay (FPS, memory, frame timings)
|
|
77
82
|
|
|
78
83
|
client options:
|
|
79
84
|
--android Install and launch the client on a connected Android device
|
|
80
|
-
--device <serial> Target a specific adb device
|
|
85
|
+
--device <serial> Target a specific adb device by serial or unique prefix
|
|
81
86
|
|
|
82
87
|
bundle options:
|
|
88
|
+
-f, --flux Bundle for the bare Flux runtime, without SolidJS (entry must be .ts|.js)
|
|
83
89
|
-d, --dev Use development build of SolidJS (default: production)
|
|
84
90
|
-m, --minify Minify the output
|
|
85
91
|
-c, --compile Compile to bytecode
|
|
@@ -87,7 +93,7 @@ bundle options:
|
|
|
87
93
|
--stdout Write bundle to stdout
|
|
88
94
|
|
|
89
95
|
pack options:
|
|
90
|
-
|
|
96
|
+
-f, --flux Pack for the bare Flux runtime instead of SolidRT (entry must be .ts|.js)
|
|
91
97
|
-m, --minify Minify the output
|
|
92
98
|
-o, --output <name> Output filename
|
|
93
99
|
|
package/src/bundler.ts
CHANGED
package/src/cache.ts
CHANGED
|
@@ -46,6 +46,22 @@ export function isEnabled(): boolean {
|
|
|
46
46
|
return enabled
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Strings returned by bun:sqlite carry an internal representation that
|
|
50
|
+
// Headers.set() rejects, even when value-identical to an acceptable string
|
|
51
|
+
// (Bun bug oven-sh/bun#28266, present through 1.3.14). Rebuilding each char
|
|
52
|
+
// yields a clean string.
|
|
53
|
+
function reflatten(s: string): string {
|
|
54
|
+
return Array.from(s, (c) => String.fromCharCode(c.charCodeAt(0))).join("")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function reflattenHeaders(obj: Record<string, string>): Record<string, string> {
|
|
58
|
+
let out: Record<string, string> = {}
|
|
59
|
+
for (let key in obj) {
|
|
60
|
+
out[reflatten(key)] = reflatten(obj[key]!)
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
function keyFor(method: string, url: string): string {
|
|
50
66
|
let h = createHash("sha256")
|
|
51
67
|
h.update(method)
|
|
@@ -101,7 +117,7 @@ export function get(method: string, url: string): Entry | null {
|
|
|
101
117
|
method: row.method,
|
|
102
118
|
url: row.url,
|
|
103
119
|
status: row.status,
|
|
104
|
-
headers: JSON.parse(row.headers),
|
|
120
|
+
headers: reflattenHeaders(JSON.parse(row.headers)),
|
|
105
121
|
body: row.body,
|
|
106
122
|
cachedAt: row.cached_at,
|
|
107
123
|
}
|
package/src/commands/bundle.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { values, source, isPrebuilt } from "../args"
|
|
2
|
-
import { bundle, bundleTo, compileToBytecode } from "../bundler"
|
|
2
|
+
import { bundle, bundleTo, bundleFlux, compileToBytecode } from "../bundler"
|
|
3
3
|
import { resolve } from "path"
|
|
4
4
|
|
|
5
|
+
// Write to stdout and resolve only once the whole payload is flushed.
|
|
6
|
+
// process.stdout.write to a pipe is async and applies backpressure; the
|
|
7
|
+
// callback fires after every byte is drained, so it is safe to exit after.
|
|
8
|
+
function writeStdout(data: string): Promise<void> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
process.stdout.write(data, (err) => (err ? reject(err) : resolve()))
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
// Compile JS to a .srt.bin file and report its size.
|
|
6
15
|
async function writeBytecode(jsCode: string, outfile: string) {
|
|
7
16
|
let bytecode = await compileToBytecode(jsCode)
|
|
@@ -11,6 +20,22 @@ async function writeBytecode(jsCode: string, outfile: string) {
|
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export async function runBundleCommand() {
|
|
23
|
+
if (values.flux) {
|
|
24
|
+
let baseName = values.output ?? source!.replace(/\.[jt]s$/, "")
|
|
25
|
+
let jsCode = await bundleFlux(source!)
|
|
26
|
+
|
|
27
|
+
if (values.stdout) {
|
|
28
|
+
await writeStdout(jsCode)
|
|
29
|
+
} else if (values.compile) {
|
|
30
|
+
await writeBytecode(jsCode, baseName + ".flux.bin")
|
|
31
|
+
} else {
|
|
32
|
+
let outfile = baseName + ".flux.js"
|
|
33
|
+
await Bun.write(outfile, jsCode)
|
|
34
|
+
console.log(`>> wrote ${jsCode.length} bytes to ${outfile}`)
|
|
35
|
+
}
|
|
36
|
+
process.exit()
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
if (isPrebuilt) {
|
|
15
40
|
if (!source!.endsWith(".srt.js")) {
|
|
16
41
|
console.error("Can only compile .srt.js files. .srt.bin is already compiled.")
|
|
@@ -31,7 +56,7 @@ export async function runBundleCommand() {
|
|
|
31
56
|
process.exit(1)
|
|
32
57
|
}
|
|
33
58
|
for (let output of result.outputs) {
|
|
34
|
-
|
|
59
|
+
await writeStdout(await output.text())
|
|
35
60
|
}
|
|
36
61
|
process.exit()
|
|
37
62
|
}
|
package/src/dev-android.ts
CHANGED
|
@@ -1,20 +1,79 @@
|
|
|
1
1
|
import { print, requireAdb } from "./util"
|
|
2
2
|
import { resolveApk } from "./artifacts"
|
|
3
3
|
import { values } from "./args"
|
|
4
|
+
import { DEV_PORT } from "./dev-server"
|
|
4
5
|
|
|
5
|
-
// Launch component of the "go" dev-client flavor (see lattice/Makefile.
|
|
6
|
+
// Launch component of the "go" dev-client flavor (see lattice/Makefile.android).
|
|
6
7
|
let PACKAGE_ACTIVITY = "com.solidrt.go/com.solidrt.app.MainActivity"
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
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}`], {
|
|
18
|
+
stdout: "pipe",
|
|
19
|
+
stderr: "pipe",
|
|
20
|
+
})
|
|
21
|
+
if (res.exitCode !== 0) {
|
|
22
|
+
print(`[cli] adb reverse failed (client will fall back to discovery):\n${res.stderr.toString()}`)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Serials of connected, authorized devices (excludes offline/unauthorized).
|
|
27
|
+
function listDevices(adb: string): string[] {
|
|
28
|
+
let listed = Bun.spawnSync([adb, "devices"], { stdout: "pipe", stderr: "pipe" })
|
|
29
|
+
return listed.stdout
|
|
30
|
+
.toString()
|
|
31
|
+
.split("\n")
|
|
32
|
+
.slice(1)
|
|
33
|
+
.map((l) => l.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.filter((l) => l.endsWith("\tdevice"))
|
|
36
|
+
.map((l) => l.split("\t")[0])
|
|
37
|
+
.filter((s): s is string => Boolean(s))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Resolve the target device serial. With --device, treat the value as a serial
|
|
41
|
+
// prefix and require it to match exactly one connected device; without it, use
|
|
42
|
+
// the sole connected device. Exits with a clear message on any ambiguity.
|
|
43
|
+
function resolveTarget(adb: string): string {
|
|
44
|
+
let devices = listDevices(adb)
|
|
45
|
+
|
|
46
|
+
if (values.device) {
|
|
47
|
+
let prefix = values.device
|
|
48
|
+
let matches = devices.filter((d) => d.startsWith(prefix))
|
|
49
|
+
if (matches.length > 1) {
|
|
50
|
+
console.error(`--device "${prefix}" is ambiguous; matches: ${matches.join(", ")}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
let [match] = matches
|
|
54
|
+
if (!match) {
|
|
55
|
+
console.error(`No connected device matches --device "${prefix}". Connected: ${devices.join(", ") || "none"}`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
return match
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (devices.length > 1) {
|
|
62
|
+
console.error(`Multiple devices connected (${devices.join(", ")}); pick one with --device <serial or prefix>.`)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
let [only] = devices
|
|
66
|
+
if (!only) {
|
|
67
|
+
console.error("No authorized Android device found. Enable USB debugging and check `adb devices`.")
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
return only
|
|
12
71
|
}
|
|
13
72
|
|
|
14
|
-
// Install + launch the Android client on a connected device over adb,
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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.
|
|
18
77
|
export async function spawnAndroidClient() {
|
|
19
78
|
let adb = requireAdb()
|
|
20
79
|
|
|
@@ -25,38 +84,18 @@ export async function spawnAndroidClient() {
|
|
|
25
84
|
process.exit(1)
|
|
26
85
|
}
|
|
27
86
|
|
|
28
|
-
|
|
29
|
-
let target = values.device
|
|
30
|
-
if (!target) {
|
|
31
|
-
let listed = Bun.spawnSync([adb, "devices"], { stdout: "pipe", stderr: "pipe" })
|
|
32
|
-
let devices = listed.stdout
|
|
33
|
-
.toString()
|
|
34
|
-
.split("\n")
|
|
35
|
-
.slice(1)
|
|
36
|
-
.map((l) => l.trim())
|
|
37
|
-
.filter(Boolean)
|
|
38
|
-
.filter((l) => l.endsWith("\tdevice"))
|
|
39
|
-
.map((l) => l.split("\t")[0])
|
|
40
|
-
|
|
41
|
-
if (devices.length === 0) {
|
|
42
|
-
console.error("No authorized Android device found. Enable USB debugging and check `adb devices`.")
|
|
43
|
-
process.exit(1)
|
|
44
|
-
}
|
|
45
|
-
if (devices.length > 1) {
|
|
46
|
-
console.error(`Multiple devices connected (${devices.join(", ")}); pick one with --device <serial>.`)
|
|
47
|
-
process.exit(1)
|
|
48
|
-
}
|
|
49
|
-
target = devices[0]
|
|
50
|
-
}
|
|
87
|
+
let target = resolveTarget(adb)
|
|
51
88
|
|
|
52
89
|
print(`[cli] Installing SolidRT-Go on ${target}`)
|
|
53
|
-
let install = Bun.spawn([adb,
|
|
90
|
+
let install = Bun.spawn([adb, "-s", target, "install", "-r", apk], { stdout: "pipe", stderr: "pipe" })
|
|
54
91
|
if ((await install.exited) !== 0) {
|
|
55
92
|
console.error("adb install failed:\n" + (await new Response(install.stderr).text()))
|
|
56
93
|
process.exit(1)
|
|
57
94
|
}
|
|
58
95
|
|
|
59
|
-
|
|
96
|
+
setupAdbReverse(adb, target)
|
|
97
|
+
|
|
98
|
+
let start = Bun.spawn([adb, "-s", target, "shell", "am", "start", "-n", PACKAGE_ACTIVITY], {
|
|
60
99
|
stdout: "pipe",
|
|
61
100
|
stderr: "pipe",
|
|
62
101
|
})
|
|
@@ -65,5 +104,5 @@ export async function spawnAndroidClient() {
|
|
|
65
104
|
process.exit(1)
|
|
66
105
|
}
|
|
67
106
|
|
|
68
|
-
print(`[cli] Launched SolidRT-Go on ${target}; waiting for it to
|
|
107
|
+
print(`[cli] Launched SolidRT-Go on ${target}; waiting for it to connect to the dev server...`)
|
|
69
108
|
}
|
package/src/dev-server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "path"
|
|
2
2
|
import { stat as fsStat, readdir } from "node:fs/promises"
|
|
3
3
|
import { networkInterfaces } from "node:os"
|
|
4
|
-
import {
|
|
4
|
+
import { Bonjour } from "bonjour-service"
|
|
5
5
|
import qrcode from "qrcode-generator"
|
|
6
6
|
import { state, print } from "./util"
|
|
7
7
|
import { values } from "./args"
|
|
@@ -28,7 +28,6 @@ function headersToObject(h: Headers): Record<string, string> {
|
|
|
28
28
|
})
|
|
29
29
|
return out
|
|
30
30
|
}
|
|
31
|
-
|
|
32
31
|
async function handleProxy(req: Request): Promise<Response> {
|
|
33
32
|
let target = req.headers.get("x-srt-proxy-url")
|
|
34
33
|
if (!target) {
|
|
@@ -156,6 +155,9 @@ export function startServer() {
|
|
|
156
155
|
open(ws) {
|
|
157
156
|
state.clients.set(ws, { platform: "unknown", version: "unknown" })
|
|
158
157
|
print(`[cli] Client connected ${ws.remoteAddress}`)
|
|
158
|
+
// Advertise our real LAN address so clients dialed over the adb loopback
|
|
159
|
+
// can show/remember the directly reachable address (see connection.rs).
|
|
160
|
+
ws.send(JSON.stringify({ type: "welcome", address: state.serverUrl, stats: values.stats }))
|
|
159
161
|
if (state.currentCode) {
|
|
160
162
|
ws.send(buildReload({ code: state.currentCode }))
|
|
161
163
|
}
|
|
@@ -199,31 +201,43 @@ export function startServer() {
|
|
|
199
201
|
qr.addData(serverUrl)
|
|
200
202
|
qr.make()
|
|
201
203
|
let modCount = qr.getModuleCount()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
// Render as a white tile with black modules (explicit ANSI colors) plus the
|
|
205
|
+
// 4-module quiet zone the QR spec requires. Drawing with the terminal's
|
|
206
|
+
// default foreground inverts the code on dark themes, which standard
|
|
207
|
+
// decoders reject.
|
|
208
|
+
const QR_INK = "\x1b[30;107m" // black modules on bright-white tile (tile = background)
|
|
209
|
+
const QR_TILE_FG = "\x1b[97m" // bright-white as foreground over the default background
|
|
210
|
+
const QR_RESET = "\x1b[0m"
|
|
211
|
+
const QUIET_ZONE = 2 // modules (spec says 4, but scanners cope and it reads tighter)
|
|
212
|
+
let qrWidth = modCount + 2 * QUIET_ZONE
|
|
213
|
+
let dark = (y: number, x: number) => y >= 0 && y < modCount && x >= 0 && x < modCount && qr.isDark(y, x)
|
|
214
|
+
// modCount is always odd, so the tile is a half-line taller than an even row
|
|
215
|
+
// count. The loop packs two module-rows per line via half-blocks and stops on
|
|
216
|
+
// the last content row, leaving the bottom quiet zone half a line short of the
|
|
217
|
+
// full-line top quiet zone.
|
|
218
|
+
for (let y = -QUIET_ZONE; y < modCount + QUIET_ZONE - 1; y += 2) {
|
|
219
|
+
let row = " " + QR_INK
|
|
220
|
+
for (let x = -QUIET_ZONE; x < modCount + QUIET_ZONE; x++) {
|
|
221
|
+
let top = dark(y, x)
|
|
222
|
+
let bot = dark(y + 1, x)
|
|
207
223
|
row += top && bot ? "\u2588" : top ? "\u2580" : bot ? "\u2584" : " "
|
|
208
224
|
}
|
|
209
|
-
console.log(row)
|
|
225
|
+
console.log(row + QR_RESET)
|
|
210
226
|
}
|
|
227
|
+
// Close that gap with a half-height tile line: upper half painted in the tile
|
|
228
|
+
// color (foreground), lower half the terminal background. The 0.5 here plus
|
|
229
|
+
// the 0.5 already under the last content row equal the full-line top margin.
|
|
230
|
+
console.log(" " + QR_TILE_FG + "\u2580".repeat(qrWidth) + QR_RESET)
|
|
211
231
|
|
|
212
232
|
console.log("")
|
|
213
233
|
console.log(`[cli] WebSocket server on ws://${serverUrl}`)
|
|
214
234
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
udp.bind(DEV_PORT, () => {
|
|
224
|
-
udp.setBroadcast(true)
|
|
225
|
-
print("[cli] UDP discovery listener on port " + DEV_PORT)
|
|
226
|
-
})
|
|
235
|
+
// LAN discovery: advertise the dev server as a DNS-SD service so go clients on
|
|
236
|
+
// the same network can find it (see lattice/src/go/connection.rs). Stored on
|
|
237
|
+
// state so shutdown() can send the mDNS goodbye.
|
|
238
|
+
state.bonjour = new Bonjour()
|
|
239
|
+
state.bonjour.publish({ name: "SolidRT Dev Server", type: "solidrt", protocol: "tcp", port: DEV_PORT })
|
|
240
|
+
print(`[cli] Advertising _solidrt._tcp on port ${DEV_PORT} via mDNS`)
|
|
227
241
|
|
|
228
242
|
// Keepalive
|
|
229
243
|
setInterval(() => {
|
package/src/util.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs"
|
|
|
3
3
|
import { resolve } from "node:path"
|
|
4
4
|
import type { Interface as ReadlineInterface } from "node:readline"
|
|
5
5
|
import type { Server as BunServer } from "bun"
|
|
6
|
+
import type { Bonjour } from "bonjour-service"
|
|
6
7
|
|
|
7
8
|
export let state = {
|
|
8
9
|
clients: new Map<any, { platform: string; version: string }>(),
|
|
@@ -13,11 +14,12 @@ export let state = {
|
|
|
13
14
|
server: null as BunServer<undefined> | null,
|
|
14
15
|
serverUrl: null as string | null,
|
|
15
16
|
rl: null as ReadlineInterface | null,
|
|
17
|
+
bonjour: null as Bonjour | null,
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
// Build target per binary, for the "not found" hint. Run from the repo root.
|
|
19
21
|
let BUILD_HINTS: Record<string, string> = {
|
|
20
|
-
"solidrt-go": "make
|
|
22
|
+
"solidrt-go": "make client",
|
|
21
23
|
solidrt: "make runtime",
|
|
22
24
|
flux: "make -C flux flux",
|
|
23
25
|
fluxc: "make -C flux fluxc",
|
|
@@ -82,5 +84,6 @@ export function printErr(...args: any[]) {
|
|
|
82
84
|
export function shutdown() {
|
|
83
85
|
if (state.child) state.child.kill()
|
|
84
86
|
if (state.server) state.server.stop()
|
|
87
|
+
if (state.bonjour) state.bonjour.destroy()
|
|
85
88
|
process.exit(0)
|
|
86
89
|
}
|