/** * PTY session manager for the dashboard terminal panel. * * Responsibilities: * 1. Lazy-load node-pty. The module is an optional dep whose prebuilt may * be missing or incompatible on unusual platforms; we fail soft so the * rest of the dashboard keeps working. * 2. Whitelist keyed commands → argv. Clients never send raw shell strings; * they send a key ('shell' | 'claude' | 'ngit-login' | …) and the * server resolves it into a fixed argv. * 3. Track live sessions in memory with a scrollback ring buffer. Clients * reconnecting after a WS drop (navigation, refresh, transient network) * rejoin by id within a 5-minute grace window and replay the buffer * before live frames resume. * 4. Broadcast PTY output to every attached client so multi-tab / multi- * viewer cases (e.g. split panes in the future) behave predictably. * * Design choices worth noting: * - Sessions are keyed by a cryptographically random id; the client stores * the id in localStorage and presents it on rejoin. No cross-user isolation * is needed because the web-server is 127.0.0.1 and gated on NIP-98 auth. * - PATH for spawned processes is captured from the user's login shell * on first PTY spawn (cached). launchd / systemd user units can start the * server with a near-empty PATH where `claude`, `ngit`, etc. won't resolve; * the login-shell probe recovers the developer-interactive PATH. * - Ring buffer is fixed-size bytes (~1 MiB) — enough for a Claude Code * session's recent scrollback without letting a chatty command balloon * memory. Clients that want full history should log externally. */ import type { WebSocket } from 'ws'; interface IPty { pid: number; onData(cb: (data: string) => void): { dispose(): void; }; onExit(cb: (ev: { exitCode: number; signal?: number; }) => void): { dispose(): void; }; resize(cols: number, rows: number): void; write(data: string): void; kill(signal?: string): void; } interface PtyModule { spawn(file: string, args: string[] | string, options: { name?: string; cols?: number; rows?: number; cwd?: string; env?: Record; }): IPty; } export declare function loadPty(): Promise; export interface CmdSpec { cmd: string; args: string[]; cwd?: string; env?: Record; label: string; } export interface CreateOpts { key: string; cwd?: string; } export interface CliSpawn { bin: string; prefix: string[]; } export declare function resolveCmd(opts: CreateOpts, cli: CliSpawn): CmdSpec | null; interface Session { id: string; pty: IPty; spec: CmdSpec; cols: number; rows: number; createdAt: number; exited: boolean; exitCode: number | null; buffer: string[]; bufferBytes: number; clients: Set; graceTimer: NodeJS.Timeout | null; } export declare function createSession(opts: CreateOpts, cli: CliSpawn): Promise<{ ok: true; id: string; label: string; } | { ok: false; error: string; }>; export declare function getSession(id: string): Session | undefined; export declare function attachClient(id: string, ws: WebSocket): Session | null; export declare function detachClient(id: string, ws: WebSocket): void; export declare function writeInput(id: string, data: string): boolean; export declare function resizeSession(id: string, cols: number, rows: number): boolean; export declare function destroySession(id: string, reason: string): boolean; export declare function listSessions(): Array<{ id: string; label: string; exited: boolean; createdAt: number; }>; export declare function destroyAllSessions(): void; export {};