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 post_plan: None,
79 verify_timeout: None,
80 review: None,
81 user: None,
82 user_email: None,
83 auto_commit: false,
84 commit_template: None,
85 research: None,
86 run_model: None,
87 plan_model: None,
88 review_model: None,
89 research_model: None,
90 batch_verify: false,
91 memory_reserve_mb: 0,
92 notify: None,
93 };
94
95 config.save(&mana_dir)?;
96
97 let rules_path = mana_dir.join("RULES.md");
98 if !rules_path.exists() {
99 fs::write(
100 &rules_path,
101 "\
102# Project Rules
103
104<!-- These rules are automatically injected into every agent context.
105 Define coding standards, conventions, and constraints here.
106 Delete these comments and add your own rules. -->
107
108<!-- Example rules:
109
110## Code Style
111- Use `snake_case` for functions and variables
112- Maximum line length: 100 characters
113- All public functions must have doc comments
114
115## Architecture
116- No direct database access outside the `db` module
117- All errors must use the `anyhow` crate
118
119## Forbidden Patterns
120- No `.unwrap()` in production code
121- No `println!` for logging (use `tracing` instead)
122-->
123",
124 )
125 .with_context(|| format!("Failed to create RULES.md at {}", rules_path.display()))?;
126 }
127
128 let gitignore_path = mana_dir.join(".gitignore");
129 if !gitignore_path.exists() {
130 fs::write(
131 &gitignore_path,
132 "# Regenerable cache — rebuilt automatically by mana sync\nindex.yaml\narchive.yaml\n\n# File lock\nindex.lock\n",
133 )
134 .with_context(|| format!("Failed to create .gitignore at {}", gitignore_path.display()))?;
135 }
136
137 Ok(InitResult {
138 project,
139 already_existed,
140 run: params.run,
141 plan: params.plan,
142 })
143}
144
145pub fn auto_detect_project_name(cwd: &Path) -> String {
147 cwd.file_name()
148 .and_then(|n| n.to_str())
149 .map(|s| s.to_string())
150 .unwrap_or_else(|| "project".to_string())
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use tempfile::TempDir;
157
158 #[test]
159 fn init_creates_mana_dir() {
160 let dir = TempDir::new().unwrap();
161 let result = init(Some(dir.path()), InitParams::default()).unwrap();
162
163 assert!(!result.already_existed);
164 assert!(dir.path().join(".mana").exists());
165 assert!(dir.path().join(".mana").is_dir());
166 }
167
168 #[test]
169 fn init_creates_config() {
170 let dir = TempDir::new().unwrap();
171 init(
172 Some(dir.path()),
173 InitParams {
174 project_name: Some("my-project".to_string()),
175 ..Default::default()
176 },
177 )
178 .unwrap();
179
180 let config = Config::load(&dir.path().join(".mana")).unwrap();
181 assert_eq!(config.project, "my-project");
182 assert_eq!(config.next_id, 1);
183 }
184
185 #[test]
186 fn init_preserves_next_id_on_reinit() {
187 let dir = TempDir::new().unwrap();
188 init(Some(dir.path()), InitParams::default()).unwrap();
189
190 let mana_dir = dir.path().join(".mana");
191 let mut config = Config::load(&mana_dir).unwrap();
192 config.next_id = 42;
193 config.save(&mana_dir).unwrap();
194
195 let result = init(Some(dir.path()), InitParams::default()).unwrap();
196
197 assert!(result.already_existed);
198 let config = Config::load(&mana_dir).unwrap();
199 assert_eq!(config.next_id, 42);
200 }
201
202 #[test]
203 fn init_creates_rules_md() {
204 let dir = TempDir::new().unwrap();
205 init(Some(dir.path()), InitParams::default()).unwrap();
206
207 let rules_path = dir.path().join(".mana").join("RULES.md");
208 assert!(rules_path.exists());
209 let content = fs::read_to_string(&rules_path).unwrap();
210 assert!(content.contains("# Project Rules"));
211 }
212
213 #[test]
214 fn init_does_not_overwrite_rules_md() {
215 let dir = TempDir::new().unwrap();
216 init(Some(dir.path()), InitParams::default()).unwrap();
217
218 let rules_path = dir.path().join(".mana").join("RULES.md");
219 fs::write(&rules_path, "# Custom rules").unwrap();
220
221 init(Some(dir.path()), InitParams::default()).unwrap();
222
223 let content = fs::read_to_string(&rules_path).unwrap();
224 assert!(content.contains("# Custom rules"));
225 }
226
227 #[test]
228 fn init_with_run_and_plan() {
229 let dir = TempDir::new().unwrap();
230 let result = init(
231 Some(dir.path()),
232 InitParams {
233 run: Some("pi run {id}".to_string()),
234 plan: Some("pi plan {id}".to_string()),
235 ..Default::default()
236 },
237 )
238 .unwrap();
239
240 assert_eq!(result.run, Some("pi run {id}".to_string()));
241 let config = Config::load(&dir.path().join(".mana")).unwrap();
242 assert_eq!(config.run, Some("pi run {id}".to_string()));
243 assert_eq!(config.plan, Some("pi plan {id}".to_string()));
244 }
245}