miyabi_cli/commands/
init.rs1use crate::error::{CliError, Result};
4use colored::Colorize;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8pub struct InitCommand {
9 pub name: String,
10 #[allow(dead_code)] pub private: bool,
12}
13
14impl InitCommand {
15 pub fn new(name: String, private: bool) -> Self {
16 Self { name, private }
17 }
18
19 pub async fn execute(&self) -> Result<()> {
20 println!("{}", "🚀 Initializing new Miyabi project...".cyan().bold());
21
22 self.validate_project_name()?;
24
25 let project_dir = self.create_project_directory()?;
27
28 self.init_git_repository(&project_dir)?;
30
31 self.create_project_structure(&project_dir)?;
33
34 self.create_config_files(&project_dir)?;
36
37 println!();
38 println!("{}", "✅ Project initialized successfully!".green().bold());
39 println!();
40 println!("Next steps:");
41 println!(" cd {}", self.name);
42 println!(" export GITHUB_TOKEN=ghp_xxx");
43 println!(" miyabi status");
44
45 Ok(())
46 }
47
48 fn validate_project_name(&self) -> Result<()> {
49 if self.name.is_empty() {
51 return Err(CliError::InvalidProjectName(
52 "Project name cannot be empty".to_string(),
53 ));
54 }
55
56 if !self
58 .name
59 .chars()
60 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
61 {
62 return Err(CliError::InvalidProjectName(
63 "Project name can only contain alphanumeric characters, hyphens, and underscores"
64 .to_string(),
65 ));
66 }
67
68 Ok(())
69 }
70
71 fn create_project_directory(&self) -> Result<PathBuf> {
72 let project_dir = PathBuf::from(&self.name);
73
74 if project_dir.exists() {
76 return Err(CliError::ProjectExists(self.name.clone()));
77 }
78
79 fs::create_dir(&project_dir)?;
81 println!(" Created directory: {}", project_dir.display());
82
83 Ok(project_dir)
84 }
85
86 fn init_git_repository(&self, project_dir: &Path) -> Result<()> {
87 use std::process::Command;
88
89 let output = Command::new("git")
91 .args(["init"])
92 .current_dir(project_dir)
93 .output()?;
94
95 if !output.status.success() {
96 return Err(CliError::Io(std::io::Error::other(
97 "Failed to initialize git repository",
98 )));
99 }
100
101 println!(" Initialized git repository");
102 Ok(())
103 }
104
105 fn create_project_structure(&self, project_dir: &Path) -> Result<()> {
106 let dirs = vec![
108 ".github/workflows",
109 ".claude/agents/specs",
110 ".claude/agents/prompts",
111 ".claude/commands",
112 "docs",
113 "scripts",
114 "logs",
115 "reports",
116 ];
117
118 for dir in dirs {
119 let dir_path = project_dir.join(dir);
120 fs::create_dir_all(&dir_path)?;
121 }
122
123 println!(" Created project structure");
124 Ok(())
125 }
126
127 fn create_config_files(&self, project_dir: &Path) -> Result<()> {
128 let miyabi_config = format!(
130 r#"# Miyabi Configuration
131project_name: {}
132version: "0.1.0"
133
134# GitHub settings (use environment variables for sensitive data)
135# github_token: ${{{{ GITHUB_TOKEN }}}}
136
137# Agent settings
138agents:
139 enabled: true
140 use_worktree: true
141 worktree_base_path: ".worktrees"
142
143# Logging
144logging:
145 level: info
146 directory: "./logs"
147
148# Reporting
149reporting:
150 directory: "./reports"
151"#,
152 self.name
153 );
154
155 fs::write(project_dir.join(".miyabi.yml"), miyabi_config)?;
156
157 let gitignore = r#"# Miyabi
159.miyabi.yml
160.worktrees/
161logs/
162reports/
163*.log
164
165# Environment
166.env
167.env.local
168
169# Dependencies
170node_modules/
171target/
172
173# IDE
174.vscode/
175.idea/
176*.swp
177*.swo
178"#;
179
180 fs::write(project_dir.join(".gitignore"), gitignore)?;
181
182 let readme = format!(
184 r#"# {}
185
186Miyabi autonomous development project.
187
188## Setup
189
1901. Set GitHub token:
191 ```bash
192 export GITHUB_TOKEN=ghp_xxx
193 ```
194
1952. Check status:
196 ```bash
197 miyabi status
198 ```
199
2003. Run agent:
201 ```bash
202 miyabi agent coordinator --issue 1
203 ```
204
205## Documentation
206
207- See `docs/` directory for detailed documentation
208- See `.claude/agents/specs/` for agent specifications
209"#,
210 self.name
211 );
212
213 fs::write(project_dir.join("README.md"), readme)?;
214
215 println!(" Created configuration files");
216 Ok(())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_validate_project_name() {
226 let valid_cmd = InitCommand::new("my-project".to_string(), false);
227 assert!(valid_cmd.validate_project_name().is_ok());
228
229 let valid_cmd = InitCommand::new("my_project_123".to_string(), false);
230 assert!(valid_cmd.validate_project_name().is_ok());
231
232 let invalid_cmd = InitCommand::new("".to_string(), false);
233 assert!(invalid_cmd.validate_project_name().is_err());
234
235 let invalid_cmd = InitCommand::new("my project".to_string(), false);
236 assert!(invalid_cmd.validate_project_name().is_err());
237
238 let invalid_cmd = InitCommand::new("my@project".to_string(), false);
239 assert!(invalid_cmd.validate_project_name().is_err());
240 }
241}