@particle-academy/fancy-term-host 0.2.3 → 0.3.0

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts","../src/cwd.ts","../src/pty-host.ts"],"names":["os","crypto","path","existsSync","statSync","spawn","socketPath","fs","net"],"mappings":"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","file":"pty-host.cjs","sourcesContent":["/**\n * Pty-host wire protocol (Tier 3).\n *\n * The detached pty-host (main/terminal/pty-host.ts) and the in-app HostClient\n * (main/terminal/host-client.ts) talk over a local IPC transport — a named pipe\n * on Windows, a unix domain socket on POSIX — using a tiny length-prefixed JSON\n * framing so there's no heavy dependency. This module is PURE (no electron, no\n * node-pty, no net): just the message shapes + the encode/decode for the framing,\n * so it can be imported by both ends AND unit-tested in isolation.\n *\n * Framing: each message is `[4-byte big-endian uint32 length][utf8 JSON body]`.\n * The length prefix is the byte length of the JSON body. A FrameDecoder buffers\n * partial reads and yields whole messages as they complete — TCP/pipe streams\n * don't preserve message boundaries, so we can't assume one `data` event == one\n * message.\n */\n\n/**\n * Protocol version. Bumped whenever the message shapes change in a way that\n * makes an old host incompatible with a new client (or vice-versa). The client\n * refuses to attach to a host whose pidfile reports a different version and\n * spawns a fresh host instead — see host-client.ts connect-or-spawn.\n */\nexport const PROTOCOL_VERSION = 2;\n\n/** Requests the client sends to the host. `seq` correlates a reply. */\nexport type ClientMessage =\n | { kind: 'hello'; seq: number; protocolVersion: number }\n | {\n kind: 'create';\n seq: number;\n opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n };\n }\n | { kind: 'write'; id: string; data: string }\n | { kind: 'resize'; id: string; cols: number; rows: number }\n | { kind: 'kill'; id: string }\n | { kind: 'list'; seq: number }\n | { kind: 'set-retained'; id: string; retained: boolean }\n | { kind: 'get-scrollback'; seq: number; id: string }\n | { kind: 'ping'; seq: number }\n | { kind: 'shutdown'; seq: number };\n\n/** Pushes + replies the host sends to the client. */\nexport type HostMessage =\n | { kind: 'hello-ok'; seq: number; protocolVersion: number; pid: number }\n | {\n kind: 'created';\n seq: number;\n result: {\n id: string;\n pid: number;\n shell: string;\n existing: boolean;\n scrollback: string;\n };\n }\n | {\n kind: 'list-result';\n seq: number;\n terminals: Array<{ id: string; pid: number; shell: string }>;\n }\n | { kind: 'scrollback-result'; seq: number; scrollback: string | null }\n | { kind: 'pong'; seq: number }\n | { kind: 'shutdown-ok'; seq: number }\n | { kind: 'data'; id: string; data: string }\n | { kind: 'exit'; id: string; exitCode: number; signal?: number };\n\nexport type Frame = ClientMessage | HostMessage;\n\nconst LENGTH_BYTES = 4;\n\n/** Encode a message as a length-prefixed JSON frame ready for the socket. */\nexport function encodeFrame(msg: Frame): Buffer {\n const body = Buffer.from(JSON.stringify(msg), 'utf8');\n const header = Buffer.allocUnsafe(LENGTH_BYTES);\n header.writeUInt32BE(body.length, 0);\n return Buffer.concat([header, body]);\n}\n\n/**\n * Streaming frame decoder. Feed it raw socket chunks via `push`; it returns the\n * complete messages that became available (zero or more), buffering any partial\n * tail until the rest arrives. One decoder per socket.\n *\n * Resilient by design: a malformed JSON body is skipped (the frame is consumed\n * but yields nothing) rather than throwing — a corrupt frame must not wedge the\n * whole stream. An absurd length prefix (> MAX_FRAME) is treated as a desync and\n * the buffer is reset; the caller can decide whether to drop the connection.\n */\nexport class FrameDecoder {\n private buffer: Buffer = Buffer.alloc(0);\n\n /** Hard cap on a single frame (16 MB). Guards against a runaway/garbage\n * length prefix allocating unbounded memory. node-pty data chunks are tiny;\n * a serialized scrollback is bounded well under this. */\n static readonly MAX_FRAME = 16 * 1024 * 1024;\n\n /** True when the last push hit an oversized/desynced frame. The caller\n * should drop the connection — the stream can't be trusted to realign. */\n desynced = false;\n\n push(chunk: Buffer): Frame[] {\n this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;\n const out: Frame[] = [];\n for (;;) {\n if (this.buffer.length < LENGTH_BYTES) break;\n const len = this.buffer.readUInt32BE(0);\n if (len > FrameDecoder.MAX_FRAME) {\n // Desync / garbage. Reset and flag — realigning a length-prefixed\n // stream after a bad prefix isn't possible without a sentinel.\n this.desynced = true;\n this.buffer = Buffer.alloc(0);\n break;\n }\n if (this.buffer.length < LENGTH_BYTES + len) break; // wait for more\n const body = this.buffer.subarray(LENGTH_BYTES, LENGTH_BYTES + len);\n this.buffer = this.buffer.subarray(LENGTH_BYTES + len);\n try {\n out.push(JSON.parse(body.toString('utf8')) as Frame);\n } catch {\n /* skip a corrupt frame; the framing itself is still aligned */\n }\n }\n return out;\n }\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\nimport crypto from 'node:crypto';\nimport { PROTOCOL_VERSION } from './host-protocol';\n\n/**\n * Path + pidfile resolution for the detached pty-host (Tier 3).\n *\n * Kept ELECTRON-FREE on the resolution side that the host itself uses (the host\n * is a plain node process — no `app`), so the userData path is passed IN. The\n * in-app side (host-client lifecycle) imports `app` separately and feeds it here.\n */\n\nexport interface Pidfile {\n pid: number;\n socketPath: string;\n protocolVersion: number;\n startedAt: number;\n}\n\n/** Short, stable per-user hash so two OS users don't collide on the Windows\n * pipe name (the pipe namespace is machine-global). */\nexport function userHash(): string {\n const seed = `${os.userInfo().username}|${os.hostname()}`;\n return crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);\n}\n\n/**\n * The local IPC transport address.\n * • Windows: a named pipe `\\\\.\\pipe\\genie-ptyhost-<userhash>`. The default\n * Windows pipe ACL is per-logon-session, so another user on the same machine\n * can't open it — that's our ACL. (Documented; we don't tighten further.)\n * • POSIX: a unix domain socket under userData (preferred — survives /tmp\n * cleaners and is per-user by directory perms) named `ptyhost.sock`.\n */\nexport function socketPathFor(userDataDir: string): string {\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\genie-ptyhost-${userHash()}`;\n }\n // Keep the path short — unix socket paths have a ~104-char limit. userData is\n // typically well under that; fall back to os.tmpdir() if it's pathologically\n // long.\n const candidate = path.join(userDataDir, 'ptyhost.sock');\n if (candidate.length < 100) return candidate;\n return path.join(os.tmpdir(), `genie-ptyhost-${userHash()}.sock`);\n}\n\nexport function pidfilePath(userDataDir: string): string {\n return path.join(userDataDir, 'ptyhost.json');\n}\n\nexport function writePidfile(userDataDir: string, pf: Pidfile): void {\n const target = pidfilePath(userDataDir);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(pf));\n fs.renameSync(tmp, target);\n}\n\nexport function readPidfile(userDataDir: string): Pidfile | null {\n try {\n const raw = fs.readFileSync(pidfilePath(userDataDir), 'utf8');\n const pf = JSON.parse(raw) as Pidfile;\n if (\n typeof pf.pid !== 'number' ||\n typeof pf.socketPath !== 'string' ||\n typeof pf.protocolVersion !== 'number'\n ) {\n return null;\n }\n return pf;\n } catch {\n return null;\n }\n}\n\nexport function deletePidfile(userDataDir: string): void {\n try {\n fs.rmSync(pidfilePath(userDataDir), { force: true });\n } catch {\n /* ignore */\n }\n}\n\n/** True when a process with `pid` is alive (signal 0 probes without killing). */\nexport function isPidAlive(pid: number): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = exists but not ours (still \"alive\"); ESRCH = gone.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Decide whether an existing pidfile points at a usable host.\n * Usable = pid alive AND protocol versions match. A stale/dead/mismatched\n * pidfile means we must spawn a fresh host.\n */\nexport function pidfileUsable(pf: Pidfile | null): boolean {\n if (!pf) return false;\n if (pf.protocolVersion !== PROTOCOL_VERSION) return false;\n if (!isPidAlive(pf.pid)) return false;\n return true;\n}\n\n/**\n * Resolve the compiled pty-host script on disk, trying multiple candidate paths\n * so it works in BOTH `npm run dev` (script at app/pty-host.js next to\n * background.js) AND a packaged asar build. node-pty's native binding can't load\n * from inside an asar, so the host (which requires node-pty) must run UNPACKED —\n * `app.asar.unpacked/...`. We try the unpacked path first, then the in-asar path,\n * then a dev-relative path. Returns the first that exists, or null.\n *\n * `dirname` is main/background's __dirname (the directory the compiled main\n * bundle lives in). The host script is emitted alongside it as `pty-host.js`.\n */\nexport function resolveHostScript(dirname: string): string | null {\n const candidates = [\n // Packaged: node-pty must be unpacked, so run the host from the unpacked\n // tree too (its require('node-pty') resolves to the unpacked .node).\n dirname.includes(`app.asar${path.sep}`) || dirname.includes('app.asar/')\n ? dirname.replace(\n /app\\.asar([\\\\/])/,\n `app.asar.unpacked$1`,\n ) + path.sep + 'pty-host.js'\n : '',\n // Same dir as the compiled main bundle (dev: app/pty-host.js).\n path.join(dirname, 'pty-host.js'),\n // Defensive: a sibling unpacked dir computed from the asar path.\n path.join(dirname.replace('app.asar', 'app.asar.unpacked'), 'pty-host.js'),\n ].filter(Boolean);\n\n for (const c of candidates) {\n try {\n if (fs.existsSync(c)) return c;\n } catch {\n /* keep trying */\n }\n }\n return null;\n}\n","/**\n * Spawn-cwd normalization (Tier 1.5 companion to osc7.ts).\n *\n * Git Bash / MSYS reports `$PWD` in MSYS form (`/c/Users/me`), not native\n * Windows (`C:\\Users\\me`). The OSC-7 hook emits that raw, and `parseFileUrl`\n * only converts the drive-colon form (`/C:/...`), so an MSYS path flows through\n * unchanged. Handing `/c/Users/me` to node-pty as a working dir makes Windows\n * fail with ERROR_DIRECTORY (error code 267) — terminal creation crashes.\n *\n * Two small, OS-agnostic helpers fix this at the source:\n * - `toNativeCwd` converts an MSYS path to native Windows form (no-op on\n * POSIX, or when already native).\n * - `resolveSpawnCwd` native-converts the requested dir AND validates it,\n * falling back to the home directory so a stale/foreign/deleted cwd can\n * never crash spawn.\n *\n * Used at both spawn sites (manager.ts, pty-host.ts) and at the OSC-7 capture\n * so the persisted `live_cwd` is already a valid native path.\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport os from 'node:os';\n\n/**\n * Convert an MSYS/Git-Bash cwd to a native Windows path.\n * /c/Users/me -> C:\\Users\\me\n * /d/work -> D:\\work\n * /c -> C:\\ (bare drive root)\n * No-op on non-win32, on an empty string, or on an already-native path.\n */\nexport function toNativeCwd(p: string): string {\n if (process.platform !== 'win32' || !p) return p;\n const m = /^\\/([A-Za-z])\\/(.*)$/.exec(p);\n if (m) return `${m[1].toUpperCase()}:\\\\${m[2].replace(/\\//g, '\\\\')}`;\n const root = /^\\/([A-Za-z])\\/?$/.exec(p);\n if (root) return `${root[1].toUpperCase()}:\\\\`;\n return p;\n}\n\n/**\n * Resolve the directory a pty should actually spawn in. Prefer the requested\n * dir (native-converted); if it isn't an existing directory, fall back to the\n * home directory — so a stale, foreign, or deleted cwd can't crash spawn.\n */\nexport function resolveSpawnCwd(requested: string | undefined | null): string {\n if (requested) {\n const native = toNativeCwd(requested);\n try {\n if (existsSync(native) && statSync(native).isDirectory()) return native;\n } catch {\n /* fall through to home */\n }\n }\n return os.homedir();\n}\n","/**\n * Genie detached pty-host (Tier 3).\n *\n * A HEADLESS Node process — NO electron import — that owns the real node-pty\n * instances so they survive a full quit of the Electron app. The in-app\n * HostClient connects over a local socket (named pipe on Windows, unix domain\n * socket on POSIX) and proxies create/write/resize/kill; the host pushes back\n * `data`/`exit`. The host keeps its OWN scrollback ring buffer per pty so a\n * reattach AFTER a full quit can replay history.\n *\n * Launched detached by background.ts:\n * spawn(process.execPath, [hostScript], {\n * detached: true, stdio: 'ignore',\n * env: { ELECTRON_RUN_AS_NODE: '1', GENIE_USERDATA: <userData>, … }\n * }).unref()\n *\n * ELECTRON_RUN_AS_NODE makes Electron's binary run as plain Node so node-pty's\n * native ABI matches the one the app was built against (critical — a system Node\n * with a different ABI would fail to load the .node).\n *\n * Self-terminates after an idle period with zero live ptys AND no connected\n * client, so a host can never become a forever-orphan.\n */\n\nimport net from 'node:net';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawn, IPty } from 'node-pty';\nimport {\n encodeFrame,\n FrameDecoder,\n PROTOCOL_VERSION,\n type ClientMessage,\n type HostMessage,\n} from './host-protocol';\nimport { socketPathFor, pidfilePath } from './host-locate';\nimport { resolveSpawnCwd } from './cwd';\n\nconst SCROLLBACK_MAX = 1_000_000;\n/** Self-exit after this long with no ptys AND no connected client. */\nconst IDLE_TIMEOUT_MS = 10 * 60 * 1000;\nconst IDLE_CHECK_MS = 60 * 1000;\n\nconst userData = process.env.GENIE_USERDATA;\nif (!userData) {\n // Without a userData path we can't write a pidfile the client can find.\n process.exit(2);\n}\n\ninterface HostPty {\n pty: IPty;\n shell: string;\n scrollback: string;\n}\n\nconst ptys = new Map<string, HostPty>();\nconst clients = new Set<net.Socket>();\nlet lastActivity = Date.now();\n/** The listening server, set once startServer binds — used by graceful shutdown. */\nlet activeServer: net.Server | null = null;\n\nfunction broadcast(msg: HostMessage): void {\n const frame = encodeFrame(msg);\n for (const sock of clients) {\n try {\n sock.write(frame);\n } catch {\n /* dropped client — close handler cleans it up */\n }\n }\n}\n\nfunction createPty(opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n}): { pid: number; shell: string; existing: boolean; scrollback: string } {\n const existing = ptys.get(opts.id);\n if (existing) {\n return {\n pid: existing.pty.pid,\n shell: existing.shell,\n existing: true,\n scrollback: existing.scrollback,\n };\n }\n const shell = opts.shell ?? defaultShell();\n const env = { ...process.env, ...(opts.env ?? {}) } as Record<string, string>;\n // node-pty's `name` WINS over env.TERM (`name = opt.name || env.TERM; env.TERM\n // = name`), so `name` must carry the resolved TERM — a hardcoded\n // 'xterm-color' silently overrode this with a terminfo lacking the `Ms`\n // (OSC 52 clipboard) cap, so TUI copy-via-OSC-52 never fired. (Detached host.)\n env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: env.TERM,\n // Native-convert + validate the requested dir; a stale/foreign/MSYS cwd\n // (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with Windows\n // ERROR_DIRECTORY (267). Falls back to home if unusable.\n cwd: resolveSpawnCwd(opts.cwd),\n cols: opts.cols ?? 80,\n rows: opts.rows ?? 24,\n env,\n // Modern bundled ConPTY (conpty.dll). THIS file IS the windowless\n // detached host (spawnDetached: a `detached` console-less node.exe), so\n // it is exactly where #4 bites: legacy ConPTY allocates a stray VISIBLE\n // console per shell, and its kill path forks `conpty_console_list_agent`\n // un-hidden (console flash + \"AttachConsole failed\" crash). conpty.dll\n // runs console-less correctly and takes neither fork. Windows-only;\n // ignored elsewhere. (Mirrors InProcessBackend in manager.ts.)\n useConptyDll: true,\n });\n\n const entry: HostPty = { pty, shell, scrollback: '' };\n ptys.set(opts.id, entry);\n\n pty.onData((data) => {\n const next = entry.scrollback + data;\n entry.scrollback =\n next.length > SCROLLBACK_MAX ? next.slice(-SCROLLBACK_MAX) : next;\n broadcast({ kind: 'data', id: opts.id, data });\n });\n pty.onExit(({ exitCode, signal }) => {\n ptys.delete(opts.id);\n broadcast({ kind: 'exit', id: opts.id, exitCode, signal });\n lastActivity = Date.now();\n });\n\n return { pid: pty.pid, shell, existing: false, scrollback: '' };\n}\n\nfunction defaultShell(): string {\n if (process.platform === 'win32') return process.env.COMSPEC ?? 'cmd.exe';\n return process.env.SHELL ?? '/bin/bash';\n}\n\nfunction handleClientMessage(sock: net.Socket, msg: ClientMessage): void {\n lastActivity = Date.now();\n switch (msg.kind) {\n case 'hello':\n reply(sock, {\n kind: 'hello-ok',\n seq: msg.seq,\n protocolVersion: PROTOCOL_VERSION,\n pid: process.pid,\n });\n break;\n case 'create': {\n const r = createPty(msg.opts);\n reply(sock, {\n kind: 'created',\n seq: msg.seq,\n result: {\n id: msg.opts.id,\n pid: r.pid,\n shell: r.shell,\n existing: r.existing,\n scrollback: r.scrollback,\n },\n });\n break;\n }\n case 'write': {\n const e = ptys.get(msg.id);\n if (e) e.pty.write(msg.data);\n break;\n }\n case 'resize': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.resize(Math.max(1, msg.cols | 0), Math.max(1, msg.rows | 0));\n } catch {\n /* transient 0×0 during layout */\n }\n }\n break;\n }\n case 'kill': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n ptys.delete(msg.id);\n }\n break;\n }\n case 'list':\n reply(sock, {\n kind: 'list-result',\n seq: msg.seq,\n terminals: Array.from(ptys.entries()).map(([id, e]) => ({\n id,\n pid: e.pty.pid,\n shell: e.shell,\n })),\n });\n break;\n case 'set-retained':\n // The host keeps EVERYTHING alive across quit regardless; the\n // retained flag is meaningful to the client (fallback/UX). The host\n // only needs to not-die, which it doesn't. Acknowledge by no-op.\n break;\n case 'get-scrollback':\n reply(sock, {\n kind: 'scrollback-result',\n seq: msg.seq,\n scrollback: ptys.get(msg.id)?.scrollback ?? null,\n });\n break;\n case 'ping':\n reply(sock, { kind: 'pong', seq: msg.seq });\n break;\n case 'shutdown':\n // Graceful teardown: ack first so the client can finalize (it has\n // already snapshotted from its mirror), then kill every pty, remove\n // the pidfile/socket, and exit promptly. This is the clean path the\n // SIGKILL-by-pidfile interim fix can't take — it lets a consumer\n // bring the host down deterministically (e.g. before an auto-update)\n // instead of waiting on the 10-min idle timeout.\n reply(sock, { kind: 'shutdown-ok', seq: msg.seq });\n shutdown();\n break;\n }\n}\n\nfunction reply(sock: net.Socket, msg: HostMessage): void {\n try {\n sock.write(encodeFrame(msg));\n } catch {\n /* client gone */\n }\n}\n\nfunction startServer(socketPath: string): void {\n // On POSIX a stale socket file blocks bind; remove it first. (On Windows the\n // pipe namespace handles this.)\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n try {\n fs.mkdirSync(path.dirname(socketPath), { recursive: true });\n } catch {\n /* ignore */\n }\n }\n\n const server = net.createServer((sock) => {\n clients.add(sock);\n lastActivity = Date.now();\n const decoder = new FrameDecoder();\n sock.on('data', (chunk: Buffer) => {\n const frames = decoder.push(chunk);\n if (decoder.desynced) {\n try {\n sock.destroy();\n } catch {\n /* ignore */\n }\n return;\n }\n for (const f of frames) handleClientMessage(sock, f as ClientMessage);\n });\n const drop = () => {\n clients.delete(sock);\n lastActivity = Date.now();\n };\n sock.on('close', drop);\n sock.on('error', drop);\n });\n activeServer = server;\n\n server.on('error', (err) => {\n // EADDRINUSE: another host beat us to it. Exit quietly — the client will\n // connect to the winner.\n // eslint-disable-next-line no-console\n console.error('[pty-host] server error:', (err as Error).message);\n process.exit(3);\n });\n\n server.listen(socketPath, () => {\n try {\n writePidfileLocal(socketPath);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[pty-host] pidfile write failed:', (err as Error).message);\n }\n });\n\n // Idle watchdog: exit when nothing is running and nobody is connected.\n const idle = setInterval(() => {\n if (ptys.size === 0 && clients.size === 0 && Date.now() - lastActivity > IDLE_TIMEOUT_MS) {\n cleanupAndExit(socketPath, server);\n }\n }, IDLE_CHECK_MS);\n if (typeof idle.unref === 'function') idle.unref();\n}\n\nfunction writePidfileLocal(socketPath: string): void {\n const target = pidfilePath(userData!);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(\n tmp,\n JSON.stringify({\n pid: process.pid,\n socketPath,\n protocolVersion: PROTOCOL_VERSION,\n startedAt: Date.now(),\n }),\n );\n fs.renameSync(tmp, target);\n}\n\nfunction cleanupAndExit(socketPath: string, server: net.Server): void {\n try {\n // Only remove the pidfile if it still points at US (avoid clobbering a\n // successor host that took over the socket).\n const pf = JSON.parse(fs.readFileSync(pidfilePath(userData!), 'utf8'));\n if (pf?.pid === process.pid) fs.rmSync(pidfilePath(userData!), { force: true });\n } catch {\n /* ignore */\n }\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n }\n try {\n server.close();\n } catch {\n /* ignore */\n }\n process.exit(0);\n}\n\n/**\n * Graceful host shutdown — the clean counterpart to the idle watchdog and to a\n * SIGKILL-by-pidfile. Kills every live pty (so no orphaned shells linger), then\n * removes the pidfile/socket, closes the server, and exits. Triggered by a\n * `shutdown` client request; the client has already snapshotted its mirror, so\n * tearing down the host here loses nothing.\n */\nfunction shutdown(): void {\n for (const [, e] of ptys) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n }\n ptys.clear();\n if (activeServer) {\n cleanupAndExit(socketPath, activeServer);\n } else {\n process.exit(0);\n }\n}\n\n// --- main ------------------------------------------------------------------\n\nconst socketPath = socketPathFor(userData);\n\n// A dead-mans-switch so we don't keep a host with no shells AND no client when\n// the parent vanished without a clean disconnect: covered by the idle watchdog.\nprocess.on('uncaughtException', (err) => {\n // eslint-disable-next-line no-console\n console.error('[pty-host] uncaught:', err);\n});\n\nstartServer(socketPath);\n"]}
1
+ {"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts","../src/cwd.ts","../src/pty-host.ts"],"names":["os","crypto","path","existsSync","statSync","spawn","socketPath","fs","net"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBO,IAAM,gBAAA,GAAmB,CAAA;AAsDhC,IAAM,YAAA,GAAe,CAAA;AAGd,SAAS,YAAY,GAAA,EAAoB;AAC5C,EAAA,MAAM,OAAO,MAAA,CAAO,IAAA,CAAK,KAAK,SAAA,CAAU,GAAG,GAAG,MAAM,CAAA;AACpD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,WAAA,CAAY,YAAY,CAAA;AAC9C,EAAA,MAAA,CAAO,aAAA,CAAc,IAAA,CAAK,MAAA,EAAQ,CAAC,CAAA;AACnC,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,CAAC,MAAA,EAAQ,IAAI,CAAC,CAAA;AACvC;AAYO,IAAM,aAAA,GAAN,MAAM,aAAA,CAAa;AAAA,EAAnB,WAAA,GAAA;AACH,IAAA,IAAA,CAAQ,MAAA,GAAiB,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AASvC;AAAA;AAAA,IAAA,IAAA,CAAA,QAAA,GAAW,KAAA;AAAA,EAAA;AAAA,EAEX,KAAK,KAAA,EAAwB;AACzB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,CAAC,IAAA,CAAK,MAAA,EAAQ,KAAK,CAAC,CAAA,GAAI,KAAA;AACzE,IAAA,MAAM,MAAe,EAAC;AACtB,IAAA,WAAS;AACL,MAAA,IAAI,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,YAAA,EAAc;AACvC,MAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,CAAO,YAAA,CAAa,CAAC,CAAA;AACtC,MAAA,IAAI,GAAA,GAAM,cAAa,SAAA,EAAW;AAG9B,QAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,QAAA,IAAA,CAAK,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA;AAC5B,QAAA;AAAA,MACJ;AACA,MAAA,IAAI,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,YAAA,GAAe,GAAA,EAAK;AAC7C,MAAA,MAAM,OAAO,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,YAAA,EAAc,eAAe,GAAG,CAAA;AAClE,MAAA,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,eAAe,GAAG,CAAA;AACrD,MAAA,IAAI;AACA,QAAA,GAAA,CAAI,KAAK,IAAA,CAAK,KAAA,CAAM,KAAK,QAAA,CAAS,MAAM,CAAC,CAAU,CAAA;AAAA,MACvD,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACJ;AACA,IAAA,OAAO,GAAA;AAAA,EACX;AACJ,CAAA;AAAA;AAAA;AAAA;AApCa,aAAA,CAMO,SAAA,GAAY,KAAK,IAAA,GAAO,IAAA;AANrC,IAAM,YAAA,GAAN,aAAA;AC1EA,SAAS,QAAA,GAAmB;AAC/B,EAAA,MAAM,IAAA,GAAO,GAAGA,mBAAA,CAAG,QAAA,GAAW,QAAQ,CAAA,CAAA,EAAIA,mBAAA,CAAG,QAAA,EAAU,CAAA,CAAA;AACvD,EAAA,OAAOC,uBAAA,CAAO,UAAA,CAAW,MAAM,CAAA,CAAE,MAAA,CAAO,IAAI,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAC3E;AAUO,SAAS,cAAc,WAAA,EAA6B;AACvD,EAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAC9B,IAAA,OAAO,CAAA,2BAAA,EAA8B,UAAU,CAAA,CAAA;AAAA,EACnD;AAIA,EAAA,MAAM,SAAA,GAAYC,qBAAA,CAAK,IAAA,CAAK,WAAA,EAAa,cAAc,CAAA;AACvD,EAAA,IAAI,SAAA,CAAU,MAAA,GAAS,GAAA,EAAK,OAAO,SAAA;AACnC,EAAA,OAAOA,qBAAA,CAAK,KAAKF,mBAAA,CAAG,MAAA,IAAU,CAAA,cAAA,EAAiB,QAAA,EAAU,CAAA,KAAA,CAAO,CAAA;AACpE;AAEO,SAAS,YAAY,WAAA,EAA6B;AACrD,EAAA,OAAOE,qBAAA,CAAK,IAAA,CAAK,WAAA,EAAa,cAAc,CAAA;AAChD;ACpBO,SAAS,YAAY,CAAA,EAAmB;AAC3C,EAAA,IAAI,OAAA,CAAQ,QAAA,KAAa,OAAA,IAAW,CAAC,GAAG,OAAO,CAAA;AAC/C,EAAA,MAAM,CAAA,GAAI,sBAAA,CAAuB,IAAA,CAAK,CAAC,CAAA;AACvC,EAAA,IAAI,CAAA,EAAG,OAAO,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,WAAA,EAAa,CAAA,GAAA,EAAM,EAAE,CAAC,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,IAAI,CAAC,CAAA,CAAA;AAClE,EAAA,MAAM,IAAA,GAAO,mBAAA,CAAoB,IAAA,CAAK,CAAC,CAAA;AACvC,EAAA,IAAI,MAAM,OAAO,CAAA,EAAG,KAAK,CAAC,CAAA,CAAE,aAAa,CAAA,GAAA,CAAA;AACzC,EAAA,OAAO,CAAA;AACX;AAOO,SAAS,gBAAgB,SAAA,EAA8C;AAC1E,EAAA,IAAI,SAAA,EAAW;AACX,IAAA,MAAM,MAAA,GAAS,YAAY,SAAS,CAAA;AACpC,IAAA,IAAI;AACA,MAAA,IAAIC,cAAA,CAAW,MAAM,CAAA,IAAKC,YAAA,CAAS,MAAM,CAAA,CAAE,WAAA,IAAe,OAAO,MAAA;AAAA,IACrE,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACJ;AACA,EAAA,OAAOJ,oBAAG,OAAA,EAAQ;AACtB;;;AChBA,IAAM,cAAA,GAAiB,GAAA;AAEvB,IAAM,eAAA,GAAkB,KAAK,EAAA,GAAK,GAAA;AAClC,IAAM,gBAAgB,EAAA,GAAK,GAAA;AAE3B,IAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,cAAA;AAC7B,IAAI,CAAC,QAAA,EAAU;AAEX,EAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAClB;AAQA,IAAM,IAAA,uBAAW,GAAA,EAAqB;AACtC,IAAM,OAAA,uBAAc,GAAA,EAAgB;AACpC,IAAI,YAAA,GAAe,KAAK,GAAA,EAAI;AAE5B,IAAI,YAAA,GAAkC,IAAA;AAEtC,SAAS,UAAU,GAAA,EAAwB;AACvC,EAAA,MAAM,KAAA,GAAQ,YAAY,GAAG,CAAA;AAC7B,EAAA,KAAA,MAAW,QAAQ,OAAA,EAAS;AACxB,IAAA,IAAI;AACA,MAAA,IAAA,CAAK,MAAM,KAAK,CAAA;AAAA,IACpB,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AAEA,SAAS,UAAU,IAAA,EAQuD;AACtE,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,EAAE,CAAA;AACjC,EAAA,IAAI,QAAA,EAAU;AACV,IAAA,OAAO;AAAA,MACH,GAAA,EAAK,SAAS,GAAA,CAAI,GAAA;AAAA,MAClB,OAAO,QAAA,CAAS,KAAA;AAAA,MAChB,QAAA,EAAU,IAAA;AAAA,MACV,YAAY,QAAA,CAAS;AAAA,KACzB;AAAA,EACJ;AACA,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,YAAA,EAAa;AACzC,EAAA,MAAM,GAAA,GAAM,EAAE,GAAG,OAAA,CAAQ,KAAK,GAAI,IAAA,CAAK,GAAA,IAAO,EAAC,EAAG;AAKlD,EAAA,GAAA,CAAI,IAAA,GAAO,IAAI,IAAA,IAAQ,gBAAA;AAEvB,EAAA,MAAM,MAAMK,aAAA,CAAM,KAAA,EAAO,IAAA,CAAK,IAAA,IAAQ,EAAC,EAAG;AAAA,IACtC,MAAM,GAAA,CAAI,IAAA;AAAA;AAAA;AAAA;AAAA,IAIV,GAAA,EAAK,eAAA,CAAgB,IAAA,CAAK,GAAG,CAAA;AAAA,IAC7B,IAAA,EAAM,KAAK,IAAA,IAAQ,EAAA;AAAA,IACnB,IAAA,EAAM,KAAK,IAAA,IAAQ,EAAA;AAAA,IACnB,GAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,YAAA,EAAc;AAAA,GACjB,CAAA;AAED,EAAA,MAAM,KAAA,GAAiB,EAAE,GAAA,EAAK,KAAA,EAAO,YAAY,EAAA,EAAG;AACpD,EAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,EAAA,EAAI,KAAK,CAAA;AAEvB,EAAA,GAAA,CAAI,MAAA,CAAO,CAAC,IAAA,KAAS;AACjB,IAAA,MAAM,IAAA,GAAO,MAAM,UAAA,GAAa,IAAA;AAChC,IAAA,KAAA,CAAM,UAAA,GACF,KAAK,MAAA,GAAS,cAAA,GAAiB,KAAK,KAAA,CAAM,CAAC,cAAc,CAAA,GAAI,IAAA;AACjE,IAAA,SAAA,CAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAI,IAAA,CAAK,EAAA,EAAI,MAAM,CAAA;AAAA,EACjD,CAAC,CAAA;AACD,EAAA,GAAA,CAAI,MAAA,CAAO,CAAC,EAAE,QAAA,EAAU,QAAO,KAAM;AACjC,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,EAAE,CAAA;AACnB,IAAA,SAAA,CAAU,EAAE,MAAM,MAAA,EAAQ,EAAA,EAAI,KAAK,EAAA,EAAI,QAAA,EAAU,QAAQ,CAAA;AACzD,IAAA,YAAA,GAAe,KAAK,GAAA,EAAI;AAAA,EAC5B,CAAC,CAAA;AAED,EAAA,OAAO,EAAE,KAAK,GAAA,CAAI,GAAA,EAAK,OAAO,QAAA,EAAU,KAAA,EAAO,YAAY,EAAA,EAAG;AAClE;AAEA,SAAS,YAAA,GAAuB;AAC5B,EAAA,IAAI,QAAQ,QAAA,KAAa,OAAA,EAAS,OAAO,OAAA,CAAQ,IAAI,OAAA,IAAW,SAAA;AAChE,EAAA,OAAO,OAAA,CAAQ,IAAI,KAAA,IAAS,WAAA;AAChC;AAEA,SAAS,mBAAA,CAAoB,MAAkB,GAAA,EAA0B;AACrE,EAAA,YAAA,GAAe,KAAK,GAAA,EAAI;AACxB,EAAA,QAAQ,IAAI,IAAA;AAAM,IACd,KAAK,OAAA;AACD,MAAA,KAAA,CAAM,IAAA,EAAM;AAAA,QACR,IAAA,EAAM,UAAA;AAAA,QACN,KAAK,GAAA,CAAI,GAAA;AAAA,QACT,eAAA,EAAiB,gBAAA;AAAA,QACjB,KAAK,OAAA,CAAQ;AAAA,OAChB,CAAA;AACD,MAAA;AAAA,IACJ,KAAK,QAAA,EAAU;AACX,MAAA,MAAM,CAAA,GAAI,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAC5B,MAAA,KAAA,CAAM,IAAA,EAAM;AAAA,QACR,IAAA,EAAM,SAAA;AAAA,QACN,KAAK,GAAA,CAAI,GAAA;AAAA,QACT,MAAA,EAAQ;AAAA,UACJ,EAAA,EAAI,IAAI,IAAA,CAAK,EAAA;AAAA,UACb,KAAK,CAAA,CAAE,GAAA;AAAA,UACP,OAAO,CAAA,CAAE,KAAA;AAAA,UACT,UAAU,CAAA,CAAE,QAAA;AAAA,UACZ,YAAY,CAAA,CAAE;AAAA;AAClB,OACH,CAAA;AACD,MAAA;AAAA,IACJ;AAAA,IACA,KAAK,OAAA,EAAS;AACV,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA;AACzB,MAAA,IAAI,CAAA,EAAG,CAAA,CAAE,GAAA,CAAI,KAAA,CAAM,IAAI,IAAI,CAAA;AAC3B,MAAA;AAAA,IACJ;AAAA,IACA,KAAK,QAAA,EAAU;AACX,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA;AACzB,MAAA,IAAI,CAAA,EAAG;AACH,QAAA,IAAI;AACA,UAAA,CAAA,CAAE,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,GAAG,GAAA,CAAI,IAAA,GAAO,CAAC,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,IAAA,GAAO,CAAC,CAAC,CAAA;AAAA,QACrE,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACJ;AACA,MAAA;AAAA,IACJ;AAAA,IACA,KAAK,MAAA,EAAQ;AACT,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,EAAE,CAAA;AACzB,MAAA,IAAI,CAAA,EAAG;AACH,QAAA,IAAI;AACA,UAAA,CAAA,CAAE,IAAI,IAAA,EAAK;AAAA,QACf,CAAA,CAAA,MAAQ;AAAA,QAER;AACA,QAAA,IAAA,CAAK,MAAA,CAAO,IAAI,EAAE,CAAA;AAAA,MACtB;AACA,MAAA;AAAA,IACJ;AAAA,IACA,KAAK,MAAA;AACD,MAAA,KAAA,CAAM,IAAA,EAAM;AAAA,QACR,IAAA,EAAM,aAAA;AAAA,QACN,KAAK,GAAA,CAAI,GAAA;AAAA,QACT,SAAA,EAAW,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,EAAA,EAAI,CAAC,CAAA,MAAO;AAAA,UACpD,EAAA;AAAA,UACA,GAAA,EAAK,EAAE,GAAA,CAAI,GAAA;AAAA,UACX,OAAO,CAAA,CAAE;AAAA,SACb,CAAE;AAAA,OACL,CAAA;AACD,MAAA;AAAA,IACJ,KAAK,cAAA;AAID,MAAA;AAAA,IACJ,KAAK,gBAAA;AACD,MAAA,KAAA,CAAM,IAAA,EAAM;AAAA,QACR,IAAA,EAAM,mBAAA;AAAA,QACN,KAAK,GAAA,CAAI,GAAA;AAAA,QACT,YAAY,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,EAAE,GAAG,UAAA,IAAc;AAAA,OAC/C,CAAA;AACD,MAAA;AAAA,IACJ,KAAK,MAAA;AACD,MAAA,KAAA,CAAM,MAAM,EAAE,IAAA,EAAM,QAAQ,GAAA,EAAK,GAAA,CAAI,KAAK,CAAA;AAC1C,MAAA;AAAA,IACJ,KAAK,UAAA;AAOD,MAAA,KAAA,CAAM,MAAM,EAAE,IAAA,EAAM,eAAe,GAAA,EAAK,GAAA,CAAI,KAAK,CAAA;AACjD,MAAA,QAAA,EAAS;AACT,MAAA;AAAA;AAEZ;AAEA,SAAS,KAAA,CAAM,MAAkB,GAAA,EAAwB;AACrD,EAAA,IAAI;AACA,IAAA,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,EAC/B,CAAA,CAAA,MAAQ;AAAA,EAER;AACJ;AAEA,SAAS,YAAYC,WAAAA,EAA0B;AAG3C,EAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAC9B,IAAA,IAAI;AACA,MAAAC,qBAAG,MAAA,CAAOD,WAAAA,EAAY,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,IACzC,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,IAAI;AACA,MAAAC,oBAAAA,CAAG,UAAUL,qBAAAA,CAAK,OAAA,CAAQI,WAAU,CAAA,EAAG,EAAE,SAAA,EAAW,IAAA,EAAM,CAAA;AAAA,IAC9D,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACJ;AAEA,EAAA,MAAM,MAAA,GAASE,oBAAA,CAAI,YAAA,CAAa,CAAC,IAAA,KAAS;AACtC,IAAA,OAAA,CAAQ,IAAI,IAAI,CAAA;AAChB,IAAA,YAAA,GAAe,KAAK,GAAA,EAAI;AACxB,IAAA,MAAM,OAAA,GAAU,IAAI,YAAA,EAAa;AACjC,IAAA,IAAA,CAAK,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AAC/B,MAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA;AACjC,MAAA,IAAI,QAAQ,QAAA,EAAU;AAClB,QAAA,IAAI;AACA,UAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,QACjB,CAAA,CAAA,MAAQ;AAAA,QAER;AACA,QAAA;AAAA,MACJ;AACA,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,mBAAA,CAAoB,IAAA,EAAM,CAAkB,CAAA;AAAA,IACxE,CAAC,CAAA;AACD,IAAA,MAAM,OAAO,MAAM;AACf,MAAA,OAAA,CAAQ,OAAO,IAAI,CAAA;AACnB,MAAA,YAAA,GAAe,KAAK,GAAA,EAAI;AAAA,IAC5B,CAAA;AACA,IAAA,IAAA,CAAK,EAAA,CAAG,SAAS,IAAI,CAAA;AACrB,IAAA,IAAA,CAAK,EAAA,CAAG,SAAS,IAAI,CAAA;AAAA,EACzB,CAAC,CAAA;AACD,EAAA,YAAA,GAAe,MAAA;AAEf,EAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAQ;AAIxB,IAAA,OAAA,CAAQ,KAAA,CAAM,0BAAA,EAA6B,GAAA,CAAc,OAAO,CAAA;AAChE,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAClB,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,MAAA,CAAOF,aAAY,MAAM;AAC5B,IAAA,IAAI;AACA,MAAA,iBAAA,CAAkBA,WAAU,CAAA;AAAA,IAChC,SAAS,GAAA,EAAK;AAEV,MAAA,OAAA,CAAQ,KAAA,CAAM,kCAAA,EAAqC,GAAA,CAAc,OAAO,CAAA;AAAA,IAC5E;AAAA,EACJ,CAAC,CAAA;AAGD,EAAA,MAAM,IAAA,GAAO,YAAY,MAAM;AAC3B,IAAA,IAAI,IAAA,CAAK,IAAA,KAAS,CAAA,IAAK,OAAA,CAAQ,IAAA,KAAS,KAAK,IAAA,CAAK,GAAA,EAAI,GAAI,YAAA,GAAe,eAAA,EAAiB;AACtF,MAAA,cAAA,CAAeA,aAAY,MAAM,CAAA;AAAA,IACrC;AAAA,EACJ,GAAG,aAAa,CAAA;AAChB,EAAA,IAAI,OAAO,IAAA,CAAK,KAAA,KAAU,UAAA,OAAiB,KAAA,EAAM;AACrD;AAEA,SAAS,kBAAkBA,WAAAA,EAA0B;AACjD,EAAA,MAAM,MAAA,GAAS,YAAY,QAAS,CAAA;AACpC,EAAA,MAAM,GAAA,GAAM,GAAG,MAAM,CAAA,IAAA,CAAA;AACrB,EAAAC,oBAAAA,CAAG,aAAA;AAAA,IACC,GAAA;AAAA,IACA,KAAK,SAAA,CAAU;AAAA,MACX,KAAK,OAAA,CAAQ,GAAA;AAAA,MACb,UAAA,EAAAD,WAAAA;AAAA,MACA,eAAA,EAAiB,gBAAA;AAAA,MACjB,SAAA,EAAW,KAAK,GAAA;AAAI,KACvB;AAAA,GACL;AACA,EAAAC,oBAAAA,CAAG,UAAA,CAAW,GAAA,EAAK,MAAM,CAAA;AAC7B;AAEA,SAAS,cAAA,CAAeD,aAAoB,MAAA,EAA0B;AAClE,EAAA,IAAI;AAGA,IAAA,MAAM,EAAA,GAAK,KAAK,KAAA,CAAMC,oBAAAA,CAAG,aAAa,WAAA,CAAY,QAAS,CAAA,EAAG,MAAM,CAAC,CAAA;AACrE,IAAA,IAAI,EAAA,EAAI,GAAA,KAAQ,OAAA,CAAQ,GAAA,EAAKA,oBAAAA,CAAG,MAAA,CAAO,WAAA,CAAY,QAAS,CAAA,EAAG,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,EAClF,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAC9B,IAAA,IAAI;AACA,MAAAA,qBAAG,MAAA,CAAOD,WAAAA,EAAY,EAAE,KAAA,EAAO,MAAM,CAAA;AAAA,IACzC,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACJ;AACA,EAAA,IAAI;AACA,IAAA,MAAA,CAAO,KAAA,EAAM;AAAA,EACjB,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAClB;AASA,SAAS,QAAA,GAAiB;AACtB,EAAA,KAAA,MAAW,GAAG,CAAC,CAAA,IAAK,IAAA,EAAM;AACtB,IAAA,IAAI;AACA,MAAA,CAAA,CAAE,IAAI,IAAA,EAAK;AAAA,IACf,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACJ;AACA,EAAA,IAAA,CAAK,KAAA,EAAM;AACX,EAAA,IAAI,YAAA,EAAc;AACd,IAAA,cAAA,CAAe,YAAY,YAAY,CAAA;AAAA,EAC3C,CAAA,MAAO;AACH,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAClB;AACJ;AAIA,IAAM,UAAA,GAAa,cAAc,QAAQ,CAAA;AAIzC,OAAA,CAAQ,EAAA,CAAG,mBAAA,EAAqB,CAAC,GAAA,KAAQ;AAErC,EAAA,OAAA,CAAQ,KAAA,CAAM,wBAAwB,GAAG,CAAA;AAC7C,CAAC,CAAA;AAED,WAAA,CAAY,UAAU,CAAA","file":"pty-host.cjs","sourcesContent":["/**\n * Pty-host wire protocol (Tier 3).\n *\n * The detached pty-host (main/terminal/pty-host.ts) and the in-app HostClient\n * (main/terminal/host-client.ts) talk over a local IPC transport — a named pipe\n * on Windows, a unix domain socket on POSIX — using a tiny length-prefixed JSON\n * framing so there's no heavy dependency. This module is PURE (no electron, no\n * node-pty, no net): just the message shapes + the encode/decode for the framing,\n * so it can be imported by both ends AND unit-tested in isolation.\n *\n * Framing: each message is `[4-byte big-endian uint32 length][utf8 JSON body]`.\n * The length prefix is the byte length of the JSON body. A FrameDecoder buffers\n * partial reads and yields whole messages as they complete — TCP/pipe streams\n * don't preserve message boundaries, so we can't assume one `data` event == one\n * message.\n */\n\n/**\n * Protocol version. Bumped whenever the message shapes change in a way that\n * makes an old host incompatible with a new client (or vice-versa). The client\n * refuses to attach to a host whose pidfile reports a different version and\n * spawns a fresh host instead — see host-client.ts connect-or-spawn.\n */\nexport const PROTOCOL_VERSION = 2;\n\n/** Requests the client sends to the host. `seq` correlates a reply. */\nexport type ClientMessage =\n | { kind: 'hello'; seq: number; protocolVersion: number }\n | {\n kind: 'create';\n seq: number;\n opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n };\n }\n | { kind: 'write'; id: string; data: string }\n | { kind: 'resize'; id: string; cols: number; rows: number }\n | { kind: 'kill'; id: string }\n | { kind: 'list'; seq: number }\n | { kind: 'set-retained'; id: string; retained: boolean }\n | { kind: 'get-scrollback'; seq: number; id: string }\n | { kind: 'ping'; seq: number }\n | { kind: 'shutdown'; seq: number };\n\n/** Pushes + replies the host sends to the client. */\nexport type HostMessage =\n | { kind: 'hello-ok'; seq: number; protocolVersion: number; pid: number }\n | {\n kind: 'created';\n seq: number;\n result: {\n id: string;\n pid: number;\n shell: string;\n existing: boolean;\n scrollback: string;\n };\n }\n | {\n kind: 'list-result';\n seq: number;\n terminals: Array<{ id: string; pid: number; shell: string }>;\n }\n | { kind: 'scrollback-result'; seq: number; scrollback: string | null }\n | { kind: 'pong'; seq: number }\n | { kind: 'shutdown-ok'; seq: number }\n | { kind: 'data'; id: string; data: string }\n | { kind: 'exit'; id: string; exitCode: number; signal?: number };\n\nexport type Frame = ClientMessage | HostMessage;\n\nconst LENGTH_BYTES = 4;\n\n/** Encode a message as a length-prefixed JSON frame ready for the socket. */\nexport function encodeFrame(msg: Frame): Buffer {\n const body = Buffer.from(JSON.stringify(msg), 'utf8');\n const header = Buffer.allocUnsafe(LENGTH_BYTES);\n header.writeUInt32BE(body.length, 0);\n return Buffer.concat([header, body]);\n}\n\n/**\n * Streaming frame decoder. Feed it raw socket chunks via `push`; it returns the\n * complete messages that became available (zero or more), buffering any partial\n * tail until the rest arrives. One decoder per socket.\n *\n * Resilient by design: a malformed JSON body is skipped (the frame is consumed\n * but yields nothing) rather than throwing — a corrupt frame must not wedge the\n * whole stream. An absurd length prefix (> MAX_FRAME) is treated as a desync and\n * the buffer is reset; the caller can decide whether to drop the connection.\n */\nexport class FrameDecoder {\n private buffer: Buffer = Buffer.alloc(0);\n\n /** Hard cap on a single frame (16 MB). Guards against a runaway/garbage\n * length prefix allocating unbounded memory. node-pty data chunks are tiny;\n * a serialized scrollback is bounded well under this. */\n static readonly MAX_FRAME = 16 * 1024 * 1024;\n\n /** True when the last push hit an oversized/desynced frame. The caller\n * should drop the connection — the stream can't be trusted to realign. */\n desynced = false;\n\n push(chunk: Buffer): Frame[] {\n this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;\n const out: Frame[] = [];\n for (;;) {\n if (this.buffer.length < LENGTH_BYTES) break;\n const len = this.buffer.readUInt32BE(0);\n if (len > FrameDecoder.MAX_FRAME) {\n // Desync / garbage. Reset and flag — realigning a length-prefixed\n // stream after a bad prefix isn't possible without a sentinel.\n this.desynced = true;\n this.buffer = Buffer.alloc(0);\n break;\n }\n if (this.buffer.length < LENGTH_BYTES + len) break; // wait for more\n const body = this.buffer.subarray(LENGTH_BYTES, LENGTH_BYTES + len);\n this.buffer = this.buffer.subarray(LENGTH_BYTES + len);\n try {\n out.push(JSON.parse(body.toString('utf8')) as Frame);\n } catch {\n /* skip a corrupt frame; the framing itself is still aligned */\n }\n }\n return out;\n }\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\nimport crypto from 'node:crypto';\nimport { PROTOCOL_VERSION } from './host-protocol';\n\n/**\n * Path + pidfile resolution for the detached pty-host (Tier 3).\n *\n * Kept ELECTRON-FREE on the resolution side that the host itself uses (the host\n * is a plain node process — no `app`), so the userData path is passed IN. The\n * in-app side (host-client lifecycle) imports `app` separately and feeds it here.\n */\n\nexport interface Pidfile {\n pid: number;\n socketPath: string;\n protocolVersion: number;\n startedAt: number;\n}\n\n/** Short, stable per-user hash so two OS users don't collide on the Windows\n * pipe name (the pipe namespace is machine-global). */\nexport function userHash(): string {\n const seed = `${os.userInfo().username}|${os.hostname()}`;\n return crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);\n}\n\n/**\n * The local IPC transport address.\n * • Windows: a named pipe `\\\\.\\pipe\\genie-ptyhost-<userhash>`. The default\n * Windows pipe ACL is per-logon-session, so another user on the same machine\n * can't open it — that's our ACL. (Documented; we don't tighten further.)\n * • POSIX: a unix domain socket under userData (preferred — survives /tmp\n * cleaners and is per-user by directory perms) named `ptyhost.sock`.\n */\nexport function socketPathFor(userDataDir: string): string {\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\genie-ptyhost-${userHash()}`;\n }\n // Keep the path short — unix socket paths have a ~104-char limit. userData is\n // typically well under that; fall back to os.tmpdir() if it's pathologically\n // long.\n const candidate = path.join(userDataDir, 'ptyhost.sock');\n if (candidate.length < 100) return candidate;\n return path.join(os.tmpdir(), `genie-ptyhost-${userHash()}.sock`);\n}\n\nexport function pidfilePath(userDataDir: string): string {\n return path.join(userDataDir, 'ptyhost.json');\n}\n\nexport function writePidfile(userDataDir: string, pf: Pidfile): void {\n const target = pidfilePath(userDataDir);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(pf));\n fs.renameSync(tmp, target);\n}\n\nexport function readPidfile(userDataDir: string): Pidfile | null {\n try {\n const raw = fs.readFileSync(pidfilePath(userDataDir), 'utf8');\n const pf = JSON.parse(raw) as Pidfile;\n if (\n typeof pf.pid !== 'number' ||\n typeof pf.socketPath !== 'string' ||\n typeof pf.protocolVersion !== 'number'\n ) {\n return null;\n }\n return pf;\n } catch {\n return null;\n }\n}\n\nexport function deletePidfile(userDataDir: string): void {\n try {\n fs.rmSync(pidfilePath(userDataDir), { force: true });\n } catch {\n /* ignore */\n }\n}\n\n/** True when a process with `pid` is alive (signal 0 probes without killing). */\nexport function isPidAlive(pid: number): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = exists but not ours (still \"alive\"); ESRCH = gone.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Terminate a stale/wedged host process so its SINGLE-INSTANCE transport (the\n * Windows named pipe / POSIX socket) frees up for a fresh host to bind. A wedged\n * incumbent that still owns the pipe would otherwise EADDRINUSE every respawn and\n * silently swallow every client `hello` — see the connect-or-spawn recovery in\n * host-lifecycle (#8).\n *\n * Best-effort — never throws. Refuses `pid <= 0` and our OWN pid (so a caller\n * that reads its own pidfile can't self-destruct). On Windows `process.kill`\n * maps to TerminateProcess (immediate); on POSIX the default SIGTERM disposition\n * exits the host promptly (it installs no handler). Returns true when a signal\n * was actually delivered.\n */\nexport function terminateHost(\n pid: number,\n signal: NodeJS.Signals = 'SIGTERM',\n): boolean {\n if (!pid || pid <= 0 || pid === process.pid) return false;\n try {\n process.kill(pid, signal);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve once `pid` is gone (polling up to `timeoutMs`). Called after\n * {@link terminateHost} so a freshly-spawned host doesn't race the reaped one\n * for the single-instance transport. Resolves true when the pid is no longer\n * alive, false if it outlives the timeout.\n */\nexport async function awaitPidGone(pid: number, timeoutMs = 2000): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isPidAlive(pid)) return true;\n await new Promise((r) => setTimeout(r, 50));\n }\n return !isPidAlive(pid);\n}\n\n/**\n * Decide whether an existing pidfile points at a usable host.\n * Usable = pid alive AND protocol versions match. A stale/dead/mismatched\n * pidfile means we must spawn a fresh host.\n */\nexport function pidfileUsable(pf: Pidfile | null): boolean {\n if (!pf) return false;\n if (pf.protocolVersion !== PROTOCOL_VERSION) return false;\n if (!isPidAlive(pf.pid)) return false;\n return true;\n}\n\n/**\n * Resolve the compiled pty-host script on disk, trying multiple candidate paths\n * so it works in BOTH `npm run dev` (script at app/pty-host.js next to\n * background.js) AND a packaged asar build. node-pty's native binding can't load\n * from inside an asar, so the host (which requires node-pty) must run UNPACKED —\n * `app.asar.unpacked/...`. We try the unpacked path first, then the in-asar path,\n * then a dev-relative path. Returns the first that exists, or null.\n *\n * `dirname` is main/background's __dirname (the directory the compiled main\n * bundle lives in). The host script is emitted alongside it as `pty-host.js`.\n */\nexport function resolveHostScript(dirname: string): string | null {\n const candidates = [\n // Packaged: node-pty must be unpacked, so run the host from the unpacked\n // tree too (its require('node-pty') resolves to the unpacked .node).\n dirname.includes(`app.asar${path.sep}`) || dirname.includes('app.asar/')\n ? dirname.replace(\n /app\\.asar([\\\\/])/,\n `app.asar.unpacked$1`,\n ) + path.sep + 'pty-host.js'\n : '',\n // Same dir as the compiled main bundle (dev: app/pty-host.js).\n path.join(dirname, 'pty-host.js'),\n // Defensive: a sibling unpacked dir computed from the asar path.\n path.join(dirname.replace('app.asar', 'app.asar.unpacked'), 'pty-host.js'),\n ].filter(Boolean);\n\n for (const c of candidates) {\n try {\n if (fs.existsSync(c)) return c;\n } catch {\n /* keep trying */\n }\n }\n return null;\n}\n","/**\n * Spawn-cwd normalization (Tier 1.5 companion to osc7.ts).\n *\n * Git Bash / MSYS reports `$PWD` in MSYS form (`/c/Users/me`), not native\n * Windows (`C:\\Users\\me`). The OSC-7 hook emits that raw, and `parseFileUrl`\n * only converts the drive-colon form (`/C:/...`), so an MSYS path flows through\n * unchanged. Handing `/c/Users/me` to node-pty as a working dir makes Windows\n * fail with ERROR_DIRECTORY (error code 267) — terminal creation crashes.\n *\n * Two small, OS-agnostic helpers fix this at the source:\n * - `toNativeCwd` converts an MSYS path to native Windows form (no-op on\n * POSIX, or when already native).\n * - `resolveSpawnCwd` native-converts the requested dir AND validates it,\n * falling back to the home directory so a stale/foreign/deleted cwd can\n * never crash spawn.\n *\n * Used at both spawn sites (manager.ts, pty-host.ts) and at the OSC-7 capture\n * so the persisted `live_cwd` is already a valid native path.\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport os from 'node:os';\n\n/**\n * Convert an MSYS/Git-Bash cwd to a native Windows path.\n * /c/Users/me -> C:\\Users\\me\n * /d/work -> D:\\work\n * /c -> C:\\ (bare drive root)\n * No-op on non-win32, on an empty string, or on an already-native path.\n */\nexport function toNativeCwd(p: string): string {\n if (process.platform !== 'win32' || !p) return p;\n const m = /^\\/([A-Za-z])\\/(.*)$/.exec(p);\n if (m) return `${m[1].toUpperCase()}:\\\\${m[2].replace(/\\//g, '\\\\')}`;\n const root = /^\\/([A-Za-z])\\/?$/.exec(p);\n if (root) return `${root[1].toUpperCase()}:\\\\`;\n return p;\n}\n\n/**\n * Resolve the directory a pty should actually spawn in. Prefer the requested\n * dir (native-converted); if it isn't an existing directory, fall back to the\n * home directory — so a stale, foreign, or deleted cwd can't crash spawn.\n */\nexport function resolveSpawnCwd(requested: string | undefined | null): string {\n if (requested) {\n const native = toNativeCwd(requested);\n try {\n if (existsSync(native) && statSync(native).isDirectory()) return native;\n } catch {\n /* fall through to home */\n }\n }\n return os.homedir();\n}\n","/**\n * Genie detached pty-host (Tier 3).\n *\n * A HEADLESS Node process — NO electron import — that owns the real node-pty\n * instances so they survive a full quit of the Electron app. The in-app\n * HostClient connects over a local socket (named pipe on Windows, unix domain\n * socket on POSIX) and proxies create/write/resize/kill; the host pushes back\n * `data`/`exit`. The host keeps its OWN scrollback ring buffer per pty so a\n * reattach AFTER a full quit can replay history.\n *\n * Launched detached by background.ts:\n * spawn(process.execPath, [hostScript], {\n * detached: true, stdio: 'ignore',\n * env: { ELECTRON_RUN_AS_NODE: '1', GENIE_USERDATA: <userData>, … }\n * }).unref()\n *\n * ELECTRON_RUN_AS_NODE makes Electron's binary run as plain Node so node-pty's\n * native ABI matches the one the app was built against (critical — a system Node\n * with a different ABI would fail to load the .node).\n *\n * Self-terminates after an idle period with zero live ptys AND no connected\n * client, so a host can never become a forever-orphan.\n */\n\nimport net from 'node:net';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawn, IPty } from 'node-pty';\nimport {\n encodeFrame,\n FrameDecoder,\n PROTOCOL_VERSION,\n type ClientMessage,\n type HostMessage,\n} from './host-protocol';\nimport { socketPathFor, pidfilePath } from './host-locate';\nimport { resolveSpawnCwd } from './cwd';\n\nconst SCROLLBACK_MAX = 1_000_000;\n/** Self-exit after this long with no ptys AND no connected client. */\nconst IDLE_TIMEOUT_MS = 10 * 60 * 1000;\nconst IDLE_CHECK_MS = 60 * 1000;\n\nconst userData = process.env.GENIE_USERDATA;\nif (!userData) {\n // Without a userData path we can't write a pidfile the client can find.\n process.exit(2);\n}\n\ninterface HostPty {\n pty: IPty;\n shell: string;\n scrollback: string;\n}\n\nconst ptys = new Map<string, HostPty>();\nconst clients = new Set<net.Socket>();\nlet lastActivity = Date.now();\n/** The listening server, set once startServer binds — used by graceful shutdown. */\nlet activeServer: net.Server | null = null;\n\nfunction broadcast(msg: HostMessage): void {\n const frame = encodeFrame(msg);\n for (const sock of clients) {\n try {\n sock.write(frame);\n } catch {\n /* dropped client — close handler cleans it up */\n }\n }\n}\n\nfunction createPty(opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n}): { pid: number; shell: string; existing: boolean; scrollback: string } {\n const existing = ptys.get(opts.id);\n if (existing) {\n return {\n pid: existing.pty.pid,\n shell: existing.shell,\n existing: true,\n scrollback: existing.scrollback,\n };\n }\n const shell = opts.shell ?? defaultShell();\n const env = { ...process.env, ...(opts.env ?? {}) } as Record<string, string>;\n // node-pty's `name` WINS over env.TERM (`name = opt.name || env.TERM; env.TERM\n // = name`), so `name` must carry the resolved TERM — a hardcoded\n // 'xterm-color' silently overrode this with a terminfo lacking the `Ms`\n // (OSC 52 clipboard) cap, so TUI copy-via-OSC-52 never fired. (Detached host.)\n env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: env.TERM,\n // Native-convert + validate the requested dir; a stale/foreign/MSYS cwd\n // (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with Windows\n // ERROR_DIRECTORY (267). Falls back to home if unusable.\n cwd: resolveSpawnCwd(opts.cwd),\n cols: opts.cols ?? 80,\n rows: opts.rows ?? 24,\n env,\n // Modern bundled ConPTY (conpty.dll). THIS file IS the windowless\n // detached host (spawnDetached: a `detached` console-less node.exe), so\n // it is exactly where #4 bites: legacy ConPTY allocates a stray VISIBLE\n // console per shell, and its kill path forks `conpty_console_list_agent`\n // un-hidden (console flash + \"AttachConsole failed\" crash). conpty.dll\n // runs console-less correctly and takes neither fork. Windows-only;\n // ignored elsewhere. (Mirrors InProcessBackend in manager.ts.)\n useConptyDll: true,\n });\n\n const entry: HostPty = { pty, shell, scrollback: '' };\n ptys.set(opts.id, entry);\n\n pty.onData((data) => {\n const next = entry.scrollback + data;\n entry.scrollback =\n next.length > SCROLLBACK_MAX ? next.slice(-SCROLLBACK_MAX) : next;\n broadcast({ kind: 'data', id: opts.id, data });\n });\n pty.onExit(({ exitCode, signal }) => {\n ptys.delete(opts.id);\n broadcast({ kind: 'exit', id: opts.id, exitCode, signal });\n lastActivity = Date.now();\n });\n\n return { pid: pty.pid, shell, existing: false, scrollback: '' };\n}\n\nfunction defaultShell(): string {\n if (process.platform === 'win32') return process.env.COMSPEC ?? 'cmd.exe';\n return process.env.SHELL ?? '/bin/bash';\n}\n\nfunction handleClientMessage(sock: net.Socket, msg: ClientMessage): void {\n lastActivity = Date.now();\n switch (msg.kind) {\n case 'hello':\n reply(sock, {\n kind: 'hello-ok',\n seq: msg.seq,\n protocolVersion: PROTOCOL_VERSION,\n pid: process.pid,\n });\n break;\n case 'create': {\n const r = createPty(msg.opts);\n reply(sock, {\n kind: 'created',\n seq: msg.seq,\n result: {\n id: msg.opts.id,\n pid: r.pid,\n shell: r.shell,\n existing: r.existing,\n scrollback: r.scrollback,\n },\n });\n break;\n }\n case 'write': {\n const e = ptys.get(msg.id);\n if (e) e.pty.write(msg.data);\n break;\n }\n case 'resize': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.resize(Math.max(1, msg.cols | 0), Math.max(1, msg.rows | 0));\n } catch {\n /* transient 0×0 during layout */\n }\n }\n break;\n }\n case 'kill': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n ptys.delete(msg.id);\n }\n break;\n }\n case 'list':\n reply(sock, {\n kind: 'list-result',\n seq: msg.seq,\n terminals: Array.from(ptys.entries()).map(([id, e]) => ({\n id,\n pid: e.pty.pid,\n shell: e.shell,\n })),\n });\n break;\n case 'set-retained':\n // The host keeps EVERYTHING alive across quit regardless; the\n // retained flag is meaningful to the client (fallback/UX). The host\n // only needs to not-die, which it doesn't. Acknowledge by no-op.\n break;\n case 'get-scrollback':\n reply(sock, {\n kind: 'scrollback-result',\n seq: msg.seq,\n scrollback: ptys.get(msg.id)?.scrollback ?? null,\n });\n break;\n case 'ping':\n reply(sock, { kind: 'pong', seq: msg.seq });\n break;\n case 'shutdown':\n // Graceful teardown: ack first so the client can finalize (it has\n // already snapshotted from its mirror), then kill every pty, remove\n // the pidfile/socket, and exit promptly. This is the clean path the\n // SIGKILL-by-pidfile interim fix can't take — it lets a consumer\n // bring the host down deterministically (e.g. before an auto-update)\n // instead of waiting on the 10-min idle timeout.\n reply(sock, { kind: 'shutdown-ok', seq: msg.seq });\n shutdown();\n break;\n }\n}\n\nfunction reply(sock: net.Socket, msg: HostMessage): void {\n try {\n sock.write(encodeFrame(msg));\n } catch {\n /* client gone */\n }\n}\n\nfunction startServer(socketPath: string): void {\n // On POSIX a stale socket file blocks bind; remove it first. (On Windows the\n // pipe namespace handles this.)\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n try {\n fs.mkdirSync(path.dirname(socketPath), { recursive: true });\n } catch {\n /* ignore */\n }\n }\n\n const server = net.createServer((sock) => {\n clients.add(sock);\n lastActivity = Date.now();\n const decoder = new FrameDecoder();\n sock.on('data', (chunk: Buffer) => {\n const frames = decoder.push(chunk);\n if (decoder.desynced) {\n try {\n sock.destroy();\n } catch {\n /* ignore */\n }\n return;\n }\n for (const f of frames) handleClientMessage(sock, f as ClientMessage);\n });\n const drop = () => {\n clients.delete(sock);\n lastActivity = Date.now();\n };\n sock.on('close', drop);\n sock.on('error', drop);\n });\n activeServer = server;\n\n server.on('error', (err) => {\n // EADDRINUSE: another host beat us to it. Exit quietly — the client will\n // connect to the winner.\n // eslint-disable-next-line no-console\n console.error('[pty-host] server error:', (err as Error).message);\n process.exit(3);\n });\n\n server.listen(socketPath, () => {\n try {\n writePidfileLocal(socketPath);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[pty-host] pidfile write failed:', (err as Error).message);\n }\n });\n\n // Idle watchdog: exit when nothing is running and nobody is connected.\n const idle = setInterval(() => {\n if (ptys.size === 0 && clients.size === 0 && Date.now() - lastActivity > IDLE_TIMEOUT_MS) {\n cleanupAndExit(socketPath, server);\n }\n }, IDLE_CHECK_MS);\n if (typeof idle.unref === 'function') idle.unref();\n}\n\nfunction writePidfileLocal(socketPath: string): void {\n const target = pidfilePath(userData!);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(\n tmp,\n JSON.stringify({\n pid: process.pid,\n socketPath,\n protocolVersion: PROTOCOL_VERSION,\n startedAt: Date.now(),\n }),\n );\n fs.renameSync(tmp, target);\n}\n\nfunction cleanupAndExit(socketPath: string, server: net.Server): void {\n try {\n // Only remove the pidfile if it still points at US (avoid clobbering a\n // successor host that took over the socket).\n const pf = JSON.parse(fs.readFileSync(pidfilePath(userData!), 'utf8'));\n if (pf?.pid === process.pid) fs.rmSync(pidfilePath(userData!), { force: true });\n } catch {\n /* ignore */\n }\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n }\n try {\n server.close();\n } catch {\n /* ignore */\n }\n process.exit(0);\n}\n\n/**\n * Graceful host shutdown — the clean counterpart to the idle watchdog and to a\n * SIGKILL-by-pidfile. Kills every live pty (so no orphaned shells linger), then\n * removes the pidfile/socket, closes the server, and exits. Triggered by a\n * `shutdown` client request; the client has already snapshotted its mirror, so\n * tearing down the host here loses nothing.\n */\nfunction shutdown(): void {\n for (const [, e] of ptys) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n }\n ptys.clear();\n if (activeServer) {\n cleanupAndExit(socketPath, activeServer);\n } else {\n process.exit(0);\n }\n}\n\n// --- main ------------------------------------------------------------------\n\nconst socketPath = socketPathFor(userData);\n\n// A dead-mans-switch so we don't keep a host with no shells AND no client when\n// the parent vanished without a clean disconnect: covered by the idle watchdog.\nprocess.on('uncaughtException', (err) => {\n // eslint-disable-next-line no-console\n console.error('[pty-host] uncaught:', err);\n});\n\nstartServer(socketPath);\n"]}
package/dist/pty-host.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolveSpawnCwd } from './chunk-V3MW5UTX.js';
2
- import { socketPathFor, FrameDecoder, pidfilePath, PROTOCOL_VERSION, encodeFrame } from './chunk-7NXYRMNA.js';
2
+ import { socketPathFor, FrameDecoder, pidfilePath, PROTOCOL_VERSION, encodeFrame } from './chunk-2A4TFTNQ.js';
3
3
  import net from 'net';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts","../src/host-script.ts","../src/service/descriptor.ts","../src/service/runtime.ts","../src/service/io.ts","../src/service/index.ts"],"names":["path","fs","fileURLToPath","os","spawn"],"mappings":"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","file":"service.cjs","sourcesContent":["/**\n * Pty-host wire protocol (Tier 3).\n *\n * The detached pty-host (main/terminal/pty-host.ts) and the in-app HostClient\n * (main/terminal/host-client.ts) talk over a local IPC transport — a named pipe\n * on Windows, a unix domain socket on POSIX — using a tiny length-prefixed JSON\n * framing so there's no heavy dependency. This module is PURE (no electron, no\n * node-pty, no net): just the message shapes + the encode/decode for the framing,\n * so it can be imported by both ends AND unit-tested in isolation.\n *\n * Framing: each message is `[4-byte big-endian uint32 length][utf8 JSON body]`.\n * The length prefix is the byte length of the JSON body. A FrameDecoder buffers\n * partial reads and yields whole messages as they complete — TCP/pipe streams\n * don't preserve message boundaries, so we can't assume one `data` event == one\n * message.\n */\n\n/**\n * Protocol version. Bumped whenever the message shapes change in a way that\n * makes an old host incompatible with a new client (or vice-versa). The client\n * refuses to attach to a host whose pidfile reports a different version and\n * spawns a fresh host instead — see host-client.ts connect-or-spawn.\n */\nexport const PROTOCOL_VERSION = 2;\n\n/** Requests the client sends to the host. `seq` correlates a reply. */\nexport type ClientMessage =\n | { kind: 'hello'; seq: number; protocolVersion: number }\n | {\n kind: 'create';\n seq: number;\n opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n };\n }\n | { kind: 'write'; id: string; data: string }\n | { kind: 'resize'; id: string; cols: number; rows: number }\n | { kind: 'kill'; id: string }\n | { kind: 'list'; seq: number }\n | { kind: 'set-retained'; id: string; retained: boolean }\n | { kind: 'get-scrollback'; seq: number; id: string }\n | { kind: 'ping'; seq: number }\n | { kind: 'shutdown'; seq: number };\n\n/** Pushes + replies the host sends to the client. */\nexport type HostMessage =\n | { kind: 'hello-ok'; seq: number; protocolVersion: number; pid: number }\n | {\n kind: 'created';\n seq: number;\n result: {\n id: string;\n pid: number;\n shell: string;\n existing: boolean;\n scrollback: string;\n };\n }\n | {\n kind: 'list-result';\n seq: number;\n terminals: Array<{ id: string; pid: number; shell: string }>;\n }\n | { kind: 'scrollback-result'; seq: number; scrollback: string | null }\n | { kind: 'pong'; seq: number }\n | { kind: 'shutdown-ok'; seq: number }\n | { kind: 'data'; id: string; data: string }\n | { kind: 'exit'; id: string; exitCode: number; signal?: number };\n\nexport type Frame = ClientMessage | HostMessage;\n\nconst LENGTH_BYTES = 4;\n\n/** Encode a message as a length-prefixed JSON frame ready for the socket. */\nexport function encodeFrame(msg: Frame): Buffer {\n const body = Buffer.from(JSON.stringify(msg), 'utf8');\n const header = Buffer.allocUnsafe(LENGTH_BYTES);\n header.writeUInt32BE(body.length, 0);\n return Buffer.concat([header, body]);\n}\n\n/**\n * Streaming frame decoder. Feed it raw socket chunks via `push`; it returns the\n * complete messages that became available (zero or more), buffering any partial\n * tail until the rest arrives. One decoder per socket.\n *\n * Resilient by design: a malformed JSON body is skipped (the frame is consumed\n * but yields nothing) rather than throwing — a corrupt frame must not wedge the\n * whole stream. An absurd length prefix (> MAX_FRAME) is treated as a desync and\n * the buffer is reset; the caller can decide whether to drop the connection.\n */\nexport class FrameDecoder {\n private buffer: Buffer = Buffer.alloc(0);\n\n /** Hard cap on a single frame (16 MB). Guards against a runaway/garbage\n * length prefix allocating unbounded memory. node-pty data chunks are tiny;\n * a serialized scrollback is bounded well under this. */\n static readonly MAX_FRAME = 16 * 1024 * 1024;\n\n /** True when the last push hit an oversized/desynced frame. The caller\n * should drop the connection — the stream can't be trusted to realign. */\n desynced = false;\n\n push(chunk: Buffer): Frame[] {\n this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;\n const out: Frame[] = [];\n for (;;) {\n if (this.buffer.length < LENGTH_BYTES) break;\n const len = this.buffer.readUInt32BE(0);\n if (len > FrameDecoder.MAX_FRAME) {\n // Desync / garbage. Reset and flag — realigning a length-prefixed\n // stream after a bad prefix isn't possible without a sentinel.\n this.desynced = true;\n this.buffer = Buffer.alloc(0);\n break;\n }\n if (this.buffer.length < LENGTH_BYTES + len) break; // wait for more\n const body = this.buffer.subarray(LENGTH_BYTES, LENGTH_BYTES + len);\n this.buffer = this.buffer.subarray(LENGTH_BYTES + len);\n try {\n out.push(JSON.parse(body.toString('utf8')) as Frame);\n } catch {\n /* skip a corrupt frame; the framing itself is still aligned */\n }\n }\n return out;\n }\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\nimport crypto from 'node:crypto';\nimport { PROTOCOL_VERSION } from './host-protocol';\n\n/**\n * Path + pidfile resolution for the detached pty-host (Tier 3).\n *\n * Kept ELECTRON-FREE on the resolution side that the host itself uses (the host\n * is a plain node process — no `app`), so the userData path is passed IN. The\n * in-app side (host-client lifecycle) imports `app` separately and feeds it here.\n */\n\nexport interface Pidfile {\n pid: number;\n socketPath: string;\n protocolVersion: number;\n startedAt: number;\n}\n\n/** Short, stable per-user hash so two OS users don't collide on the Windows\n * pipe name (the pipe namespace is machine-global). */\nexport function userHash(): string {\n const seed = `${os.userInfo().username}|${os.hostname()}`;\n return crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);\n}\n\n/**\n * The local IPC transport address.\n * • Windows: a named pipe `\\\\.\\pipe\\genie-ptyhost-<userhash>`. The default\n * Windows pipe ACL is per-logon-session, so another user on the same machine\n * can't open it — that's our ACL. (Documented; we don't tighten further.)\n * • POSIX: a unix domain socket under userData (preferred — survives /tmp\n * cleaners and is per-user by directory perms) named `ptyhost.sock`.\n */\nexport function socketPathFor(userDataDir: string): string {\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\genie-ptyhost-${userHash()}`;\n }\n // Keep the path short — unix socket paths have a ~104-char limit. userData is\n // typically well under that; fall back to os.tmpdir() if it's pathologically\n // long.\n const candidate = path.join(userDataDir, 'ptyhost.sock');\n if (candidate.length < 100) return candidate;\n return path.join(os.tmpdir(), `genie-ptyhost-${userHash()}.sock`);\n}\n\nexport function pidfilePath(userDataDir: string): string {\n return path.join(userDataDir, 'ptyhost.json');\n}\n\nexport function writePidfile(userDataDir: string, pf: Pidfile): void {\n const target = pidfilePath(userDataDir);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(pf));\n fs.renameSync(tmp, target);\n}\n\nexport function readPidfile(userDataDir: string): Pidfile | null {\n try {\n const raw = fs.readFileSync(pidfilePath(userDataDir), 'utf8');\n const pf = JSON.parse(raw) as Pidfile;\n if (\n typeof pf.pid !== 'number' ||\n typeof pf.socketPath !== 'string' ||\n typeof pf.protocolVersion !== 'number'\n ) {\n return null;\n }\n return pf;\n } catch {\n return null;\n }\n}\n\nexport function deletePidfile(userDataDir: string): void {\n try {\n fs.rmSync(pidfilePath(userDataDir), { force: true });\n } catch {\n /* ignore */\n }\n}\n\n/** True when a process with `pid` is alive (signal 0 probes without killing). */\nexport function isPidAlive(pid: number): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = exists but not ours (still \"alive\"); ESRCH = gone.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Decide whether an existing pidfile points at a usable host.\n * Usable = pid alive AND protocol versions match. A stale/dead/mismatched\n * pidfile means we must spawn a fresh host.\n */\nexport function pidfileUsable(pf: Pidfile | null): boolean {\n if (!pf) return false;\n if (pf.protocolVersion !== PROTOCOL_VERSION) return false;\n if (!isPidAlive(pf.pid)) return false;\n return true;\n}\n\n/**\n * Resolve the compiled pty-host script on disk, trying multiple candidate paths\n * so it works in BOTH `npm run dev` (script at app/pty-host.js next to\n * background.js) AND a packaged asar build. node-pty's native binding can't load\n * from inside an asar, so the host (which requires node-pty) must run UNPACKED —\n * `app.asar.unpacked/...`. We try the unpacked path first, then the in-asar path,\n * then a dev-relative path. Returns the first that exists, or null.\n *\n * `dirname` is main/background's __dirname (the directory the compiled main\n * bundle lives in). The host script is emitted alongside it as `pty-host.js`.\n */\nexport function resolveHostScript(dirname: string): string | null {\n const candidates = [\n // Packaged: node-pty must be unpacked, so run the host from the unpacked\n // tree too (its require('node-pty') resolves to the unpacked .node).\n dirname.includes(`app.asar${path.sep}`) || dirname.includes('app.asar/')\n ? dirname.replace(\n /app\\.asar([\\\\/])/,\n `app.asar.unpacked$1`,\n ) + path.sep + 'pty-host.js'\n : '',\n // Same dir as the compiled main bundle (dev: app/pty-host.js).\n path.join(dirname, 'pty-host.js'),\n // Defensive: a sibling unpacked dir computed from the asar path.\n path.join(dirname.replace('app.asar', 'app.asar.unpacked'), 'pty-host.js'),\n ].filter(Boolean);\n\n for (const c of candidates) {\n try {\n if (fs.existsSync(c)) return c;\n } catch {\n /* keep trying */\n }\n }\n return null;\n}\n","import path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { resolveHostScript } from './host-locate';\n\n/**\n * Absolute path to the bundled detached pty-host script (Tier 3), so a\n * `HostSpawner` can launch it as a detached child without knowing this\n * package's dist layout:\n *\n * ```ts\n * import { spawn } from 'node:child_process';\n * import { ptyHostScriptPath } from '@particle-academy/fancy-term-host';\n *\n * const child = spawn(process.execPath, [ptyHostScriptPath(), userDataDir], {\n * detached: true, stdio: 'ignore',\n * });\n * child.unref();\n * ```\n *\n * The host script is emitted alongside this module in `dist/` as\n * `pty-host.js`. Works in both the ESM and CJS builds (esbuild fills in\n * `import.meta.url` for the CJS output; `__dirname` is used when present).\n */\nexport function ptyHostScriptPath(): string {\n const here =\n typeof __dirname !== 'undefined'\n ? __dirname\n : path.dirname(fileURLToPath(import.meta.url));\n // Reuse host-locate's candidate logic (handles dev/packaged layouts); fall\n // back to the expected emitted location if the existence probe misses.\n return resolveHostScript(here) ?? path.join(here, 'pty-host.js');\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport { PROTOCOL_VERSION } from '../host-protocol';\nimport type { HostServiceConfig, ServicePlatform, ServiceRuntime } from './types';\n\n/**\n * Default service revision. Encodes the host PROTOCOL_VERSION so that a protocol\n * bump (which makes an old host incompatible) forces a reinstall — the installed\n * unit carries this marker and `ensureHostService` reinstalls on a mismatch.\n */\nexport const SERVICE_REVISION = `svc1+proto${PROTOCOL_VERSION}`;\n\n/** The marker line embedded in every generated unit, parsed back on upgrade. */\nexport const REVISION_MARKER = 'fancy-term-service-revision';\n\n/** A fully-resolved, per-OS description of the service to install. */\nexport interface ServiceDescriptor {\n platform: ServicePlatform;\n label: string;\n revision: string;\n /** The file we write + own (plist / unit / launcher .cmd). */\n unitPath: string;\n unitContents: string;\n /** POSIX file mode for the unit (e.g. 0o600 plist, 0o700 launcher). */\n unitMode: number;\n /** Commands (after the file is written) to register + start the service. */\n installArgv: string[][];\n /** Commands to stop + deregister. */\n uninstallArgv: string[][];\n startArgv: string[][];\n stopArgv: string[][];\n /** Single command whose output tells us installed/running. */\n statusArgv: string[];\n /** Files to remove on uninstall (unit + any logs we created). */\n removePaths: string[];\n}\n\nexport interface DescriptorContext {\n /** Override the platform (tests). Defaults to the current OS. */\n platform?: ServicePlatform;\n /** Home dir (tests). Defaults to `os.homedir()`. */\n home?: string;\n /** Numeric uid for launchd domain targets (tests / non-posix). */\n uid?: number;\n}\n\n/** Map `process.platform` to a service mechanism, or null when unsupported. */\nexport function servicePlatformFor(\n platform: NodeJS.Platform = process.platform,\n): ServicePlatform | null {\n switch (platform) {\n case 'darwin':\n return 'launchd';\n case 'linux':\n return 'systemd';\n case 'win32':\n return 'windows-task';\n default:\n return null;\n }\n}\n\n/**\n * Build the per-OS descriptor for a service config. PURE — no fs, no spawning —\n * so the generated units + command argv are fully unit-testable.\n */\nexport function buildServiceDescriptor(\n config: ResolvedServiceConfig,\n ctx: DescriptorContext = {},\n): ServiceDescriptor {\n const platform =\n ctx.platform ?? servicePlatformFor() ?? unsupported(config);\n const home = ctx.home ?? os.homedir();\n switch (platform) {\n case 'launchd':\n return launchd(config, home, ctx.uid ?? safeUid());\n case 'systemd':\n return systemd(config, home);\n case 'windows-task':\n return windowsTask(config);\n }\n}\n\nfunction unsupported(config: ResolvedServiceConfig): never {\n throw new Error(\n `fancy-term-host service: unsupported platform for \"${config.label}\"`,\n );\n}\n\nfunction safeUid(): number {\n const getuid = (process as { getuid?: () => number }).getuid;\n return typeof getuid === 'function' ? getuid() : 0;\n}\n\n/**\n * A config with all defaults filled in — produced by `resolveServiceConfig`\n * (see index.ts) and consumed by the descriptor builder.\n */\nexport interface ResolvedServiceConfig {\n label: string;\n userDataDir: string;\n hostScript: string;\n runtime: ServiceRuntime;\n env: Record<string, string>;\n revision: string;\n logDir: string;\n}\n\n/** The environment every platform injects (user-data dir, NODE_PATH, revision). */\nfunction serviceEnv(config: ResolvedServiceConfig): Record<string, string> {\n const env: Record<string, string> = {\n ...config.env,\n // GENIE_USERDATA is the var the existing detached-spawn path already uses\n // (see host-lifecycle.spawnDetached); keep it so the host finds its data.\n GENIE_USERDATA: config.userDataDir,\n FANCY_TERM_SERVICE_REVISION: config.revision,\n };\n if (config.runtime.nodePtyDir) {\n env.NODE_PATH = config.runtime.nodePtyDir;\n }\n return env;\n}\n\n// ── macOS: launchd LaunchAgent ──────────────────────────────────────────────\n\nfunction launchd(\n config: ResolvedServiceConfig,\n home: string,\n uid: number,\n): ServiceDescriptor {\n const label = config.label;\n // launchd targets macOS — always POSIX paths, even if generated elsewhere.\n const plistPath = path.posix.join(home, 'Library', 'LaunchAgents', `${label}.plist`);\n const env = serviceEnv(config);\n const outLog = path.posix.join(config.logDir, 'ptyhost.out.log');\n const errLog = path.posix.join(config.logDir, 'ptyhost.err.log');\n\n const envXml = Object.entries(env)\n .map(\n ([k, v]) =>\n ` <key>${xml(k)}</key>\\n <string>${xml(v)}</string>`,\n )\n .join('\\n');\n\n const contents = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- ${REVISION_MARKER}: ${config.revision} -->\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n <dict>\n <key>Label</key>\n <string>${xml(label)}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${xml(config.runtime.nodePath)}</string>\n <string>${xml(config.hostScript)}</string>\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n${envXml}\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <false/>\n <key>ProcessType</key>\n <string>Background</string>\n <key>StandardOutPath</key>\n <string>${xml(outLog)}</string>\n <key>StandardErrorPath</key>\n <string>${xml(errLog)}</string>\n </dict>\n</plist>\n`;\n\n const domain = `gui/${uid}`;\n const target = `${domain}/${label}`;\n return {\n platform: 'launchd',\n label,\n revision: config.revision,\n unitPath: plistPath,\n unitContents: contents,\n unitMode: 0o600,\n // bootstrap loads + (RunAtLoad) starts it.\n installArgv: [['launchctl', 'bootstrap', domain, plistPath]],\n uninstallArgv: [['launchctl', 'bootout', target]],\n startArgv: [['launchctl', 'kickstart', '-k', target]],\n stopArgv: [['launchctl', 'kill', 'SIGTERM', target]],\n statusArgv: ['launchctl', 'print', target],\n removePaths: [plistPath],\n };\n}\n\n// ── Linux: systemd --user unit ──────────────────────────────────────────────\n\nfunction systemd(config: ResolvedServiceConfig, home: string): ServiceDescriptor {\n const unit = `${config.label}.service`;\n // systemd --user targets Linux — always POSIX paths.\n const unitPath = path.posix.join(home, '.config', 'systemd', 'user', unit);\n const env = serviceEnv(config);\n const envLines = Object.entries(env)\n .map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`)\n .join('\\n');\n\n const contents = `# ${REVISION_MARKER}: ${config.revision}\n[Unit]\nDescription=fancy-term pty-host (${config.label})\nAfter=default.target\n\n[Service]\nType=simple\nExecStart=${quoteForExec(config.runtime.nodePath)} ${quoteForExec(config.hostScript)}\n${envLines}\nRestart=no\n\n[Install]\nWantedBy=default.target\n`;\n\n return {\n platform: 'systemd',\n label: config.label,\n revision: config.revision,\n unitPath,\n unitContents: contents,\n unitMode: 0o644,\n installArgv: [\n ['systemctl', '--user', 'daemon-reload'],\n ['systemctl', '--user', 'enable', '--now', unit],\n ],\n uninstallArgv: [['systemctl', '--user', 'disable', '--now', unit]],\n startArgv: [['systemctl', '--user', 'start', unit]],\n stopArgv: [['systemctl', '--user', 'stop', unit]],\n statusArgv: ['systemctl', '--user', 'is-active', unit],\n removePaths: [unitPath],\n };\n}\n\n// ── Windows: per-user scheduled task (ONLOGON, no elevation) ─────────────────\n\nfunction windowsTask(config: ResolvedServiceConfig): ServiceDescriptor {\n const env = serviceEnv(config);\n // A launcher .cmd sets env then runs the host — schtasks can't carry rich\n // env cleanly, and the .cmd also records the revision marker.\n const cmdPath = path.win32.join(config.userDataDir, `${sanitizeFileName(config.label)}.cmd`);\n const setLines = Object.entries(env)\n .map(([k, v]) => `set \"${k}=${v}\"`)\n .join('\\r\\n');\n const contents = [\n '@echo off',\n `rem ${REVISION_MARKER}: ${config.revision}`,\n setLines,\n `\"${config.runtime.nodePath}\" \"${config.hostScript}\"`,\n '',\n ].join('\\r\\n');\n\n const taskName = config.label;\n // /RL LIMITED = run with the user's normal (non-elevated) rights.\n // /SC ONLOGON = start at this user's logon. /F = overwrite if present.\n const tr = `cmd /c \"${cmdPath}\"`;\n return {\n platform: 'windows-task',\n label: config.label,\n revision: config.revision,\n unitPath: cmdPath,\n unitContents: contents,\n unitMode: 0o700,\n installArgv: [\n ['schtasks', '/Create', '/TN', taskName, '/TR', tr, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F'],\n ],\n uninstallArgv: [['schtasks', '/Delete', '/TN', taskName, '/F']],\n startArgv: [['schtasks', '/Run', '/TN', taskName]],\n stopArgv: [['schtasks', '/End', '/TN', taskName]],\n statusArgv: ['schtasks', '/Query', '/TN', taskName, '/FO', 'LIST'],\n removePaths: [cmdPath],\n };\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────────\n\nfunction xml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/** systemd `Environment=` values: wrap in quotes if they contain whitespace. */\nfunction systemdEnvValue(v: string): string {\n return /\\s/.test(v) ? `\"${v.replace(/\"/g, '\\\\\"')}\"` : v;\n}\n\n/** Quote a path for a systemd ExecStart token. */\nfunction quoteForExec(p: string): string {\n return /\\s/.test(p) ? `\"${p}\"` : p;\n}\n\nfunction sanitizeFileName(label: string): string {\n return label.replace(/[^A-Za-z0-9._-]+/g, '_');\n}\n\n/** Read the revision marker out of an installed unit's contents, or null. */\nexport function parseInstalledRevision(unitContents: string): string | null {\n const m = unitContents.match(\n new RegExp(`${REVISION_MARKER}:\\\\s*(\\\\S+)`),\n );\n return m ? m[1] : null;\n}\n","import path from 'node:path';\nimport fs from 'node:fs';\nimport type { ServiceRuntime } from './types';\n\n/**\n * Locate a STANDALONE Node runtime to run the host service on — explicitly NOT\n * the consumer's Electron binary, whose whole problem is that running the host\n * on it pins the executable across auto-updates.\n *\n * Precedence:\n * 1. An explicit `nodePath` (the consumer ships/locates its own node).\n * 2. `$FANCY_TERM_NODE` (a deploy-time override).\n * 3. `process.execPath` — but ONLY if the current process is plain Node, not\n * Electron (`process.versions.electron`), and the binary is named `node`.\n * 4. A `node` / `node.exe` found on `$PATH`.\n *\n * Returns null when no safe standalone runtime is found — the caller should then\n * fall back to the existing detached-spawn (which works for a normal quit) or\n * in-process. Never throws.\n */\nexport interface ResolveRuntimeOptions {\n /** Explicit standalone node path (wins). */\n nodePath?: string;\n /** Directory with an ABI-matched node-pty for that runtime. */\n nodePtyDir?: string;\n /** Environment to read overrides from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Current process binary. Defaults to `process.execPath`. */\n execPath?: string;\n /** Whether the current process is Electron. Defaults to detecting it. */\n isElectron?: boolean;\n /** PATH lookup probe (injectable for tests). Defaults to an fs probe. */\n pathProbe?: (binNames: string[], env: NodeJS.ProcessEnv) => string | null;\n}\n\nexport function resolveServiceRuntime(\n opts: ResolveRuntimeOptions = {},\n): ServiceRuntime | null {\n const env = opts.env ?? process.env;\n const execPath = opts.execPath ?? process.execPath;\n const isElectron =\n opts.isElectron ?? Boolean((process as { versions?: Record<string, string> }).versions?.electron);\n const nodePtyDir = opts.nodePtyDir ?? env.FANCY_TERM_NODE_PTY ?? undefined;\n const probe = opts.pathProbe ?? defaultPathProbe;\n\n const finish = (nodePath: string, source: string): ServiceRuntime => ({\n nodePath,\n ...(nodePtyDir ? { nodePtyDir } : {}),\n source,\n });\n\n // 1) Explicit.\n if (opts.nodePath) return finish(opts.nodePath, 'explicit');\n\n // 2) Env override.\n if (env.FANCY_TERM_NODE) return finish(env.FANCY_TERM_NODE, 'env:FANCY_TERM_NODE');\n\n // 3) The current process — only if it's a real standalone node.\n if (!isElectron && looksLikeNode(execPath)) {\n return finish(execPath, 'process.execPath');\n }\n\n // 4) PATH lookup.\n const onPath = probe(nodeBinNames(), env);\n if (onPath) return finish(onPath, 'PATH');\n\n return null;\n}\n\nfunction nodeBinNames(): string[] {\n return process.platform === 'win32' ? ['node.exe', 'node'] : ['node'];\n}\n\nfunction looksLikeNode(execPath: string): boolean {\n const base = path.basename(execPath).toLowerCase();\n return base === 'node' || base === 'node.exe';\n}\n\n/** Walk `$PATH`, returning the first existing `node`/`node.exe`. */\nfunction defaultPathProbe(\n binNames: string[],\n env: NodeJS.ProcessEnv,\n): string | null {\n const PATH = env.PATH ?? env.Path ?? '';\n const dirs = PATH.split(path.delimiter).filter(Boolean);\n for (const dir of dirs) {\n for (const bin of binNames) {\n const candidate = path.join(dir, bin);\n try {\n if (fs.existsSync(candidate)) return candidate;\n } catch {\n /* keep looking */\n }\n }\n }\n return null;\n}\n","import { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { ServiceIo } from './types';\n\n/**\n * Production {@link ServiceIo}: real fs + child_process. The orchestration in\n * index.ts takes a ServiceIo so tests can inject a fake and never touch the OS.\n */\nexport function nodeServiceIo(): ServiceIo {\n return {\n run(argv) {\n const [cmd, ...args] = argv;\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n const child = spawn(cmd, args, {\n // schtasks/launchctl/systemctl resolve via the shell on win32.\n shell: process.platform === 'win32',\n windowsHide: true,\n });\n child.stdout?.on('data', (d) => (stdout += d.toString()));\n child.stderr?.on('data', (d) => (stderr += d.toString()));\n child.on('error', (err) =>\n resolve({ code: -1, stdout, stderr: stderr + String(err) }),\n );\n child.on('close', (code) =>\n resolve({ code: code ?? -1, stdout, stderr }),\n );\n });\n },\n async writeFile(p, contents, opts) {\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, contents, { mode: opts?.mode ?? 0o600 });\n },\n async readFile(p) {\n try {\n return await fs.readFile(p, 'utf8');\n } catch {\n return null;\n }\n },\n async mkdirp(dir) {\n await fs.mkdir(dir, { recursive: true });\n },\n async rm(p) {\n await fs.rm(p, { force: true }).catch(() => {});\n },\n async exists(p) {\n try {\n await fs.access(p);\n return true;\n } catch {\n return false;\n }\n },\n };\n}\n","/**\n * Per-user OS-service lifecycle for the pty-host. Install / start / stop /\n * status / uninstall + a single `ensureHostService()` that does the\n * install-if-missing-or-stale → start-if-stopped dance.\n *\n * Pairs with the existing detached-host path: the wire protocol, pidfile, and\n * `HostClient` are unchanged, so the consumer connects exactly as today. The\n * difference is WHO launches the host — a per-user service on a standalone Node\n * runtime, not a child of the consumer's Electron binary.\n *\n * Everything here is graceful: `ensureHostService` never throws (inspect `ok`),\n * so a consumer can try the service and, on failure, fall back to\n * `HostSpawner.spawnDetached` (normal-quit survival) → in-process.\n */\n\nimport { ptyHostScriptPath } from '../host-script';\nimport {\n buildServiceDescriptor,\n parseInstalledRevision,\n SERVICE_REVISION,\n servicePlatformFor,\n type ResolvedServiceConfig,\n type ServiceDescriptor,\n} from './descriptor';\nimport { resolveServiceRuntime } from './runtime';\nimport { nodeServiceIo } from './io';\nimport type {\n EnsureResult,\n HostServiceConfig,\n ServiceIo,\n ServiceRuntime,\n ServiceStatus,\n} from './types';\n\nexport { resolveServiceRuntime } from './runtime';\nexport { nodeServiceIo } from './io';\nexport {\n buildServiceDescriptor,\n servicePlatformFor,\n parseInstalledRevision,\n SERVICE_REVISION,\n} from './descriptor';\nexport type { ServiceDescriptor, ResolvedServiceConfig } from './descriptor';\nexport type {\n HostServiceConfig,\n ServiceRuntime,\n ServiceStatus,\n ServiceState,\n ServicePlatform,\n ServiceIo,\n EnsureResult,\n EnsureAction,\n} from './types';\n\n/** True when the current OS has a supported service mechanism. */\nexport function isServiceSupported(): boolean {\n return servicePlatformFor() !== null;\n}\n\n/**\n * Fill in every default (host script, runtime, revision, log dir) so the\n * descriptor builder has a complete config. Throws only if no standalone Node\n * runtime can be resolved — callers that want graceful behaviour should use\n * `ensureHostService`, which catches this.\n */\nexport function resolveServiceConfig(\n config: HostServiceConfig,\n): ResolvedServiceConfig {\n const runtime: ServiceRuntime | null =\n config.runtime ?? resolveServiceRuntime();\n if (!runtime) {\n throw new Error(\n 'fancy-term-host service: no standalone Node runtime found ' +\n '(set config.runtime or $FANCY_TERM_NODE). Refusing to pin the ' +\n 'consumer binary by running on Electron.',\n );\n }\n return {\n label: config.label,\n userDataDir: config.userDataDir,\n hostScript: config.hostScript ?? ptyHostScriptPath(),\n runtime,\n env: config.env ?? {},\n revision: config.revision ?? SERVICE_REVISION,\n logDir: config.logDir ?? config.userDataDir,\n };\n}\n\nfunction descriptorFor(config: HostServiceConfig): ServiceDescriptor {\n return buildServiceDescriptor(resolveServiceConfig(config));\n}\n\n/** Run a list of commands in order; throw on the first non-zero exit. */\nasync function runAll(io: ServiceIo, argvList: string[][]): Promise<void> {\n for (const argv of argvList) {\n const { code, stderr } = await io.run(argv);\n if (code !== 0) {\n throw new Error(\n `command failed (${code}): ${argv.join(' ')}${\n stderr ? ` — ${stderr.trim()}` : ''\n }`,\n );\n }\n }\n}\n\n/** Write the unit file(s) and register + start the service. */\nexport async function installHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<ServiceStatus> {\n const desc = descriptorFor(config);\n await io.mkdirp(config.userDataDir);\n await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });\n await runAll(io, desc.installArgv);\n return serviceStatusFor(desc, io);\n}\n\n/** Stop + deregister the service and remove its unit file(s). */\nexport async function uninstallHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n const desc = descriptorFor(config);\n // Best-effort: a not-installed service shouldn't make uninstall throw.\n for (const argv of desc.uninstallArgv) {\n await io.run(argv);\n }\n for (const p of desc.removePaths) {\n await io.rm(p);\n }\n}\n\nexport async function startHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n await runAll(io, descriptorFor(config).startArgv);\n}\n\nexport async function stopHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n await runAll(io, descriptorFor(config).stopArgv);\n}\n\nexport async function isServiceInstalled(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<boolean> {\n return io.exists(descriptorFor(config).unitPath);\n}\n\nexport async function serviceStatus(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<ServiceStatus> {\n return serviceStatusFor(descriptorFor(config), io);\n}\n\nasync function serviceStatusFor(\n desc: ServiceDescriptor,\n io: ServiceIo,\n): Promise<ServiceStatus> {\n const unit = await io.readFile(desc.unitPath);\n const installed = unit !== null;\n const installedRevision = unit ? parseInstalledRevision(unit) ?? undefined : undefined;\n\n let running = false;\n let detail: string | undefined;\n if (installed) {\n const { code, stdout, stderr } = await io.run(desc.statusArgv);\n detail = (stdout || stderr).trim() || undefined;\n running = isRunningOutput(desc, code, stdout);\n }\n\n return {\n platform: desc.platform,\n state: !installed ? 'not-installed' : running ? 'running' : 'installed',\n installed,\n running,\n label: desc.label,\n unitPath: desc.unitPath,\n installedRevision,\n detail,\n };\n}\n\n/** Interpret each platform's status command output. */\nfunction isRunningOutput(\n desc: ServiceDescriptor,\n code: number,\n stdout: string,\n): boolean {\n const out = stdout.toLowerCase();\n switch (desc.platform) {\n case 'systemd':\n // `systemctl --user is-active` → \"active\" (exit 0) when running.\n return code === 0 && out.includes('active') && !out.includes('inactive');\n case 'launchd':\n // `launchctl print` exits 0 when loaded; \"state = running\" when up.\n return code === 0 && (out.includes('state = running') || out.includes('pid ='));\n case 'windows-task':\n // `schtasks /Query /FO LIST` → a \"Status:\" line of \"Running\".\n return code === 0 && out.includes('running');\n }\n}\n\n/**\n * Ensure the service is installed at the current revision AND running.\n *\n * - already running at this revision → no-op\n * - installed (this revision), stopped → start\n * - installed at a DIFFERENT revision → uninstall + reinstall + start\n * - not installed → install (+ start)\n *\n * NEVER throws. On any failure (no runtime, unsupported OS, a command error) it\n * returns `{ ok: false, … }` with an `error` so the caller can fall back to the\n * detached-spawn path. Snapshot live sessions before a reinstall if you need\n * history to survive it (a reinstall restarts the host).\n */\nexport async function ensureHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<EnsureResult> {\n if (!isServiceSupported()) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'unsupported',\n error: `unsupported platform: ${process.platform}`,\n };\n }\n\n let resolved: ResolvedServiceConfig;\n try {\n resolved = resolveServiceConfig(config);\n } catch (err) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'failed',\n error: (err as Error).message,\n };\n }\n\n const desc = buildServiceDescriptor(resolved);\n const runtime = resolved.runtime;\n try {\n const status = await serviceStatusFor(desc, io);\n\n if (status.installed && status.installedRevision === resolved.revision) {\n if (status.running) {\n return { ok: true, installed: true, running: true, action: 'already-running', runtime };\n }\n await runAll(io, desc.startArgv);\n return { ok: true, installed: true, running: true, action: 'started', runtime };\n }\n\n if (status.installed) {\n // Stale revision → tear down then reinstall fresh.\n for (const argv of desc.uninstallArgv) await io.run(argv);\n for (const p of desc.removePaths) await io.rm(p);\n }\n\n await io.mkdirp(resolved.userDataDir);\n await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });\n await runAll(io, desc.installArgv);\n // launchd RunAtLoad / systemd --now start on install; nudge to be sure.\n await runAll(io, desc.startArgv).catch(() => {});\n\n return {\n ok: true,\n installed: true,\n running: true,\n action: status.installed ? 'reinstalled' : 'installed-and-started',\n runtime,\n };\n } catch (err) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'failed',\n error: (err as Error).message,\n runtime,\n };\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts","../src/host-script.ts","../src/service/descriptor.ts","../src/service/runtime.ts","../src/service/io.ts","../src/service/index.ts"],"names":["path","fs","fileURLToPath","os","spawn"],"mappings":"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","file":"service.cjs","sourcesContent":["/**\n * Pty-host wire protocol (Tier 3).\n *\n * The detached pty-host (main/terminal/pty-host.ts) and the in-app HostClient\n * (main/terminal/host-client.ts) talk over a local IPC transport — a named pipe\n * on Windows, a unix domain socket on POSIX — using a tiny length-prefixed JSON\n * framing so there's no heavy dependency. This module is PURE (no electron, no\n * node-pty, no net): just the message shapes + the encode/decode for the framing,\n * so it can be imported by both ends AND unit-tested in isolation.\n *\n * Framing: each message is `[4-byte big-endian uint32 length][utf8 JSON body]`.\n * The length prefix is the byte length of the JSON body. A FrameDecoder buffers\n * partial reads and yields whole messages as they complete — TCP/pipe streams\n * don't preserve message boundaries, so we can't assume one `data` event == one\n * message.\n */\n\n/**\n * Protocol version. Bumped whenever the message shapes change in a way that\n * makes an old host incompatible with a new client (or vice-versa). The client\n * refuses to attach to a host whose pidfile reports a different version and\n * spawns a fresh host instead — see host-client.ts connect-or-spawn.\n */\nexport const PROTOCOL_VERSION = 2;\n\n/** Requests the client sends to the host. `seq` correlates a reply. */\nexport type ClientMessage =\n | { kind: 'hello'; seq: number; protocolVersion: number }\n | {\n kind: 'create';\n seq: number;\n opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n };\n }\n | { kind: 'write'; id: string; data: string }\n | { kind: 'resize'; id: string; cols: number; rows: number }\n | { kind: 'kill'; id: string }\n | { kind: 'list'; seq: number }\n | { kind: 'set-retained'; id: string; retained: boolean }\n | { kind: 'get-scrollback'; seq: number; id: string }\n | { kind: 'ping'; seq: number }\n | { kind: 'shutdown'; seq: number };\n\n/** Pushes + replies the host sends to the client. */\nexport type HostMessage =\n | { kind: 'hello-ok'; seq: number; protocolVersion: number; pid: number }\n | {\n kind: 'created';\n seq: number;\n result: {\n id: string;\n pid: number;\n shell: string;\n existing: boolean;\n scrollback: string;\n };\n }\n | {\n kind: 'list-result';\n seq: number;\n terminals: Array<{ id: string; pid: number; shell: string }>;\n }\n | { kind: 'scrollback-result'; seq: number; scrollback: string | null }\n | { kind: 'pong'; seq: number }\n | { kind: 'shutdown-ok'; seq: number }\n | { kind: 'data'; id: string; data: string }\n | { kind: 'exit'; id: string; exitCode: number; signal?: number };\n\nexport type Frame = ClientMessage | HostMessage;\n\nconst LENGTH_BYTES = 4;\n\n/** Encode a message as a length-prefixed JSON frame ready for the socket. */\nexport function encodeFrame(msg: Frame): Buffer {\n const body = Buffer.from(JSON.stringify(msg), 'utf8');\n const header = Buffer.allocUnsafe(LENGTH_BYTES);\n header.writeUInt32BE(body.length, 0);\n return Buffer.concat([header, body]);\n}\n\n/**\n * Streaming frame decoder. Feed it raw socket chunks via `push`; it returns the\n * complete messages that became available (zero or more), buffering any partial\n * tail until the rest arrives. One decoder per socket.\n *\n * Resilient by design: a malformed JSON body is skipped (the frame is consumed\n * but yields nothing) rather than throwing — a corrupt frame must not wedge the\n * whole stream. An absurd length prefix (> MAX_FRAME) is treated as a desync and\n * the buffer is reset; the caller can decide whether to drop the connection.\n */\nexport class FrameDecoder {\n private buffer: Buffer = Buffer.alloc(0);\n\n /** Hard cap on a single frame (16 MB). Guards against a runaway/garbage\n * length prefix allocating unbounded memory. node-pty data chunks are tiny;\n * a serialized scrollback is bounded well under this. */\n static readonly MAX_FRAME = 16 * 1024 * 1024;\n\n /** True when the last push hit an oversized/desynced frame. The caller\n * should drop the connection — the stream can't be trusted to realign. */\n desynced = false;\n\n push(chunk: Buffer): Frame[] {\n this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;\n const out: Frame[] = [];\n for (;;) {\n if (this.buffer.length < LENGTH_BYTES) break;\n const len = this.buffer.readUInt32BE(0);\n if (len > FrameDecoder.MAX_FRAME) {\n // Desync / garbage. Reset and flag — realigning a length-prefixed\n // stream after a bad prefix isn't possible without a sentinel.\n this.desynced = true;\n this.buffer = Buffer.alloc(0);\n break;\n }\n if (this.buffer.length < LENGTH_BYTES + len) break; // wait for more\n const body = this.buffer.subarray(LENGTH_BYTES, LENGTH_BYTES + len);\n this.buffer = this.buffer.subarray(LENGTH_BYTES + len);\n try {\n out.push(JSON.parse(body.toString('utf8')) as Frame);\n } catch {\n /* skip a corrupt frame; the framing itself is still aligned */\n }\n }\n return out;\n }\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\nimport crypto from 'node:crypto';\nimport { PROTOCOL_VERSION } from './host-protocol';\n\n/**\n * Path + pidfile resolution for the detached pty-host (Tier 3).\n *\n * Kept ELECTRON-FREE on the resolution side that the host itself uses (the host\n * is a plain node process — no `app`), so the userData path is passed IN. The\n * in-app side (host-client lifecycle) imports `app` separately and feeds it here.\n */\n\nexport interface Pidfile {\n pid: number;\n socketPath: string;\n protocolVersion: number;\n startedAt: number;\n}\n\n/** Short, stable per-user hash so two OS users don't collide on the Windows\n * pipe name (the pipe namespace is machine-global). */\nexport function userHash(): string {\n const seed = `${os.userInfo().username}|${os.hostname()}`;\n return crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);\n}\n\n/**\n * The local IPC transport address.\n * • Windows: a named pipe `\\\\.\\pipe\\genie-ptyhost-<userhash>`. The default\n * Windows pipe ACL is per-logon-session, so another user on the same machine\n * can't open it — that's our ACL. (Documented; we don't tighten further.)\n * • POSIX: a unix domain socket under userData (preferred — survives /tmp\n * cleaners and is per-user by directory perms) named `ptyhost.sock`.\n */\nexport function socketPathFor(userDataDir: string): string {\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\genie-ptyhost-${userHash()}`;\n }\n // Keep the path short — unix socket paths have a ~104-char limit. userData is\n // typically well under that; fall back to os.tmpdir() if it's pathologically\n // long.\n const candidate = path.join(userDataDir, 'ptyhost.sock');\n if (candidate.length < 100) return candidate;\n return path.join(os.tmpdir(), `genie-ptyhost-${userHash()}.sock`);\n}\n\nexport function pidfilePath(userDataDir: string): string {\n return path.join(userDataDir, 'ptyhost.json');\n}\n\nexport function writePidfile(userDataDir: string, pf: Pidfile): void {\n const target = pidfilePath(userDataDir);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(pf));\n fs.renameSync(tmp, target);\n}\n\nexport function readPidfile(userDataDir: string): Pidfile | null {\n try {\n const raw = fs.readFileSync(pidfilePath(userDataDir), 'utf8');\n const pf = JSON.parse(raw) as Pidfile;\n if (\n typeof pf.pid !== 'number' ||\n typeof pf.socketPath !== 'string' ||\n typeof pf.protocolVersion !== 'number'\n ) {\n return null;\n }\n return pf;\n } catch {\n return null;\n }\n}\n\nexport function deletePidfile(userDataDir: string): void {\n try {\n fs.rmSync(pidfilePath(userDataDir), { force: true });\n } catch {\n /* ignore */\n }\n}\n\n/** True when a process with `pid` is alive (signal 0 probes without killing). */\nexport function isPidAlive(pid: number): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = exists but not ours (still \"alive\"); ESRCH = gone.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Terminate a stale/wedged host process so its SINGLE-INSTANCE transport (the\n * Windows named pipe / POSIX socket) frees up for a fresh host to bind. A wedged\n * incumbent that still owns the pipe would otherwise EADDRINUSE every respawn and\n * silently swallow every client `hello` — see the connect-or-spawn recovery in\n * host-lifecycle (#8).\n *\n * Best-effort — never throws. Refuses `pid <= 0` and our OWN pid (so a caller\n * that reads its own pidfile can't self-destruct). On Windows `process.kill`\n * maps to TerminateProcess (immediate); on POSIX the default SIGTERM disposition\n * exits the host promptly (it installs no handler). Returns true when a signal\n * was actually delivered.\n */\nexport function terminateHost(\n pid: number,\n signal: NodeJS.Signals = 'SIGTERM',\n): boolean {\n if (!pid || pid <= 0 || pid === process.pid) return false;\n try {\n process.kill(pid, signal);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve once `pid` is gone (polling up to `timeoutMs`). Called after\n * {@link terminateHost} so a freshly-spawned host doesn't race the reaped one\n * for the single-instance transport. Resolves true when the pid is no longer\n * alive, false if it outlives the timeout.\n */\nexport async function awaitPidGone(pid: number, timeoutMs = 2000): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isPidAlive(pid)) return true;\n await new Promise((r) => setTimeout(r, 50));\n }\n return !isPidAlive(pid);\n}\n\n/**\n * Decide whether an existing pidfile points at a usable host.\n * Usable = pid alive AND protocol versions match. A stale/dead/mismatched\n * pidfile means we must spawn a fresh host.\n */\nexport function pidfileUsable(pf: Pidfile | null): boolean {\n if (!pf) return false;\n if (pf.protocolVersion !== PROTOCOL_VERSION) return false;\n if (!isPidAlive(pf.pid)) return false;\n return true;\n}\n\n/**\n * Resolve the compiled pty-host script on disk, trying multiple candidate paths\n * so it works in BOTH `npm run dev` (script at app/pty-host.js next to\n * background.js) AND a packaged asar build. node-pty's native binding can't load\n * from inside an asar, so the host (which requires node-pty) must run UNPACKED —\n * `app.asar.unpacked/...`. We try the unpacked path first, then the in-asar path,\n * then a dev-relative path. Returns the first that exists, or null.\n *\n * `dirname` is main/background's __dirname (the directory the compiled main\n * bundle lives in). The host script is emitted alongside it as `pty-host.js`.\n */\nexport function resolveHostScript(dirname: string): string | null {\n const candidates = [\n // Packaged: node-pty must be unpacked, so run the host from the unpacked\n // tree too (its require('node-pty') resolves to the unpacked .node).\n dirname.includes(`app.asar${path.sep}`) || dirname.includes('app.asar/')\n ? dirname.replace(\n /app\\.asar([\\\\/])/,\n `app.asar.unpacked$1`,\n ) + path.sep + 'pty-host.js'\n : '',\n // Same dir as the compiled main bundle (dev: app/pty-host.js).\n path.join(dirname, 'pty-host.js'),\n // Defensive: a sibling unpacked dir computed from the asar path.\n path.join(dirname.replace('app.asar', 'app.asar.unpacked'), 'pty-host.js'),\n ].filter(Boolean);\n\n for (const c of candidates) {\n try {\n if (fs.existsSync(c)) return c;\n } catch {\n /* keep trying */\n }\n }\n return null;\n}\n","import path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { resolveHostScript } from './host-locate';\n\n/**\n * Absolute path to the bundled detached pty-host script (Tier 3), so a\n * `HostSpawner` can launch it as a detached child without knowing this\n * package's dist layout:\n *\n * ```ts\n * import { spawn } from 'node:child_process';\n * import { ptyHostScriptPath } from '@particle-academy/fancy-term-host';\n *\n * const child = spawn(process.execPath, [ptyHostScriptPath(), userDataDir], {\n * detached: true, stdio: 'ignore',\n * });\n * child.unref();\n * ```\n *\n * The host script is emitted alongside this module in `dist/` as\n * `pty-host.js`. Works in both the ESM and CJS builds (esbuild fills in\n * `import.meta.url` for the CJS output; `__dirname` is used when present).\n */\nexport function ptyHostScriptPath(): string {\n const here =\n typeof __dirname !== 'undefined'\n ? __dirname\n : path.dirname(fileURLToPath(import.meta.url));\n // Reuse host-locate's candidate logic (handles dev/packaged layouts); fall\n // back to the expected emitted location if the existence probe misses.\n return resolveHostScript(here) ?? path.join(here, 'pty-host.js');\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport { PROTOCOL_VERSION } from '../host-protocol';\nimport type { HostServiceConfig, ServicePlatform, ServiceRuntime } from './types';\n\n/**\n * Default service revision. Encodes the host PROTOCOL_VERSION so that a protocol\n * bump (which makes an old host incompatible) forces a reinstall — the installed\n * unit carries this marker and `ensureHostService` reinstalls on a mismatch.\n */\nexport const SERVICE_REVISION = `svc1+proto${PROTOCOL_VERSION}`;\n\n/** The marker line embedded in every generated unit, parsed back on upgrade. */\nexport const REVISION_MARKER = 'fancy-term-service-revision';\n\n/** A fully-resolved, per-OS description of the service to install. */\nexport interface ServiceDescriptor {\n platform: ServicePlatform;\n label: string;\n revision: string;\n /** The file we write + own (plist / unit / launcher .cmd). */\n unitPath: string;\n unitContents: string;\n /** POSIX file mode for the unit (e.g. 0o600 plist, 0o700 launcher). */\n unitMode: number;\n /** Commands (after the file is written) to register + start the service. */\n installArgv: string[][];\n /** Commands to stop + deregister. */\n uninstallArgv: string[][];\n startArgv: string[][];\n stopArgv: string[][];\n /** Single command whose output tells us installed/running. */\n statusArgv: string[];\n /** Files to remove on uninstall (unit + any logs we created). */\n removePaths: string[];\n}\n\nexport interface DescriptorContext {\n /** Override the platform (tests). Defaults to the current OS. */\n platform?: ServicePlatform;\n /** Home dir (tests). Defaults to `os.homedir()`. */\n home?: string;\n /** Numeric uid for launchd domain targets (tests / non-posix). */\n uid?: number;\n}\n\n/** Map `process.platform` to a service mechanism, or null when unsupported. */\nexport function servicePlatformFor(\n platform: NodeJS.Platform = process.platform,\n): ServicePlatform | null {\n switch (platform) {\n case 'darwin':\n return 'launchd';\n case 'linux':\n return 'systemd';\n case 'win32':\n return 'windows-task';\n default:\n return null;\n }\n}\n\n/**\n * Build the per-OS descriptor for a service config. PURE — no fs, no spawning —\n * so the generated units + command argv are fully unit-testable.\n */\nexport function buildServiceDescriptor(\n config: ResolvedServiceConfig,\n ctx: DescriptorContext = {},\n): ServiceDescriptor {\n const platform =\n ctx.platform ?? servicePlatformFor() ?? unsupported(config);\n const home = ctx.home ?? os.homedir();\n switch (platform) {\n case 'launchd':\n return launchd(config, home, ctx.uid ?? safeUid());\n case 'systemd':\n return systemd(config, home);\n case 'windows-task':\n return windowsTask(config);\n }\n}\n\nfunction unsupported(config: ResolvedServiceConfig): never {\n throw new Error(\n `fancy-term-host service: unsupported platform for \"${config.label}\"`,\n );\n}\n\nfunction safeUid(): number {\n const getuid = (process as { getuid?: () => number }).getuid;\n return typeof getuid === 'function' ? getuid() : 0;\n}\n\n/**\n * A config with all defaults filled in — produced by `resolveServiceConfig`\n * (see index.ts) and consumed by the descriptor builder.\n */\nexport interface ResolvedServiceConfig {\n label: string;\n userDataDir: string;\n hostScript: string;\n runtime: ServiceRuntime;\n env: Record<string, string>;\n revision: string;\n logDir: string;\n}\n\n/** The environment every platform injects (user-data dir, NODE_PATH, revision). */\nfunction serviceEnv(config: ResolvedServiceConfig): Record<string, string> {\n const env: Record<string, string> = {\n ...config.env,\n // GENIE_USERDATA is the var the existing detached-spawn path already uses\n // (see host-lifecycle.spawnDetached); keep it so the host finds its data.\n GENIE_USERDATA: config.userDataDir,\n FANCY_TERM_SERVICE_REVISION: config.revision,\n };\n if (config.runtime.nodePtyDir) {\n env.NODE_PATH = config.runtime.nodePtyDir;\n }\n return env;\n}\n\n// ── macOS: launchd LaunchAgent ──────────────────────────────────────────────\n\nfunction launchd(\n config: ResolvedServiceConfig,\n home: string,\n uid: number,\n): ServiceDescriptor {\n const label = config.label;\n // launchd targets macOS — always POSIX paths, even if generated elsewhere.\n const plistPath = path.posix.join(home, 'Library', 'LaunchAgents', `${label}.plist`);\n const env = serviceEnv(config);\n const outLog = path.posix.join(config.logDir, 'ptyhost.out.log');\n const errLog = path.posix.join(config.logDir, 'ptyhost.err.log');\n\n const envXml = Object.entries(env)\n .map(\n ([k, v]) =>\n ` <key>${xml(k)}</key>\\n <string>${xml(v)}</string>`,\n )\n .join('\\n');\n\n const contents = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- ${REVISION_MARKER}: ${config.revision} -->\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n <dict>\n <key>Label</key>\n <string>${xml(label)}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${xml(config.runtime.nodePath)}</string>\n <string>${xml(config.hostScript)}</string>\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n${envXml}\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <false/>\n <key>ProcessType</key>\n <string>Background</string>\n <key>StandardOutPath</key>\n <string>${xml(outLog)}</string>\n <key>StandardErrorPath</key>\n <string>${xml(errLog)}</string>\n </dict>\n</plist>\n`;\n\n const domain = `gui/${uid}`;\n const target = `${domain}/${label}`;\n return {\n platform: 'launchd',\n label,\n revision: config.revision,\n unitPath: plistPath,\n unitContents: contents,\n unitMode: 0o600,\n // bootstrap loads + (RunAtLoad) starts it.\n installArgv: [['launchctl', 'bootstrap', domain, plistPath]],\n uninstallArgv: [['launchctl', 'bootout', target]],\n startArgv: [['launchctl', 'kickstart', '-k', target]],\n stopArgv: [['launchctl', 'kill', 'SIGTERM', target]],\n statusArgv: ['launchctl', 'print', target],\n removePaths: [plistPath],\n };\n}\n\n// ── Linux: systemd --user unit ──────────────────────────────────────────────\n\nfunction systemd(config: ResolvedServiceConfig, home: string): ServiceDescriptor {\n const unit = `${config.label}.service`;\n // systemd --user targets Linux — always POSIX paths.\n const unitPath = path.posix.join(home, '.config', 'systemd', 'user', unit);\n const env = serviceEnv(config);\n const envLines = Object.entries(env)\n .map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`)\n .join('\\n');\n\n const contents = `# ${REVISION_MARKER}: ${config.revision}\n[Unit]\nDescription=fancy-term pty-host (${config.label})\nAfter=default.target\n\n[Service]\nType=simple\nExecStart=${quoteForExec(config.runtime.nodePath)} ${quoteForExec(config.hostScript)}\n${envLines}\nRestart=no\n\n[Install]\nWantedBy=default.target\n`;\n\n return {\n platform: 'systemd',\n label: config.label,\n revision: config.revision,\n unitPath,\n unitContents: contents,\n unitMode: 0o644,\n installArgv: [\n ['systemctl', '--user', 'daemon-reload'],\n ['systemctl', '--user', 'enable', '--now', unit],\n ],\n uninstallArgv: [['systemctl', '--user', 'disable', '--now', unit]],\n startArgv: [['systemctl', '--user', 'start', unit]],\n stopArgv: [['systemctl', '--user', 'stop', unit]],\n statusArgv: ['systemctl', '--user', 'is-active', unit],\n removePaths: [unitPath],\n };\n}\n\n// ── Windows: per-user scheduled task (ONLOGON, no elevation) ─────────────────\n\nfunction windowsTask(config: ResolvedServiceConfig): ServiceDescriptor {\n const env = serviceEnv(config);\n // A launcher .cmd sets env then runs the host — schtasks can't carry rich\n // env cleanly, and the .cmd also records the revision marker.\n const cmdPath = path.win32.join(config.userDataDir, `${sanitizeFileName(config.label)}.cmd`);\n const setLines = Object.entries(env)\n .map(([k, v]) => `set \"${k}=${v}\"`)\n .join('\\r\\n');\n const contents = [\n '@echo off',\n `rem ${REVISION_MARKER}: ${config.revision}`,\n setLines,\n `\"${config.runtime.nodePath}\" \"${config.hostScript}\"`,\n '',\n ].join('\\r\\n');\n\n const taskName = config.label;\n // /RL LIMITED = run with the user's normal (non-elevated) rights.\n // /SC ONLOGON = start at this user's logon. /F = overwrite if present.\n const tr = `cmd /c \"${cmdPath}\"`;\n return {\n platform: 'windows-task',\n label: config.label,\n revision: config.revision,\n unitPath: cmdPath,\n unitContents: contents,\n unitMode: 0o700,\n installArgv: [\n ['schtasks', '/Create', '/TN', taskName, '/TR', tr, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F'],\n ],\n uninstallArgv: [['schtasks', '/Delete', '/TN', taskName, '/F']],\n startArgv: [['schtasks', '/Run', '/TN', taskName]],\n stopArgv: [['schtasks', '/End', '/TN', taskName]],\n statusArgv: ['schtasks', '/Query', '/TN', taskName, '/FO', 'LIST'],\n removePaths: [cmdPath],\n };\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────────\n\nfunction xml(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/** systemd `Environment=` values: wrap in quotes if they contain whitespace. */\nfunction systemdEnvValue(v: string): string {\n return /\\s/.test(v) ? `\"${v.replace(/\"/g, '\\\\\"')}\"` : v;\n}\n\n/** Quote a path for a systemd ExecStart token. */\nfunction quoteForExec(p: string): string {\n return /\\s/.test(p) ? `\"${p}\"` : p;\n}\n\nfunction sanitizeFileName(label: string): string {\n return label.replace(/[^A-Za-z0-9._-]+/g, '_');\n}\n\n/** Read the revision marker out of an installed unit's contents, or null. */\nexport function parseInstalledRevision(unitContents: string): string | null {\n const m = unitContents.match(\n new RegExp(`${REVISION_MARKER}:\\\\s*(\\\\S+)`),\n );\n return m ? m[1] : null;\n}\n","import path from 'node:path';\nimport fs from 'node:fs';\nimport type { ServiceRuntime } from './types';\n\n/**\n * Locate a STANDALONE Node runtime to run the host service on — explicitly NOT\n * the consumer's Electron binary, whose whole problem is that running the host\n * on it pins the executable across auto-updates.\n *\n * Precedence:\n * 1. An explicit `nodePath` (the consumer ships/locates its own node).\n * 2. `$FANCY_TERM_NODE` (a deploy-time override).\n * 3. `process.execPath` — but ONLY if the current process is plain Node, not\n * Electron (`process.versions.electron`), and the binary is named `node`.\n * 4. A `node` / `node.exe` found on `$PATH`.\n *\n * Returns null when no safe standalone runtime is found — the caller should then\n * fall back to the existing detached-spawn (which works for a normal quit) or\n * in-process. Never throws.\n */\nexport interface ResolveRuntimeOptions {\n /** Explicit standalone node path (wins). */\n nodePath?: string;\n /** Directory with an ABI-matched node-pty for that runtime. */\n nodePtyDir?: string;\n /** Environment to read overrides from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Current process binary. Defaults to `process.execPath`. */\n execPath?: string;\n /** Whether the current process is Electron. Defaults to detecting it. */\n isElectron?: boolean;\n /** PATH lookup probe (injectable for tests). Defaults to an fs probe. */\n pathProbe?: (binNames: string[], env: NodeJS.ProcessEnv) => string | null;\n}\n\nexport function resolveServiceRuntime(\n opts: ResolveRuntimeOptions = {},\n): ServiceRuntime | null {\n const env = opts.env ?? process.env;\n const execPath = opts.execPath ?? process.execPath;\n const isElectron =\n opts.isElectron ?? Boolean((process as { versions?: Record<string, string> }).versions?.electron);\n const nodePtyDir = opts.nodePtyDir ?? env.FANCY_TERM_NODE_PTY ?? undefined;\n const probe = opts.pathProbe ?? defaultPathProbe;\n\n const finish = (nodePath: string, source: string): ServiceRuntime => ({\n nodePath,\n ...(nodePtyDir ? { nodePtyDir } : {}),\n source,\n });\n\n // 1) Explicit.\n if (opts.nodePath) return finish(opts.nodePath, 'explicit');\n\n // 2) Env override.\n if (env.FANCY_TERM_NODE) return finish(env.FANCY_TERM_NODE, 'env:FANCY_TERM_NODE');\n\n // 3) The current process — only if it's a real standalone node.\n if (!isElectron && looksLikeNode(execPath)) {\n return finish(execPath, 'process.execPath');\n }\n\n // 4) PATH lookup.\n const onPath = probe(nodeBinNames(), env);\n if (onPath) return finish(onPath, 'PATH');\n\n return null;\n}\n\nfunction nodeBinNames(): string[] {\n return process.platform === 'win32' ? ['node.exe', 'node'] : ['node'];\n}\n\nfunction looksLikeNode(execPath: string): boolean {\n const base = path.basename(execPath).toLowerCase();\n return base === 'node' || base === 'node.exe';\n}\n\n/** Walk `$PATH`, returning the first existing `node`/`node.exe`. */\nfunction defaultPathProbe(\n binNames: string[],\n env: NodeJS.ProcessEnv,\n): string | null {\n const PATH = env.PATH ?? env.Path ?? '';\n const dirs = PATH.split(path.delimiter).filter(Boolean);\n for (const dir of dirs) {\n for (const bin of binNames) {\n const candidate = path.join(dir, bin);\n try {\n if (fs.existsSync(candidate)) return candidate;\n } catch {\n /* keep looking */\n }\n }\n }\n return null;\n}\n","import { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { ServiceIo } from './types';\n\n/**\n * Production {@link ServiceIo}: real fs + child_process. The orchestration in\n * index.ts takes a ServiceIo so tests can inject a fake and never touch the OS.\n */\nexport function nodeServiceIo(): ServiceIo {\n return {\n run(argv) {\n const [cmd, ...args] = argv;\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n const child = spawn(cmd, args, {\n // schtasks/launchctl/systemctl resolve via the shell on win32.\n shell: process.platform === 'win32',\n windowsHide: true,\n });\n child.stdout?.on('data', (d) => (stdout += d.toString()));\n child.stderr?.on('data', (d) => (stderr += d.toString()));\n child.on('error', (err) =>\n resolve({ code: -1, stdout, stderr: stderr + String(err) }),\n );\n child.on('close', (code) =>\n resolve({ code: code ?? -1, stdout, stderr }),\n );\n });\n },\n async writeFile(p, contents, opts) {\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, contents, { mode: opts?.mode ?? 0o600 });\n },\n async readFile(p) {\n try {\n return await fs.readFile(p, 'utf8');\n } catch {\n return null;\n }\n },\n async mkdirp(dir) {\n await fs.mkdir(dir, { recursive: true });\n },\n async rm(p) {\n await fs.rm(p, { force: true }).catch(() => {});\n },\n async exists(p) {\n try {\n await fs.access(p);\n return true;\n } catch {\n return false;\n }\n },\n };\n}\n","/**\n * Per-user OS-service lifecycle for the pty-host. Install / start / stop /\n * status / uninstall + a single `ensureHostService()` that does the\n * install-if-missing-or-stale → start-if-stopped dance.\n *\n * Pairs with the existing detached-host path: the wire protocol, pidfile, and\n * `HostClient` are unchanged, so the consumer connects exactly as today. The\n * difference is WHO launches the host — a per-user service on a standalone Node\n * runtime, not a child of the consumer's Electron binary.\n *\n * Everything here is graceful: `ensureHostService` never throws (inspect `ok`),\n * so a consumer can try the service and, on failure, fall back to\n * `HostSpawner.spawnDetached` (normal-quit survival) → in-process.\n */\n\nimport { ptyHostScriptPath } from '../host-script';\nimport {\n buildServiceDescriptor,\n parseInstalledRevision,\n SERVICE_REVISION,\n servicePlatformFor,\n type ResolvedServiceConfig,\n type ServiceDescriptor,\n} from './descriptor';\nimport { resolveServiceRuntime } from './runtime';\nimport { nodeServiceIo } from './io';\nimport type {\n EnsureResult,\n HostServiceConfig,\n ServiceIo,\n ServiceRuntime,\n ServiceStatus,\n} from './types';\n\nexport { resolveServiceRuntime } from './runtime';\nexport { nodeServiceIo } from './io';\nexport {\n buildServiceDescriptor,\n servicePlatformFor,\n parseInstalledRevision,\n SERVICE_REVISION,\n} from './descriptor';\nexport type { ServiceDescriptor, ResolvedServiceConfig } from './descriptor';\nexport type {\n HostServiceConfig,\n ServiceRuntime,\n ServiceStatus,\n ServiceState,\n ServicePlatform,\n ServiceIo,\n EnsureResult,\n EnsureAction,\n} from './types';\n\n/** True when the current OS has a supported service mechanism. */\nexport function isServiceSupported(): boolean {\n return servicePlatformFor() !== null;\n}\n\n/**\n * Fill in every default (host script, runtime, revision, log dir) so the\n * descriptor builder has a complete config. Throws only if no standalone Node\n * runtime can be resolved — callers that want graceful behaviour should use\n * `ensureHostService`, which catches this.\n */\nexport function resolveServiceConfig(\n config: HostServiceConfig,\n): ResolvedServiceConfig {\n const runtime: ServiceRuntime | null =\n config.runtime ?? resolveServiceRuntime();\n if (!runtime) {\n throw new Error(\n 'fancy-term-host service: no standalone Node runtime found ' +\n '(set config.runtime or $FANCY_TERM_NODE). Refusing to pin the ' +\n 'consumer binary by running on Electron.',\n );\n }\n return {\n label: config.label,\n userDataDir: config.userDataDir,\n hostScript: config.hostScript ?? ptyHostScriptPath(),\n runtime,\n env: config.env ?? {},\n revision: config.revision ?? SERVICE_REVISION,\n logDir: config.logDir ?? config.userDataDir,\n };\n}\n\nfunction descriptorFor(config: HostServiceConfig): ServiceDescriptor {\n return buildServiceDescriptor(resolveServiceConfig(config));\n}\n\n/** Run a list of commands in order; throw on the first non-zero exit. */\nasync function runAll(io: ServiceIo, argvList: string[][]): Promise<void> {\n for (const argv of argvList) {\n const { code, stderr } = await io.run(argv);\n if (code !== 0) {\n throw new Error(\n `command failed (${code}): ${argv.join(' ')}${\n stderr ? ` — ${stderr.trim()}` : ''\n }`,\n );\n }\n }\n}\n\n/** Write the unit file(s) and register + start the service. */\nexport async function installHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<ServiceStatus> {\n const desc = descriptorFor(config);\n await io.mkdirp(config.userDataDir);\n await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });\n await runAll(io, desc.installArgv);\n return serviceStatusFor(desc, io);\n}\n\n/** Stop + deregister the service and remove its unit file(s). */\nexport async function uninstallHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n const desc = descriptorFor(config);\n // Best-effort: a not-installed service shouldn't make uninstall throw.\n for (const argv of desc.uninstallArgv) {\n await io.run(argv);\n }\n for (const p of desc.removePaths) {\n await io.rm(p);\n }\n}\n\nexport async function startHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n await runAll(io, descriptorFor(config).startArgv);\n}\n\nexport async function stopHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n await runAll(io, descriptorFor(config).stopArgv);\n}\n\nexport async function isServiceInstalled(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<boolean> {\n return io.exists(descriptorFor(config).unitPath);\n}\n\nexport async function serviceStatus(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<ServiceStatus> {\n return serviceStatusFor(descriptorFor(config), io);\n}\n\nasync function serviceStatusFor(\n desc: ServiceDescriptor,\n io: ServiceIo,\n): Promise<ServiceStatus> {\n const unit = await io.readFile(desc.unitPath);\n const installed = unit !== null;\n const installedRevision = unit ? parseInstalledRevision(unit) ?? undefined : undefined;\n\n let running = false;\n let detail: string | undefined;\n if (installed) {\n const { code, stdout, stderr } = await io.run(desc.statusArgv);\n detail = (stdout || stderr).trim() || undefined;\n running = isRunningOutput(desc, code, stdout);\n }\n\n return {\n platform: desc.platform,\n state: !installed ? 'not-installed' : running ? 'running' : 'installed',\n installed,\n running,\n label: desc.label,\n unitPath: desc.unitPath,\n installedRevision,\n detail,\n };\n}\n\n/** Interpret each platform's status command output. */\nfunction isRunningOutput(\n desc: ServiceDescriptor,\n code: number,\n stdout: string,\n): boolean {\n const out = stdout.toLowerCase();\n switch (desc.platform) {\n case 'systemd':\n // `systemctl --user is-active` → \"active\" (exit 0) when running.\n return code === 0 && out.includes('active') && !out.includes('inactive');\n case 'launchd':\n // `launchctl print` exits 0 when loaded; \"state = running\" when up.\n return code === 0 && (out.includes('state = running') || out.includes('pid ='));\n case 'windows-task':\n // `schtasks /Query /FO LIST` → a \"Status:\" line of \"Running\".\n return code === 0 && out.includes('running');\n }\n}\n\n/**\n * Ensure the service is installed at the current revision AND running.\n *\n * - already running at this revision → no-op\n * - installed (this revision), stopped → start\n * - installed at a DIFFERENT revision → uninstall + reinstall + start\n * - not installed → install (+ start)\n *\n * NEVER throws. On any failure (no runtime, unsupported OS, a command error) it\n * returns `{ ok: false, … }` with an `error` so the caller can fall back to the\n * detached-spawn path. Snapshot live sessions before a reinstall if you need\n * history to survive it (a reinstall restarts the host).\n */\nexport async function ensureHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<EnsureResult> {\n if (!isServiceSupported()) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'unsupported',\n error: `unsupported platform: ${process.platform}`,\n };\n }\n\n let resolved: ResolvedServiceConfig;\n try {\n resolved = resolveServiceConfig(config);\n } catch (err) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'failed',\n error: (err as Error).message,\n };\n }\n\n const desc = buildServiceDescriptor(resolved);\n const runtime = resolved.runtime;\n try {\n const status = await serviceStatusFor(desc, io);\n\n if (status.installed && status.installedRevision === resolved.revision) {\n if (status.running) {\n return { ok: true, installed: true, running: true, action: 'already-running', runtime };\n }\n await runAll(io, desc.startArgv);\n return { ok: true, installed: true, running: true, action: 'started', runtime };\n }\n\n if (status.installed) {\n // Stale revision → tear down then reinstall fresh.\n for (const argv of desc.uninstallArgv) await io.run(argv);\n for (const p of desc.removePaths) await io.rm(p);\n }\n\n await io.mkdirp(resolved.userDataDir);\n await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });\n await runAll(io, desc.installArgv);\n // launchd RunAtLoad / systemd --now start on install; nudge to be sure.\n await runAll(io, desc.startArgv).catch(() => {});\n\n return {\n ok: true,\n installed: true,\n running: true,\n action: status.installed ? 'reinstalled' : 'installed-and-started',\n runtime,\n };\n } catch (err) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'failed',\n error: (err as Error).message,\n runtime,\n };\n }\n}\n"]}
package/dist/service.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ptyHostScriptPath } from './chunk-DB3SIE53.js';
2
- import { PROTOCOL_VERSION } from './chunk-7NXYRMNA.js';
1
+ import { ptyHostScriptPath } from './chunk-Q4WC2TIE.js';
2
+ import { PROTOCOL_VERSION } from './chunk-2A4TFTNQ.js';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
  import fs from 'fs';
@@ -0,0 +1,96 @@
1
+ # Packaging node-pty for a desktop app (Electron)
2
+
3
+ `fancy-term-host` owns the PTYs through **`node-pty`**, a native addon you build
4
+ yourself (it's a peer dependency). Bundling a native addon into a packaged
5
+ Electron app is where terminals silently break: `electron-builder`'s
6
+ `install-app-deps` rebuilds `node-pty`, but the rebuild output is left unusable
7
+ **per-OS**, so a standard packaged build spawns **no terminal at all** until you
8
+ patch it. This page is the patch — and `fancy-term-host` ships it for you.
9
+
10
+ ## The three breakages
11
+
12
+ | OS | Symptom | Cause |
13
+ |---|---|---|
14
+ | **Windows** | Runtime: `Cannot find conpty.dll at …/build/Release/conpty/conpty.dll (code 3)` | The rebuild produces `build/Release/conpty.node` but **not** the `conpty/` subdir it `LoadLibrary`s. `conpty.dll` + `OpenConsole.exe` ship only under `prebuilds/<plat>/conpty/` and `third_party/conpty/`. |
15
+ | **macOS (arm64)** | Shell child never starts (cursor, no output); `spawn-helper` SIGKILLed | `node-pty`'s `spawn-helper` ships **unsigned**. All arm64 code must be at least **ad-hoc signed** or the kernel kills it on exec. |
16
+ | **Linux** | Shell child never starts | `spawn-helper` must remain **executable** after packaging. |
17
+
18
+ ## Fix it in one line (`afterPack`)
19
+
20
+ `fancy-term-host` exports an `afterPack` hook that applies the right fix for the
21
+ platform being packed. Point your `electron-builder` config at it:
22
+
23
+ ```js
24
+ // electron-builder.config.js (or the "build" block in package.json)
25
+ const { fancyTermAfterPack } = require('@particle-academy/fancy-term-host/electron');
26
+
27
+ module.exports = {
28
+ // node-pty is native — keep it OUT of the asar so its .node + helpers load,
29
+ // and so the afterPack hook can find + fix them.
30
+ asarUnpack: ['**/node_modules/node-pty/**'],
31
+ afterPack: (context) => fancyTermAfterPack(context),
32
+ };
33
+ ```
34
+
35
+ ESM:
36
+
37
+ ```js
38
+ import { fancyTermAfterPack } from '@particle-academy/fancy-term-host/electron';
39
+ export default {
40
+ asarUnpack: ['**/node_modules/node-pty/**'],
41
+ afterPack: (context) => fancyTermAfterPack(context),
42
+ };
43
+ ```
44
+
45
+ It's **idempotent** (safe on every build) and **never throws** — it returns an
46
+ `AfterPackResult` you can log:
47
+
48
+ ```js
49
+ afterPack: async (context) => {
50
+ const r = await fancyTermAfterPack(context);
51
+ console.log(`[fancy-term-host] ${r.platform}: ${r.action}${r.detail ? ` — ${r.detail}` : ''}`);
52
+ if (!r.ok) throw new Error(`node-pty packaging fix failed: ${r.detail}`);
53
+ },
54
+ ```
55
+
56
+ `action` is one of `fixed` · `already-present` · `skipped` · `failed`. When it
57
+ can't find node-pty under the packed app, it returns `{ action: 'skipped', ok:
58
+ false }` — check your `asarUnpack` glob, or pass an explicit dir:
59
+ `fancyTermAfterPack(context, { nodePtyDir })`.
60
+
61
+ ### macOS signing note
62
+
63
+ The hook **ad-hoc** signs `spawn-helper` (`codesign --force --sign -`) so it runs
64
+ on Apple Silicon. If you ship a notarized app with your own Developer ID, sign
65
+ `spawn-helper` with your identity instead (electron-builder's own signing step,
66
+ run after `afterPack`, will typically re-sign the bundle) — the ad-hoc signature
67
+ is the floor that keeps unsigned dev / self-distributed builds working.
68
+
69
+ ## What the hook does (equivalent shell)
70
+
71
+ If you'd rather inline it, this is exactly what the helper runs against the
72
+ packaged `node-pty` (`…/Resources/app.asar.unpacked/node_modules/node-pty`):
73
+
74
+ ```sh
75
+ # Windows — create the conpty subdir node-pty LoadLibrary's
76
+ mkdir -p build/Release/conpty
77
+ cp prebuilds/win32-*/conpty/conpty.dll build/Release/conpty/
78
+ cp prebuilds/win32-*/conpty/OpenConsole.exe build/Release/conpty/
79
+ # (falls back to third_party/conpty/ when prebuilds/ is absent)
80
+
81
+ # macOS — ad-hoc sign spawn-helper so arm64 doesn't SIGKILL it
82
+ codesign --force --sign - build/Release/spawn-helper
83
+
84
+ # Linux — keep spawn-helper executable
85
+ chmod +x build/Release/spawn-helper
86
+ ```
87
+
88
+ ## Electron ABI note
89
+
90
+ `node-pty` is a native addon: it must be built against the **same ABI** as the
91
+ runtime that loads it. Run `electron-builder install-app-deps` (or
92
+ `electron-rebuild`) so `node-pty` matches your Electron version, and when you run
93
+ the **detached pty-host** on Electron-as-Node, spawn it with
94
+ `ELECTRON_RUN_AS_NODE=1` so the ABI still matches (see the README). For the
95
+ **T3+ per-user service**, the host runs on a *standalone* Node — give that
96
+ runtime its own ABI-matched `node-pty` (`ServiceRuntime.nodePtyDir`).