1use std::io::Write as IoWrite;
39use std::path::Path;
40use std::process::{Command, Stdio};
41
42use serde::{Deserialize, Serialize};
43
44use crate::error::{DotAgentError, Result};
45
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct LlmConfig {
53 #[serde(default)]
55 pub enabled: bool,
56}
57
58pub fn check_claude_cli() -> bool {
66 Command::new("claude")
67 .arg("--version")
68 .stdout(Stdio::null())
69 .stderr(Stdio::null())
70 .status()
71 .map(|s| s.success())
72 .unwrap_or(false)
73}
74
75pub fn execute_claude(working_dir: &Path, prompt: &str) -> Result<String> {
88 let mut cmd = Command::new("claude");
89 cmd.arg("--print");
90 cmd.arg("--dangerously-skip-permissions");
91 cmd.current_dir(working_dir);
92 cmd.stdin(Stdio::piped());
93 cmd.stdout(Stdio::piped());
94 cmd.stderr(Stdio::piped());
95
96 let mut child = cmd
97 .spawn()
98 .map_err(|e| DotAgentError::ClaudeExecutionFailed {
99 message: format!("Failed to spawn claude: {}", e),
100 })?;
101
102 if let Some(mut stdin) = child.stdin.take() {
103 stdin
104 .write_all(prompt.as_bytes())
105 .map_err(|e| DotAgentError::ClaudeExecutionFailed {
106 message: format!("Failed to write prompt: {}", e),
107 })?;
108 }
109
110 let output = child
111 .wait_with_output()
112 .map_err(|e| DotAgentError::ClaudeExecutionFailed {
113 message: format!("Execution failed: {}", e),
114 })?;
115
116 if !output.status.success() {
117 let stderr = String::from_utf8_lossy(&output.stderr);
118 return Err(DotAgentError::ClaudeExecutionFailed {
119 message: format!("Claude exited with error: {}", stderr),
120 });
121 }
122
123 Ok(String::from_utf8_lossy(&output.stdout).to_string())
124}
125
126pub fn require_claude_cli() -> Result<()> {
128 if !check_claude_cli() {
129 return Err(DotAgentError::ClaudeNotFound);
130 }
131 Ok(())
132}
133
134#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_llm_config_default() {
144 let config = LlmConfig::default();
145 assert!(!config.enabled);
146 }
147
148 #[test]
149 fn test_llm_config_deserialize() {
150 let toml_str = r#"
151 enabled = true
152 "#;
153 let config: LlmConfig = toml::from_str(toml_str).unwrap();
154 assert!(config.enabled);
155 }
156
157 #[test]
158 fn test_llm_config_deserialize_empty() {
159 let toml_str = "";
160 let config: LlmConfig = toml::from_str(toml_str).unwrap();
161 assert!(!config.enabled);
162 }
163}