miyabi_cli/commands/
init.rs

1//! Init command - Initialize new Miyabi project
2
3use 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)] // Reserved for GitHub repo creation (public vs private)
11    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        // Validate project name
23        self.validate_project_name()?;
24
25        // Create project directory
26        let project_dir = self.create_project_directory()?;
27
28        // Initialize git repository
29        self.init_git_repository(&project_dir)?;
30
31        // Create basic structure
32        self.create_project_structure(&project_dir)?;
33
34        // Create configuration files
35        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        // Check if name is valid
50        if self.name.is_empty() {
51            return Err(CliError::InvalidProjectName(
52                "Project name cannot be empty".to_string(),
53            ));
54        }
55
56        // Check if name contains invalid characters
57        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        // Check if directory already exists
75        if project_dir.exists() {
76            return Err(CliError::ProjectExists(self.name.clone()));
77        }
78
79        // Create directory
80        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        // Initialize git repository
90        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        // Create standard directories
107        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        // Create .miyabi.yml
129        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        // Create .gitignore
158        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        // Create README.md
183        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}