Skip to main content

mana_core/ops/
init.rs

1use std::env;
2use std::fs;
3use std::path::Path;
4
5use anyhow::{Context, Result};
6
7use crate::config::Config;
8
9/// Parameters for initializing a units project.
10#[derive(Debug, Default)]
11pub struct InitParams {
12    pub project_name: Option<String>,
13    pub run: Option<String>,
14    pub plan: Option<String>,
15}
16
17/// Result of initialization.
18pub struct InitResult {
19    pub project: String,
20    pub already_existed: bool,
21    pub run: Option<String>,
22    pub plan: Option<String>,
23}
24
25/// Initialize a .mana/ directory with config and default files.
26///
27/// Creates .mana/, config.yaml, RULES.md stub, and .gitignore.
28/// Preserves existing project name and next_id on re-init.
29/// Returns structured result so callers can format output.
30pub 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
145/// Auto-detect project name from directory name.
146pub 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}