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        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
144/// Auto-detect project name from directory name.
145pub 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}