1use std::env;
2use std::fs;
3use std::path::Path;
4
5use anyhow::{Context, Result};
6
7use crate::config::Config;
8
9#[derive(Debug, Default)]
11pub struct InitParams {
12 pub project_name: Option<String>,
13 pub run: Option<String>,
14 pub plan: Option<String>,
15}
16
17pub struct InitResult {
19 pub project: String,
20 pub already_existed: bool,
21 pub run: Option<String>,
22 pub plan: Option<String>,
23}
24
25pub fn init(path: Option<&Path>, params: InitParams) -> Result<InitResult> {
31 let cwd = if let Some(p) = path {
32 p.to_path_buf()
33 } else {
34 env::current_dir()?
35 };
36 let mana_dir = cwd.join(".mana");
37 let already_existed = mana_dir.exists() && mana_dir.is_dir();
38
39 if !mana_dir.exists() {
40 fs::create_dir(&mana_dir).with_context(|| {
41 format!("Failed to create .mana directory at {}", mana_dir.display())
42 })?;
43 } else if !mana_dir.is_dir() {
44 anyhow::bail!(".mana exists but is not a directory");
45 }
46
47 let project = if let Some(ref name) = params.project_name {
48 name.clone()
49 } else if already_existed {
50 Config::load(&mana_dir)
51 .map(|c| c.project)
52 .unwrap_or_else(|_| auto_detect_project_name(&cwd))
53 } else {
54 auto_detect_project_name(&cwd)
55 };
56
57 let next_id = if already_existed {
58 Config::load(&mana_dir).map(|c| c.next_id).unwrap_or(1)
59 } else {
60 1
61 };
62
63 let config = Config {
64 project: project.clone(),
65 next_id,
66 auto_close_parent: true,
67 run: params.run.clone(),
68 plan: params.plan.clone(),
69 max_loops: 10,
70 max_concurrent: 4,
71 poll_interval: 30,
72 extends: vec![],
73 rules_file: None,
74 file_locking: false,
75 worktree: false,
76 on_close: None,
77 on_fail: None,
78 verify_timeout: None,
79 review: None,
80 user: None,
81 user_email: None,
82 auto_commit: false,
83 commit_template: None,
84 research: None,
85 run_model: None,
86 plan_model: None,
87 review_model: None,
88 research_model: None,
89 batch_verify: false,
90 memory_reserve_mb: 0,
91 notify: None,
92 };
93
94 config.save(&mana_dir)?;
95
96 let rules_path = mana_dir.join("RULES.md");
97 if !rules_path.exists() {
98 fs::write(
99 &rules_path,
100 "\
101# Project Rules
102
103<!-- These rules are automatically injected into every agent context.
104 Define coding standards, conventions, and constraints here.
105 Delete these comments and add your own rules. -->
106
107<!-- Example rules:
108
109## Code Style
110- Use `snake_case` for functions and variables
111- Maximum line length: 100 characters
112- All public functions must have doc comments
113
114## Architecture
115- No direct database access outside the `db` module
116- All errors must use the `anyhow` crate
117
118## Forbidden Patterns
119- No `.unwrap()` in production code
120- No `println!` for logging (use `tracing` instead)
121-->
122",
123 )
124 .with_context(|| format!("Failed to create RULES.md at {}", rules_path.display()))?;
125 }
126
127 let gitignore_path = mana_dir.join(".gitignore");
128 if !gitignore_path.exists() {
129 fs::write(
130 &gitignore_path,
131 "# Regenerable cache — rebuilt automatically by mana sync\nindex.yaml\narchive.yaml\n\n# File lock\nindex.lock\n",
132 )
133 .with_context(|| format!("Failed to create .gitignore at {}", gitignore_path.display()))?;
134 }
135
136 Ok(InitResult {
137 project,
138 already_existed,
139 run: params.run,
140 plan: params.plan,
141 })
142}
143
144pub fn auto_detect_project_name(cwd: &Path) -> String {
146 cwd.file_name()
147 .and_then(|n| n.to_str())
148 .map(|s| s.to_string())
149 .unwrap_or_else(|| "project".to_string())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use tempfile::TempDir;
156
157 #[test]
158 fn init_creates_mana_dir() {
159 let dir = TempDir::new().unwrap();
160 let result = init(Some(dir.path()), InitParams::default()).unwrap();
161
162 assert!(!result.already_existed);
163 assert!(dir.path().join(".mana").exists());
164 assert!(dir.path().join(".mana").is_dir());
165 }
166
167 #[test]
168 fn init_creates_config() {
169 let dir = TempDir::new().unwrap();
170 init(
171 Some(dir.path()),
172 InitParams {
173 project_name: Some("my-project".to_string()),
174 ..Default::default()
175 },
176 )
177 .unwrap();
178
179 let config = Config::load(&dir.path().join(".mana")).unwrap();
180 assert_eq!(config.project, "my-project");
181 assert_eq!(config.next_id, 1);
182 }
183
184 #[test]
185 fn init_preserves_next_id_on_reinit() {
186 let dir = TempDir::new().unwrap();
187 init(Some(dir.path()), InitParams::default()).unwrap();
188
189 let mana_dir = dir.path().join(".mana");
190 let mut config = Config::load(&mana_dir).unwrap();
191 config.next_id = 42;
192 config.save(&mana_dir).unwrap();
193
194 let result = init(Some(dir.path()), InitParams::default()).unwrap();
195
196 assert!(result.already_existed);
197 let config = Config::load(&mana_dir).unwrap();
198 assert_eq!(config.next_id, 42);
199 }
200
201 #[test]
202 fn init_creates_rules_md() {
203 let dir = TempDir::new().unwrap();
204 init(Some(dir.path()), InitParams::default()).unwrap();
205
206 let rules_path = dir.path().join(".mana").join("RULES.md");
207 assert!(rules_path.exists());
208 let content = fs::read_to_string(&rules_path).unwrap();
209 assert!(content.contains("# Project Rules"));
210 }
211
212 #[test]
213 fn init_does_not_overwrite_rules_md() {
214 let dir = TempDir::new().unwrap();
215 init(Some(dir.path()), InitParams::default()).unwrap();
216
217 let rules_path = dir.path().join(".mana").join("RULES.md");
218 fs::write(&rules_path, "# Custom rules").unwrap();
219
220 init(Some(dir.path()), InitParams::default()).unwrap();
221
222 let content = fs::read_to_string(&rules_path).unwrap();
223 assert!(content.contains("# Custom rules"));
224 }
225
226 #[test]
227 fn init_with_run_and_plan() {
228 let dir = TempDir::new().unwrap();
229 let result = init(
230 Some(dir.path()),
231 InitParams {
232 run: Some("pi run {id}".to_string()),
233 plan: Some("pi plan {id}".to_string()),
234 ..Default::default()
235 },
236 )
237 .unwrap();
238
239 assert_eq!(result.run, Some("pi run {id}".to_string()));
240 let config = Config::load(&dir.path().join(".mana")).unwrap();
241 assert_eq!(config.run, Some("pi run {id}".to_string()));
242 assert_eq!(config.plan, Some("pi plan {id}".to_string()));
243 }
244}