/** * @file tmux.js * @description Thin wrapper around the `tmux` CLI for the terminal-run * feature. Every dashboard-managed session is named `ccam-lane-` (see * `pty-run.js`) so a real terminal can attach to the exact same session with * `tmux attach -t ccam-lane-` (or `ccam lanes shell`). Never builds a * shell string — every call is `execFileSync("tmux", [...argv])` with an * explicit argument array (matches this repo's rule for git in worktree.js). * @author Nguyễn Ngọc Trí Vĩ */ const { execFileSync } = require("node:child_process"); // Test seam: swap the exec implementation so unit tests never invoke a real // tmux binary. Mirrors run-spawner.js's __injectChildForTest/__reset style. let execImpl = (args) => execFileSync("tmux", args, { encoding: "utf8" }); function __setExecImpl(fn) { execImpl = fn; } function __reset() { execImpl = (args) => execFileSync("tmux", args, { encoding: "utf8" }); } function hasSession(name) { try { execImpl(["has-session", "-t", name]); return true; } catch { return false; } } /** * Create a detached tmux session running `argv` as the pane's command. Throws * if tmux itself fails to start (caller decides how to surface that). */ function newSession({ name, cwd, argv }) { execImpl(["new-session", "-d", "-s", name, "-c", cwd, "--", ...argv]); } /** Idempotent — a session that's already gone is not an error. */ function killSession(name) { try { execImpl(["kill-session", "-t", name]); } catch { /* already gone */ } } /** Session names starting with `prefix`. Empty array if tmux has no server running at all. */ function listSessions(prefix) { let out; try { out = execImpl(["list-sessions", "-F", "#{session_name}"]); } catch { return []; } return out .split("\n") .map((s) => s.trim()) .filter((s) => s && s.startsWith(prefix)); } function isTmuxAvailable() { try { execImpl(["-V"]); return true; } catch { return false; } } module.exports = { hasSession, newSession, killSession, listSessions, isTmuxAvailable, __setExecImpl, __reset, };