use claude_runner_core::{ ClaudeCommand, ErrorKind, ExecutionOutput, signal_exit_code };
use super::parse::{ CliArgs, ExpectStrategy };
use super::fence::strip_fences;
fn spawn_error_msg( e : &std::io::Error ) -> String
{
if e.kind() == std::io::ErrorKind::NotFound
{
"claude binary not found in PATH — install with: npm i -g @anthropic-ai/claude-code".to_string()
}
else
{
format!( "Failed to execute Claude Code: {e}" )
}
}
fn poll_timeout( child : &mut std::process::Child, deadline : std::time::Instant, timeout_secs : u32 )
{
if std::time::Instant::now() >= deadline
{
let _ = child.kill();
let _ = child.wait();
eprintln!( "Error: timeout after {timeout_secs}s" );
std::process::exit( 2 );
}
std::thread::sleep( core::time::Duration::from_millis( 50 ) );
}
fn write_output_file( path : Option< &str >, content : &str )
{
if let Some( p ) = path
{
if let Err( e ) = std::fs::write( p, content.as_bytes() )
{
eprintln!( "Error: failed to write output file '{p}': {e}" );
std::process::exit( 1 );
}
}
}
fn apply_expect_validation( cli : &CliArgs, builder : &ClaudeCommand, out : String ) -> String
{
let Some( ref pattern ) = cli.expect else { return out; };
let allowed : Vec< String > = pattern.split( '|' )
.map( | s | s.trim().to_lowercase() )
.collect();
let trimmed = out.trim().to_lowercase();
if allowed.iter().any( | v | v.as_str() == trimmed ) { return out; }
match &cli.expect_strategy
{
Some( ExpectStrategy::Retry ) =>
{
let retries = cli.expect_retries.unwrap_or( 0 ) as usize;
for _ in 0 .. retries
{
let retry_output = match builder.execute()
{
Ok( o ) => o,
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
};
if !retry_output.stderr.is_empty() { eprint!( "{}", retry_output.stderr ); }
if retry_output.exit_code != 0 { std::process::exit( retry_output.exit_code ); }
let retry_out = if cli.strip_fences
{
strip_fences( &retry_output.stdout )
}
else
{
retry_output.stdout
};
if allowed.iter().any( | v | v.as_str() == retry_out.trim().to_lowercase() )
{
write_output_file( cli.output_file.as_deref(), &retry_out );
print!( "{retry_out}" );
std::process::exit( 0 );
}
}
std::process::exit( 3 );
}
Some( ExpectStrategy::Default( fallback ) ) =>
{
let fallback = fallback.clone();
write_output_file( cli.output_file.as_deref(), &fallback );
print!( "{fallback}" );
std::process::exit( 0 );
}
Some( ExpectStrategy::Fail ) | None => std::process::exit( 3 ),
}
}
fn execute_print_attempt( builder : &ClaudeCommand, timeout_secs : u32 ) -> ExecutionOutput
{
if timeout_secs == 0
{
return match builder.execute()
{
Ok( o ) => o,
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
};
}
let mut child = match builder.spawn_piped()
{
Ok( c ) => c,
Err( e ) => { eprintln!( "Error: {}", spawn_error_msg( &e ) ); std::process::exit( 1 ); }
};
let deadline = std::time::Instant::now()
+ core::time::Duration::from_secs( u64::from( timeout_secs ) );
loop
{
match child.try_wait()
{
Ok( Some( _ ) ) =>
{
let raw = match child.wait_with_output()
{
Ok( o ) => o,
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
};
let exit_code = signal_exit_code( &raw.status );
let stdout = String::from_utf8_lossy( &raw.stdout ).to_string();
let stderr = String::from_utf8_lossy( &raw.stderr ).to_string();
return ExecutionOutput { stdout, stderr, exit_code };
}
Ok( None ) => poll_timeout( &mut child, deadline, timeout_secs ),
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
}
}
}
pub( super ) fn run_print_mode( builder : &ClaudeCommand, cli : &CliArgs )
{
let verbosity = cli.verbosity.unwrap_or_default();
let retry_limit = cli.retry_on_rate_limit.unwrap_or( 1 ) as usize;
let retry_delay = cli.retry_delay.unwrap_or( 30 );
let timeout_secs = cli.timeout.unwrap_or( 0 );
let mut attempts = 0usize;
loop
{
let output = execute_print_attempt( builder, timeout_secs );
if !output.stderr.is_empty() { eprint!( "{}", output.stderr ); }
if output.exit_code != 0
{
let kind = output.classify_error();
if let Some( ErrorKind::RateLimit ) = &kind
{
if attempts < retry_limit
{
attempts += 1;
if verbosity.shows_warnings()
{
eprintln!(
"Rate limit (attempt {attempts}/{}); retrying in {retry_delay}s…",
retry_limit + 1
);
}
if retry_delay > 0
{
std::thread::sleep( core::time::Duration::from_secs( u64::from( retry_delay ) ) );
}
continue;
}
}
if verbosity.shows_errors()
{
let label = match &kind
{
Some( ErrorKind::RateLimit ) if attempts > 0 => "rate limit retries exhausted",
Some( ErrorKind::RateLimit ) => "rate limit",
Some( ErrorKind::QuotaExhausted ) => "quota exhausted",
Some( ErrorKind::ApiError ) => "API error",
Some( ErrorKind::AuthError ) => "auth error",
Some( ErrorKind::Signal ) => "terminated by signal",
Some( ErrorKind::Unknown ) | None => "unknown error",
};
eprintln!( "Error: {label} (exit {})", output.exit_code );
}
if !output.stdout.is_empty() { eprint!( "{}", output.stdout ); }
std::process::exit( output.exit_code );
}
let out = if cli.strip_fences { strip_fences( &output.stdout ) } else { output.stdout };
let out = apply_expect_validation( cli, builder, out );
write_output_file( cli.output_file.as_deref(), &out );
print!( "{out}" );
return;
}
}
pub( super ) fn run_interactive( builder : &ClaudeCommand, cli : &CliArgs )
{
let timeout_secs = cli.timeout.unwrap_or( 0 );
if timeout_secs == 0
{
let status = match builder.execute_interactive()
{
Ok( s ) => s,
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
};
if !status.success()
{
std::process::exit( signal_exit_code( &status ) );
}
return;
}
let mut child = match builder.spawn_tty()
{
Ok( c ) => c,
Err( e ) => { eprintln!( "Error: {}", spawn_error_msg( &e ) ); std::process::exit( 1 ); }
};
let deadline = std::time::Instant::now()
+ core::time::Duration::from_secs( u64::from( timeout_secs ) );
loop
{
match child.try_wait()
{
Ok( Some( status ) ) =>
{
if !status.success()
{
std::process::exit( signal_exit_code( &status ) );
}
return;
}
Ok( None ) => poll_timeout( &mut child, deadline, timeout_secs ),
Err( e ) => { eprintln!( "Error: {e}" ); std::process::exit( 1 ); }
}
}
}