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!(
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 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 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 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 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 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 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}