Files
smartshell/ts/classes.smartshell.ts

874 lines
30 KiB
TypeScript
Raw Normal View History

import * as plugins from './plugins.js';
import { ShellEnv } from './classes.shellenv.js';
import type { IShellEnvContructorOptions, TExecutor } from './classes.shellenv.js';
import { ShellLog } from './classes.shelllog.js';
2018-07-30 16:08:14 +02:00
import * as cp from 'child_process';
2017-03-08 16:51:02 +01:00
// -- interfaces --
export interface IExecResult {
exitCode: number;
stdout: string;
signal?: NodeJS.Signals;
stderr?: string;
}
export interface IExecResultInteractive extends IExecResult {
sendInput: (input: string) => Promise<void>;
sendLine: (line: string) => Promise<void>;
endInput: () => void;
finalPromise: Promise<IExecResult>;
2017-03-08 16:51:02 +01:00
}
export interface IExecResultStreaming {
childProcess: cp.ChildProcess;
finalPromise: Promise<IExecResult>;
2024-04-18 13:42:51 +02:00
kill: () => Promise<void>;
terminate: () => Promise<void>;
keyboardInterrupt: () => Promise<void>;
customSignal: (signal: plugins.smartexit.TProcessSignal) => Promise<void>;
sendInput: (input: string) => Promise<void>;
sendLine: (line: string) => Promise<void>;
endInput: () => void;
}
interface IExecOptions {
commandString: string;
silent?: boolean;
strict?: boolean;
streaming?: boolean;
interactive?: boolean;
passthrough?: boolean;
interactiveControl?: boolean;
usePty?: boolean;
ptyCols?: number;
ptyRows?: number;
ptyTerm?: string;
ptyShell?: string;
maxBuffer?: number;
onData?: (chunk: Buffer | string) => void;
timeout?: number;
debug?: boolean;
env?: NodeJS.ProcessEnv;
signal?: AbortSignal;
}
export interface ISpawnOptions extends Omit<IExecOptions, 'commandString'> {
command: string;
args?: string[];
}
2017-03-08 16:51:02 +01:00
export class Smartshell {
2019-05-19 22:41:20 +02:00
public shellEnv: ShellEnv;
public smartexit = new plugins.smartexit.SmartExit();
constructor(optionsArg: IShellEnvContructorOptions) {
this.shellEnv = new ShellEnv(optionsArg);
2018-11-26 17:55:15 +01:00
}
2017-03-10 20:14:40 +01:00
/**
* Executes a given command asynchronously.
*/
private async _exec(options: IExecOptions): Promise<IExecResult | IExecResultStreaming | void> {
if (options.interactive) {
return await this._execInteractive({ commandString: options.commandString });
}
return await this._execCommand(options);
}
2023-06-22 14:16:16 +02:00
/**
* Executes an interactive command.
*/
private async _execInteractive(options: Pick<IExecOptions, 'commandString'>): Promise<void> {
// Skip interactive execution in CI environments.
if (process.env.CI) {
return;
}
2023-06-22 14:16:16 +02:00
return new Promise<void>((resolve) => {
const shell = cp.spawn(options.commandString, {
stdio: 'inherit',
shell: true,
detached: true,
});
2023-06-22 14:16:16 +02:00
this.smartexit.addProcess(shell);
shell.on('close', (code) => {
console.log(`Interactive shell terminated with code ${code}`);
this.smartexit.removeProcess(shell);
resolve();
});
});
}
/**
* Executes a command with args array (shell:false) for security
*/
private async _execSpawn(options: ISpawnOptions): Promise<IExecResult | IExecResultStreaming | IExecResultInteractive> {
const shellLogInstance = new ShellLog();
let stderrBuffer = '';
const maxBuffer = options.maxBuffer || 200 * 1024 * 1024; // Default 200MB
let bufferExceeded = false;
// Handle PTY mode if requested
if (options.usePty) {
throw new Error('PTY mode is not yet supported with execSpawn. Use exec methods with shell:true for PTY.');
}
const execChildProcess = cp.spawn(options.command, options.args || [], {
shell: false, // SECURITY: Never use shell with untrusted input
cwd: process.cwd(),
env: options.env || process.env,
detached: false,
signal: options.signal,
});
this.smartexit.addProcess(execChildProcess);
// Handle timeout
let timeoutHandle: NodeJS.Timeout | null = null;
if (options.timeout) {
timeoutHandle = setTimeout(() => {
if (options.debug) {
console.log(`[smartshell] Timeout reached for process ${execChildProcess.pid}, terminating...`);
}
execChildProcess.kill('SIGTERM');
}, options.timeout);
}
// Connect stdin if passthrough is enabled (but not for interactive control)
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
process.stdin.pipe(execChildProcess.stdin);
}
// Create input methods for interactive control
const sendInput = async (input: string): Promise<void> => {
if (!execChildProcess.stdin) {
throw new Error('stdin is not available for this process');
}
if (execChildProcess.stdin.destroyed || !execChildProcess.stdin.writable) {
throw new Error('stdin has been destroyed or is not writable');
}
return new Promise((resolve, reject) => {
execChildProcess.stdin.write(input, 'utf8', (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
};
const sendLine = async (line: string): Promise<void> => {
return sendInput(line + '\n');
};
const endInput = (): void => {
if (execChildProcess.stdin && !execChildProcess.stdin.destroyed) {
execChildProcess.stdin.end();
}
};
// Capture stdout and stderr output
execChildProcess.stdout.on('data', (data) => {
if (!options.silent) {
shellLogInstance.writeToConsole(data);
}
if (options.onData) {
options.onData(data);
}
if (!bufferExceeded) {
shellLogInstance.addToBuffer(data);
if (shellLogInstance.logStore.length > maxBuffer) {
bufferExceeded = true;
shellLogInstance.logStore = Buffer.from('[Output truncated - exceeded maxBuffer]');
}
}
});
execChildProcess.stderr.on('data', (data) => {
if (!options.silent) {
shellLogInstance.writeToConsole(data);
}
const dataStr = data.toString();
stderrBuffer += dataStr;
if (options.onData) {
options.onData(data);
}
if (!bufferExceeded) {
shellLogInstance.addToBuffer(data);
if (shellLogInstance.logStore.length > maxBuffer) {
bufferExceeded = true;
shellLogInstance.logStore = Buffer.from('[Output truncated - exceeded maxBuffer]');
}
}
});
// Wrap child process termination into a Promise
const childProcessEnded: Promise<IExecResult> = new Promise((resolve, reject) => {
const handleExit = (code: number | null, signal: NodeJS.Signals | null) => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
this.smartexit.removeProcess(execChildProcess);
// Safely unpipe stdin when process ends if passthrough was enabled
if (options.passthrough && !options.interactiveControl) {
try {
if (execChildProcess.stdin && !execChildProcess.stdin.destroyed) {
process.stdin.unpipe(execChildProcess.stdin);
}
} catch (err) {
if (options.debug) {
console.log(`[smartshell] Error unpiping stdin: ${err}`);
}
}
}
const exitCode = typeof code === 'number' ? code : (signal ? 1 : 0);
const execResult: IExecResult = {
exitCode,
stdout: shellLogInstance.logStore.toString(),
signal: signal || undefined,
stderr: stderrBuffer,
};
if (options.strict && exitCode !== 0) {
const errorMsg = signal
? `Command "${options.command}" terminated by signal ${signal}`
: `Command "${options.command}" exited with code ${exitCode}`;
reject(new Error(errorMsg));
} else {
resolve(execResult);
}
};
execChildProcess.once('exit', handleExit);
execChildProcess.once('error', (error) => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
this.smartexit.removeProcess(execChildProcess);
// Safely unpipe stdin when process errors if passthrough was enabled
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
try {
if (!execChildProcess.stdin.destroyed) {
process.stdin.unpipe(execChildProcess.stdin);
}
} catch (err) {
if (options.debug) {
console.log(`[smartshell] Error unpiping stdin on error: ${err}`);
}
}
}
reject(error);
});
});
// If interactive control is enabled but not streaming, return interactive interface
if (options.interactiveControl && !options.streaming) {
return {
exitCode: 0, // Will be updated when process ends
stdout: '', // Will be updated when process ends
sendInput,
sendLine,
endInput,
finalPromise: childProcessEnded,
} as IExecResultInteractive;
}
// If streaming mode is enabled, return a streaming interface
if (options.streaming) {
return {
childProcess: execChildProcess,
finalPromise: childProcessEnded,
sendInput,
sendLine,
endInput,
kill: async () => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with SIGKILL on process ${execChildProcess.pid}`);
}
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGKILL');
},
terminate: async () => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with SIGTERM on process ${execChildProcess.pid}`);
}
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGTERM');
},
keyboardInterrupt: async () => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with SIGINT on process ${execChildProcess.pid}`);
}
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGINT');
},
customSignal: async (signal: plugins.smartexit.TProcessSignal) => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`);
}
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, signal);
},
} as IExecResultStreaming;
}
// For non-streaming mode, wait for the process to complete
return await childProcessEnded;
}
/**
* Executes a command and returns either a non-streaming result or a streaming interface.
*/
private async _execCommand(options: IExecOptions): Promise<IExecResult | IExecResultStreaming | IExecResultInteractive> {
const commandToExecute = this.shellEnv.createEnvExecString(options.commandString);
const shellLogInstance = new ShellLog();
let stderrBuffer = '';
const maxBuffer = options.maxBuffer || 200 * 1024 * 1024; // Default 200MB
let bufferExceeded = false;
// Handle PTY mode if requested
if (options.usePty) {
return await this._execCommandPty(options, commandToExecute, shellLogInstance);
}
const execChildProcess = cp.spawn(commandToExecute, [], {
shell: true,
2021-11-26 15:17:52 +01:00
cwd: process.cwd(),
env: options.env || process.env,
2020-05-22 01:23:27 +00:00
detached: false,
signal: options.signal,
});
2019-05-19 22:41:20 +02:00
this.smartexit.addProcess(execChildProcess);
// Handle timeout
let timeoutHandle: NodeJS.Timeout | null = null;
if (options.timeout) {
timeoutHandle = setTimeout(() => {
if (options.debug) {
console.log(`[smartshell] Timeout reached for process ${execChildProcess.pid}, terminating...`);
}
execChildProcess.kill('SIGTERM');
}, options.timeout);
}
// Connect stdin if passthrough is enabled (but not for interactive control)
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
process.stdin.pipe(execChildProcess.stdin);
}
// Create input methods for interactive control
const sendInput = async (input: string): Promise<void> => {
if (!execChildProcess.stdin) {
throw new Error('stdin is not available for this process');
}
if (execChildProcess.stdin.destroyed || !execChildProcess.stdin.writable) {
throw new Error('stdin has been destroyed or is not writable');
}
return new Promise((resolve, reject) => {
execChildProcess.stdin.write(input, 'utf8', (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
};
const sendLine = async (line: string): Promise<void> => {
return sendInput(line + '\n');
};
const endInput = (): void => {
if (execChildProcess.stdin && !execChildProcess.stdin.destroyed) {
execChildProcess.stdin.end();
}
};
// Capture stdout and stderr output
2020-05-22 01:23:27 +00:00
execChildProcess.stdout.on('data', (data) => {
if (!options.silent) {
shellLogInstance.writeToConsole(data);
}
if (options.onData) {
options.onData(data);
}
if (!bufferExceeded) {
shellLogInstance.addToBuffer(data);
if (shellLogInstance.logStore.length > maxBuffer) {
bufferExceeded = true;
shellLogInstance.logStore = Buffer.from('[Output truncated - exceeded maxBuffer]');
}
}
});
2020-05-22 01:23:27 +00:00
execChildProcess.stderr.on('data', (data) => {
if (!options.silent) {
shellLogInstance.writeToConsole(data);
}
const dataStr = data.toString();
stderrBuffer += dataStr;
if (options.onData) {
options.onData(data);
}
if (!bufferExceeded) {
shellLogInstance.addToBuffer(data);
if (shellLogInstance.logStore.length > maxBuffer) {
bufferExceeded = true;
shellLogInstance.logStore = Buffer.from('[Output truncated - exceeded maxBuffer]');
}
}
});
// Wrap child process termination into a Promise
const childProcessEnded: Promise<IExecResult> = new Promise((resolve, reject) => {
const handleExit = (code: number | null, signal: NodeJS.Signals | null) => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
this.smartexit.removeProcess(execChildProcess);
// Safely unpipe stdin when process ends if passthrough was enabled
if (options.passthrough && !options.interactiveControl) {
try {
if (execChildProcess.stdin && !execChildProcess.stdin.destroyed) {
process.stdin.unpipe(execChildProcess.stdin);
}
} catch (err) {
if (options.debug) {
console.log(`[smartshell] Error unpiping stdin: ${err}`);
}
}
}
const exitCode = typeof code === 'number' ? code : (signal ? 1 : 0);
const execResult: IExecResult = {
exitCode,
stdout: shellLogInstance.logStore.toString(),
signal: signal || undefined,
stderr: stderrBuffer,
};
if (options.strict && exitCode !== 0) {
const errorMsg = signal
? `Command "${options.commandString}" terminated by signal ${signal}`
: `Command "${options.commandString}" exited with code ${exitCode}`;
reject(new Error(errorMsg));
} else {
resolve(execResult);
}
};
execChildProcess.once('exit', handleExit);
execChildProcess.once('error', (error) => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
this.smartexit.removeProcess(execChildProcess);
// Safely unpipe stdin when process errors if passthrough was enabled
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
try {
if (!execChildProcess.stdin.destroyed) {
process.stdin.unpipe(execChildProcess.stdin);
}
} catch (err) {
if (options.debug) {
console.log(`[smartshell] Error unpiping stdin on error: ${err}`);
}
}
}
reject(error);
});
});
// If interactive control is enabled but not streaming, return interactive interface
if (options.interactiveControl && !options.streaming) {
return {
exitCode: 0, // Will be updated when process ends
stdout: '', // Will be updated when process ends
sendInput,
sendLine,
endInput,
finalPromise: childProcessEnded,
} as IExecResultInteractive;
}
// If streaming mode is enabled, return a streaming interface
if (options.streaming) {
return {
2021-07-26 21:24:13 +02:00
childProcess: execChildProcess,
finalPromise: childProcessEnded,
sendInput,
sendLine,
endInput,
2024-04-18 13:42:51 +02:00
kill: async () => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with SIGKILL on process ${execChildProcess.pid}`);
}
2024-04-18 13:42:51 +02:00
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGKILL');
2021-08-17 18:19:52 +02:00
},
2024-04-18 13:42:51 +02:00
terminate: async () => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with SIGTERM on process ${execChildProcess.pid}`);
}
2024-04-18 13:42:51 +02:00
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGTERM');
},
keyboardInterrupt: async () => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with SIGINT on process ${execChildProcess.pid}`);
}
2024-04-18 13:42:51 +02:00
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGINT');
},
customSignal: async (signal: plugins.smartexit.TProcessSignal) => {
if (options.debug) {
console.log(`[smartshell] Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`);
}
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, signal);
2021-07-26 21:24:13 +02:00
},
} as IExecResultStreaming;
2021-07-26 21:24:13 +02:00
}
// For non-streaming mode, wait for the process to complete.
return await childProcessEnded;
2018-07-30 16:08:14 +02:00
}
public async exec(commandString: string): Promise<IExecResult> {
const result = await this._exec({ commandString });
// Type assertion is safe here because non-streaming, non-interactive exec always returns IExecResult
return result as IExecResult;
2018-07-30 16:08:14 +02:00
}
public async execSilent(commandString: string): Promise<IExecResult> {
return (await this._exec({ commandString, silent: true })) as IExecResult;
2018-07-30 16:08:14 +02:00
}
2017-03-10 22:08:04 +01:00
public async execStrict(commandString: string): Promise<IExecResult> {
return (await this._exec({ commandString, strict: true })) as IExecResult;
2019-05-29 10:56:45 +02:00
}
public async execStrictSilent(commandString: string): Promise<IExecResult> {
return (await this._exec({ commandString, silent: true, strict: true })) as IExecResult;
2018-07-30 16:08:14 +02:00
}
2017-03-11 02:36:27 +01:00
public async execStreaming(commandString: string, silent: boolean = false): Promise<IExecResultStreaming> {
return (await this._exec({ commandString, silent, streaming: true })) as IExecResultStreaming;
2018-07-30 16:08:14 +02:00
}
public async execStreamingSilent(commandString: string): Promise<IExecResultStreaming> {
return (await this._exec({ commandString, silent: true, streaming: true })) as IExecResultStreaming;
2018-07-30 16:08:14 +02:00
}
public async execInteractive(commandString: string): Promise<void> {
await this._exec({ commandString, interactive: true });
}
public async execPassthrough(commandString: string): Promise<IExecResult> {
return await this._exec({ commandString, passthrough: true }) as IExecResult;
}
public async execStreamingPassthrough(commandString: string): Promise<IExecResultStreaming> {
return await this._exec({ commandString, streaming: true, passthrough: true }) as IExecResultStreaming;
}
public async execInteractiveControl(commandString: string): Promise<IExecResultInteractive> {
return await this._exec({ commandString, interactiveControl: true }) as IExecResultInteractive;
}
public async execStreamingInteractiveControl(commandString: string): Promise<IExecResultStreaming> {
return await this._exec({ commandString, streaming: true, interactiveControl: true }) as IExecResultStreaming;
}
public async execInteractiveControlPty(commandString: string): Promise<IExecResultInteractive> {
return await this._exec({ commandString, interactiveControl: true, usePty: true }) as IExecResultInteractive;
}
public async execStreamingInteractiveControlPty(commandString: string): Promise<IExecResultStreaming> {
return await this._exec({ commandString, streaming: true, interactiveControl: true, usePty: true }) as IExecResultStreaming;
}
/**
* Executes a command with args array (shell:false) for security
* This is the recommended API for untrusted input
*/
public async execSpawn(command: string, args: string[] = [], options: Omit<ISpawnOptions, 'command' | 'args'> = {}): Promise<IExecResult> {
const result = await this._execSpawn({ command, args, ...options });
// Type assertion is safe here because non-streaming, non-interactive exec always returns IExecResult
return result as IExecResult;
}
/**
* Executes a command with args array in streaming mode
*/
public async execSpawnStreaming(command: string, args: string[] = [], options: Omit<ISpawnOptions, 'command' | 'args' | 'streaming'> = {}): Promise<IExecResultStreaming> {
return await this._execSpawn({ command, args, streaming: true, ...options }) as IExecResultStreaming;
}
/**
* Executes a command with args array with interactive control
*/
public async execSpawnInteractiveControl(command: string, args: string[] = [], options: Omit<ISpawnOptions, 'command' | 'args' | 'interactiveControl'> = {}): Promise<IExecResultInteractive> {
return await this._execSpawn({ command, args, interactiveControl: true, ...options }) as IExecResultInteractive;
}
2023-06-22 14:16:16 +02:00
public async execAndWaitForLine(
commandString: string,
regex: RegExp,
silent: boolean = false,
options: { timeout?: number; terminateOnMatch?: boolean } = {}
): Promise<void> {
const execStreamingResult = await this.execStreaming(commandString, silent);
return new Promise<void>((resolve, reject) => {
let matched = false;
let timeoutHandle: NodeJS.Timeout | null = null;
// Set up timeout if specified
if (options.timeout) {
timeoutHandle = setTimeout(async () => {
if (!matched) {
matched = true;
// Remove listener to prevent memory leak
execStreamingResult.childProcess.stdout.removeAllListeners('data');
await execStreamingResult.terminate();
reject(new Error(`Timeout waiting for pattern after ${options.timeout}ms`));
}
}, options.timeout);
}
const dataHandler = async (chunk: Buffer | string) => {
const data = typeof chunk === 'string' ? chunk : chunk.toString();
if (!matched && regex.test(data)) {
matched = true;
// Clear timeout if set
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
// Remove listener to prevent memory leak
execStreamingResult.childProcess.stdout.removeListener('data', dataHandler);
// Terminate process if requested
if (options.terminateOnMatch) {
await execStreamingResult.terminate();
await execStreamingResult.finalPromise;
}
resolve();
}
};
execStreamingResult.childProcess.stdout.on('data', dataHandler);
// Also resolve/reject when process ends
execStreamingResult.finalPromise.then(() => {
if (!matched) {
matched = true;
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
reject(new Error('Process ended without matching pattern'));
}
}).catch((err) => {
if (!matched) {
matched = true;
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
reject(err);
}
});
});
2018-07-30 16:08:14 +02:00
}
public async execAndWaitForLineSilent(commandString: string, regex: RegExp, options?: { timeout?: number; terminateOnMatch?: boolean }): Promise<void> {
return this.execAndWaitForLine(commandString, regex, true, options);
2019-05-28 10:43:54 +02:00
}
private nodePty: any = null;
private async lazyLoadNodePty(): Promise<any> {
if (this.nodePty) {
return this.nodePty;
}
try {
// Try to load node-pty if available
// @ts-ignore - node-pty is optional
this.nodePty = await import('node-pty');
return this.nodePty;
} catch (error) {
throw new Error(
'node-pty is required for PTY support but is not installed.\n' +
'Please install it as an optional dependency:\n' +
' pnpm add --save-optional node-pty\n' +
'Note: node-pty requires compilation and may have platform-specific requirements.'
);
}
}
private async _execCommandPty(
options: IExecOptions,
commandToExecute: string,
shellLogInstance: ShellLog
): Promise<IExecResult | IExecResultStreaming | IExecResultInteractive> {
const pty = await this.lazyLoadNodePty();
// Platform-aware shell selection
let shell: string;
let shellArgs: string[];
if (options.ptyShell) {
// User-provided shell override
shell = options.ptyShell;
shellArgs = ['-c', commandToExecute];
} else if (process.platform === 'win32') {
// Windows: Use PowerShell by default, or cmd as fallback
const powershell = process.env.PROGRAMFILES
? `${process.env.PROGRAMFILES}\\PowerShell\\7\\pwsh.exe`
: 'powershell.exe';
// Check if PowerShell Core exists, otherwise use Windows PowerShell
const fs = await import('fs');
if (fs.existsSync(powershell)) {
shell = powershell;
shellArgs = ['-NoProfile', '-NonInteractive', '-Command', commandToExecute];
} else if (process.env.COMSPEC) {
shell = process.env.COMSPEC;
shellArgs = ['/d', '/s', '/c', commandToExecute];
} else {
shell = 'cmd.exe';
shellArgs = ['/d', '/s', '/c', commandToExecute];
}
} else {
// POSIX: Use SHELL env var or bash as default
shell = process.env.SHELL || '/bin/bash';
shellArgs = ['-c', commandToExecute];
}
// Create PTY process
const ptyProcess = pty.spawn(shell, shellArgs, {
name: options.ptyTerm || 'xterm-256color',
cols: options.ptyCols || 120,
rows: options.ptyRows || 30,
cwd: process.cwd(),
env: options.env || process.env,
});
// Add to smartexit (wrap in a minimal object with pid)
this.smartexit.addProcess({ pid: ptyProcess.pid } as any);
// Handle output (stdout and stderr are combined in PTY)
ptyProcess.onData((data: string) => {
if (!options.silent) {
shellLogInstance.writeToConsole(data);
}
shellLogInstance.addToBuffer(data);
});
// Wrap PTY termination into a Promise
const childProcessEnded: Promise<IExecResult> = new Promise((resolve, reject) => {
ptyProcess.onExit(({ exitCode, signal }: { exitCode: number; signal?: number }) => {
this.smartexit.removeProcess({ pid: ptyProcess.pid } as any);
const execResult: IExecResult = {
exitCode: exitCode ?? (signal ? 1 : 0),
stdout: shellLogInstance.logStore.toString(),
};
if (options.strict && exitCode !== 0) {
reject(new Error(`Command "${options.commandString}" exited with code ${exitCode}`));
} else {
resolve(execResult);
}
});
});
// Create input methods for PTY
const sendInput = async (input: string): Promise<void> => {
return new Promise((resolve, reject) => {
try {
ptyProcess.write(input);
resolve();
} catch (error) {
reject(error);
}
});
};
const sendLine = async (line: string): Promise<void> => {
// Use \r for PTY (carriage return is typical for terminal line discipline)
return sendInput(line + '\r');
};
const endInput = (): void => {
// Send EOF (Ctrl+D) to PTY
ptyProcess.write('\x04');
};
// If interactive control is enabled but not streaming, return interactive interface
if (options.interactiveControl && !options.streaming) {
return {
exitCode: 0, // Will be updated when process ends
stdout: '', // Will be updated when process ends
sendInput,
sendLine,
endInput,
finalPromise: childProcessEnded,
} as IExecResultInteractive;
}
// If streaming mode is enabled, return a streaming interface
if (options.streaming) {
return {
childProcess: { pid: ptyProcess.pid } as any, // Minimal compatibility object
finalPromise: childProcessEnded,
sendInput,
sendLine,
endInput,
kill: async () => {
if (options.debug) {
console.log(`[smartshell] Killing PTY process ${ptyProcess.pid}`);
}
ptyProcess.kill();
},
terminate: async () => {
if (options.debug) {
console.log(`[smartshell] Terminating PTY process ${ptyProcess.pid}`);
}
ptyProcess.kill('SIGTERM');
},
keyboardInterrupt: async () => {
if (options.debug) {
console.log(`[smartshell] Sending SIGINT to PTY process ${ptyProcess.pid}`);
}
ptyProcess.kill('SIGINT');
},
customSignal: async (signal: plugins.smartexit.TProcessSignal) => {
if (options.debug) {
console.log(`[smartshell] Sending ${signal} to PTY process ${ptyProcess.pid}`);
}
ptyProcess.kill(signal as any);
},
} as IExecResultStreaming;
}
// For non-streaming mode, wait for the process to complete
return await childProcessEnded;
}
}