This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Add launcher override to SpawnDaemonOptions (closes #17)

Non-Node callers (Bun, Deno) can now route the detached daemon launch
through a Node binary so the PTY server can load its node-pty native
addon. Defaults to process.execPath, so existing Node callers are
unaffected.

Nathan Herald (Apr 15, 2026, 8:24 PM +0200) b112689e cf443a91

+50 -1
+1
CHANGELOG.md
··· 17 17 ### Client API 18 18 - Export `extractFilterTags` and `matchesAllTags` from `@myobie/pty/client` so third-party tools (e.g., pty-relay) can accept and apply the same `--filter-tag key=value` syntax 19 19 - Add optional `tags?: Record<string, string>` on remote session entries so pty-relay can surface tags in `ls --json` and have the interactive TUI filter remote sessions by them 20 + - Add `launcher?: { command: string; args?: string[] }` to `SpawnDaemonOptions` so non-Node callers (Bun, Deno) can route the detached daemon launch through a Node binary — the daemon needs Node to load the `node-pty` native addon (closes #17) 20 21 21 22 ### Project files 22 23 - `pty up` now removes tags that were removed from `pty.toml` — toml-managed tag keys are tracked in a `ptyfile.tags` meta tag so manually-added tags (set via `pty tag`) are preserved
+22 -1
src/spawn.ts
··· 21 21 rows?: number; 22 22 cols?: number; 23 23 tags?: Record<string, string>; 24 + /** Override the runtime used to launch the detached daemon process. 25 + * 26 + * By default the daemon is spawned with `process.execPath` — the same 27 + * runtime as the caller. That breaks when the caller is running under a 28 + * non-Node runtime (e.g. Bun): the daemon needs to be launched under Node 29 + * so the PTY server can load its `node-pty` native addon. 30 + * 31 + * Set `launcher` to point at a Node binary (and optional leading args) to 32 + * route daemon launches through it, regardless of the caller's runtime. 33 + * 34 + * @example 35 + * ```ts 36 + * await spawnDaemon({ 37 + * // ...existing fields... 38 + * launcher: { command: "/usr/local/bin/node" }, 39 + * }); 40 + * ``` 41 + */ 42 + launcher?: { command: string; args?: string[] }; 24 43 } 25 44 26 45 export async function spawnDaemon(options: SpawnDaemonOptions): Promise<void> { ··· 41 60 ...(options.tags && Object.keys(options.tags).length > 0 ? { tags: options.tags } : {}), 42 61 }); 43 62 44 - const child = spawn(process.execPath, [serverModule], { 63 + const launcherCmd = options.launcher?.command ?? process.execPath; 64 + const launcherArgs = options.launcher?.args ?? []; 65 + const child = spawn(launcherCmd, [...launcherArgs, serverModule], { 45 66 detached: true, 46 67 stdio: ["ignore", "ignore", "pipe"], 47 68 env: { ...process.env, PTY_SERVER_CONFIG: config },
+27
tests/spawn-options.test.ts
··· 5 5 import { fileURLToPath } from "node:url"; 6 6 import { spawn, spawnSync } from "node:child_process"; 7 7 import { queryStats } from "../src/client.ts"; 8 + import { spawnDaemon } from "../src/spawn.ts"; 8 9 9 10 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 11 const nodeBin = process.execPath; ··· 188 189 const stats = await queryStats(name); 189 190 expect(stats.terminal.rows).toBe(24); 190 191 expect(stats.terminal.cols).toBe(80); 192 + }, 15000); 193 + 194 + it("launcher override routes the daemon through the given command", async () => { 195 + const dir = makeSessionDir(); 196 + const argFile = path.join(dir, "launcher-args.txt"); 197 + const launcherScript = path.join(dir, "launcher.sh"); 198 + fs.writeFileSync(launcherScript, `#!/bin/sh\nprintf '%s\\n' "$@" > "${argFile}"\nexit 0\n`); 199 + fs.chmodSync(launcherScript, 0o755); 200 + 201 + process.env.PTY_SESSION_DIR = dir; 202 + const name = uniqueName(); 203 + 204 + // The stub launcher exits without starting a real daemon, so spawnDaemon 205 + // will reject on socket timeout — we only care that the launcher was invoked. 206 + await expect(spawnDaemon({ 207 + name, 208 + command: "cat", 209 + args: [], 210 + displayCommand: "cat", 211 + cwd: dir, 212 + launcher: { command: launcherScript, args: ["--prelude"] }, 213 + })).rejects.toThrow(); 214 + 215 + const recorded = fs.readFileSync(argFile, "utf-8").split("\n").filter(Boolean); 216 + expect(recorded[0]).toBe("--prelude"); 217 + expect(recorded[1]).toMatch(/server\.js$/); 191 218 }, 15000); 192 219 193 220 it("surfaces a missing cwd explicitly instead of failing silently", async () => {