feat(main): wire prompt resolution, session dispatch, and emit
Replace the 'not yet implemented' stub with full execution path: - Prompt resolution (precedence: --input-file, positional, stdin) - Build claude_args to forward flags to child process - Call session::run() and match results - Emit success/error outputs per format (text/json/stream-json) - Handle AS-5 (binary not found) with human-readable error - Exit codes: 0=success, 2=setup/child errors, 3=timeout, 4=input errors, 130=interrupted Completes bead bf-4aw. Co-Authored-By: Claude <noreply@anthropic.com> Bead-Id: bf-4aw
This commit is contained in:
parent
5110f0bf57
commit
d942572870
1 changed files with 200 additions and 2 deletions
202
src/main.rs
202
src/main.rs
|
|
@ -1,6 +1,12 @@
|
|||
use clap::Parser;
|
||||
use claude_print::cli::{version_string, Cli};
|
||||
use claude_print::emitter;
|
||||
use claude_print::error::{ClaudePrintError, Error};
|
||||
use claude_print::session;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::time::Instant;
|
||||
|
||||
fn resolve_claude_version(binary: Option<&std::path::Path>) -> Option<String> {
|
||||
let binary = binary
|
||||
|
|
@ -33,6 +39,198 @@ fn main() {
|
|||
process::exit(code);
|
||||
}
|
||||
|
||||
eprintln!("claude-print: not yet implemented");
|
||||
process::exit(2);
|
||||
// Resolve the claude binary path
|
||||
let claude_bin = cli
|
||||
.claude_binary
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("claude"));
|
||||
|
||||
// AS-5: Check if claude binary exists before calling session::run()
|
||||
if which::which(&claude_bin).is_err() {
|
||||
eprintln!(
|
||||
"claude-print: '{}' not found in PATH",
|
||||
claude_bin.to_string_lossy()
|
||||
);
|
||||
process::exit(2);
|
||||
}
|
||||
|
||||
// Prompt resolution (in order of precedence)
|
||||
let prompt_bytes = if let Some(ref input_file) = cli.input_file {
|
||||
// --input-file <path>: read file bytes
|
||||
match std::fs::read(input_file) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"claude-print: failed to read input file '{}': {}",
|
||||
input_file.display(),
|
||||
e
|
||||
);
|
||||
process::exit(4);
|
||||
}
|
||||
}
|
||||
} else if let Some(ref prompt_str) = cli.prompt {
|
||||
// positional <prompt>: encode as UTF-8 bytes
|
||||
prompt_str.as_bytes().to_vec()
|
||||
} else {
|
||||
// stdin (when !stdin.is_terminal())
|
||||
if !atty::is(atty::Stream::Stdin) {
|
||||
let mut buffer = Vec::new();
|
||||
if let Err(e) = io::stdin().read_to_end(&mut buffer) {
|
||||
eprintln!("claude-print: failed to read stdin: {}", e);
|
||||
process::exit(4);
|
||||
}
|
||||
if buffer.is_empty() {
|
||||
eprintln!("claude-print: no prompt provided (pass as argument, --input-file, or stdin)");
|
||||
process::exit(4);
|
||||
}
|
||||
buffer
|
||||
} else {
|
||||
// None found → exit 4
|
||||
eprintln!("claude-print: no prompt provided (pass as argument, --input-file, or stdin)");
|
||||
process::exit(4);
|
||||
}
|
||||
};
|
||||
|
||||
// Build claude_args: collect flags to forward to child
|
||||
let mut claude_args: Vec<std::ffi::OsString> = Vec::new();
|
||||
|
||||
if let Some(ref model) = cli.model {
|
||||
claude_args.push("--model".into());
|
||||
claude_args.push(model.as_str().into());
|
||||
}
|
||||
|
||||
if cli.max_turns != 30 {
|
||||
// Only pass if non-default
|
||||
claude_args.push("--max-turns".into());
|
||||
claude_args.push(cli.max_turns.to_string().into());
|
||||
}
|
||||
|
||||
if cli.no_inherit_hooks {
|
||||
claude_args.push("--setting-sources=".into());
|
||||
}
|
||||
|
||||
let t0 = Instant::now();
|
||||
|
||||
// Call session::Session::run()
|
||||
let result = session::Session::run(&claude_bin, &claude_args, prompt_bytes, Some(cli.timeout));
|
||||
|
||||
// Lock stdout and stderr for output
|
||||
let mut stdout = io::stdout().lock();
|
||||
let mut stderr = io::stderr().lock();
|
||||
|
||||
// Match result
|
||||
match result {
|
||||
Ok(session_result) => {
|
||||
let duration_ms = t0.elapsed().as_millis() as u64;
|
||||
|
||||
// For stream-json format, replay the transcript line by line
|
||||
if cli.output_format == claude_print::cli::OutputFormat::StreamJson {
|
||||
if let Err(e) = replay_stream_json(&session_result.transcript_path) {
|
||||
let _ = emit_error(
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
&ClaudePrintError::Setup(format!(
|
||||
"failed to replay transcript: {}",
|
||||
e
|
||||
)),
|
||||
&cli.output_format,
|
||||
&session_result.claude_version,
|
||||
false,
|
||||
);
|
||||
process::exit(2);
|
||||
}
|
||||
} else {
|
||||
// For text and json formats, emit success
|
||||
if let Err(e) = emitter::emit_success(
|
||||
&mut stdout,
|
||||
&session_result.transcript,
|
||||
&cli.output_format,
|
||||
&session_result.claude_version,
|
||||
duration_ms,
|
||||
) {
|
||||
eprintln!("claude-print: failed to write output: {}", e);
|
||||
process::exit(2);
|
||||
}
|
||||
}
|
||||
process::exit(0);
|
||||
}
|
||||
Err(Error::Interrupted(_msg)) => {
|
||||
let _ = emit_error(
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
&ClaudePrintError::Interrupted,
|
||||
&cli.output_format,
|
||||
&resolve_claude_version(cli.claude_binary.as_deref()).unwrap_or_else(|| "unknown".to_string()),
|
||||
true,
|
||||
);
|
||||
process::exit(130);
|
||||
}
|
||||
Err(Error::Timeout(msg)) => {
|
||||
let _ = emit_error(
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
&ClaudePrintError::Timeout,
|
||||
&cli.output_format,
|
||||
&resolve_claude_version(cli.claude_binary.as_deref()).unwrap_or_else(|| "unknown".to_string()),
|
||||
true,
|
||||
);
|
||||
process::exit(3);
|
||||
}
|
||||
Err(Error::Internal(e)) => {
|
||||
let msg = if e.to_string().contains("Child exited without sending Stop payload") {
|
||||
"claude exited before Stop hook fired".to_string()
|
||||
} else {
|
||||
e.to_string()
|
||||
};
|
||||
let _ = emit_error(
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
&ClaudePrintError::Setup(msg),
|
||||
&cli.output_format,
|
||||
&resolve_claude_version(cli.claude_binary.as_deref()).unwrap_or_else(|| "unknown".to_string()),
|
||||
true,
|
||||
);
|
||||
process::exit(2);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = emit_error(
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
&ClaudePrintError::Setup(e.to_string()),
|
||||
&cli.output_format,
|
||||
&resolve_claude_version(cli.claude_binary.as_deref()).unwrap_or_else(|| "unknown".to_string()),
|
||||
true,
|
||||
);
|
||||
process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replay the transcript as stream-json output.
|
||||
fn replay_stream_json(transcript_path: &std::path::Path) -> std::io::Result<()> {
|
||||
use std::io::{BufRead, BufReader};
|
||||
let file = std::fs::File::open(transcript_path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
writeln!(stdout, "{}", trimmed)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emit an error in the appropriate format.
|
||||
fn emit_error(
|
||||
stdout: &mut impl Write,
|
||||
stderr: &mut impl Write,
|
||||
error: &ClaudePrintError,
|
||||
format: &claude_print::cli::OutputFormat,
|
||||
claude_version: &str,
|
||||
stream_json_after_inject: bool,
|
||||
) -> std::io::Result<()> {
|
||||
emitter::emit_error(stdout, stderr, error, format, claude_version, stream_json_after_inject)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue