1use std::io::{BufRead, BufReader, Write};
6use std::process::{Command, Stdio};
7
8use crate::error::{Autom8Error, Result};
9use crate::git;
10use crate::prompts::COMMIT_PROMPT;
11use crate::spec::Spec;
12
13use super::stream::{extract_text_from_stream_line, extract_usage_from_result_line};
14use super::types::{ClaudeErrorInfo, ClaudeUsage};
15
16#[derive(Debug, Clone)]
18pub struct CommitResult {
19 pub outcome: CommitOutcome,
20 pub usage: Option<ClaudeUsage>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum CommitOutcome {
26 Success(String),
28 NothingToCommit,
29 Error(ClaudeErrorInfo),
30}
31
32pub fn run_for_commit<F>(spec: &Spec, mut on_output: F) -> Result<CommitResult>
34where
35 F: FnMut(&str),
36{
37 let stories_summary = spec
39 .user_stories
40 .iter()
41 .map(|s| format!("- {}: {}", s.id, s.title))
42 .collect::<Vec<_>>()
43 .join("\n");
44
45 let prompt = COMMIT_PROMPT
46 .replace("{project}", &spec.project)
47 .replace("{feature_description}", &spec.description)
48 .replace("{stories_summary}", &stories_summary);
49
50 let mut child = Command::new("claude")
51 .args([
52 "--dangerously-skip-permissions",
53 "--print",
54 "--output-format",
55 "stream-json",
56 "--verbose",
57 ])
58 .stdin(Stdio::piped())
59 .stdout(Stdio::piped())
60 .stderr(Stdio::piped())
61 .spawn()
62 .map_err(|e| Autom8Error::ClaudeError(format!("Failed to spawn claude: {}", e)))?;
63
64 if let Some(mut stdin) = child.stdin.take() {
66 stdin
67 .write_all(prompt.as_bytes())
68 .map_err(|e| Autom8Error::ClaudeError(format!("Failed to write to stdin: {}", e)))?;
69 }
70
71 let stderr = child.stderr.take();
73
74 let stdout = child
76 .stdout
77 .take()
78 .ok_or_else(|| Autom8Error::ClaudeError("Failed to capture stdout".into()))?;
79
80 let reader = BufReader::new(stdout);
81 let mut nothing_to_commit = false;
82 let mut accumulated_text = String::new();
83 let mut usage: Option<ClaudeUsage> = None;
84
85 for line in reader.lines() {
86 let line = line.map_err(|e| Autom8Error::ClaudeError(format!("Read error: {}", e)))?;
87
88 if let Some(text) = extract_text_from_stream_line(&line) {
90 on_output(&text);
91 accumulated_text.push_str(&text);
92
93 if text.to_lowercase().contains("nothing to commit")
94 || accumulated_text
95 .to_lowercase()
96 .contains("nothing to commit")
97 {
98 nothing_to_commit = true;
99 }
100 }
101
102 if let Some(line_usage) = extract_usage_from_result_line(&line) {
104 usage = Some(line_usage);
105 }
106 }
107
108 let status = child
110 .wait()
111 .map_err(|e| Autom8Error::ClaudeError(format!("Wait error: {}", e)))?;
112
113 if !status.success() {
114 let stderr_content = stderr
115 .map(|s| std::io::read_to_string(s).unwrap_or_default())
116 .unwrap_or_default();
117 let error_info = ClaudeErrorInfo::from_process_failure(
118 status,
119 if stderr_content.is_empty() {
120 None
121 } else {
122 Some(stderr_content)
123 },
124 );
125 return Ok(CommitResult {
126 outcome: CommitOutcome::Error(error_info),
127 usage,
128 });
129 }
130
131 let outcome = if nothing_to_commit {
132 CommitOutcome::NothingToCommit
133 } else {
134 let commit_hash = git::latest_commit_short().unwrap_or_else(|_| "unknown".to_string());
136 CommitOutcome::Success(commit_hash)
137 };
138
139 Ok(CommitResult { outcome, usage })
140}