Skip to main content

mana/commands/
init.rs

1use std::env;
2use std::fs;
3use std::io::{self, IsTerminal, Write as _};
4use std::path::Path;
5use std::process::Command;
6
7use anyhow::{Context, Result};
8
9use crate::config::Config;
10
11/// Known agent presets with their run/plan templates and detection info.
12#[derive(Debug, Clone)]
13struct AgentPreset {
14    /// Display name (e.g. "pi")
15    name: &'static str,
16    /// Shell command template for `run`. `{id}` is replaced with unit ID.
17    run: &'static str,
18    /// Shell command template for `plan`. `{id}` is replaced with unit ID.
19    plan: &'static str,
20    /// Command to check if the agent is installed (e.g. "pi --version").
21    version_cmd: &'static str,
22    /// CLI binary name to search for in PATH.
23    binary: &'static str,
24}
25
26const PRESETS: &[AgentPreset] = &[
27    AgentPreset {
28        name: "pi",
29        run: "pi run {id}",
30        plan: "pi plan {id}",
31        version_cmd: "pi --version",
32        binary: "pi",
33    },
34    AgentPreset {
35        name: "claude",
36        run: "claude -p 'Implement unit {id}. Read unit with mana show {id}. Read referenced files with mana context {id}. When done run mana close {id}.'",
37        plan: "claude -p 'Decompose unit {id}. Read unit with mana show {id}. Break into child units with mana create --parent {id}.'",
38        version_cmd: "claude --version",
39        binary: "claude",
40    },
41    AgentPreset {
42        name: "aider",
43        run: "aider --message 'Implement unit {id}. Read unit with mana show {id}. Read referenced files with mana context {id}. When done run mana close {id}.'",
44        plan: "aider --message 'Decompose unit {id}. Read unit with mana show {id}. Break into child units with mana create --parent {id}.'",
45        version_cmd: "aider --version",
46        binary: "aider",
47    },
48];
49
50/// Arguments for `mana init`.
51#[derive(Debug, Default)]
52pub struct InitArgs {
53    pub project_name: Option<String>,
54    pub agent: Option<String>,
55    pub run: Option<String>,
56    pub plan: Option<String>,
57    pub setup: bool,
58    pub no_agent: bool,
59}
60
61/// Find a preset by name (case-insensitive).
62fn find_preset(name: &str) -> Option<&'static AgentPreset> {
63    let lower = name.to_lowercase();
64    PRESETS.iter().find(|p| p.name == lower)
65}
66
67/// Check if a binary exists in PATH using `which`.
68fn binary_exists(name: &str) -> Option<String> {
69    Command::new("which")
70        .arg(name)
71        .output()
72        .ok()
73        .filter(|o| o.status.success())
74        .and_then(|o| String::from_utf8(o.stdout).ok())
75        .map(|s| s.trim().to_string())
76}
77
78/// Detect which agent CLIs are installed.
79/// Returns a list of (preset, Option<path>).
80fn detect_agents() -> Vec<(&'static AgentPreset, Option<String>)> {
81    PRESETS
82        .iter()
83        .map(|p| (p, binary_exists(p.binary)))
84        .collect()
85}
86
87/// Run the agent's version command and return the output.
88fn verify_agent(preset: &AgentPreset) -> Option<String> {
89    let parts: Vec<&str> = preset.version_cmd.split_whitespace().collect();
90    if parts.is_empty() {
91        return None;
92    }
93    Command::new(parts[0])
94        .args(&parts[1..])
95        .output()
96        .ok()
97        .filter(|o| o.status.success())
98        .and_then(|o| {
99            String::from_utf8(o.stdout)
100                .or_else(|_| String::from_utf8(o.stderr.clone()))
101                .ok()
102        })
103        .map(|s| s.trim().to_string())
104}
105
106/// Interactive agent setup wizard (for TTY).
107/// Returns (run_template, plan_template) or None if user skips.
108fn interactive_agent_setup() -> Result<Option<(String, String)>> {
109    let detected = detect_agents();
110
111    eprintln!("Agent setup");
112    eprintln!("  Checking for agent CLIs...");
113
114    for (preset, path) in &detected {
115        if let Some(p) = path {
116            eprintln!("  ✓ {} found ({})", preset.name, p);
117        } else {
118            eprintln!("  ✗ {} not found", preset.name);
119        }
120    }
121    eprintln!();
122
123    // Build menu
124    let mut options: Vec<String> = Vec::new();
125    for (i, (preset, path)) in detected.iter().enumerate() {
126        let marker = if path.is_some() { "✓" } else { " " };
127        options.push(format!("[{}] {} {}", i + 1, marker, preset.name));
128    }
129    options.push(format!("[{}] custom", PRESETS.len() + 1));
130    options.push(format!("[{}] skip", PRESETS.len() + 2));
131
132    eprintln!("Which agent?  {}", options.join("  "));
133    eprint!("> ");
134    io::stderr().flush()?;
135
136    let mut input = String::new();
137    io::stdin().read_line(&mut input)?;
138    let input = input.trim();
139
140    // Parse choice
141    let choice: usize = match input.parse() {
142        Ok(n) => n,
143        Err(_) => {
144            // Try matching by name
145            if let Some(preset) = find_preset(input) {
146                return finish_preset_selection(preset);
147            }
148            eprintln!("Skipping agent setup.");
149            return Ok(None);
150        }
151    };
152
153    if choice == 0 || choice > PRESETS.len() + 2 {
154        eprintln!("Skipping agent setup.");
155        return Ok(None);
156    }
157
158    // Skip
159    if choice == PRESETS.len() + 2 {
160        return Ok(None);
161    }
162
163    // Custom
164    if choice == PRESETS.len() + 1 {
165        eprint!("Run command template (use {{id}} for unit ID): ");
166        io::stderr().flush()?;
167        let mut run_input = String::new();
168        io::stdin().read_line(&mut run_input)?;
169        let run_cmd = run_input.trim().to_string();
170
171        eprint!("Plan command template (use {{id}} for unit ID, Enter to skip): ");
172        io::stderr().flush()?;
173        let mut plan_input = String::new();
174        io::stdin().read_line(&mut plan_input)?;
175        let plan_cmd = plan_input.trim().to_string();
176
177        if run_cmd.is_empty() {
178            eprintln!("No run command provided. Skipping agent setup.");
179            return Ok(None);
180        }
181
182        let plan = if plan_cmd.is_empty() {
183            run_cmd.clone()
184        } else {
185            plan_cmd
186        };
187
188        return Ok(Some((run_cmd, plan)));
189    }
190
191    // Preset selection (1-indexed)
192    let preset = &PRESETS[choice - 1];
193    finish_preset_selection(preset)
194}
195
196/// Apply a preset: verify agent and return templates.
197fn finish_preset_selection(preset: &AgentPreset) -> Result<Option<(String, String)>> {
198    eprintln!();
199    eprintln!("Verifying {}...", preset.name);
200    match verify_agent(preset) {
201        Some(version) => eprintln!("  ✓ {} → {}", preset.version_cmd, version),
202        None => eprintln!(
203            "  ⚠ {} not responding (you can still configure it)",
204            preset.name
205        ),
206    }
207
208    Ok(Some((preset.run.to_string(), preset.plan.to_string())))
209}
210
211/// Initialize a .mana/ directory with a config.yaml file.
212///
213/// Supports agent setup via presets, custom commands, or interactive wizard.
214pub fn cmd_init(path: Option<&Path>, args: InitArgs) -> Result<()> {
215    let cwd = if let Some(p) = path {
216        p.to_path_buf()
217    } else {
218        env::current_dir()?
219    };
220    let mana_dir = cwd.join(".mana");
221    let already_exists = mana_dir.exists() && mana_dir.is_dir();
222
223    // Re-init without --setup: show current config and hint
224    if already_exists && !args.setup && args.agent.is_none() && args.run.is_none() {
225        if let Ok(config) = Config::load(&mana_dir) {
226            eprintln!("Project: {}", config.project);
227            match &config.run {
228                Some(run) => eprintln!("Run: {}", run),
229                None => eprintln!("Run: (not configured)"),
230            }
231            match &config.plan {
232                Some(plan) => eprintln!("Plan: {}", plan),
233                None => eprintln!("Plan: (not configured)"),
234            }
235            eprintln!();
236            eprintln!("To reconfigure: mana init --setup");
237            return Ok(());
238        }
239        // Config missing/corrupt — fall through to create it
240    }
241
242    // Create .mana/ directory if it doesn't exist
243    if !mana_dir.exists() {
244        fs::create_dir(&mana_dir).with_context(|| {
245            format!("Failed to create .mana directory at {}", mana_dir.display())
246        })?;
247    } else if !mana_dir.is_dir() {
248        anyhow::bail!(".mana exists but is not a directory");
249    }
250
251    // Determine project name
252    let project = if let Some(ref name) = args.project_name {
253        name.clone()
254    } else if already_exists {
255        // Preserve existing project name on --setup
256        Config::load(&mana_dir)
257            .map(|c| c.project)
258            .unwrap_or_else(|_| auto_detect_project_name(&cwd))
259    } else {
260        auto_detect_project_name(&cwd)
261    };
262
263    // Preserve next_id on re-init
264    let next_id = if already_exists {
265        Config::load(&mana_dir).map(|c| c.next_id).unwrap_or(1)
266    } else {
267        1
268    };
269
270    // Determine agent config (run/plan)
271    let (run, plan) = resolve_agent_config(&args)?;
272
273    // Create config
274    let config = Config {
275        project: project.clone(),
276        next_id,
277        auto_close_parent: true,
278        run,
279        plan,
280        max_loops: 10,
281        max_concurrent: 4,
282        poll_interval: 30,
283        extends: vec![],
284        rules_file: None,
285        file_locking: false,
286        worktree: false,
287        on_close: None,
288        on_fail: None,
289        post_plan: None,
290        verify_timeout: None,
291        review: None,
292        user: None,
293        user_email: None,
294        auto_commit: false,
295        commit_template: None,
296        research: None,
297        run_model: None,
298        plan_model: None,
299        review_model: None,
300        research_model: None,
301        batch_verify: false,
302        memory_reserve_mb: 0,
303        notify: None,
304    };
305
306    config.save(&mana_dir)?;
307
308    // Create stub RULES.md if it doesn't exist
309    let rules_path = mana_dir.join("RULES.md");
310    if !rules_path.exists() {
311        fs::write(
312            &rules_path,
313            "\
314# Project Rules
315
316<!-- These rules are automatically injected into every agent context.
317     Define coding standards, conventions, and constraints here.
318     Delete these comments and add your own rules. -->
319
320<!-- Example rules:
321
322## Code Style
323- Use `snake_case` for functions and variables
324- Maximum line length: 100 characters
325- All public functions must have doc comments
326
327## Architecture
328- No direct database access outside the `db` module
329- All errors must use the `anyhow` crate
330
331## Forbidden Patterns
332- No `.unwrap()` in production code
333- No `println!` for logging (use `tracing` instead)
334-->
335",
336        )
337        .with_context(|| format!("Failed to create RULES.md at {}", rules_path.display()))?;
338    }
339
340    // Create .mana/.gitignore if it doesn't exist — index.yaml is a regenerable cache
341    let gitignore_path = mana_dir.join(".gitignore");
342    if !gitignore_path.exists() {
343        fs::write(
344            &gitignore_path,
345            "# Regenerable cache — rebuilt automatically by mana sync\nindex.yaml\narchive.yaml\n\n# File lock\nindex.lock\n",
346        )
347        .with_context(|| format!("Failed to create .gitignore at {}", gitignore_path.display()))?;
348    }
349
350    if already_exists && args.setup {
351        eprintln!("Reconfigured units in .mana/");
352    } else if !already_exists {
353        eprintln!("Initialized units in .mana/");
354    }
355
356    // Print next steps
357    if config.run.is_some() {
358        eprintln!();
359        eprintln!("Next steps:");
360        eprintln!("  mana create \"my first task\" --verify \"test command\"");
361    } else {
362        eprintln!();
363        eprintln!("Next steps:");
364        eprintln!("  mana init --setup          # configure an agent");
365        eprintln!("  mana config set run \"...\"  # or set run command directly");
366        eprintln!("  mana create \"task\" --verify \"test command\"");
367    }
368
369    Ok(())
370}
371
372/// Auto-detect project name from directory.
373fn auto_detect_project_name(cwd: &Path) -> String {
374    cwd.file_name()
375        .and_then(|n| n.to_str())
376        .map(|s| s.to_string())
377        .unwrap_or_else(|| "project".to_string())
378}
379
380/// Resolve the run/plan config from InitArgs.
381///
382/// Priority: --run/--plan flags > --agent preset > interactive wizard > None
383fn resolve_agent_config(args: &InitArgs) -> Result<(Option<String>, Option<String>)> {
384    // --no-agent: skip entirely
385    if args.no_agent {
386        return Ok((None, None));
387    }
388
389    // --run/--plan provided directly
390    if args.run.is_some() || args.plan.is_some() {
391        return Ok((args.run.clone(), args.plan.clone()));
392    }
393
394    // --agent <name>: look up preset
395    if let Some(ref agent_name) = args.agent {
396        let preset = find_preset(agent_name).ok_or_else(|| {
397            anyhow::anyhow!(
398                "Unknown agent '{}'. Known agents: {}",
399                agent_name,
400                PRESETS
401                    .iter()
402                    .map(|p| p.name)
403                    .collect::<Vec<_>>()
404                    .join(", ")
405            )
406        })?;
407
408        eprintln!("Verifying {}...", preset.name);
409        match verify_agent(preset) {
410            Some(version) => eprintln!("  ✓ {} → {}", preset.version_cmd, version),
411            None => eprintln!("  ⚠ {} not responding (configured anyway)", preset.name),
412        }
413
414        return Ok((Some(preset.run.to_string()), Some(preset.plan.to_string())));
415    }
416
417    // Interactive (TTY only)
418    if io::stderr().is_terminal() && (args.setup || !args.no_agent) {
419        if let Some((run, plan)) = interactive_agent_setup()? {
420            return Ok((Some(run), Some(plan)));
421        }
422    }
423
424    // Non-interactive or user skipped
425    Ok((None, None))
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431    use std::fs;
432    use tempfile::TempDir;
433
434    /// Helper to create InitArgs with defaults.
435    fn default_args() -> InitArgs {
436        InitArgs {
437            project_name: None,
438            agent: None,
439            run: None,
440            plan: None,
441            setup: false,
442            no_agent: true, // Skip interactive in tests
443        }
444    }
445
446    #[test]
447    fn init_creates_mana_dir() {
448        let dir = TempDir::new().unwrap();
449        let result = cmd_init(Some(dir.path()), default_args());
450
451        assert!(result.is_ok());
452        assert!(dir.path().join(".mana").exists());
453        assert!(dir.path().join(".mana").is_dir());
454    }
455
456    #[test]
457    fn init_creates_config_with_explicit_name() {
458        let dir = TempDir::new().unwrap();
459        let mut args = default_args();
460        args.project_name = Some("my-project".to_string());
461        let result = cmd_init(Some(dir.path()), args);
462
463        assert!(result.is_ok());
464
465        let config = Config::load(&dir.path().join(".mana")).unwrap();
466        assert_eq!(config.project, "my-project");
467        assert_eq!(config.next_id, 1);
468    }
469
470    #[test]
471    fn init_auto_detects_project_name_from_dir() {
472        let dir = TempDir::new().unwrap();
473        let result = cmd_init(Some(dir.path()), default_args());
474
475        assert!(result.is_ok());
476
477        let config = Config::load(&dir.path().join(".mana")).unwrap();
478        let dir_name = dir
479            .path()
480            .file_name()
481            .and_then(|n| n.to_str())
482            .unwrap_or("project");
483        assert_eq!(config.project, dir_name);
484    }
485
486    #[test]
487    fn init_idempotent() {
488        let dir = TempDir::new().unwrap();
489
490        let mut args1 = default_args();
491        args1.project_name = Some("test-project".to_string());
492        let result1 = cmd_init(Some(dir.path()), args1);
493        assert!(result1.is_ok());
494
495        // Second init with --setup so it actually re-writes
496        let mut args2 = default_args();
497        args2.project_name = Some("test-project".to_string());
498        args2.setup = true;
499        let result2 = cmd_init(Some(dir.path()), args2);
500        assert!(result2.is_ok());
501
502        let config = Config::load(&dir.path().join(".mana")).unwrap();
503        assert_eq!(config.project, "test-project");
504    }
505
506    #[test]
507    fn init_config_is_valid_yaml() {
508        let dir = TempDir::new().unwrap();
509        let mut args = default_args();
510        args.project_name = Some("yaml-test".to_string());
511        let result = cmd_init(Some(dir.path()), args);
512
513        assert!(result.is_ok());
514
515        let config_path = dir.path().join(".mana").join("config.yaml");
516        assert!(config_path.exists());
517
518        let contents = fs::read_to_string(&config_path).unwrap();
519        assert!(contents.contains("project: yaml-test"));
520        assert!(contents.contains("next_id: 1"));
521    }
522
523    #[test]
524    fn init_with_agent_pi_sets_run_and_plan() {
525        let dir = TempDir::new().unwrap();
526        let mut args = default_args();
527        args.agent = Some("pi".to_string());
528        args.no_agent = false;
529        let result = cmd_init(Some(dir.path()), args);
530
531        assert!(result.is_ok());
532
533        let config = Config::load(&dir.path().join(".mana")).unwrap();
534        assert!(config.run.is_some());
535        assert!(config.plan.is_some());
536        assert!(config.run.unwrap().contains("pi"));
537        assert!(config.plan.unwrap().contains("pi"));
538    }
539
540    #[test]
541    fn init_with_agent_claude_sets_run_and_plan() {
542        let dir = TempDir::new().unwrap();
543        let mut args = default_args();
544        args.agent = Some("claude".to_string());
545        args.no_agent = false;
546        let result = cmd_init(Some(dir.path()), args);
547
548        assert!(result.is_ok());
549
550        let config = Config::load(&dir.path().join(".mana")).unwrap();
551        assert!(config.run.is_some());
552        assert!(config.plan.is_some());
553        assert!(config.run.unwrap().contains("claude"));
554        assert!(config.plan.unwrap().contains("claude"));
555    }
556
557    #[test]
558    fn init_with_agent_aider_sets_run_and_plan() {
559        let dir = TempDir::new().unwrap();
560        let mut args = default_args();
561        args.agent = Some("aider".to_string());
562        args.no_agent = false;
563        let result = cmd_init(Some(dir.path()), args);
564
565        assert!(result.is_ok());
566
567        let config = Config::load(&dir.path().join(".mana")).unwrap();
568        assert!(config.run.is_some());
569        assert!(config.plan.is_some());
570        assert!(config.run.unwrap().contains("aider"));
571        assert!(config.plan.unwrap().contains("aider"));
572    }
573
574    #[test]
575    fn init_with_unknown_agent_errors() {
576        let dir = TempDir::new().unwrap();
577        let mut args = default_args();
578        args.agent = Some("unknown-agent".to_string());
579        args.no_agent = false;
580        let result = cmd_init(Some(dir.path()), args);
581
582        assert!(result.is_err());
583        let err = format!("{}", result.unwrap_err());
584        assert!(err.contains("Unknown agent"));
585        assert!(err.contains("unknown-agent"));
586    }
587
588    #[test]
589    fn init_with_custom_run_and_plan() {
590        let dir = TempDir::new().unwrap();
591        let mut args = default_args();
592        args.run = Some("my-agent run {id}".to_string());
593        args.plan = Some("my-agent plan {id}".to_string());
594        args.no_agent = false;
595        let result = cmd_init(Some(dir.path()), args);
596
597        assert!(result.is_ok());
598
599        let config = Config::load(&dir.path().join(".mana")).unwrap();
600        assert_eq!(config.run, Some("my-agent run {id}".to_string()));
601        assert_eq!(config.plan, Some("my-agent plan {id}".to_string()));
602    }
603
604    #[test]
605    fn init_with_run_only() {
606        let dir = TempDir::new().unwrap();
607        let mut args = default_args();
608        args.run = Some("my-agent {id}".to_string());
609        args.no_agent = false;
610        let result = cmd_init(Some(dir.path()), args);
611
612        assert!(result.is_ok());
613
614        let config = Config::load(&dir.path().join(".mana")).unwrap();
615        assert_eq!(config.run, Some("my-agent {id}".to_string()));
616        assert_eq!(config.plan, None);
617    }
618
619    #[test]
620    fn init_with_no_agent_skips_setup() {
621        let dir = TempDir::new().unwrap();
622        let mut args = default_args();
623        args.no_agent = true;
624        let result = cmd_init(Some(dir.path()), args);
625
626        assert!(result.is_ok());
627
628        let config = Config::load(&dir.path().join(".mana")).unwrap();
629        assert_eq!(config.run, None);
630        assert_eq!(config.plan, None);
631    }
632
633    #[test]
634    fn init_setup_on_existing_reconfigures() {
635        let dir = TempDir::new().unwrap();
636
637        // First init — no agent
638        let mut args1 = default_args();
639        args1.project_name = Some("my-project".to_string());
640        cmd_init(Some(dir.path()), args1).unwrap();
641
642        let config1 = Config::load(&dir.path().join(".mana")).unwrap();
643        assert_eq!(config1.run, None);
644
645        // Bump next_id to simulate usage
646        let mut config_modified = config1;
647        config_modified.next_id = 5;
648        config_modified.save(&dir.path().join(".mana")).unwrap();
649
650        // Re-init with --setup --agent pi
651        let mut args2 = default_args();
652        args2.setup = true;
653        args2.agent = Some("pi".to_string());
654        args2.no_agent = false;
655        cmd_init(Some(dir.path()), args2).unwrap();
656
657        let config2 = Config::load(&dir.path().join(".mana")).unwrap();
658        // Agent configured
659        assert!(config2.run.is_some());
660        assert!(config2.run.unwrap().contains("pi"));
661        // Preserved
662        assert_eq!(config2.project, "my-project");
663        assert_eq!(config2.next_id, 5);
664    }
665
666    #[test]
667    fn reinit_without_setup_shows_config() {
668        let dir = TempDir::new().unwrap();
669
670        // First init
671        let mut args1 = default_args();
672        args1.project_name = Some("show-test".to_string());
673        cmd_init(Some(dir.path()), args1).unwrap();
674
675        // Second init without --setup (no flags that would trigger re-write)
676        let args2 = default_args();
677        let result = cmd_init(Some(dir.path()), args2);
678        assert!(result.is_ok());
679
680        // Config unchanged
681        let config = Config::load(&dir.path().join(".mana")).unwrap();
682        assert_eq!(config.project, "show-test");
683    }
684
685    #[test]
686    fn find_preset_is_case_insensitive() {
687        assert!(find_preset("Pi").is_some());
688        assert!(find_preset("PI").is_some());
689        assert!(find_preset("pi").is_some());
690        assert!(find_preset("Claude").is_some());
691        assert!(find_preset("AIDER").is_some());
692        assert!(find_preset("unknown").is_none());
693    }
694
695    #[test]
696    fn detect_agents_returns_all_presets() {
697        let agents = detect_agents();
698        assert_eq!(agents.len(), PRESETS.len());
699        // Each entry maps to a known preset
700        for (preset, _) in &agents {
701            assert!(PRESETS.iter().any(|p| p.name == preset.name));
702        }
703    }
704
705    #[test]
706    fn init_creates_rules_md_stub() {
707        let dir = TempDir::new().unwrap();
708        cmd_init(Some(dir.path()), default_args()).unwrap();
709
710        let rules_path = dir.path().join(".mana").join("RULES.md");
711        assert!(rules_path.exists(), "RULES.md should be created by init");
712
713        let content = fs::read_to_string(&rules_path).unwrap();
714        assert!(content.contains("# Project Rules"));
715    }
716
717    #[test]
718    fn init_does_not_overwrite_existing_rules_md() {
719        let dir = TempDir::new().unwrap();
720        cmd_init(Some(dir.path()), default_args()).unwrap();
721
722        // Overwrite RULES.md with custom content
723        let rules_path = dir.path().join(".mana").join("RULES.md");
724        fs::write(&rules_path, "# Custom rules\nNo panics allowed.").unwrap();
725
726        // Re-init with --setup
727        let mut args = default_args();
728        args.setup = true;
729        cmd_init(Some(dir.path()), args).unwrap();
730
731        // Custom content preserved
732        let content = fs::read_to_string(&rules_path).unwrap();
733        assert!(content.contains("No panics allowed."));
734    }
735
736    #[test]
737    fn init_preserves_next_id_on_setup() {
738        let dir = TempDir::new().unwrap();
739
740        // Create initial config with bumped next_id
741        let mut args1 = default_args();
742        args1.project_name = Some("preserve-test".to_string());
743        cmd_init(Some(dir.path()), args1).unwrap();
744
745        let mana_dir = dir.path().join(".mana");
746        let mut config = Config::load(&mana_dir).unwrap();
747        config.next_id = 42;
748        config.save(&mana_dir).unwrap();
749
750        // Re-init with --setup
751        let mut args2 = default_args();
752        args2.setup = true;
753        cmd_init(Some(dir.path()), args2).unwrap();
754
755        let config2 = Config::load(&mana_dir).unwrap();
756        assert_eq!(config2.next_id, 42);
757    }
758}