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'; import * as cp from 'child_process'; // -- interfaces -- export interface IExecResult { exitCode: number; stdout: string; /** stdout currently preserves smartshell's legacy combined stdout/stderr buffer. */ combinedOutput?: string; signal?: NodeJS.Signals; stderr?: string; } export class SmartshellError extends Error { public command: string; public result: IExecResult; public exitCode: number; public stdout: string; public combinedOutput?: string; public stderr?: string; public signal?: NodeJS.Signals; constructor(command: string, result: IExecResult) { const reason = result.signal ? `terminated by signal ${result.signal}` : `exited with code ${result.exitCode}`; super(`Command "${command}" ${reason}`); this.name = 'SmartshellError'; this.command = command; this.result = result; this.exitCode = result.exitCode; this.stdout = result.stdout; this.combinedOutput = result.combinedOutput; this.stderr = result.stderr; this.signal = result.signal; } } export interface IExecResultInteractive extends IExecResult { sendInput: (input: string) => Promise; sendLine: (line: string) => Promise; endInput: () => void; finalPromise: Promise; } export interface IExecResultStreaming { childProcess: cp.ChildProcess; finalPromise: Promise; kill: () => Promise; terminate: () => Promise; keyboardInterrupt: () => Promise; customSignal: (signal: plugins.smartexit.TProcessSignal) => Promise; sendInput: (input: string) => Promise; sendLine: (line: string) => Promise; endInput: () => void; } export interface IExecRuntimeOptions { ptyCols?: number; ptyRows?: number; ptyTerm?: string; ptyShell?: string; maxBuffer?: number; onData?: (chunk: Buffer | string) => void; timeout?: number; debug?: boolean; env?: NodeJS.ProcessEnv; cwd?: string; signal?: AbortSignal; } interface IExecOptions extends IExecRuntimeOptions { commandString: string; silent?: boolean; strict?: boolean; streaming?: boolean; interactive?: boolean; passthrough?: boolean; interactiveControl?: boolean; usePty?: boolean; } export interface ISpawnOptions extends IExecRuntimeOptions { command: string; args?: string[]; silent?: boolean; strict?: boolean; streaming?: boolean; interactive?: boolean; passthrough?: boolean; interactiveControl?: boolean; usePty?: boolean; /** * When set to `inherit`, the child process uses the parent terminal directly. * This is useful for trusted interactive CLIs while still avoiding shell parsing. */ stdio?: 'pipe' | 'inherit'; } export type TExecCommandOptions = IExecRuntimeOptions; export class Smartshell { public shellEnv: ShellEnv; public smartexit = new plugins.smartexit.SmartExit(); constructor(optionsArg: IShellEnvContructorOptions) { this.shellEnv = new ShellEnv(optionsArg); } /** * Executes a given command asynchronously. */ private async _exec(options: IExecOptions): Promise { if (options.interactive) { return await this._execInteractive({ commandString: options.commandString, cwd: options.cwd, env: options.env, }); } return await this._execCommand(options); } /** * Executes an interactive command. */ private async _execInteractive(options: Pick): Promise { // Skip interactive execution in CI environments. if (process.env.CI) { return; } return new Promise((resolve) => { const shell = cp.spawn(options.commandString, { stdio: 'inherit', shell: true, detached: true, cwd: options.cwd || process.cwd(), env: options.env || process.env, }); 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 { const shellLogInstance = new ShellLog(); let stderrBuffer = ''; const maxBuffer = options.maxBuffer || 200 * 1024 * 1024; // Default 200MB let bufferExceeded = false; const inheritStdio = options.stdio === 'inherit'; // 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.'); } if (inheritStdio && (options.streaming || options.interactiveControl)) { throw new Error('execSpawn stdio: inherit cannot be combined with streaming or interactiveControl.'); } const execChildProcess = cp.spawn(options.command, options.args || [], { shell: false, // SECURITY: Never use shell with untrusted input cwd: options.cwd || process.cwd(), env: options.env || process.env, stdio: inheritStdio ? 'inherit' : 'pipe', detached: inheritStdio ? false : true, // Inherited TTY needs normal terminal signal handling. 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...`); } void this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug); }, options.timeout); } // Connect stdin if passthrough is enabled (but not for interactive control) if (!inheritStdio && options.passthrough && !options.interactiveControl && execChildProcess.stdin) { process.stdin.pipe(execChildProcess.stdin); } // Create input methods for interactive control const sendInput = async (input: string): Promise => { if (!execChildProcess.stdin) { throw new Error('stdin is not available for this process'); } const childStdin = execChildProcess.stdin; if (childStdin.destroyed || !childStdin.writable) { throw new Error('stdin has been destroyed or is not writable'); } return new Promise((resolve, reject) => { childStdin.write(input, 'utf8', (error) => { if (error) { reject(error); } else { resolve(); } }); }); }; const sendLine = async (line: string): Promise => { 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.logLength > 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.logLength > maxBuffer) { bufferExceeded = true; shellLogInstance.logStore = Buffer.from('[Output truncated - exceeded maxBuffer]'); } } }); // Wrap child process termination into a Promise const childProcessEnded: Promise = 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 (!inheritStdio && 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 combinedOutput = shellLogInstance.logStore.toString(); const execResult: IExecResult = { exitCode, stdout: combinedOutput, combinedOutput, signal: signal || undefined, stderr: stderrBuffer, }; if (options.strict && exitCode !== 0) { reject(new SmartshellError(options.command, execResult)); } else { resolve(execResult); } }; execChildProcess.once('close', handleExit); execChildProcess.once('error', (error) => { if (timeoutHandle) { clearTimeout(timeoutHandle); } this.smartexit.removeProcess(execChildProcess); // Safely unpipe stdin when process errors if passthrough was enabled if (!inheritStdio && 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 this.killProcessTree(execChildProcess.pid, 'SIGKILL', options.debug); }, terminate: async () => { if (options.debug) { console.log(`[smartshell] Running tree kill with SIGTERM on process ${execChildProcess.pid}`); } await this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug); }, keyboardInterrupt: async () => { if (options.debug) { console.log(`[smartshell] Running tree kill with SIGINT on process ${execChildProcess.pid}`); } await this.killProcessTree(execChildProcess.pid, 'SIGINT', options.debug); }, customSignal: async (signal: plugins.smartexit.TProcessSignal) => { if (options.debug) { console.log(`[smartshell] Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`); } await this.killProcessTree(execChildProcess.pid, signal, options.debug); }, } 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 { 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); } // Use the executor's shell binary directly to avoid triple shell nesting. // Previously: Node spawn(shell:true) → /bin/sh → bash -c → command (3 layers) // Now: Node spawn(shell:bash) → command (1 layer) const shellBinary = this.shellEnv.executor === 'bash' ? '/bin/bash' : true; const execChildProcess = cp.spawn(commandToExecute, [], { shell: shellBinary, cwd: options.cwd || process.cwd(), env: options.env || process.env, detached: true, // Own process group — immune to terminal SIGINT, managed by smartexit 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...`); } void this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug); }, 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 => { 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 => { 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.logLength > 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.logLength > maxBuffer) { bufferExceeded = true; shellLogInstance.logStore = Buffer.from('[Output truncated - exceeded maxBuffer]'); } } }); // Wrap child process termination into a Promise const childProcessEnded: Promise = 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 combinedOutput = shellLogInstance.logStore.toString(); const execResult: IExecResult = { exitCode, stdout: combinedOutput, combinedOutput, signal: signal || undefined, stderr: stderrBuffer, }; if (options.strict && exitCode !== 0) { reject(new SmartshellError(options.commandString, execResult)); } else { resolve(execResult); } }; execChildProcess.once('close', 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 this.killProcessTree(execChildProcess.pid, 'SIGKILL', options.debug); }, terminate: async () => { if (options.debug) { console.log(`[smartshell] Running tree kill with SIGTERM on process ${execChildProcess.pid}`); } await this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug); }, keyboardInterrupt: async () => { if (options.debug) { console.log(`[smartshell] Running tree kill with SIGINT on process ${execChildProcess.pid}`); } await this.killProcessTree(execChildProcess.pid, 'SIGINT', options.debug); }, customSignal: async (signal: plugins.smartexit.TProcessSignal) => { if (options.debug) { console.log(`[smartshell] Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`); } await this.killProcessTree(execChildProcess.pid, signal, options.debug); }, } as IExecResultStreaming; } // For non-streaming mode, wait for the process to complete. return await childProcessEnded; } private async killProcessTree( pid: number | undefined, signal: plugins.smartexit.TProcessSignal, debug?: boolean, ): Promise { if (!pid) { return; } if (debug) { console.log(`[smartshell] Killing process tree ${pid} with ${signal}`); } try { await plugins.smartexit.SmartExit.killTreeByPid(pid, signal); } catch (error) { if (debug) { console.log(`[smartshell] Tree kill failed for ${pid}: ${error}`); } try { process.kill(pid, signal as NodeJS.Signals); } catch { // Process already exited or is not accessible. } } } public async exec(commandString: string, options: TExecCommandOptions = {}): Promise { const result = await this._exec({ commandString, ...options }); // Type assertion is safe here because non-streaming, non-interactive exec always returns IExecResult return result as IExecResult; } public async execSilent(commandString: string, options: TExecCommandOptions = {}): Promise { return (await this._exec({ commandString, ...options, silent: true })) as IExecResult; } public async execStrict(commandString: string, options: TExecCommandOptions = {}): Promise { return (await this._exec({ commandString, ...options, strict: true })) as IExecResult; } public async execStrictSilent(commandString: string, options: TExecCommandOptions = {}): Promise { return (await this._exec({ commandString, ...options, silent: true, strict: true })) as IExecResult; } public async execStreaming( commandString: string, silent: boolean = false, options: TExecCommandOptions = {}, ): Promise { return (await this._exec({ commandString, ...options, silent, streaming: true })) as IExecResultStreaming; } public async execStreamingSilent(commandString: string, options: TExecCommandOptions = {}): Promise { return (await this._exec({ commandString, ...options, silent: true, streaming: true })) as IExecResultStreaming; } public async execInteractive(commandString: string, options: TExecCommandOptions = {}): Promise { await this._exec({ commandString, ...options, interactive: true }); } public async execPassthrough(commandString: string, options: TExecCommandOptions = {}): Promise { return await this._exec({ commandString, ...options, passthrough: true }) as IExecResult; } public async execStreamingPassthrough(commandString: string, options: TExecCommandOptions = {}): Promise { return await this._exec({ commandString, ...options, streaming: true, passthrough: true }) as IExecResultStreaming; } public async execInteractiveControl(commandString: string, options: TExecCommandOptions = {}): Promise { return await this._exec({ commandString, ...options, interactiveControl: true }) as IExecResultInteractive; } public async execStreamingInteractiveControl(commandString: string, options: TExecCommandOptions = {}): Promise { return await this._exec({ commandString, ...options, streaming: true, interactiveControl: true }) as IExecResultStreaming; } public async execInteractiveControlPty(commandString: string, options: TExecCommandOptions = {}): Promise { return await this._exec({ commandString, ...options, interactiveControl: true, usePty: true }) as IExecResultInteractive; } public async execStreamingInteractiveControlPty(commandString: string, options: TExecCommandOptions = {}): Promise { return await this._exec({ commandString, ...options, 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 = {}): Promise { 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 = {}): Promise { return await this._execSpawn({ command, args, ...options, streaming: true }) as IExecResultStreaming; } /** * Executes a command with args array with interactive control */ public async execSpawnInteractiveControl(command: string, args: string[] = [], options: Omit = {}): Promise { return await this._execSpawn({ command, args, ...options, interactiveControl: true }) as IExecResultInteractive; } public async execAndWaitForLine( commandString: string, regex: RegExp, silent: boolean = false, options: { timeout?: number; terminateOnMatch?: boolean; cwd?: string; env?: NodeJS.ProcessEnv } = {} ): Promise { const execStreamingResult = await this.execStreaming(commandString, silent, { cwd: options.cwd, env: options.env, }); const stdout = execStreamingResult.childProcess.stdout; if (!stdout) { await execStreamingResult.terminate(); throw new Error('stdout is not available for this process'); } return new Promise((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 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 stdout.removeListener('data', dataHandler); // Terminate process if requested if (options.terminateOnMatch) { await execStreamingResult.terminate(); await execStreamingResult.finalPromise; } resolve(); } }; 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); } }); }); } public async execAndWaitForLineSilent(commandString: string, regex: RegExp, options?: { timeout?: number; terminateOnMatch?: boolean; cwd?: string; env?: NodeJS.ProcessEnv }): Promise { return this.execAndWaitForLine(commandString, regex, true, options); } private nodePty: any = null; private async lazyLoadNodePty(): Promise { 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 { 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: options.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); }); let timeoutHandle: NodeJS.Timeout | null = null; if (options.timeout) { timeoutHandle = setTimeout(() => { if (options.debug) { console.log(`[smartshell] Timeout reached for PTY process ${ptyProcess.pid}, terminating...`); } ptyProcess.kill('SIGTERM'); }, options.timeout); } // Wrap PTY termination into a Promise const childProcessEnded: Promise = new Promise((resolve, reject) => { ptyProcess.onExit(({ exitCode, signal }: { exitCode: number; signal?: number }) => { if (timeoutHandle) { clearTimeout(timeoutHandle); } this.smartexit.removeProcess({ pid: ptyProcess.pid } as any); const combinedOutput = shellLogInstance.logStore.toString(); const execResult: IExecResult = { exitCode: exitCode ?? (signal ? 1 : 0), stdout: combinedOutput, combinedOutput, }; if (options.strict && exitCode !== 0) { reject(new SmartshellError(options.commandString, execResult)); } else { resolve(execResult); } }); }); // Create input methods for PTY const sendInput = async (input: string): Promise => { return new Promise((resolve, reject) => { try { ptyProcess.write(input); resolve(); } catch (error) { reject(error); } }); }; const sendLine = async (line: string): Promise => { // 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; } }