55
66import type { AgentFrontmatter } from "./types" ;
77import { basename } from "path" ;
8+ import { teeToStdoutAndCollect , teeToStderrAndCollect } from "./stream" ;
89
910/**
1011 * Module-level reference to the current child process
@@ -177,6 +178,14 @@ export function extractEnvVars(frontmatter: AgentFrontmatter): Record<string, st
177178 return undefined ;
178179}
179180
181+ /**
182+ * Output capture mode for runCommand
183+ * - "none": Inherit stdout/stderr, no capture (streaming to terminal)
184+ * - "capture": Pipe and buffer output, print after completion
185+ * - "tee": Tee streams - simultaneous display and capture (best of both)
186+ */
187+ export type CaptureMode = "none" | "capture" | "tee" ;
188+
180189export interface RunContext {
181190 /** The command to execute */
182191 command : string ;
@@ -186,33 +195,67 @@ export interface RunContext {
186195 positionals : string [ ] ;
187196 /** Positional mappings ($1 → flag name) */
188197 positionalMappings : Map < number , string > ;
189- /** Whether to capture output */
190- captureOutput : boolean ;
198+ /**
199+ * Whether to capture output (legacy boolean) or capture mode
200+ * - false / "none": inherit stdout, no capture
201+ * - true / "capture": pipe and buffer, print after completion
202+ * - "tee": stream to stdout while capturing (simultaneous display + capture)
203+ */
204+ captureOutput : boolean | CaptureMode ;
191205 /** Environment variables to add */
192206 env ?: Record < string , string > ;
207+ /**
208+ * Whether to also capture stderr (only applies when captureOutput is enabled)
209+ * Default: false (stderr goes to inherit)
210+ */
211+ captureStderr ?: boolean ;
193212}
194213
195214export interface RunResult {
196215 exitCode : number ;
216+ /** Captured stdout content (empty string if not capturing) */
217+ stdout : string ;
218+ /** Captured stderr content (empty string if not capturing stderr) */
219+ stderr : string ;
220+ /**
221+ * @deprecated Use `stdout` instead. Kept for backward compatibility.
222+ */
197223 output : string ;
198224 /** The subprocess reference for signal handling */
199225 process : ReturnType < typeof Bun . spawn > ;
200226}
201227
228+ /**
229+ * Normalize capture mode from boolean or string to CaptureMode
230+ */
231+ function normalizeCaptureMode ( mode : boolean | CaptureMode ) : CaptureMode {
232+ if ( mode === true ) return "capture" ;
233+ if ( mode === false ) return "none" ;
234+ return mode ;
235+ }
236+
202237/**
203238 * Execute command with positional arguments
204239 * Positionals are either passed as-is or mapped to flags via $N mappings
240+ *
241+ * Capture modes:
242+ * - "none": Inherit stdout/stderr (streaming to terminal, no capture)
243+ * - "capture": Pipe and buffer output, print after completion
244+ * - "tee": Stream to stdout/stderr while capturing (simultaneous display + capture)
205245 */
206246export async function runCommand ( ctx : RunContext ) : Promise < RunResult > {
207- const { command, args, positionals, positionalMappings, captureOutput, env } = ctx ;
247+ const { command, args, positionals, positionalMappings, captureOutput, env, captureStderr = false } = ctx ;
248+
249+ const mode = normalizeCaptureMode ( captureOutput ) ;
208250
209251 // Pre-flight check: verify the command exists
210252 const binaryPath = Bun . which ( command ) ;
211253 if ( ! binaryPath ) {
212254 console . error ( `Command not found: '${ command } '` ) ;
213255 console . error ( `This agent requires '${ command } ' to be installed and available in your PATH.` ) ;
214256 console . error ( `Please install it and try again.` ) ;
215- return { exitCode : 127 , output : "" } ; // 127 = command not found
257+ // Return empty process-like object for backward compatibility
258+ return { exitCode : 127 , stdout : "" , stderr : "" , output : "" , process : null as unknown as ReturnType < typeof Bun . spawn > } ;
216259 }
217260
218261 // Build final command args
@@ -238,27 +281,71 @@ export async function runCommand(ctx: RunContext): Promise<RunResult> {
238281 ? { ...process . env , ...env }
239282 : undefined ;
240283
284+ // Determine stdout/stderr pipe config based on mode
285+ const shouldPipeStdout = mode === "capture" || mode === "tee" ;
286+ const shouldPipeStderr = ( mode === "capture" || mode === "tee" ) && captureStderr ;
287+
241288 const proc = Bun . spawn ( [ command , ...finalArgs ] , {
242- stdout : captureOutput ? "pipe" : "inherit" ,
243- stderr : "inherit" ,
289+ stdout : shouldPipeStdout ? "pipe" : "inherit" ,
290+ stderr : shouldPipeStderr ? "pipe" : "inherit" ,
244291 stdin : "inherit" ,
245292 env : runEnv ,
246293 } ) ;
247294
248295 // Store reference for signal handling
249296 currentChildProcess = proc ;
250297
251- let output = "" ;
252- if ( captureOutput && proc . stdout ) {
253- output = await new Response ( proc . stdout ) . text ( ) ;
254- // Still print to console so user sees it
255- console . log ( output ) ;
298+ let stdout = "" ;
299+ let stderr = "" ;
300+
301+ // Handle output based on mode
302+ if ( mode === "tee" ) {
303+ // Tee mode: stream to console while capturing
304+ const promises : Promise < void > [ ] = [ ] ;
305+
306+ if ( proc . stdout ) {
307+ promises . push (
308+ teeToStdoutAndCollect ( proc . stdout ) . then ( ( content ) => {
309+ stdout = content ;
310+ } )
311+ ) ;
312+ }
313+
314+ if ( proc . stderr && shouldPipeStderr ) {
315+ promises . push (
316+ teeToStderrAndCollect ( proc . stderr ) . then ( ( content ) => {
317+ stderr = content ;
318+ } )
319+ ) ;
320+ }
321+
322+ await Promise . all ( promises ) ;
323+ } else if ( mode === "capture" ) {
324+ // Capture mode: buffer then print
325+ if ( proc . stdout ) {
326+ stdout = await new Response ( proc . stdout ) . text ( ) ;
327+ // Still print to console so user sees it
328+ console . log ( stdout ) ;
329+ }
330+
331+ if ( proc . stderr && shouldPipeStderr ) {
332+ stderr = await new Response ( proc . stderr ) . text ( ) ;
333+ // Print stderr to console
334+ console . error ( stderr ) ;
335+ }
256336 }
337+ // mode === "none": stdout/stderr are inherited, nothing to capture
257338
258339 const exitCode = await proc . exited ;
259340
260341 // Clear reference after process exits
261342 currentChildProcess = null ;
262343
263- return { exitCode, output, process : proc } ;
344+ return {
345+ exitCode,
346+ stdout,
347+ stderr,
348+ output : stdout , // backward compatibility
349+ process : proc ,
350+ } ;
264351}
0 commit comments