terminal-pilot 0.0.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/README.md +416 -0
- package/dist/ansi.d.ts +1 -0
- package/dist/ansi.js +71 -0
- package/dist/exports.compile-check.d.ts +1 -0
- package/dist/exports.compile-check.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +53 -0
- package/dist/mcp-server.d.ts +11 -0
- package/dist/mcp-server.js +40 -0
- package/dist/mcp-tools.d.ts +46 -0
- package/dist/mcp-tools.js +169 -0
- package/dist/terminal-pilot.d.ts +18 -0
- package/dist/terminal-pilot.js +46 -0
- package/dist/terminal-screen.d.ts +27 -0
- package/dist/terminal-screen.js +28 -0
- package/dist/terminal-session.d.ts +48 -0
- package/dist/terminal-session.js +320 -0
- package/dist/testing/menu-cli.d.ts +2 -0
- package/dist/testing/menu-cli.js +63 -0
- package/dist/testing/test-cli.d.ts +2 -0
- package/dist/testing/test-cli.js +22 -0
- package/package.json +50 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { defineSchema } from "tiny-stdio-mcp-server";
|
|
2
|
+
export function terminalCreateSessionTool(agent) {
|
|
3
|
+
return {
|
|
4
|
+
name: "terminal_create_session",
|
|
5
|
+
description: "Spawn an interactive CLI in a PTY",
|
|
6
|
+
inputSchema: defineSchema({
|
|
7
|
+
command: { type: "string", description: "Command to execute" },
|
|
8
|
+
args: { type: "array", description: "Command arguments", optional: true },
|
|
9
|
+
cwd: { type: "string", description: "Working directory", optional: true },
|
|
10
|
+
cols: { type: "number", description: "Terminal width in columns", optional: true },
|
|
11
|
+
rows: { type: "number", description: "Terminal height in rows", optional: true },
|
|
12
|
+
observe: { type: "boolean", description: "Mirror PTY output to stderr", optional: true }
|
|
13
|
+
}),
|
|
14
|
+
async handler(input) {
|
|
15
|
+
const session = await agent.newSession(input);
|
|
16
|
+
return { sessionId: session.id, pid: session.pid };
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function terminalTypeTool(agent) {
|
|
21
|
+
return {
|
|
22
|
+
name: "terminal_type",
|
|
23
|
+
description: "Write text to an active terminal session",
|
|
24
|
+
inputSchema: defineSchema({
|
|
25
|
+
sessionId: { type: "string", description: "Terminal session id" },
|
|
26
|
+
text: { type: "string", description: "Text to write to the session" }
|
|
27
|
+
}),
|
|
28
|
+
async handler(input) {
|
|
29
|
+
await agent.getSession(input.sessionId).fill(input.text);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function terminalPressKeyTool(agent) {
|
|
35
|
+
return {
|
|
36
|
+
name: "terminal_press_key",
|
|
37
|
+
description: "Send a named key press to an active terminal session",
|
|
38
|
+
inputSchema: defineSchema({
|
|
39
|
+
sessionId: { type: "string", description: "Terminal session id" },
|
|
40
|
+
key: { type: "string", description: "Named key to press" }
|
|
41
|
+
}),
|
|
42
|
+
async handler(input) {
|
|
43
|
+
await agent.getSession(input.sessionId).press(input.key);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function terminalSendSignalTool(agent) {
|
|
49
|
+
return {
|
|
50
|
+
name: "terminal_send_signal",
|
|
51
|
+
description: "Send a process signal to an active terminal session",
|
|
52
|
+
inputSchema: defineSchema({
|
|
53
|
+
sessionId: { type: "string", description: "Terminal session id" },
|
|
54
|
+
signal: { type: "string", description: "Signal to send to the session process" }
|
|
55
|
+
}),
|
|
56
|
+
async handler(input) {
|
|
57
|
+
await agent.getSession(input.sessionId).signal(input.signal);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function terminalWaitForTool(agent) {
|
|
63
|
+
return {
|
|
64
|
+
name: "terminal_wait_for",
|
|
65
|
+
description: "Wait for terminal output to match a pattern",
|
|
66
|
+
inputSchema: defineSchema({
|
|
67
|
+
sessionId: { type: "string", description: "Terminal session id" },
|
|
68
|
+
pattern: { type: "string", description: "Regular expression pattern to wait for" },
|
|
69
|
+
timeout: { type: "number", description: "Maximum wait time in milliseconds", optional: true }
|
|
70
|
+
}),
|
|
71
|
+
async handler(input) {
|
|
72
|
+
const session = agent.getSession(input.sessionId);
|
|
73
|
+
const pattern = new RegExp(input.pattern);
|
|
74
|
+
const output = input.timeout === undefined
|
|
75
|
+
? await session.waitFor(pattern)
|
|
76
|
+
: await session.waitFor(pattern, { timeout: input.timeout });
|
|
77
|
+
return { matched: true, output };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function terminalReadScreenTool(agent) {
|
|
82
|
+
return {
|
|
83
|
+
name: "terminal_read_screen",
|
|
84
|
+
description: "Read the current visible terminal screen",
|
|
85
|
+
inputSchema: defineSchema({
|
|
86
|
+
sessionId: { type: "string", description: "Terminal session id" }
|
|
87
|
+
}),
|
|
88
|
+
async handler(input) {
|
|
89
|
+
const screen = await agent.getSession(input.sessionId).screen();
|
|
90
|
+
return {
|
|
91
|
+
lines: [...screen.lines],
|
|
92
|
+
cursor: { ...screen.cursor },
|
|
93
|
+
size: { ...screen.size }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function terminalReadHistoryTool(agent) {
|
|
99
|
+
return {
|
|
100
|
+
name: "terminal_read_history",
|
|
101
|
+
description: "Read terminal output history",
|
|
102
|
+
inputSchema: defineSchema({
|
|
103
|
+
sessionId: { type: "string", description: "Terminal session id" },
|
|
104
|
+
last: { type: "number", description: "Return only the last N lines", optional: true }
|
|
105
|
+
}),
|
|
106
|
+
async handler(input) {
|
|
107
|
+
const lines = await agent.getSession(input.sessionId).history({ last: input.last });
|
|
108
|
+
return { lines };
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function terminalResizeTool(agent) {
|
|
113
|
+
return {
|
|
114
|
+
name: "terminal_resize",
|
|
115
|
+
description: "Resize an active terminal session",
|
|
116
|
+
inputSchema: defineSchema({
|
|
117
|
+
sessionId: { type: "string", description: "Terminal session id" },
|
|
118
|
+
cols: { type: "number", description: "Terminal width in columns" },
|
|
119
|
+
rows: { type: "number", description: "Terminal height in rows" }
|
|
120
|
+
}),
|
|
121
|
+
async handler(input) {
|
|
122
|
+
await agent.getSession(input.sessionId).resize(input.cols, input.rows);
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function terminalCloseSessionTool(agent) {
|
|
128
|
+
return {
|
|
129
|
+
name: "terminal_close_session",
|
|
130
|
+
description: "Close an active terminal session",
|
|
131
|
+
inputSchema: defineSchema({
|
|
132
|
+
sessionId: { type: "string", description: "Terminal session id" }
|
|
133
|
+
}),
|
|
134
|
+
async handler(input) {
|
|
135
|
+
const exitCode = await agent.getSession(input.sessionId).close();
|
|
136
|
+
return { exitCode };
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function terminalPilotMcpTools(agent) {
|
|
141
|
+
return [
|
|
142
|
+
terminalCreateSessionTool(agent),
|
|
143
|
+
terminalTypeTool(agent),
|
|
144
|
+
terminalPressKeyTool(agent),
|
|
145
|
+
terminalSendSignalTool(agent),
|
|
146
|
+
terminalWaitForTool(agent),
|
|
147
|
+
terminalReadScreenTool(agent),
|
|
148
|
+
terminalReadHistoryTool(agent),
|
|
149
|
+
terminalResizeTool(agent),
|
|
150
|
+
terminalCloseSessionTool(agent),
|
|
151
|
+
terminalListSessionsTool(agent)
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
export function terminalListSessionsTool(agent) {
|
|
155
|
+
return {
|
|
156
|
+
name: "terminal_list_sessions",
|
|
157
|
+
description: "List active terminal sessions",
|
|
158
|
+
inputSchema: defineSchema({}),
|
|
159
|
+
async handler() {
|
|
160
|
+
return {
|
|
161
|
+
sessions: agent.sessions().map((session) => ({
|
|
162
|
+
id: session.id,
|
|
163
|
+
command: session.command,
|
|
164
|
+
pid: session.pid
|
|
165
|
+
}))
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TerminalSession } from "./terminal-session.js";
|
|
2
|
+
export interface NewSessionOptions {
|
|
3
|
+
command: string;
|
|
4
|
+
args?: string[];
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
cols?: number;
|
|
8
|
+
rows?: number;
|
|
9
|
+
observe?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class TerminalPilot {
|
|
12
|
+
private readonly sessionMap;
|
|
13
|
+
static launch(): Promise<TerminalPilot>;
|
|
14
|
+
newSession(opts: NewSessionOptions): Promise<TerminalSession>;
|
|
15
|
+
getSession(id: string): TerminalSession;
|
|
16
|
+
sessions(): TerminalSession[];
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { TerminalSession } from "./terminal-session.js";
|
|
3
|
+
const DEFAULT_COLS = 120;
|
|
4
|
+
const DEFAULT_ROWS = 40;
|
|
5
|
+
export class TerminalPilot {
|
|
6
|
+
sessionMap = new Map();
|
|
7
|
+
static async launch() {
|
|
8
|
+
return new TerminalPilot();
|
|
9
|
+
}
|
|
10
|
+
async newSession(opts) {
|
|
11
|
+
const session = new TerminalSession({
|
|
12
|
+
id: randomUUID(),
|
|
13
|
+
command: opts.command,
|
|
14
|
+
args: opts.args,
|
|
15
|
+
cwd: opts.cwd,
|
|
16
|
+
env: opts.env,
|
|
17
|
+
cols: opts.cols ?? DEFAULT_COLS,
|
|
18
|
+
rows: opts.rows ?? DEFAULT_ROWS,
|
|
19
|
+
observe: opts.observe ?? false
|
|
20
|
+
});
|
|
21
|
+
session.on("exit", () => {
|
|
22
|
+
this.sessionMap.delete(session.id);
|
|
23
|
+
});
|
|
24
|
+
this.sessionMap.set(session.id, session);
|
|
25
|
+
return session;
|
|
26
|
+
}
|
|
27
|
+
getSession(id) {
|
|
28
|
+
const session = this.sessionMap.get(id);
|
|
29
|
+
if (session === undefined) {
|
|
30
|
+
throw new Error(`Session not found: ${id}`);
|
|
31
|
+
}
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
sessions() {
|
|
35
|
+
return [...this.sessionMap.values()];
|
|
36
|
+
}
|
|
37
|
+
async close() {
|
|
38
|
+
const sessions = [...this.sessionMap.values()];
|
|
39
|
+
try {
|
|
40
|
+
await Promise.all(sessions.map((session) => session.close()));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
this.sessionMap.clear();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare class TerminalScreen {
|
|
2
|
+
readonly lines: readonly string[];
|
|
3
|
+
readonly rawLines: readonly string[];
|
|
4
|
+
readonly cursor: {
|
|
5
|
+
row: number;
|
|
6
|
+
col: number;
|
|
7
|
+
};
|
|
8
|
+
readonly size: {
|
|
9
|
+
rows: number;
|
|
10
|
+
cols: number;
|
|
11
|
+
};
|
|
12
|
+
constructor({ lines, rawLines, cursor, size }: {
|
|
13
|
+
lines: string[];
|
|
14
|
+
rawLines: string[];
|
|
15
|
+
cursor: {
|
|
16
|
+
row: number;
|
|
17
|
+
col: number;
|
|
18
|
+
};
|
|
19
|
+
size: {
|
|
20
|
+
rows: number;
|
|
21
|
+
cols: number;
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
get text(): string;
|
|
25
|
+
contains(substring: string): boolean;
|
|
26
|
+
line(index: number): string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { stripAnsi } from "./ansi.js";
|
|
2
|
+
export class TerminalScreen {
|
|
3
|
+
lines;
|
|
4
|
+
rawLines;
|
|
5
|
+
cursor;
|
|
6
|
+
size;
|
|
7
|
+
constructor({ lines, rawLines, cursor, size }) {
|
|
8
|
+
this.lines = Object.freeze(lines.map((line) => stripAnsi(line)));
|
|
9
|
+
this.rawLines = Object.freeze([...rawLines]);
|
|
10
|
+
this.cursor = Object.freeze({ ...cursor });
|
|
11
|
+
this.size = Object.freeze({ ...size });
|
|
12
|
+
Object.freeze(this);
|
|
13
|
+
}
|
|
14
|
+
get text() {
|
|
15
|
+
return this.lines.join("\n");
|
|
16
|
+
}
|
|
17
|
+
contains(substring) {
|
|
18
|
+
return this.text.includes(substring);
|
|
19
|
+
}
|
|
20
|
+
line(index) {
|
|
21
|
+
const normalizedIndex = index < 0 ? this.lines.length + index : index;
|
|
22
|
+
const line = this.lines[normalizedIndex];
|
|
23
|
+
if (line === undefined) {
|
|
24
|
+
throw new RangeError(`Line index out of bounds: ${index}`);
|
|
25
|
+
}
|
|
26
|
+
return line;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type TerminalKey } from "./keys.js";
|
|
2
|
+
import { TerminalScreen } from "./terminal-screen.js";
|
|
3
|
+
type TerminalSessionOptions = {
|
|
4
|
+
id: string;
|
|
5
|
+
command: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string | undefined>;
|
|
9
|
+
cols?: number;
|
|
10
|
+
rows?: number;
|
|
11
|
+
observe?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type WaitForOptions = {
|
|
14
|
+
timeout?: number;
|
|
15
|
+
};
|
|
16
|
+
export type HistoryOptions = {
|
|
17
|
+
last?: number;
|
|
18
|
+
};
|
|
19
|
+
export declare class TerminalSession {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly command: string;
|
|
22
|
+
readonly pid: number;
|
|
23
|
+
exitCode: number | null;
|
|
24
|
+
private readonly pty;
|
|
25
|
+
private readonly terminal;
|
|
26
|
+
private readonly emitter;
|
|
27
|
+
private readonly exitPromise;
|
|
28
|
+
private rawBuffer;
|
|
29
|
+
private lastDataAt;
|
|
30
|
+
private currentCols;
|
|
31
|
+
private currentRows;
|
|
32
|
+
private closeRequested;
|
|
33
|
+
private signalRequested;
|
|
34
|
+
constructor({ id, command, args, cwd, env, cols, rows, observe }: TerminalSessionOptions);
|
|
35
|
+
type(text: string): Promise<void>;
|
|
36
|
+
fill(text: string): Promise<void>;
|
|
37
|
+
press(key: TerminalKey): Promise<void>;
|
|
38
|
+
send(raw: string): Promise<void>;
|
|
39
|
+
signal(sig: string): Promise<void>;
|
|
40
|
+
waitFor(pattern: string | RegExp, opts?: WaitForOptions): Promise<string>;
|
|
41
|
+
waitForQuiet(ms: number): Promise<void>;
|
|
42
|
+
screen(): Promise<TerminalScreen>;
|
|
43
|
+
history(opts?: HistoryOptions): Promise<string[]>;
|
|
44
|
+
resize(cols: number, rows: number): Promise<void>;
|
|
45
|
+
close(): Promise<number>;
|
|
46
|
+
on(event: "exit", cb: (code: number) => void): void;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import * as nodePty from "node-pty";
|
|
5
|
+
import { stripAnsi } from "./ansi.js";
|
|
6
|
+
import { keyToSequence } from "./keys.js";
|
|
7
|
+
import { TerminalScreen } from "./terminal-screen.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const HeadlessTerminal = require("headless-terminal");
|
|
10
|
+
const DEFAULT_COLS = 120;
|
|
11
|
+
const DEFAULT_ROWS = 40;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
13
|
+
const WAIT_FOR_POLL_MS = 100;
|
|
14
|
+
const TYPE_DELAY_MS = 15;
|
|
15
|
+
const CLOSE_AFTER_SIGNAL_GRACE_MS = 250;
|
|
16
|
+
export class TerminalSession {
|
|
17
|
+
id;
|
|
18
|
+
command;
|
|
19
|
+
pid;
|
|
20
|
+
exitCode = null;
|
|
21
|
+
pty;
|
|
22
|
+
terminal;
|
|
23
|
+
emitter = new EventEmitter();
|
|
24
|
+
exitPromise;
|
|
25
|
+
rawBuffer = "";
|
|
26
|
+
lastDataAt = Date.now();
|
|
27
|
+
currentCols;
|
|
28
|
+
currentRows;
|
|
29
|
+
closeRequested = false;
|
|
30
|
+
signalRequested = false;
|
|
31
|
+
constructor({ id, command, args = [], cwd = process.cwd(), env = process.env, cols = DEFAULT_COLS, rows = DEFAULT_ROWS, observe = false }) {
|
|
32
|
+
this.id = id;
|
|
33
|
+
this.command = command;
|
|
34
|
+
this.currentCols = cols;
|
|
35
|
+
this.currentRows = rows;
|
|
36
|
+
this.terminal = new HeadlessTerminal(cols, rows);
|
|
37
|
+
this.pty = createPtyProcess({ command, args, cwd, env, cols, rows });
|
|
38
|
+
this.pid = this.pty.pid;
|
|
39
|
+
const dataSubscription = this.pty.onData((chunk) => {
|
|
40
|
+
this.rawBuffer += chunk;
|
|
41
|
+
this.lastDataAt = Date.now();
|
|
42
|
+
this.terminal.write(chunk);
|
|
43
|
+
if (observe) {
|
|
44
|
+
process.stderr.write(chunk);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
let exitSubscription;
|
|
48
|
+
this.exitPromise = new Promise((resolve) => {
|
|
49
|
+
exitSubscription = this.pty.onExit(({ exitCode }) => {
|
|
50
|
+
if (this.exitCode !== null) {
|
|
51
|
+
resolve(this.exitCode);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.exitCode = exitCode;
|
|
55
|
+
dataSubscription.dispose();
|
|
56
|
+
exitSubscription?.dispose();
|
|
57
|
+
this.emitter.emit("exit", exitCode);
|
|
58
|
+
resolve(exitCode);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async type(text) {
|
|
63
|
+
for (const character of text) {
|
|
64
|
+
await this.send(character);
|
|
65
|
+
await sleep(TYPE_DELAY_MS);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async fill(text) {
|
|
69
|
+
await this.send(text);
|
|
70
|
+
}
|
|
71
|
+
async press(key) {
|
|
72
|
+
await this.send(keyToSequence(key));
|
|
73
|
+
}
|
|
74
|
+
async send(raw) {
|
|
75
|
+
if (this.exitCode !== null) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.pty.write(raw);
|
|
79
|
+
}
|
|
80
|
+
async signal(sig) {
|
|
81
|
+
if (this.exitCode !== null) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.signalRequested = true;
|
|
85
|
+
this.pty.kill(sig);
|
|
86
|
+
}
|
|
87
|
+
async waitFor(pattern, opts) {
|
|
88
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
89
|
+
const startedAt = Date.now();
|
|
90
|
+
while (Date.now() - startedAt <= timeout) {
|
|
91
|
+
const matched = matchPattern(this.rawBuffer, pattern);
|
|
92
|
+
if (matched !== null) {
|
|
93
|
+
return matched;
|
|
94
|
+
}
|
|
95
|
+
await sleep(WAIT_FOR_POLL_MS);
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Timed out waiting for pattern after ${timeout}ms: ${String(pattern)}`);
|
|
98
|
+
}
|
|
99
|
+
async waitForQuiet(ms) {
|
|
100
|
+
while (true) {
|
|
101
|
+
const remaining = ms - (Date.now() - this.lastDataAt);
|
|
102
|
+
if (remaining <= 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await sleep(remaining);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async screen() {
|
|
109
|
+
const rawLines = [];
|
|
110
|
+
for (let row = 0; row < this.currentRows; row += 1) {
|
|
111
|
+
const cells = this.terminal.displayBuffer.data[row] ?? [];
|
|
112
|
+
let rawLine = "";
|
|
113
|
+
for (const cell of cells) {
|
|
114
|
+
rawLine += cell?.[1] ?? " ";
|
|
115
|
+
}
|
|
116
|
+
rawLines.push(trimTrailingSpaces(rawLine));
|
|
117
|
+
}
|
|
118
|
+
return new TerminalScreen({
|
|
119
|
+
lines: rawLines,
|
|
120
|
+
rawLines,
|
|
121
|
+
cursor: {
|
|
122
|
+
row: this.terminal.displayBuffer.cursorY,
|
|
123
|
+
col: this.terminal.displayBuffer.cursorX
|
|
124
|
+
},
|
|
125
|
+
size: {
|
|
126
|
+
rows: this.currentRows,
|
|
127
|
+
cols: this.currentCols
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async history(opts) {
|
|
132
|
+
const normalized = normalizeHistoryBuffer(stripAnsi(this.rawBuffer));
|
|
133
|
+
const lines = splitHistoryLines(normalized);
|
|
134
|
+
if (opts?.last === undefined) {
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
const start = Math.max(0, lines.length - opts.last);
|
|
138
|
+
return lines.slice(start);
|
|
139
|
+
}
|
|
140
|
+
async resize(cols, rows) {
|
|
141
|
+
this.currentCols = cols;
|
|
142
|
+
this.currentRows = rows;
|
|
143
|
+
this.pty.resize(cols, rows);
|
|
144
|
+
this.terminal.resize(cols, rows);
|
|
145
|
+
}
|
|
146
|
+
async close() {
|
|
147
|
+
if (this.exitCode !== null) {
|
|
148
|
+
return this.exitCode;
|
|
149
|
+
}
|
|
150
|
+
if (!this.closeRequested) {
|
|
151
|
+
this.closeRequested = true;
|
|
152
|
+
if (this.signalRequested) {
|
|
153
|
+
const exitCode = await waitForExit(this.exitPromise, CLOSE_AFTER_SIGNAL_GRACE_MS);
|
|
154
|
+
if (exitCode !== null) {
|
|
155
|
+
return exitCode;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (this.exitCode === null) {
|
|
159
|
+
this.pty.kill("SIGTERM");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return this.exitPromise;
|
|
163
|
+
}
|
|
164
|
+
on(event, cb) {
|
|
165
|
+
this.emitter.on(event, cb);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function createPtyProcess({ command, args, cwd, env, cols, rows }) {
|
|
169
|
+
try {
|
|
170
|
+
return nodePty.spawn(command, args, {
|
|
171
|
+
cwd,
|
|
172
|
+
env,
|
|
173
|
+
cols,
|
|
174
|
+
rows,
|
|
175
|
+
encoding: "utf8"
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return createChildProcessFallback({ command, args, cwd, env });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function createChildProcessFallback({ command, args, cwd, env }) {
|
|
183
|
+
const child = spawnChildProcess(command, args, {
|
|
184
|
+
cwd,
|
|
185
|
+
env,
|
|
186
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
187
|
+
});
|
|
188
|
+
return new ChildProcessFallback(child);
|
|
189
|
+
}
|
|
190
|
+
class ChildProcessFallback {
|
|
191
|
+
pid;
|
|
192
|
+
child;
|
|
193
|
+
dataEmitter = new EventEmitter();
|
|
194
|
+
exitEmitter = new EventEmitter();
|
|
195
|
+
constructor(child) {
|
|
196
|
+
this.child = child;
|
|
197
|
+
this.pid = child.pid ?? -1;
|
|
198
|
+
child.stdout.setEncoding("utf8");
|
|
199
|
+
child.stderr.setEncoding("utf8");
|
|
200
|
+
child.stdout.on("data", this.handleData);
|
|
201
|
+
child.stderr.on("data", this.handleData);
|
|
202
|
+
child.on("exit", (exitCode, signal) => {
|
|
203
|
+
this.exitEmitter.emit("exit", {
|
|
204
|
+
exitCode: exitCode ?? signalToExitCode(signal),
|
|
205
|
+
signal: undefined
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
write(data) {
|
|
210
|
+
this.child.stdin.write(data);
|
|
211
|
+
}
|
|
212
|
+
resize() {
|
|
213
|
+
// No-op in fallback mode.
|
|
214
|
+
}
|
|
215
|
+
kill(signal) {
|
|
216
|
+
this.child.kill(signal);
|
|
217
|
+
}
|
|
218
|
+
onData(listener) {
|
|
219
|
+
this.dataEmitter.on("data", listener);
|
|
220
|
+
return {
|
|
221
|
+
dispose: () => {
|
|
222
|
+
this.dataEmitter.off("data", listener);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
onExit(listener) {
|
|
227
|
+
this.exitEmitter.on("exit", listener);
|
|
228
|
+
return {
|
|
229
|
+
dispose: () => {
|
|
230
|
+
this.exitEmitter.off("exit", listener);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
handleData = (chunk) => {
|
|
235
|
+
this.dataEmitter.emit("data", String(chunk));
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function signalToExitCode(signal) {
|
|
239
|
+
if (signal === null) {
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
const signalNumbers = {
|
|
243
|
+
SIGTERM: 15,
|
|
244
|
+
SIGINT: 2,
|
|
245
|
+
SIGHUP: 1,
|
|
246
|
+
SIGKILL: 9
|
|
247
|
+
};
|
|
248
|
+
const signalNumber = signalNumbers[signal];
|
|
249
|
+
if (signalNumber === undefined) {
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
return 128 + signalNumber;
|
|
253
|
+
}
|
|
254
|
+
function matchPattern(buffer, pattern) {
|
|
255
|
+
if (typeof pattern === "string") {
|
|
256
|
+
return buffer.includes(pattern) ? pattern : null;
|
|
257
|
+
}
|
|
258
|
+
const flags = removeCharacter(pattern.flags, "g");
|
|
259
|
+
const localPattern = new RegExp(pattern.source, flags);
|
|
260
|
+
const match = localPattern.exec(buffer);
|
|
261
|
+
return match?.[0] ?? null;
|
|
262
|
+
}
|
|
263
|
+
function removeCharacter(input, charToRemove) {
|
|
264
|
+
let output = "";
|
|
265
|
+
for (const character of input) {
|
|
266
|
+
if (character !== charToRemove) {
|
|
267
|
+
output += character;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return output;
|
|
271
|
+
}
|
|
272
|
+
function splitHistoryLines(input) {
|
|
273
|
+
const lines = input.split("\n");
|
|
274
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
275
|
+
lines.pop();
|
|
276
|
+
}
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
function normalizeHistoryBuffer(input) {
|
|
280
|
+
let output = "";
|
|
281
|
+
for (const character of input) {
|
|
282
|
+
if (character === "\r" || character === "\b") {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
output += character;
|
|
286
|
+
}
|
|
287
|
+
return output;
|
|
288
|
+
}
|
|
289
|
+
function trimTrailingSpaces(input) {
|
|
290
|
+
let end = input.length;
|
|
291
|
+
while (end > 0 && input[end - 1] === " ") {
|
|
292
|
+
end -= 1;
|
|
293
|
+
}
|
|
294
|
+
return input.slice(0, end);
|
|
295
|
+
}
|
|
296
|
+
function sleep(ms) {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
setTimeout(resolve, ms);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async function waitForExit(exitPromise, timeout) {
|
|
302
|
+
return new Promise((resolve) => {
|
|
303
|
+
let settled = false;
|
|
304
|
+
const timer = setTimeout(() => {
|
|
305
|
+
if (settled) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
settled = true;
|
|
309
|
+
resolve(null);
|
|
310
|
+
}, timeout);
|
|
311
|
+
void exitPromise.then((code) => {
|
|
312
|
+
if (settled) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
settled = true;
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
resolve(code);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|