miyabi_cli/commands/
setup.rs

1//! Setup command - Interactive onboarding wizard
2
3use crate::error::{CliError, Result};
4use colored::Colorize;
5use dialoguer::Confirm;
6use std::fs;
7use std::path::Path;
8use std::process::Command;
9
10pub struct SetupCommand {
11    pub skip_prompts: bool,
12}
13
14impl SetupCommand {
15    pub fn new(skip_prompts: bool) -> Self {
16        Self { skip_prompts }
17    }
18
19    pub async fn execute(&self) -> Result<()> {
20        println!("{}", "🚀 Miyabi Setup Wizard".cyan().bold());
21        println!();
22        println!("This wizard will help you set up Miyabi in a few simple steps.");
23        println!();
24
25        // Step 1: Check GitHub CLI authentication
26        println!("{}", "Step 1: Checking GitHub authentication...".bold());
27        self.check_github_auth()?;
28        println!("{}", "  ✅ GitHub authentication verified".green());
29        println!();
30
31        // Step 2: Detect Git repository
32        println!("{}", "Step 2: Detecting Git repository...".bold());
33        let (owner, repo) = self.detect_git_repository()?;
34        println!("  ✅ Detected repository: {}/{}", owner.cyan(), repo.cyan());
35        println!();
36
37        // Step 3: Create configuration files
38        println!("{}", "Step 3: Creating configuration files...".bold());
39        self.create_env_file(&owner, &repo)?;
40        self.create_miyabi_yml(&owner, &repo)?;
41        println!("{}", "  ✅ Configuration files created".green());
42        println!();
43
44        // Step 4: Create directories
45        println!("{}", "Step 4: Creating directories...".bold());
46        self.create_directories()?;
47        println!("{}", "  ✅ Directories created".green());
48        println!();
49
50        // Step 5: Verify setup
51        println!("{}", "Step 5: Verifying setup...".bold());
52        self.verify_setup()?;
53        println!("{}", "  ✅ Setup verified".green());
54        println!();
55
56        // Final message
57        println!("{}", "🎉 Setup complete!".green().bold());
58        println!();
59        println!("You can now run:");
60        println!(
61            "  {} {}",
62            "miyabi agent coordinator --issue".dimmed(),
63            "<issue-number>".yellow()
64        );
65        println!("  {} {}", "miyabi status".dimmed(), "".dimmed());
66        println!();
67
68        Ok(())
69    }
70
71    fn check_github_auth(&self) -> Result<()> {
72        // Check if gh CLI is installed
73        let gh_version = Command::new("gh").arg("--version").output().map_err(|_| {
74            CliError::GitConfig(
75                "gh CLI not found. Please install GitHub CLI: https://cli.github.com/".to_string(),
76            )
77        })?;
78
79        if !gh_version.status.success() {
80            return Err(CliError::GitConfig(
81                "gh CLI not working properly".to_string(),
82            ));
83        }
84
85        // Check authentication status
86        let auth_status = Command::new("gh")
87            .args(["auth", "status"])
88            .output()
89            .map_err(|e| CliError::GitConfig(format!("Failed to check gh auth status: {}", e)))?;
90
91        if !auth_status.status.success() {
92            println!("  ⚠️  GitHub authentication not found");
93            println!();
94
95            if !self.skip_prompts {
96                let should_auth = Confirm::new()
97                    .with_prompt("Would you like to authenticate now?")
98                    .default(true)
99                    .interact()
100                    .map_err(|e| CliError::GitConfig(format!("Failed to prompt: {}", e)))?;
101
102                if should_auth {
103                    println!();
104                    println!("  Running: gh auth login");
105                    let login_result = Command::new("gh")
106                        .args(["auth", "login"])
107                        .status()
108                        .map_err(|e| {
109                            CliError::GitConfig(format!("Failed to run gh auth login: {}", e))
110                        })?;
111
112                    if !login_result.success() {
113                        return Err(CliError::GitConfig(
114                            "GitHub authentication failed".to_string(),
115                        ));
116                    }
117                } else {
118                    return Err(CliError::GitConfig(
119                        "GitHub authentication is required. Run: gh auth login".to_string(),
120                    ));
121                }
122            } else {
123                return Err(CliError::GitConfig(
124                    "GitHub authentication is required. Run: gh auth login".to_string(),
125                ));
126            }
127        }
128
129        Ok(())
130    }
131
132    fn detect_git_repository(&self) -> Result<(String, String)> {
133        // Run git remote get-url origin
134        let output = Command::new("git")
135            .args(["remote", "get-url", "origin"])
136            .output()
137            .map_err(|e| CliError::GitConfig(format!("Failed to run git command: {}", e)))?;
138
139        if !output.status.success() {
140            return Err(CliError::GitConfig(
141                "Not a git repository. Please initialize git first:\n  git init\n  git remote add origin <url>".to_string(),
142            ));
143        }
144
145        let remote_url = String::from_utf8_lossy(&output.stdout).trim().to_string();
146
147        // Parse HTTPS format
148        if remote_url.starts_with("http") && remote_url.contains("github.com/") {
149            let parts: Vec<&str> = remote_url
150                .split("github.com/")
151                .nth(1)
152                .ok_or_else(|| CliError::GitConfig("Invalid GitHub URL".to_string()))?
153                .trim_end_matches(".git")
154                .split('/')
155                .collect();
156
157            if parts.len() >= 2 {
158                return Ok((parts[0].to_string(), parts[1].to_string()));
159            }
160        }
161
162        // Parse SSH format
163        if remote_url.starts_with("git@github.com:") {
164            let repo_part = remote_url
165                .strip_prefix("git@github.com:")
166                .ok_or_else(|| CliError::GitConfig("Invalid SSH URL".to_string()))?
167                .trim_end_matches(".git");
168
169            let parts: Vec<&str> = repo_part.split('/').collect();
170            if parts.len() >= 2 {
171                return Ok((parts[0].to_string(), parts[1].to_string()));
172            }
173        }
174
175        Err(CliError::GitConfig(format!(
176            "Could not parse GitHub owner/repo from remote URL: {}",
177            remote_url
178        )))
179    }
180
181    fn create_env_file(&self, owner: &str, repo: &str) -> Result<()> {
182        let env_path = Path::new(".env");
183
184        if env_path.exists() && !self.skip_prompts {
185            let overwrite = Confirm::new()
186                .with_prompt(".env already exists. Overwrite?")
187                .default(false)
188                .interact()
189                .map_err(|e| CliError::GitConfig(format!("Failed to prompt: {}", e)))?;
190
191            if !overwrite {
192                println!("  ⏭️  Skipping .env creation");
193                return Ok(());
194            }
195        }
196
197        let content = format!(
198            r#"# ==============================================================================
199# Miyabi - Environment Configuration
200# ==============================================================================
201# Generated: {}
202# Platform: {}
203# ==============================================================================
204
205# -----------------------------------------------------------------------------
206# Required: API Keys
207# -----------------------------------------------------------------------------
208
209# GitHub Personal Access Token
210# The application automatically uses 'gh auth token' if available
211# Only set this if gh CLI is not available
212# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
213
214# -----------------------------------------------------------------------------
215# Repository Configuration
216# -----------------------------------------------------------------------------
217
218GITHUB_REPOSITORY={}/{}
219GITHUB_REPOSITORY_OWNER={}
220
221# Device Identifier (for logs and monitoring)
222DEVICE_IDENTIFIER={}
223
224# -----------------------------------------------------------------------------
225# Rust Configuration
226# -----------------------------------------------------------------------------
227
228RUST_LOG=info
229RUST_BACKTRACE=1
230
231# -----------------------------------------------------------------------------
232# Agent Configuration
233# -----------------------------------------------------------------------------
234
235LOG_DIRECTORY=.ai/logs
236REPORT_DIRECTORY=.ai/parallel-reports
237DEFAULT_CONCURRENCY=2
238
239# -----------------------------------------------------------------------------
240# Worktree Configuration
241# -----------------------------------------------------------------------------
242
243USE_WORKTREE=true
244WORKTREE_BASE_DIR=.worktrees
245"#,
246            chrono::Local::now().format("%Y-%m-%d"),
247            std::env::consts::OS,
248            owner,
249            repo,
250            owner,
251            hostname::get().unwrap().to_string_lossy()
252        );
253
254        fs::write(env_path, content)
255            .map_err(|e| CliError::GitConfig(format!("Failed to write .env: {}", e)))?;
256
257        println!("  ✅ Created .env");
258        Ok(())
259    }
260
261    fn create_miyabi_yml(&self, owner: &str, repo: &str) -> Result<()> {
262        let yml_path = Path::new(".miyabi.yml");
263
264        if yml_path.exists() && !self.skip_prompts {
265            let overwrite = Confirm::new()
266                .with_prompt(".miyabi.yml already exists. Overwrite?")
267                .default(false)
268                .interact()
269                .map_err(|e| CliError::GitConfig(format!("Failed to prompt: {}", e)))?;
270
271            if !overwrite {
272                println!("  ⏭️  Skipping .miyabi.yml creation");
273                return Ok(());
274            }
275        }
276
277        let content = format!(
278            r#"github:
279  defaultPrivate: true
280  owner: {}
281  repo: {}
282project:
283  gitignoreTemplate: Node
284  licenseTemplate: Apache-2.0
285labels: {{}}
286workflows:
287  autoLabel: true
288  autoReview: true
289  autoSync: true
290cli:
291  language: ja
292  theme: default
293  verboseErrors: true
294"#,
295            owner, repo
296        );
297
298        fs::write(yml_path, content)
299            .map_err(|e| CliError::GitConfig(format!("Failed to write .miyabi.yml: {}", e)))?;
300
301        println!("  ✅ Created .miyabi.yml");
302        Ok(())
303    }
304
305    fn create_directories(&self) -> Result<()> {
306        let dirs = vec![".ai/logs", ".ai/parallel-reports", ".worktrees"];
307
308        for dir in dirs {
309            fs::create_dir_all(dir)
310                .map_err(|e| CliError::GitConfig(format!("Failed to create {}: {}", dir, e)))?;
311            println!("  ✅ Created {}", dir);
312        }
313
314        Ok(())
315    }
316
317    fn verify_setup(&self) -> Result<()> {
318        // Check if files exist
319        let required_files = vec![".env", ".miyabi.yml"];
320        let required_dirs = vec![".ai/logs", ".ai/parallel-reports", ".worktrees"];
321
322        for file in required_files {
323            if !Path::new(file).exists() {
324                return Err(CliError::GitConfig(format!("Missing file: {}", file)));
325            }
326        }
327
328        for dir in required_dirs {
329            if !Path::new(dir).exists() {
330                return Err(CliError::GitConfig(format!("Missing directory: {}", dir)));
331            }
332        }
333
334        Ok(())
335    }
336}