@particle-academy/fancy-term-host 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pty-host.cjs CHANGED
@@ -75,6 +75,24 @@ function socketPathFor(userDataDir) {
75
75
  function pidfilePath(userDataDir) {
76
76
  return path__default.default.join(userDataDir, "ptyhost.json");
77
77
  }
78
+ function toNativeCwd(p) {
79
+ if (process.platform !== "win32" || !p) return p;
80
+ const m = /^\/([A-Za-z])\/(.*)$/.exec(p);
81
+ if (m) return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
82
+ const root = /^\/([A-Za-z])\/?$/.exec(p);
83
+ if (root) return `${root[1].toUpperCase()}:\\`;
84
+ return p;
85
+ }
86
+ function resolveSpawnCwd(requested) {
87
+ if (requested) {
88
+ const native = toNativeCwd(requested);
89
+ try {
90
+ if (fs2.existsSync(native) && fs2.statSync(native).isDirectory()) return native;
91
+ } catch {
92
+ }
93
+ }
94
+ return os__default.default.homedir();
95
+ }
78
96
 
79
97
  // src/pty-host.ts
80
98
  var SCROLLBACK_MAX = 1e6;
@@ -111,7 +129,10 @@ function createPty(opts) {
111
129
  env.TERM = env.TERM || "xterm-256color";
112
130
  const pty = nodePty.spawn(shell, opts.args ?? [], {
113
131
  name: "xterm-color",
114
- cwd: opts.cwd,
132
+ // Native-convert + validate the requested dir; a stale/foreign/MSYS cwd
133
+ // (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with Windows
134
+ // ERROR_DIRECTORY (267). Falls back to home if unusable.
135
+ cwd: resolveSpawnCwd(opts.cwd),
115
136
  cols: opts.cols ?? 80,
116
137
  rows: opts.rows ?? 24,
117
138
  env
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts","../src/pty-host.ts"],"names":["os","crypto","path","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 = 1;\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\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: '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 * 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';\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\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 env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: 'xterm-color',\n cwd: opts.cwd,\n cols: opts.cols ?? 80,\n rows: opts.rows ?? 24,\n env,\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 }\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\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// --- 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":"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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 = 1;\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\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: '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\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 env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: 'xterm-color',\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 });\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 }\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\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// --- 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,4 +1,4 @@
1
- import { socketPathFor, FrameDecoder, pidfilePath, PROTOCOL_VERSION, encodeFrame } from './chunk-2DQJKTG5.js';
1
+ import { socketPathFor, FrameDecoder, pidfilePath, PROTOCOL_VERSION, encodeFrame, resolveSpawnCwd } from './chunk-WMP4YLM5.js';
2
2
  import net from 'net';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -38,7 +38,10 @@ function createPty(opts) {
38
38
  env.TERM = env.TERM || "xterm-256color";
39
39
  const pty = spawn(shell, opts.args ?? [], {
40
40
  name: "xterm-color",
41
- cwd: opts.cwd,
41
+ // Native-convert + validate the requested dir; a stale/foreign/MSYS cwd
42
+ // (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with Windows
43
+ // ERROR_DIRECTORY (267). Falls back to home if unusable.
44
+ cwd: resolveSpawnCwd(opts.cwd),
42
45
  cols: opts.cols ?? 80,
43
46
  rows: opts.rows ?? 24,
44
47
  env
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pty-host.ts"],"names":["socketPath"],"mappings":"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","file":"pty-host.js","sourcesContent":["/**\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';\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\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 env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: 'xterm-color',\n cwd: opts.cwd,\n cols: opts.cols ?? 80,\n rows: opts.rows ?? 24,\n env,\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 }\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\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// --- 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/pty-host.ts"],"names":["socketPath"],"mappings":"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","file":"pty-host.js","sourcesContent":["/**\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\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 env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: 'xterm-color',\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 });\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 }\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\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// --- 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-term-host",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Headless Node terminal backend for @particle-academy/fancy-term — owns the PTYs (node-pty) and the T1/T2/T3 persistence engine (snapshot+replay, retained PTYs, detached pty-host) behind four injected ports. OS-agnostic, zero hard third-party deps.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts"],"names":[],"mappings":"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","file":"chunk-2DQJKTG5.js","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 = 1;\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\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: '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"]}