feat(spawn): support inherited stdio
This commit is contained in:
@@ -93,6 +93,11 @@ export interface ISpawnOptions extends IExecRuntimeOptions {
|
||||
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;
|
||||
@@ -155,17 +160,23 @@ export class Smartshell {
|
||||
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,
|
||||
detached: true, // Own process group — immune to terminal SIGINT, managed by smartexit
|
||||
stdio: inheritStdio ? 'inherit' : 'pipe',
|
||||
detached: inheritStdio ? false : true, // Inherited TTY needs normal terminal signal handling.
|
||||
signal: options.signal,
|
||||
});
|
||||
|
||||
@@ -183,7 +194,7 @@ export class Smartshell {
|
||||
}
|
||||
|
||||
// Connect stdin if passthrough is enabled (but not for interactive control)
|
||||
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
if (!inheritStdio && options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
process.stdin.pipe(execChildProcess.stdin);
|
||||
}
|
||||
|
||||
@@ -192,11 +203,12 @@ export class Smartshell {
|
||||
if (!execChildProcess.stdin) {
|
||||
throw new Error('stdin is not available for this process');
|
||||
}
|
||||
if (execChildProcess.stdin.destroyed || !execChildProcess.stdin.writable) {
|
||||
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) => {
|
||||
execChildProcess.stdin.write(input, 'utf8', (error) => {
|
||||
childStdin.write(input, 'utf8', (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
@@ -217,7 +229,7 @@ export class Smartshell {
|
||||
};
|
||||
|
||||
// Capture stdout and stderr output
|
||||
execChildProcess.stdout.on('data', (data) => {
|
||||
execChildProcess.stdout?.on('data', (data) => {
|
||||
if (!options.silent) {
|
||||
shellLogInstance.writeToConsole(data);
|
||||
}
|
||||
@@ -235,7 +247,7 @@ export class Smartshell {
|
||||
}
|
||||
});
|
||||
|
||||
execChildProcess.stderr.on('data', (data) => {
|
||||
execChildProcess.stderr?.on('data', (data) => {
|
||||
if (!options.silent) {
|
||||
shellLogInstance.writeToConsole(data);
|
||||
}
|
||||
@@ -265,7 +277,7 @@ export class Smartshell {
|
||||
this.smartexit.removeProcess(execChildProcess);
|
||||
|
||||
// Safely unpipe stdin when process ends if passthrough was enabled
|
||||
if (options.passthrough && !options.interactiveControl) {
|
||||
if (!inheritStdio && options.passthrough && !options.interactiveControl) {
|
||||
try {
|
||||
if (execChildProcess.stdin && !execChildProcess.stdin.destroyed) {
|
||||
process.stdin.unpipe(execChildProcess.stdin);
|
||||
@@ -302,7 +314,7 @@ export class Smartshell {
|
||||
this.smartexit.removeProcess(execChildProcess);
|
||||
|
||||
// Safely unpipe stdin when process errors if passthrough was enabled
|
||||
if (options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
if (!inheritStdio && options.passthrough && !options.interactiveControl && execChildProcess.stdin) {
|
||||
try {
|
||||
if (!execChildProcess.stdin.destroyed) {
|
||||
process.stdin.unpipe(execChildProcess.stdin);
|
||||
|
||||
Reference in New Issue
Block a user