@@ -11,15 +11,9 @@ export interface CommandResult {
1111export interface CommandOptions extends SpawnOptions {
1212 timeout ?: number ;
1313 maxBuffer ?: number ;
14+ onData ?: ( chunk : string , type : "stdout" | "stderr" ) => void ;
1415}
1516
16- /**
17- * Executes a command securely using spawn.
18- * @param command The command to execute (e.g., 'tflocal').
19- * @param args An array of string arguments.
20- * @param options Spawn options including cwd, env, timeout, etc.
21- * @returns A promise that resolves with the command result.
22- */
2317export function runCommand (
2418 command : string ,
2519 args : string [ ] ,
@@ -29,45 +23,75 @@ export function runCommand(
2923 const {
3024 timeout = DEFAULT_COMMAND_TIMEOUT ,
3125 maxBuffer = DEFAULT_COMMAND_MAX_BUFFER ,
26+ onData,
3227 ...spawnOptions
3328 } = options ;
3429
35- const child = spawn ( command , args , {
36- ...spawnOptions ,
37- timeout,
38- } ) ;
30+ const child = spawn ( command , args , { ...spawnOptions } ) ;
3931
4032 let stdout = "" ;
4133 let stderr = "" ;
34+ let outBytes = 0 ;
35+ let errBytes = 0 ;
36+ let timedOut = false ;
37+ let bufferExceeded = false ;
38+ let error : Error | undefined ;
4239
43- child . stdout ?. on ( "data" , ( data ) => {
44- stdout += data . toString ( ) ;
45- } ) ;
40+ const killProcess = ( reason : string ) => {
41+ if ( child . killed ) return ;
42+ error = new Error ( reason ) ;
43+ child . kill ( spawnOptions . killSignal ?? "SIGTERM" ) ;
44+ setTimeout ( ( ) => {
45+ if ( ! child . killed ) child . kill ( "SIGKILL" ) ;
46+ } , 2000 ) ;
47+ } ;
4648
47- child . stderr ?. on ( "data" , ( data ) => {
48- stderr += data . toString ( ) ;
49- } ) ;
49+ const timer = setTimeout ( ( ) => {
50+ timedOut = true ;
51+ killProcess ( `Command timed out after ${ timeout } ms` ) ;
52+ } , timeout ) ;
53+
54+ const onChunk = ( isStdout : boolean ) => ( chunk : Buffer ) => {
55+ if ( timedOut || bufferExceeded ) return ;
56+ const len = chunk . length ;
57+ if ( isStdout ) {
58+ outBytes += len ;
59+ const data = chunk . toString ( ) ;
60+ stdout += data ;
61+ if ( onData ) onData ( data , "stdout" ) ;
62+ if ( outBytes > maxBuffer ) {
63+ bufferExceeded = true ;
64+ killProcess ( `stdout exceeded maxBuffer size of ${ maxBuffer } bytes` ) ;
65+ }
66+ } else {
67+ errBytes += len ;
68+ const data = chunk . toString ( ) ;
69+ stderr += data ;
70+ if ( onData ) onData ( data , "stderr" ) ;
71+ if ( errBytes > maxBuffer ) {
72+ bufferExceeded = true ;
73+ killProcess ( `stderr exceeded maxBuffer size of ${ maxBuffer } bytes` ) ;
74+ }
75+ }
76+ } ;
77+
78+ child . stdout ?. on ( "data" , onChunk ( true ) ) ;
79+ child . stderr ?. on ( "data" , onChunk ( false ) ) ;
5080
51- child . on ( "error" , ( error ) => {
52- resolve ( { stdout , stderr , error, exitCode : child . exitCode } ) ;
81+ child . on ( "error" , ( err ) => {
82+ error = err ;
5383 } ) ;
5484
5585 child . on ( "close" , ( code ) => {
56- let error : Error | undefined ;
57- if ( code !== 0 ) {
86+ clearTimeout ( timer ) ;
87+ if ( ! timedOut && ! bufferExceeded && code !== 0 && ! error ) {
5888 error = new Error ( `Command failed with exit code ${ code } : ${ stderr . trim ( ) } ` ) ;
5989 }
6090 resolve ( { stdout, stderr, error, exitCode : code } ) ;
6191 } ) ;
6292 } ) ;
6393}
6494
65- /**
66- * Strip ANSI escape codes from command output for clean display.
67- * This is the exact same function from the original deployment-utils.ts, now centralized.
68- * @param text The text containing ANSI escape codes.
69- * @returns Cleaned text without ANSI codes.
70- */
7195export function stripAnsiCodes ( text : string ) : string {
7296 return text . replace ( / \x1b \[ [ 0 - 9 ; ] * [ a - z A - Z ] / g, "" ) ;
7397}
0 commit comments