1use crate::error::{CliError, Result};
4use colored::Colorize;
5use miyabi_agents::base::BaseAgent;
6use miyabi_agents::codegen::CodeGenAgent;
7use miyabi_agents::coordinator::CoordinatorAgent;
8use miyabi_agents::issue::IssueAgent;
9use miyabi_types::{AgentConfig, AgentType, Task};
10use std::collections::HashMap;
11
12pub struct AgentCommand {
13 pub agent_type: String,
14 pub issue: Option<u64>,
15}
16
17impl AgentCommand {
18 pub fn new(agent_type: String, issue: Option<u64>) -> Self {
19 Self { agent_type, issue }
20 }
21
22 pub async fn execute(&self) -> Result<()> {
23 println!(
24 "{}",
25 format!("🤖 Running {} agent...", self.agent_type)
26 .cyan()
27 .bold()
28 );
29
30 let agent_type = self.parse_agent_type()?;
32
33 let config = self.load_config()?;
35
36 match agent_type {
38 AgentType::CoordinatorAgent => {
39 self.run_coordinator_agent(config).await?;
40 }
41 AgentType::CodeGenAgent => {
42 self.run_codegen_agent(config).await?;
43 }
44 AgentType::IssueAgent => {
45 self.run_issue_agent(config).await?;
46 }
47 _ => {
48 println!(
49 "{}",
50 format!("Agent type {:?} not yet implemented", agent_type).yellow()
51 );
52 }
53 }
54
55 println!();
56 println!("{}", "✅ Agent completed successfully!".green().bold());
57
58 Ok(())
59 }
60
61 pub fn parse_agent_type(&self) -> Result<AgentType> {
62 match self.agent_type.to_lowercase().as_str() {
63 "coordinator" => Ok(AgentType::CoordinatorAgent),
64 "codegen" | "code-gen" => Ok(AgentType::CodeGenAgent),
65 "review" => Ok(AgentType::ReviewAgent),
66 "issue" => Ok(AgentType::IssueAgent),
67 "pr" => Ok(AgentType::PRAgent),
68 "deployment" | "deploy" => Ok(AgentType::DeploymentAgent),
69 _ => Err(CliError::InvalidAgentType(self.agent_type.clone())),
70 }
71 }
72
73 fn load_config(&self) -> Result<AgentConfig> {
74 let github_token =
76 std::env::var("GITHUB_TOKEN").map_err(|_| CliError::MissingGitHubToken)?;
77
78 let device_identifier = std::env::var("DEVICE_IDENTIFIER")
80 .unwrap_or_else(|_| hostname::get().unwrap().to_string_lossy().to_string());
81
82 let (repo_owner, repo_name) = self.parse_git_remote()?;
84
85 Ok(AgentConfig {
87 device_identifier,
88 github_token,
89 repo_owner: Some(repo_owner),
90 repo_name: Some(repo_name),
91 use_task_tool: false,
92 use_worktree: true,
93 worktree_base_path: Some(".worktrees".to_string()),
94 log_directory: "./logs".to_string(),
95 report_directory: "./reports".to_string(),
96 tech_lead_github_username: None,
97 ciso_github_username: None,
98 po_github_username: None,
99 firebase_production_project: None,
100 firebase_staging_project: None,
101 production_url: None,
102 staging_url: None,
103 })
104 }
105
106 fn parse_git_remote(&self) -> Result<(String, String)> {
113 let output = std::process::Command::new("git")
115 .args(["remote", "get-url", "origin"])
116 .output()
117 .map_err(|e| CliError::GitConfig(format!("Failed to run git command: {}", e)))?;
118
119 if !output.status.success() {
120 return Err(CliError::GitConfig(
121 "Failed to get git remote URL. Not a git repository?".to_string(),
122 ));
123 }
124
125 let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
126
127 if remote_url.starts_with("http") && remote_url.contains("github.com/") {
129 let parts: Vec<&str> = remote_url
130 .split("github.com/")
131 .nth(1)
132 .ok_or_else(|| CliError::GitConfig("Invalid GitHub URL".to_string()))?
133 .trim_end_matches(".git")
134 .split('/')
135 .collect();
136
137 if parts.len() >= 2 {
138 return Ok((parts[0].to_string(), parts[1].to_string()));
139 }
140 }
141
142 if remote_url.starts_with("git@github.com:") {
144 let repo_part = remote_url
145 .strip_prefix("git@github.com:")
146 .ok_or_else(|| CliError::GitConfig("Invalid SSH URL".to_string()))?
147 .trim_end_matches(".git");
148
149 let parts: Vec<&str> = repo_part.split('/').collect();
150 if parts.len() >= 2 {
151 return Ok((parts[0].to_string(), parts[1].to_string()));
152 }
153 }
154
155 Err(CliError::GitConfig(format!(
156 "Could not parse GitHub owner/repo from remote URL: {}",
157 remote_url
158 )))
159 }
160
161 async fn run_coordinator_agent(&self, config: AgentConfig) -> Result<()> {
162 let issue_number = self.issue.ok_or(CliError::MissingIssueNumber)?;
163
164 println!(" Issue: #{}", issue_number);
165 println!(" Type: CoordinatorAgent (Task decomposition & DAG)");
166 println!();
167
168 let agent = CoordinatorAgent::new(config);
170
171 let task = Task {
173 id: format!("coordinator-issue-{}", issue_number),
174 title: format!("Coordinate Issue #{}", issue_number),
175 description: format!("Decompose Issue #{} into executable tasks", issue_number),
176 task_type: miyabi_types::task::TaskType::Feature,
177 priority: 1,
178 severity: None,
179 impact: None,
180 assigned_agent: Some(AgentType::CoordinatorAgent),
181 dependencies: vec![],
182 estimated_duration: Some(5),
183 status: None,
184 start_time: None,
185 end_time: None,
186 metadata: Some(HashMap::from([(
187 "issue_number".to_string(),
188 serde_json::json!(issue_number),
189 )])),
190 };
191
192 println!("{}", " Executing...".dimmed());
194 let result = agent.execute(&task).await?;
195
196 println!();
198 println!(" Results:");
199 println!(" Status: {:?}", result.status);
200
201 if let Some(metrics) = result.metrics {
202 println!(" Duration: {}ms", metrics.duration_ms);
203 }
204
205 if let Some(data) = result.data {
206 println!(" Data: {}", serde_json::to_string_pretty(&data)?);
207 }
208
209 Ok(())
210 }
211
212 async fn run_codegen_agent(&self, config: AgentConfig) -> Result<()> {
213 let issue_number = self.issue.ok_or(CliError::MissingIssueNumber)?;
214
215 println!(" Issue: #{}", issue_number);
216 println!(" Type: CodeGenAgent (Code generation)");
217 println!();
218
219 let agent = CodeGenAgent::new(config);
221
222 let task = Task {
224 id: format!("codegen-issue-{}", issue_number),
225 title: format!("Generate code for Issue #{}", issue_number),
226 description: format!("Implement solution for Issue #{}", issue_number),
227 task_type: miyabi_types::task::TaskType::Feature,
228 priority: 1,
229 severity: None,
230 impact: None,
231 assigned_agent: Some(AgentType::CodeGenAgent),
232 dependencies: vec![],
233 estimated_duration: Some(30),
234 status: None,
235 start_time: None,
236 end_time: None,
237 metadata: Some(HashMap::from([(
238 "issue_number".to_string(),
239 serde_json::json!(issue_number),
240 )])),
241 };
242
243 println!("{}", " Executing...".dimmed());
245 let result = agent.execute(&task).await?;
246
247 println!();
249 println!(" Results:");
250 println!(" Status: {:?}", result.status);
251
252 if let Some(metrics) = result.metrics {
253 println!(" Duration: {}ms", metrics.duration_ms);
254 if let Some(lines_changed) = metrics.lines_changed {
255 println!(" Lines changed: {}", lines_changed);
256 }
257 if let Some(tests_added) = metrics.tests_added {
258 println!(" Tests added: {}", tests_added);
259 }
260 }
261
262 Ok(())
263 }
264
265 async fn run_issue_agent(&self, config: AgentConfig) -> Result<()> {
266 let issue_number = self.issue.ok_or(CliError::MissingIssueNumber)?;
267
268 println!(" Issue: #{}", issue_number);
269 println!(" Type: IssueAgent (Issue analysis & labeling)");
270 println!();
271
272 let agent = IssueAgent::new(config);
274
275 let task = Task {
277 id: format!("issue-analysis-{}", issue_number),
278 title: format!("Analyze Issue #{}", issue_number),
279 description: format!("Classify Issue #{} and apply labels", issue_number),
280 task_type: miyabi_types::task::TaskType::Feature,
281 priority: 1,
282 severity: None,
283 impact: None,
284 assigned_agent: Some(AgentType::IssueAgent),
285 dependencies: vec![],
286 estimated_duration: Some(5),
287 status: None,
288 start_time: None,
289 end_time: None,
290 metadata: Some(HashMap::from([(
291 "issue_number".to_string(),
292 serde_json::json!(issue_number),
293 )])),
294 };
295
296 println!("{}", " Executing...".dimmed());
298 let result = agent.execute(&task).await?;
299
300 println!();
302 println!(" Results:");
303 println!(" Status: {:?}", result.status);
304
305 if let Some(metrics) = result.metrics {
306 println!(" Duration: {}ms", metrics.duration_ms);
307 }
308
309 if let Some(data) = result.data {
310 if let Ok(analysis) = serde_json::from_value::<miyabi_types::IssueAnalysis>(data) {
312 println!(" Analysis:");
313 println!(" Issue Type: {:?}", analysis.issue_type);
314 println!(" Severity: {:?}", analysis.severity);
315 println!(" Impact: {:?}", analysis.impact);
316 println!(" Assigned Agent: {:?}", analysis.assigned_agent);
317 println!(" Estimated Duration: {} minutes", analysis.estimated_duration);
318 println!(" Dependencies: {}", analysis.dependencies.join(", "));
319 println!(" Applied Labels: {}", analysis.labels.join(", "));
320 }
321 }
322
323 Ok(())
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_parse_agent_type() {
333 let cmd = AgentCommand::new("coordinator".to_string(), None);
334 assert!(matches!(
335 cmd.parse_agent_type().unwrap(),
336 AgentType::CoordinatorAgent
337 ));
338
339 let cmd = AgentCommand::new("codegen".to_string(), None);
340 assert!(matches!(
341 cmd.parse_agent_type().unwrap(),
342 AgentType::CodeGenAgent
343 ));
344
345 let cmd = AgentCommand::new("code-gen".to_string(), None);
346 assert!(matches!(
347 cmd.parse_agent_type().unwrap(),
348 AgentType::CodeGenAgent
349 ));
350
351 let cmd = AgentCommand::new("invalid".to_string(), None);
352 assert!(cmd.parse_agent_type().is_err());
353 }
354
355 #[test]
356 fn test_agent_command_creation() {
357 let cmd = AgentCommand::new("coordinator".to_string(), Some(123));
358 assert_eq!(cmd.agent_type, "coordinator");
359 assert_eq!(cmd.issue, Some(123));
360
361 let cmd = AgentCommand::new("codegen".to_string(), None);
362 assert_eq!(cmd.agent_type, "codegen");
363 assert_eq!(cmd.issue, None);
364 }
365}