import fs from "node:fs";import { createRequire } from "node:module";import path from "node:path"; import * as nodePty from "node-pty"; export interface PtyExitEvent { exitCode: number; signal: number | null;} export interface PtyProcess { readonly pid: number; write(data: string): void; resize(cols: number, rows: number): void; kill(signal?: string): void; onData(callback: (data: string) => void): () => void; onExit(callback: (event: PtyExitEvent) => void): () => void;} export interface PtySpawnInput { shell: string; cwd: string; cols: number; rows: number; env: NodeJS.ProcessEnv;} export interface PtyAdapter { spawn(input: PtySpawnInput): PtyProcess;} const requireForNodePty = createRequire(import.meta.url);let didEnsureSpawnHelperExecutable = false; function resolveNodePtySpawnHelperPath(): string | null { try { const packageJsonPath = requireForNodePty.resolve("node-pty/package.json"); const packageDir = path.dirname(packageJsonPath); const candidates = [ path.join(packageDir, "build", "Release", "spawn-helper"), path.join(packageDir, "build", "Debug", "spawn-helper"), path.join( packageDir, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper", ), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } return null; } catch { return null; }} export function ensureNodePtySpawnHelperExecutable(explicitPath?: string): void { if (process.platform === "win32") return; if (!explicitPath && didEnsureSpawnHelperExecutable) return; const helperPath = explicitPath ?? resolveNodePtySpawnHelperPath(); if (!helperPath) return; if (!explicitPath) { didEnsureSpawnHelperExecutable = true; } try { const stat = fs.statSync(helperPath); const mode = stat.mode & 0o777; if ((mode & 0o111) === 0) { fs.chmodSync(helperPath, mode | 0o111); } } catch { // Best effort only. If chmod fails, node-pty spawn will surface the real error. }} class NodePtyProcess implements PtyProcess { constructor(private readonly process: nodePty.IPty) {} get pid(): number { return this.process.pid; } write(data: string): void { this.process.write(data); } resize(cols: number, rows: number): void { this.process.resize(cols, rows); } kill(signal?: string): void { this.process.kill(signal); } onData(callback: (data: string) => void): () => void { const disposable = this.process.onData(callback); return () => { disposable.dispose(); }; } onExit(callback: (event: PtyExitEvent) => void): () => void { const disposable = this.process.onExit((event) => { callback({ exitCode: event.exitCode, signal: event.signal ?? null, }); }); return () => { disposable.dispose(); }; }} export class NodePtyAdapter implements PtyAdapter { spawn(input: PtySpawnInput): PtyProcess { ensureNodePtySpawnHelperExecutable(); const ptyProcess = nodePty.spawn(input.shell, [], { cwd: input.cwd, cols: input.cols, rows: input.rows, env: input.env, name: globalThis.process.platform === "win32" ? "xterm-color" : "xterm-256color", }); return new NodePtyProcess(ptyProcess); }}
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import * as nodePty from "node-pty";
export interface PtyExitEvent {
exitCode: number;
signal: number | null;
}
export interface PtyProcess {
readonly pid: number;
write(data: string): void;
resize(cols: number, rows: number): void;
kill(signal?: string): void;
onData(callback: (data: string) => void): () => void;
onExit(callback: (event: PtyExitEvent) => void): () => void;
}
export interface PtySpawnInput {
shell: string;
cwd: string;
cols: number;
rows: number;
env: NodeJS.ProcessEnv;
}
export interface PtyAdapter {
spawn(input: PtySpawnInput): PtyProcess;
}
const requireForNodePty = createRequire(import.meta.url);
let didEnsureSpawnHelperExecutable = false;
function resolveNodePtySpawnHelperPath(): string | null {
try {
const packageJsonPath = requireForNodePty.resolve("node-pty/package.json");
const packageDir = path.dirname(packageJsonPath);
const candidates = [
path.join(packageDir, "build", "Release", "spawn-helper"),
path.join(packageDir, "build", "Debug", "spawn-helper"),
path.join(
packageDir,
"prebuilds",
`${process.platform}-${process.arch}`,
"spawn-helper",
),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
} catch {
return null;
}
}
export function ensureNodePtySpawnHelperExecutable(explicitPath?: string): void {
if (process.platform === "win32") return;
if (!explicitPath && didEnsureSpawnHelperExecutable) return;
const helperPath = explicitPath ?? resolveNodePtySpawnHelperPath();
if (!helperPath) return;
if (!explicitPath) {
didEnsureSpawnHelperExecutable = true;
}
try {
const stat = fs.statSync(helperPath);
const mode = stat.mode & 0o777;
if ((mode & 0o111) === 0) {
fs.chmodSync(helperPath, mode | 0o111);
}
} catch {
// Best effort only. If chmod fails, node-pty spawn will surface the real error.
}
}
class NodePtyProcess implements PtyProcess {
constructor(private readonly process: nodePty.IPty) {}
get pid(): number {
return this.process.pid;
}
write(data: string): void {
this.process.write(data);
}
resize(cols: number, rows: number): void {
this.process.resize(cols, rows);
}
kill(signal?: string): void {
this.process.kill(signal);
}
onData(callback: (data: string) => void): () => void {
const disposable = this.process.onData(callback);
return () => {
disposable.dispose();
};
}
onExit(callback: (event: PtyExitEvent) => void): () => void {
const disposable = this.process.onExit((event) => {
callback({
exitCode: event.exitCode,
signal: event.signal ?? null,
});
});
return () => {
disposable.dispose();
};
}
}
export class NodePtyAdapter implements PtyAdapter {
spawn(input: PtySpawnInput): PtyProcess {
ensureNodePtySpawnHelperExecutable();
const ptyProcess = nodePty.spawn(input.shell, [], {
cwd: input.cwd,
cols: input.cols,
rows: input.rows,
env: input.env,
name:
globalThis.process.platform === "win32"
? "xterm-color"
: "xterm-256color",
});
return new NodePtyProcess(ptyProcess);
}
}