@@ -3,20 +3,24 @@ package shell
10// progressWriter wraps an io.Writer and calls onProgress with each write.
10// progressWriter wraps an io.Writer and calls onProgress with each write.
11+// It is safe for concurrent use from multiple goroutines (e.g. stdout and
12+// stderr writing simultaneously).
11type progressWriter struct {
13type progressWriter struct {
12 buf bytes.Buffer
15 buf bytes.Buffer
13 onProgress func(string)
16 onProgress func(string)
16func (w *progressWriter) Write(p []byte) (int, error) {
19func (w *progressWriter) Write(p []byte) (int, error) {
17 n, err := w.buf.Write(p)
22 n, err := w.buf.Write(p)
18 if n > 0 && w.onProgress != nil {
23 if n > 0 && w.onProgress != nil {
19− slog.Debug("Shell stream progress", "bytes", n)
20 w.onProgress(string(p[:n]))
24 w.onProgress(string(p[:n]))
22 return n, err
26 return n, err
@@ -30,10 +34,9 @@ func RunAndCaptureStream(ctx context.Context, opts RunOptions, onProgress func(s
31 opts.Env = append(opts.Env, ptyColorEnvVars...)
35 opts.Env = append(opts.Env, ptyColorEnvVars...)
33− stdout := &progressWriter{onProgress: onProgress}
34− stderr := &progressWriter{onProgress: onProgress}
37+ buf := &progressWriter{onProgress: onProgress}
35− opts.Stdout = stdout
38+ opts.Stdout = buf
36− opts.Stderr = stderr
39+ opts.Stderr = buf
38 runErr := Run(ctx, opts)
41 runErr := Run(ctx, opts)
@@ -42,16 +45,8 @@ func RunAndCaptureStream(ctx context.Context, opts RunOptions, onProgress func(s
42 exitCode = ExitCode(runErr)
45 exitCode = ExitCode(runErr)
45− output := stdout.buf.String()
46− if stderr.buf.Len() > 0 {
50− output += stderr.buf.String()
53 return CaptureResult{
48 return CaptureResult{
54− Output: output,
49+ Output: buf.buf.String(),
55 ExitCode: exitCode,
50 ExitCode: exitCode,