use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use crate::error::{Autom8Error, Result};
use crate::git;
use crate::prompts::COMMIT_PROMPT;
use crate::spec::Spec;
use super::stream::{extract_text_from_stream_line, extract_usage_from_result_line};
use super::types::{ClaudeErrorInfo, ClaudeUsage};
#[derive(Debug, Clone)]
pub struct CommitResult {
pub outcome: CommitOutcome,
pub usage: Option<ClaudeUsage>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CommitOutcome {
Success(String),
NothingToCommit,
Error(ClaudeErrorInfo),
}
pub fn run_for_commit<F>(spec: &Spec, mut on_output: F) -> Result<CommitResult>
where
F: FnMut(&str),
{
let stories_summary = spec
.user_stories
.iter()
.map(|s| format!("- {}: {}", s.id, s.title))
.collect::<Vec<_>>()
.join("\n");
let prompt = COMMIT_PROMPT
.replace("{project}", &spec.project)
.replace("{feature_description}", &spec.description)
.replace("{stories_summary}", &stories_summary);
let mut child = Command::new("claude")
.args([
"--dangerously-skip-permissions",
"--print",
"--output-format",
"stream-json",
"--verbose",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| Autom8Error::ClaudeError(format!("Failed to spawn claude: {}", e)))?;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(prompt.as_bytes())
.map_err(|e| Autom8Error::ClaudeError(format!("Failed to write to stdin: {}", e)))?;
}
let stderr = child.stderr.take();
let stdout = child
.stdout
.take()
.ok_or_else(|| Autom8Error::ClaudeError("Failed to capture stdout".into()))?;
let reader = BufReader::new(stdout);
let mut nothing_to_commit = false;
let mut accumulated_text = String::new();
let mut usage: Option<ClaudeUsage> = None;
for line in reader.lines() {
let line = line.map_err(|e| Autom8Error::ClaudeError(format!("Read error: {}", e)))?;
if let Some(text) = extract_text_from_stream_line(&line) {
on_output(&text);
accumulated_text.push_str(&text);
if text.to_lowercase().contains("nothing to commit")
|| accumulated_text
.to_lowercase()
.contains("nothing to commit")
{
nothing_to_commit = true;
}
}
if let Some(line_usage) = extract_usage_from_result_line(&line) {
usage = Some(line_usage);
}
}
let status = child
.wait()
.map_err(|e| Autom8Error::ClaudeError(format!("Wait error: {}", e)))?;
if !status.success() {
let stderr_content = stderr
.map(|s| std::io::read_to_string(s).unwrap_or_default())
.unwrap_or_default();
let error_info = ClaudeErrorInfo::from_process_failure(
status,
if stderr_content.is_empty() {
None
} else {
Some(stderr_content)
},
);
return Ok(CommitResult {
outcome: CommitOutcome::Error(error_info),
usage,
});
}
let outcome = if nothing_to_commit {
CommitOutcome::NothingToCommit
} else {
let commit_hash = git::latest_commit_short().unwrap_or_else(|_| "unknown".to_string());
CommitOutcome::Success(commit_hash)
};
Ok(CommitResult { outcome, usage })
}