@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 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
@@ -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.9",
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
- "LICENSE"
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.9",
23
- "@solidrt/linux-x64-gnu": "0.0.9",
24
- "@solidrt/win32-x64-msvc": "0.0.9"
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.9",
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 (!source || (!isSource && !isPrebuilt))
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 (when several are connected)
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
- --flux Pack for the bare Flux runtime instead of SolidRT (entry must be .ts|.js)
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
@@ -41,7 +41,7 @@ export async function bundle(entry = source) {
41
41
  target: "browser",
42
42
  format: "esm",
43
43
  minify: values.minify,
44
- external: ["flux:*"],
44
+ external: ["flux:*", "srt:*"],
45
45
  define,
46
46
  plugins: [solidPlugin()],
47
47
  })
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
  }
@@ -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
- process.stdout.write(await output.text())
59
+ await writeStdout(await output.text())
35
60
  }
36
61
  process.exit()
37
62
  }
@@ -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.x-android).
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
- // Prefix adb args with `-s <serial>` only when the user pinned a device; with a
9
- // single connected device adb selects it on its own.
10
- function adbArgs(extra: string[]) {
11
- return values.device ? ["-s", values.device, ...extra] : extra
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, then let
15
- // the device discover the running dev server over LAN UDP (the same path as a
16
- // manually launched client). Fire-and-forget: the client's lifecycle is tracked
17
- // via WS connect/disconnect in dev-server.ts, not as a child process here.
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
- // Resolve the target device: 0 -> error, 1 -> use it, many -> require --device.
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, ...adbArgs(["install", "-r", apk])], { stdout: "pipe", stderr: "pipe" })
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
- let start = Bun.spawn([adb, ...adbArgs(["shell", "am", "start", "-n", PACKAGE_ACTIVITY])], {
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 discover the dev server...`)
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 { createSocket } from "node:dgram"
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
- for (let y = 0; y < modCount; y += 2) {
203
- let row = " "
204
- for (let x = 0; x < modCount; x++) {
205
- let top = qr.isDark(y, x)
206
- let bot = y + 1 < modCount && qr.isDark(y + 1, x)
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
- // UDP discovery
216
- let udp = createSocket("udp4")
217
- udp.on("message", (msg, rinfo) => {
218
- if (msg.toString() === "SRT_DISCOVER") {
219
- print(`[cli] Discovery request from ${rinfo.address}:${rinfo.port}`)
220
- udp.send("SRT_SERVER", rinfo.port, rinfo.address)
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 solidrt-go",
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
  }