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