miyabi_cli/commands/
setup.rs1use 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 println!("{}", "Step 1: Checking GitHub authentication...".bold());
27 self.check_github_auth()?;
28 println!("{}", " ✅ GitHub authentication verified".green());
29 println!();
30
31 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 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 println!("{}", "Step 4: Creating directories...".bold());
46 self.create_directories()?;
47 println!("{}", " ✅ Directories created".green());
48 println!();
49
50 println!("{}", "Step 5: Verifying setup...".bold());
52 self.verify_setup()?;
53 println!("{}", " ✅ Setup verified".green());
54 println!();
55
56 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 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 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 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 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 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 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}