@@ -47,24 +47,28 @@ export const runCommandWithExitCodes = <E>(
4747 yield * _ ( ensureExitCode ( numericExitCode , okExitCodes , onFailure ) )
4848 } )
4949
50- // CHANGE: run a command and return the exit code
51- // WHY: enable status checks without throwing on non-zero exits
50+ // CHANGE: run a command and return the exit code, draining stdout/stderr to prevent buffer deadlock
51+ // WHY: piped stdout/stderr fill the OS buffer (~64 KB) causing the child process to hang indefinitely
5252// QUOTE(ТЗ): "система авторизации"
5353// REF: user-request-2026-01-28-auth
5454// SOURCE: n/a
5555// FORMAT THEOREM: forall cmd: exitCode(cmd) = n
5656// PURITY: SHELL
5757// EFFECT: Effect<number, PlatformError, CommandExecutor>
58- // INVARIANT: stdout/stderr are suppressed for status checks
58+ // INVARIANT: stdout/stderr are drained asynchronously so the child process never blocks
5959// COMPLEXITY: O(command)
6060export const runCommandExitCode = (
6161 spec : RunCommandSpec
6262) : Effect . Effect < number , PlatformError , CommandExecutor . CommandExecutor > =>
63- Effect . map (
64- Command . exitCode (
65- buildCommand ( spec , "pipe" , "pipe" , "pipe" )
66- ) ,
67- Number
63+ Effect . scoped (
64+ Effect . gen ( function * ( _ ) {
65+ const executor = yield * _ ( CommandExecutor . CommandExecutor )
66+ const process = yield * _ ( executor . start ( buildCommand ( spec , "pipe" , "pipe" , "pipe" ) ) )
67+ yield * _ ( Effect . forkDaemon ( Stream . runDrain ( process . stdout ) ) )
68+ yield * _ ( Effect . forkDaemon ( Stream . runDrain ( process . stderr ) ) )
69+ const exitCode = yield * _ ( process . exitCode )
70+ return Number ( exitCode )
71+ } )
6872 )
6973
7074const collectUint8Array = ( chunks : Chunk . Chunk < Uint8Array > ) : Uint8Array =>
@@ -75,15 +79,15 @@ const collectUint8Array = (chunks: Chunk.Chunk<Uint8Array>): Uint8Array =>
7579 return next
7680 } )
7781
78- // CHANGE: run a command and capture stdout
79- // WHY: allow auth flows to retrieve tokens from CLI tools
82+ // CHANGE: run a command and capture stdout, draining stderr to prevent buffer deadlock
83+ // WHY: if stderr fills the OS buffer (~64 KB) the child process hangs; drain it asynchronously
8084// QUOTE(ТЗ): "система авторизации"
8185// REF: user-request-2026-01-28-auth
8286// SOURCE: n/a
8387// FORMAT THEOREM: forall cmd: capture(cmd) -> stdout(cmd)
8488// PURITY: SHELL
8589// EFFECT: Effect<string, E | PlatformError, CommandExecutor>
86- // INVARIANT: stderr is captured but ignored for output
90+ // INVARIANT: stderr is drained asynchronously; stdout is fully collected before returning
8791// COMPLEXITY: O(command)
8892export const runCommandCapture = < E > (
8993 spec : RunCommandSpec ,
@@ -94,11 +98,13 @@ export const runCommandCapture = <E>(
9498 Effect . gen ( function * ( _ ) {
9599 const executor = yield * _ ( CommandExecutor . CommandExecutor )
96100 const process = yield * _ ( executor . start ( buildCommand ( spec , "pipe" , "pipe" , "pipe" ) ) )
101+ yield * _ ( Effect . forkDaemon ( Stream . runDrain ( process . stderr ) ) )
97102 const bytes = yield * _ (
98103 pipe ( process . stdout , Stream . runCollect , Effect . map ( ( chunks ) => collectUint8Array ( chunks ) ) )
99104 )
100105 const exitCode = yield * _ ( process . exitCode )
101- yield * _ ( ensureExitCode ( Number ( exitCode ) , okExitCodes , onFailure ) )
106+ const numericExitCode = Number ( exitCode )
107+ yield * _ ( ensureExitCode ( numericExitCode , okExitCodes , onFailure ) )
102108 return new TextDecoder ( "utf-8" ) . decode ( bytes )
103109 } )
104110 )
0 commit comments