miyabi_cli/commands/
install.rs

1//! Install command - Install Miyabi to existing project
2
3use crate::error::{CliError, Result};
4use colored::Colorize;
5use std::fs;
6use std::path::Path;
7
8pub struct InstallCommand {
9    pub dry_run: bool,
10}
11
12impl InstallCommand {
13    pub fn new(dry_run: bool) -> Self {
14        Self { dry_run }
15    }
16
17    pub async fn execute(&self) -> Result<()> {
18        println!(
19            "{}",
20            "📦 Installing Miyabi to existing project...".cyan().bold()
21        );
22
23        if self.dry_run {
24            println!("{}", "  (Dry run - no changes will be made)".yellow());
25        }
26
27        // Verify we're in a git repository
28        self.verify_git_repository()?;
29
30        // Check if Miyabi is already installed
31        if self.is_miyabi_installed() {
32            println!(
33                "{}",
34                "⚠️  Miyabi is already installed in this project".yellow()
35            );
36            return Ok(());
37        }
38
39        // Create directory structure
40        self.create_directory_structure()?;
41
42        // Create configuration files
43        self.create_configuration_files()?;
44
45        // Update .gitignore
46        self.update_gitignore()?;
47
48        println!();
49        println!("{}", "✅ Miyabi installed successfully!".green().bold());
50        println!();
51        println!("Next steps:");
52        println!("  export GITHUB_TOKEN=ghp_xxx");
53        println!("  miyabi status");
54
55        Ok(())
56    }
57
58    fn verify_git_repository(&self) -> Result<()> {
59        use std::process::Command;
60
61        let output = Command::new("git")
62            .args(["rev-parse", "--git-dir"])
63            .output()?;
64
65        if !output.status.success() {
66            return Err(CliError::NotGitRepository);
67        }
68
69        println!("  ✓ Git repository detected");
70        Ok(())
71    }
72
73    fn is_miyabi_installed(&self) -> bool {
74        Path::new(".miyabi.yml").exists()
75    }
76
77    fn create_directory_structure(&self) -> Result<()> {
78        let dirs = vec![
79            ".github/workflows",
80            ".claude/agents/specs",
81            ".claude/agents/prompts",
82            ".claude/commands",
83            "docs",
84            "logs",
85            "reports",
86        ];
87
88        for dir in dirs {
89            if self.dry_run {
90                println!("  [DRY RUN] Would create: {}", dir);
91            } else {
92                fs::create_dir_all(dir)?;
93                println!("  Created: {}", dir);
94            }
95        }
96
97        Ok(())
98    }
99
100    fn create_configuration_files(&self) -> Result<()> {
101        // Get project name from git remote or directory name
102        let project_name = self.get_project_name()?;
103
104        let miyabi_config = format!(
105            r#"# Miyabi Configuration
106project_name: {}
107version: "0.1.0"
108
109# GitHub settings (use environment variables for sensitive data)
110# github_token: ${{{{ GITHUB_TOKEN }}}}
111
112# Agent settings
113agents:
114  enabled: true
115  use_worktree: true
116  worktree_base_path: ".worktrees"
117
118# Logging
119logging:
120  level: info
121  directory: "./logs"
122
123# Reporting
124reporting:
125  directory: "./reports"
126"#,
127            project_name
128        );
129
130        if self.dry_run {
131            println!("  [DRY RUN] Would create: .miyabi.yml");
132        } else {
133            fs::write(".miyabi.yml", miyabi_config)?;
134            println!("  Created: .miyabi.yml");
135        }
136
137        Ok(())
138    }
139
140    fn update_gitignore(&self) -> Result<()> {
141        let gitignore_entries = r#"
142# Miyabi
143.miyabi.yml
144.worktrees/
145logs/
146reports/
147*.log
148"#;
149
150        if self.dry_run {
151            println!("  [DRY RUN] Would update: .gitignore");
152        } else {
153            // Read existing .gitignore if it exists
154            let existing_gitignore = fs::read_to_string(".gitignore").unwrap_or_default();
155
156            // Check if Miyabi entries already exist
157            if !existing_gitignore.contains("# Miyabi") {
158                let updated_gitignore = format!("{}{}", existing_gitignore, gitignore_entries);
159                fs::write(".gitignore", updated_gitignore)?;
160                println!("  Updated: .gitignore");
161            } else {
162                println!("  Skipped: .gitignore (already has Miyabi entries)");
163            }
164        }
165
166        Ok(())
167    }
168
169    fn get_project_name(&self) -> Result<String> {
170        use std::process::Command;
171
172        // Try to get from git remote
173        let output = Command::new("git")
174            .args(["remote", "get-url", "origin"])
175            .output();
176
177        if let Ok(output) = output {
178            if output.status.success() {
179                let url = String::from_utf8_lossy(&output.stdout);
180                if let Some(name) = url.split('/').next_back() {
181                    return Ok(name.trim().trim_end_matches(".git").to_string());
182                }
183            }
184        }
185
186        // Fallback to directory name
187        let current_dir = std::env::current_dir()?;
188        let name = current_dir
189            .file_name()
190            .and_then(|n| n.to_str())
191            .unwrap_or("miyabi-project");
192
193        Ok(name.to_string())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_install_command_creation() {
203        let cmd = InstallCommand::new(false);
204        assert!(!cmd.dry_run);
205
206        let cmd = InstallCommand::new(true);
207        assert!(cmd.dry_run);
208    }
209}