@volter/supercode-terminal 0.2.30

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.
@@ -0,0 +1,192 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ function runEnvironment(options) {
3
+ if (options.inheritEnv === false) {
4
+ return options.env ?? {};
5
+ }
6
+ return options.env ? { ...process.env, ...options.env } : process.env;
7
+ }
8
+ // Memoized by command name: PATH is finalized once at process startup
9
+ // (ensureSystemToolsOnPath, called before any subprocess runs) and never changes
10
+ // for the rest of the process's life in this codebase's usage, so re-spawning
11
+ // `command -v <name>` on every call is pure repeated cost. This matters because
12
+ // requireCommand/assertTmux-style guards run on hot per-request paths (e.g.
13
+ // captureTerminal calling assertTmux on every capture) — a synchronous spawnSync
14
+ // there would otherwise reintroduce a small blocking cost on every single call,
15
+ // undermining the whole point of an async capture path (see CLAUDE.md's
16
+ // event-loop invariant).
17
+ const commandExistsCache = new Map();
18
+ export function commandExists(command) {
19
+ const cached = commandExistsCache.get(command);
20
+ if (cached !== undefined) {
21
+ return cached;
22
+ }
23
+ const result = spawnSync("command", ["-v", command], {
24
+ encoding: "utf8",
25
+ shell: true
26
+ });
27
+ const exists = result.status === 0;
28
+ commandExistsCache.set(command, exists);
29
+ return exists;
30
+ }
31
+ export function requireCommand(command, installHint) {
32
+ if (!commandExists(command)) {
33
+ const hint = installHint ? ` ${installHint}` : "";
34
+ throw new Error(`${command} is required but was not found on PATH.${hint}`);
35
+ }
36
+ }
37
+ // The standard system tool directories. Appended (never reordered) to PATH at
38
+ // process startup so `run`/`execFile` of ps, tmux, and coreutils resolve even when
39
+ // the process was launched from a stripped-PATH context — cron, launchd, a GUI
40
+ // app, or a nested agent shell whose PATH was degraded. A provider booted that way
41
+ // otherwise silently loses its lifecycle observation (`spawn ps ENOENT`) while
42
+ // healthz still answers. Non-existent dirs (e.g. /opt/homebrew on Intel/Linux) are
43
+ // harmless — the OS skips them during lookup.
44
+ const systemToolDirs = ["/usr/bin", "/bin", "/usr/sbin", "/sbin", "/usr/local/bin", "/opt/homebrew/bin"];
45
+ // Guarantee the system tool dirs are on PATH without disturbing the caller's own
46
+ // order or entries: only the missing ones are appended. Call once at entrypoint
47
+ // startup, before any subprocess runs. Idempotent.
48
+ export function ensureSystemToolsOnPath() {
49
+ const current = (process.env.PATH ?? "").split(":").filter(Boolean);
50
+ const have = new Set(current);
51
+ const missing = systemToolDirs.filter((dir) => !have.has(dir));
52
+ if (missing.length > 0) {
53
+ process.env.PATH = [...current, ...missing].join(":");
54
+ }
55
+ }
56
+ export function run(command, args, options = {}) {
57
+ const result = spawnSync(command, args, {
58
+ cwd: options.cwd,
59
+ encoding: "utf8",
60
+ env: runEnvironment(options),
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ timeout: options.timeoutMs
63
+ });
64
+ if (result.error) {
65
+ const message = result.error.name === "Error" && "code" in result.error && result.error.code === "ETIMEDOUT"
66
+ ? `${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms.`
67
+ : result.error.message;
68
+ const detail = runOutputDetail(result.stdout, result.stderr);
69
+ if (detail) {
70
+ throw new Error(`${message}\n${detail}`);
71
+ }
72
+ throw result.error;
73
+ }
74
+ if (result.status !== 0) {
75
+ const output = runOutputDetail(result.stdout, result.stderr);
76
+ const detail = output ? `\n${output}` : "";
77
+ throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}.${detail}`);
78
+ }
79
+ return result.stdout;
80
+ }
81
+ function runOutputDetail(stdout, stderr) {
82
+ const stderrText = outputText(stderr);
83
+ const stdoutText = outputText(stdout);
84
+ if (stderrText && stdoutText) {
85
+ return `stderr:\n${stderrText}\nstdout:\n${stdoutText}`;
86
+ }
87
+ if (stderrText) {
88
+ return `stderr:\n${stderrText}`;
89
+ }
90
+ if (stdoutText) {
91
+ return `stdout:\n${stdoutText}`;
92
+ }
93
+ return "";
94
+ }
95
+ function outputText(value) {
96
+ if (Buffer.isBuffer(value)) {
97
+ return value.toString("utf8").trim();
98
+ }
99
+ return String(value ?? "").trim();
100
+ }
101
+ export function runBuffer(command, args, options = {}) {
102
+ const result = spawnSync(command, args, {
103
+ cwd: options.cwd,
104
+ env: runEnvironment(options),
105
+ stdio: ["ignore", "pipe", "pipe"]
106
+ });
107
+ if (result.error) {
108
+ throw result.error;
109
+ }
110
+ if (result.status !== 0) {
111
+ const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8").trim() : String(result.stderr ?? "").trim();
112
+ const detail = stderr ? `\n${stderr}` : "";
113
+ throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}.${detail}`);
114
+ }
115
+ return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "");
116
+ }
117
+ export function runWithInput(command, args, input, options = {}) {
118
+ const result = spawnSync(command, args, {
119
+ cwd: options.cwd,
120
+ env: runEnvironment(options),
121
+ input,
122
+ stdio: ["pipe", "pipe", "pipe"]
123
+ });
124
+ if (result.error) {
125
+ throw result.error;
126
+ }
127
+ if (result.status !== 0) {
128
+ const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8").trim() : String(result.stderr ?? "").trim();
129
+ const detail = stderr ? `\n${stderr}` : "";
130
+ throw new Error(`${command} ${args.join(" ")} failed with exit ${result.status}.${detail}`);
131
+ }
132
+ return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? "");
133
+ }
134
+ export async function runAsync(command, args, options = {}) {
135
+ return await spawnAsync(command, args, options);
136
+ }
137
+ export async function runWithInputAsync(command, args, input, options = {}) {
138
+ return await spawnAsync(command, args, options, input);
139
+ }
140
+ async function spawnAsync(command, args, options, input) {
141
+ return await new Promise((resolve, reject) => {
142
+ const child = spawn(command, args, {
143
+ cwd: options.cwd,
144
+ env: runEnvironment(options),
145
+ stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
146
+ });
147
+ const stdout = [];
148
+ const stderr = [];
149
+ // Honor timeoutMs without blocking the loop (the sync `run` relied on
150
+ // spawnSync's timeout): SIGKILL the child and reject if it overruns.
151
+ const timer = options.timeoutMs === undefined
152
+ ? undefined
153
+ : setTimeout(() => {
154
+ child.kill("SIGKILL");
155
+ reject(new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms.`));
156
+ }, options.timeoutMs);
157
+ timer?.unref?.();
158
+ const clear = () => {
159
+ if (timer)
160
+ clearTimeout(timer);
161
+ };
162
+ child.stdout?.on("data", (chunk) => stdout.push(chunk));
163
+ child.stderr?.on("data", (chunk) => stderr.push(chunk));
164
+ child.on("error", (error) => {
165
+ clear();
166
+ reject(error);
167
+ });
168
+ child.on("close", (code) => {
169
+ clear();
170
+ if (code !== 0) {
171
+ const detail = Buffer.concat(stderr).toString("utf8").trim();
172
+ reject(new Error(`${command} ${args.join(" ")} failed with exit ${code}.${detail ? `\n${detail}` : ""}`));
173
+ return;
174
+ }
175
+ resolve(Buffer.concat(stdout).toString("utf8"));
176
+ });
177
+ if (input !== undefined && child.stdin) {
178
+ child.stdin.on("error", reject);
179
+ child.stdin.end(input);
180
+ }
181
+ });
182
+ }
183
+ export function spawnInherited(command, args) {
184
+ const child = spawn(command, args, { stdio: "inherit" });
185
+ child.on("exit", (code, signal) => {
186
+ if (signal) {
187
+ process.kill(process.pid, signal);
188
+ return;
189
+ }
190
+ process.exit(code ?? 1);
191
+ });
192
+ }
@@ -0,0 +1 @@
1
+ export declare function snapshotDescendantPids(rootPids: number[]): number[];
@@ -0,0 +1,41 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const PROCESS_SNAPSHOT_MAX_BUFFER = 16 * 1024 * 1024;
3
+ // Return only each requested root and its descendants. This is the safe input
4
+ // to the tmux session cleanup sweep: the tmux server is a parent and therefore
5
+ // can never be selected, while helpers detached below a pane remain reachable.
6
+ export function snapshotDescendantPids(rootPids) {
7
+ if (rootPids.length === 0)
8
+ return [];
9
+ const stdout = execFileSync("ps", ["-axo", "pid=,ppid="], {
10
+ encoding: "utf8",
11
+ maxBuffer: PROCESS_SNAPSHOT_MAX_BUFFER,
12
+ timeout: 5000
13
+ });
14
+ const children = new Map();
15
+ const processIds = new Set();
16
+ for (const line of stdout.split("\n")) {
17
+ const match = line.trim().match(/^(\d+)\s+(\d+)$/);
18
+ if (!match)
19
+ continue;
20
+ const pid = Number(match[1]);
21
+ const parent = Number(match[2]);
22
+ if (!Number.isInteger(pid) || pid < 1 || !Number.isInteger(parent) || parent < 0)
23
+ continue;
24
+ processIds.add(pid);
25
+ const siblings = children.get(parent) ?? [];
26
+ siblings.push(pid);
27
+ children.set(parent, siblings);
28
+ }
29
+ const result = [];
30
+ const pending = [...new Set(rootPids.filter((pid) => processIds.has(pid)))];
31
+ const seen = new Set();
32
+ while (pending.length > 0) {
33
+ const pid = pending.pop();
34
+ if (pid === undefined || seen.has(pid))
35
+ continue;
36
+ seen.add(pid);
37
+ result.push(pid);
38
+ pending.push(...(children.get(pid) ?? []));
39
+ }
40
+ return result.sort((a, b) => a - b);
41
+ }
@@ -0,0 +1,13 @@
1
+ export interface LocalTerminalCommand {
2
+ program: string;
3
+ arguments?: readonly string[];
4
+ cwd?: string;
5
+ env?: Readonly<Record<string, string>>;
6
+ }
7
+ export type LocalTerminalRunner = (program: string, arguments_: readonly string[]) => Promise<void>;
8
+ export interface OpenLocalTerminalOptions {
9
+ platform?: NodeJS.Platform;
10
+ runner?: LocalTerminalRunner;
11
+ }
12
+ export declare function terminalShellCommand(command: LocalTerminalCommand): string;
13
+ export declare function openLocalTerminal(command: LocalTerminalCommand, options?: OpenLocalTerminalOptions): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { runAsync } from "./internal/exec.js";
2
+ const APPLE_SCRIPT = [
3
+ "on run argv",
4
+ "ignoring application responses",
5
+ 'tell application "Terminal"',
6
+ "activate",
7
+ "do script (item 1 of argv)",
8
+ "end tell",
9
+ "end ignoring",
10
+ "end run",
11
+ ].join("\n");
12
+ export function terminalShellCommand(command) {
13
+ if (!command.program)
14
+ throw new Error("A terminal command program is required.");
15
+ const environment = Object.entries(command.env ?? {}).map(([name, value]) => {
16
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
17
+ throw new Error(`Invalid terminal environment variable name: ${JSON.stringify(name)}.`);
18
+ }
19
+ return `${name}=${value}`;
20
+ });
21
+ const launch = [
22
+ ...(environment.length ? ["env", ...environment] : []),
23
+ command.program,
24
+ ...(command.arguments ?? []),
25
+ ].map(shellQuote).join(" ");
26
+ return command.cwd ? `cd ${shellQuote(command.cwd)} && exec ${launch}` : `exec ${launch}`;
27
+ }
28
+ export async function openLocalTerminal(command, options = {}) {
29
+ const platform = options.platform ?? process.platform;
30
+ if (platform !== "darwin") {
31
+ throw new Error(`Opening a local terminal is not supported on ${platform}.`);
32
+ }
33
+ const runner = options.runner ?? defaultRunner;
34
+ await runner("osascript", ["-e", APPLE_SCRIPT, terminalShellCommand(command)]);
35
+ }
36
+ async function defaultRunner(program, arguments_) {
37
+ await runAsync(program, [...arguments_], { timeoutMs: 5_000 });
38
+ }
39
+ function shellQuote(value) {
40
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
41
+ }
@@ -0,0 +1,57 @@
1
+ import { type LocalTerminalCommand } from "./local-terminal.js";
2
+ import { type SessionLifecycleState } from "./session-lifecycle.js";
3
+ export type NativeTerminalBackend = "iterm" | "native-terminal-app";
4
+ export type NativeTerminalSession = Readonly<{
5
+ agentSessionId: string;
6
+ backend: NativeTerminalBackend;
7
+ state: SessionLifecycleState;
8
+ }>;
9
+ export type NativeSessionIdleProof = Readonly<{
10
+ observedAtMs: number;
11
+ source: string;
12
+ turn: "idle";
13
+ }>;
14
+ export type NativeSessionResolution = Readonly<{
15
+ proof: NativeSessionIdleProof;
16
+ session: NativeTerminalSession;
17
+ }>;
18
+ export type NativeTerminalApplication = "iterm" | "terminal";
19
+ /**
20
+ * Host-only local terminal capability for embedders.
21
+ *
22
+ * The public surface contains durable agent session ids and semantic state only.
23
+ * Native window ids, tty paths, process ids, AppleScript references, and tmux
24
+ * handles remain private to this process.
25
+ */
26
+ export declare class NativeTerminalSessionHost {
27
+ private readonly lifecycleStore;
28
+ private readonly rollTracker;
29
+ private observationPending;
30
+ private readonly claudeConfigDir;
31
+ private readonly resolvedSessionIds;
32
+ private sessions;
33
+ constructor(options?: {
34
+ home?: string;
35
+ claudeConfigDir?: string;
36
+ });
37
+ listSessions(): Promise<NativeTerminalSession[]>;
38
+ private observeSessions;
39
+ captureSession(agentSessionId: string, options?: {
40
+ lines?: number;
41
+ }): Promise<string>;
42
+ openSession(command: LocalTerminalCommand, options: {
43
+ application: NativeTerminalApplication;
44
+ }): Promise<void>;
45
+ hasSessionCandidate(agentSessionId: string, options: {
46
+ cwd: string;
47
+ }): boolean;
48
+ resolveSession(agentSessionId: string, options: {
49
+ cwd: string;
50
+ }): Promise<NativeSessionResolution | null>;
51
+ relinquishSession(agentSessionId: string, proof: NativeSessionIdleProof): Promise<{
52
+ agentSessionId: string;
53
+ released: true;
54
+ }>;
55
+ private requireSession;
56
+ private observeSurfaces;
57
+ }