Skip to main content

bn/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 bean ID.
17    run: &'static str,
18    /// Shell command template for `plan`. `{id}` is replaced with bean 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 bean {id}. Read bean with bn show {id}. Read referenced files with bn context {id}. When done run bn close {id}.'",
37        plan: "claude -p 'Decompose bean {id}. Read bean with bn show {id}. Break into child beans with bn create --parent {id}.'",
38        version_cmd: "claude --version",
39        binary: "claude",
40    },
41    AgentPreset {
42        name: "aider",
43        run: "aider --message 'Implement bean {id}. Read bean with bn show {id}. Read referenced files with bn context {id}. When done run bn close {id}.'",
44        plan: "aider --message 'Decompose bean {id}. Read bean with bn show {id}. Break into child beans with bn create --parent {id}.'",
45        version_cmd: "aider --version",
46        binary: "aider",
47    },
48];
49
50/// Arguments for `bn 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 bean 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 bean 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 .beans/ 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 beans_dir = cwd.join(".beans");
221    let already_exists = beans_dir.exists() && beans_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(&beans_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: bn init --setup");
237            return Ok(());
238        }
239        // Config missing/corrupt — fall through to create it
240    }
241
242    // Create .beans/ directory if it doesn't exist
243    if !beans_dir.exists() {
244        fs::create_dir(&beans_dir).with_context(|| {
245            format!(
246                "Failed to create .beans directory at {}",
247                beans_dir.display()
248            )
249        })?;
250    } else if !beans_dir.is_dir() {
251        anyhow::bail!(".beans exists but is not a directory");
252    }
253
254    // Determine project name
255    let project = if let Some(ref name) = args.project_name {
256        name.clone()
257    } else if already_exists {
258        // Preserve existing project name on --setup
259        Config::load(&beans_dir)
260            .map(|c| c.project)
261            .unwrap_or_else(|_| auto_detect_project_name(&cwd))
262    } else {
263        auto_detect_project_name(&cwd)
264    };
265
266    // Preserve next_id on re-init
267    let next_id = if already_exists {
268        Config::load(&beans_dir).map(|c| c.next_id).unwrap_or(1)
269    } else {
270        1
271    };
272
273    // Determine agent config (run/plan)
274    let (run, plan) = resolve_agent_config(&args)?;
275
276    // Create config
277    let config = Config {
278        project: project.clone(),
279        next_id,
280        auto_close_parent: true,
281        max_tokens: 30000,
282        run,
283        plan,
284        max_loops: 10,
285        max_concurrent: 4,
286        poll_interval: 30,
287        extends: vec![],
288        rules_file: None,
289        file_locking: false,
290        on_close: None,
291        on_fail: None,
292        post_plan: None,
293        verify_timeout: None,
294        review: None,
295    };
296
297    config.save(&beans_dir)?;
298
299    // Create stub RULES.md if it doesn't exist
300    let rules_path = beans_dir.join("RULES.md");
301    if !rules_path.exists() {
302        fs::write(
303            &rules_path,
304            "\
305# Project Rules
306
307<!-- These rules are automatically injected into every agent context.
308     Define coding standards, conventions, and constraints here.
309     Delete these comments and add your own rules. -->
310
311<!-- Example rules:
312
313## Code Style
314- Use `snake_case` for functions and variables
315- Maximum line length: 100 characters
316- All public functions must have doc comments
317
318## Architecture
319- No direct database access outside the `db` module
320- All errors must use the `anyhow` crate
321
322## Forbidden Patterns
323- No `.unwrap()` in production code
324- No `println!` for logging (use `tracing` instead)
325-->
326",
327        )
328        .with_context(|| format!("Failed to create RULES.md at {}", rules_path.display()))?;
329    }
330
331    // Create .beans/.gitignore if it doesn't exist — index.yaml is a regenerable cache
332    let gitignore_path = beans_dir.join(".gitignore");
333    if !gitignore_path.exists() {
334        fs::write(
335            &gitignore_path,
336            "# Regenerable cache — rebuilt automatically by bn sync\nindex.yaml\n\n# File lock\nindex.lock\n",
337        )
338        .with_context(|| format!("Failed to create .gitignore at {}", gitignore_path.display()))?;
339    }
340
341    if already_exists && args.setup {
342        eprintln!("Reconfigured beans in .beans/");
343    } else if !already_exists {
344        eprintln!("Initialized beans in .beans/");
345    }
346
347    // Print next steps
348    if config.run.is_some() {
349        eprintln!();
350        eprintln!("Next steps:");
351        eprintln!("  bn create \"my first task\" --verify \"test command\"");
352    } else {
353        eprintln!();
354        eprintln!("Next steps:");
355        eprintln!("  bn init --setup          # configure an agent");
356        eprintln!("  bn config set run \"...\"  # or set run command directly");
357        eprintln!("  bn create \"task\" --verify \"test command\"");
358    }
359
360    Ok(())
361}
362
363/// Auto-detect project name from directory.
364fn auto_detect_project_name(cwd: &Path) -> String {
365    cwd.file_name()
366        .and_then(|n| n.to_str())
367        .map(|s| s.to_string())
368        .unwrap_or_else(|| "project".to_string())
369}
370
371/// Resolve the run/plan config from InitArgs.
372///
373/// Priority: --run/--plan flags > --agent preset > interactive wizard > None
374fn resolve_agent_config(args: &InitArgs) -> Result<(Option<String>, Option<String>)> {
375    // --no-agent: skip entirely
376    if args.no_agent {
377        return Ok((None, None));
378    }
379
380    // --run/--plan provided directly
381    if args.run.is_some() || args.plan.is_some() {
382        return Ok((args.run.clone(), args.plan.clone()));
383    }
384
385    // --agent <name>: look up preset
386    if let Some(ref agent_name) = args.agent {
387        let preset = find_preset(agent_name).ok_or_else(|| {
388            anyhow::anyhow!(
389                "Unknown agent '{}'. Known agents: {}",
390                agent_name,
391                PRESETS
392                    .iter()
393                    .map(|p| p.name)
394                    .collect::<Vec<_>>()
395                    .join(", ")
396            )
397        })?;
398
399        eprintln!("Verifying {}...", preset.name);
400        match verify_agent(preset) {
401            Some(version) => eprintln!("  ✓ {} → {}", preset.version_cmd, version),
402            None => eprintln!("  ⚠ {} not responding (configured anyway)", preset.name),
403        }
404
405        return Ok((Some(preset.run.to_string()), Some(preset.plan.to_string())));
406    }
407
408    // Interactive (TTY only)
409    if io::stderr().is_terminal() && (args.setup || !args.no_agent) {
410        if let Some((run, plan)) = interactive_agent_setup()? {
411            return Ok((Some(run), Some(plan)));
412        }
413    }
414
415    // Non-interactive or user skipped
416    Ok((None, None))
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use std::fs;
423    use tempfile::TempDir;
424
425    /// Helper to create InitArgs with defaults.
426    fn default_args() -> InitArgs {
427        InitArgs {
428            project_name: None,
429            agent: None,
430            run: None,
431            plan: None,
432            setup: false,
433            no_agent: true, // Skip interactive in tests
434        }
435    }
436
437    #[test]
438    fn init_creates_beans_dir() {
439        let dir = TempDir::new().unwrap();
440        let result = cmd_init(Some(dir.path()), default_args());
441
442        assert!(result.is_ok());
443        assert!(dir.path().join(".beans").exists());
444        assert!(dir.path().join(".beans").is_dir());
445    }
446
447    #[test]
448    fn init_creates_config_with_explicit_name() {
449        let dir = TempDir::new().unwrap();
450        let mut args = default_args();
451        args.project_name = Some("my-project".to_string());
452        let result = cmd_init(Some(dir.path()), args);
453
454        assert!(result.is_ok());
455
456        let config = Config::load(&dir.path().join(".beans")).unwrap();
457        assert_eq!(config.project, "my-project");
458        assert_eq!(config.next_id, 1);
459    }
460
461    #[test]
462    fn init_auto_detects_project_name_from_dir() {
463        let dir = TempDir::new().unwrap();
464        let result = cmd_init(Some(dir.path()), default_args());
465
466        assert!(result.is_ok());
467
468        let config = Config::load(&dir.path().join(".beans")).unwrap();
469        let dir_name = dir
470            .path()
471            .file_name()
472            .and_then(|n| n.to_str())
473            .unwrap_or("project");
474        assert_eq!(config.project, dir_name);
475    }
476
477    #[test]
478    fn init_idempotent() {
479        let dir = TempDir::new().unwrap();
480
481        let mut args1 = default_args();
482        args1.project_name = Some("test-project".to_string());
483        let result1 = cmd_init(Some(dir.path()), args1);
484        assert!(result1.is_ok());
485
486        // Second init with --setup so it actually re-writes
487        let mut args2 = default_args();
488        args2.project_name = Some("test-project".to_string());
489        args2.setup = true;
490        let result2 = cmd_init(Some(dir.path()), args2);
491        assert!(result2.is_ok());
492
493        let config = Config::load(&dir.path().join(".beans")).unwrap();
494        assert_eq!(config.project, "test-project");
495    }
496
497    #[test]
498    fn init_config_is_valid_yaml() {
499        let dir = TempDir::new().unwrap();
500        let mut args = default_args();
501        args.project_name = Some("yaml-test".to_string());
502        let result = cmd_init(Some(dir.path()), args);
503
504        assert!(result.is_ok());
505
506        let config_path = dir.path().join(".beans").join("config.yaml");
507        assert!(config_path.exists());
508
509        let contents = fs::read_to_string(&config_path).unwrap();
510        assert!(contents.contains("project: yaml-test"));
511        assert!(contents.contains("next_id: 1"));
512    }
513
514    #[test]
515    fn init_with_agent_pi_sets_run_and_plan() {
516        let dir = TempDir::new().unwrap();
517        let mut args = default_args();
518        args.agent = Some("pi".to_string());
519        args.no_agent = false;
520        let result = cmd_init(Some(dir.path()), args);
521
522        assert!(result.is_ok());
523
524        let config = Config::load(&dir.path().join(".beans")).unwrap();
525        assert!(config.run.is_some());
526        assert!(config.plan.is_some());
527        assert!(config.run.unwrap().contains("pi"));
528        assert!(config.plan.unwrap().contains("pi"));
529    }
530
531    #[test]
532    fn init_with_agent_claude_sets_run_and_plan() {
533        let dir = TempDir::new().unwrap();
534        let mut args = default_args();
535        args.agent = Some("claude".to_string());
536        args.no_agent = false;
537        let result = cmd_init(Some(dir.path()), args);
538
539        assert!(result.is_ok());
540
541        let config = Config::load(&dir.path().join(".beans")).unwrap();
542        assert!(config.run.is_some());
543        assert!(config.plan.is_some());
544        assert!(config.run.unwrap().contains("claude"));
545        assert!(config.plan.unwrap().contains("claude"));
546    }
547
548    #[test]
549    fn init_with_agent_aider_sets_run_and_plan() {
550        let dir = TempDir::new().unwrap();
551        let mut args = default_args();
552        args.agent = Some("aider".to_string());
553        args.no_agent = false;
554        let result = cmd_init(Some(dir.path()), args);
555
556        assert!(result.is_ok());
557
558        let config = Config::load(&dir.path().join(".beans")).unwrap();
559        assert!(config.run.is_some());
560        assert!(config.plan.is_some());
561        assert!(config.run.unwrap().contains("aider"));
562        assert!(config.plan.unwrap().contains("aider"));
563    }
564
565    #[test]
566    fn init_with_unknown_agent_errors() {
567        let dir = TempDir::new().unwrap();
568        let mut args = default_args();
569        args.agent = Some("unknown-agent".to_string());
570        args.no_agent = false;
571        let result = cmd_init(Some(dir.path()), args);
572
573        assert!(result.is_err());
574        let err = format!("{}", result.unwrap_err());
575        assert!(err.contains("Unknown agent"));
576        assert!(err.contains("unknown-agent"));
577    }
578
579    #[test]
580    fn init_with_custom_run_and_plan() {
581        let dir = TempDir::new().unwrap();
582        let mut args = default_args();
583        args.run = Some("my-agent run {id}".to_string());
584        args.plan = Some("my-agent plan {id}".to_string());
585        args.no_agent = false;
586        let result = cmd_init(Some(dir.path()), args);
587
588        assert!(result.is_ok());
589
590        let config = Config::load(&dir.path().join(".beans")).unwrap();
591        assert_eq!(config.run, Some("my-agent run {id}".to_string()));
592        assert_eq!(config.plan, Some("my-agent plan {id}".to_string()));
593    }
594
595    #[test]
596    fn init_with_run_only() {
597        let dir = TempDir::new().unwrap();
598        let mut args = default_args();
599        args.run = Some("my-agent {id}".to_string());
600        args.no_agent = false;
601        let result = cmd_init(Some(dir.path()), args);
602
603        assert!(result.is_ok());
604
605        let config = Config::load(&dir.path().join(".beans")).unwrap();
606        assert_eq!(config.run, Some("my-agent {id}".to_string()));
607        assert_eq!(config.plan, None);
608    }
609
610    #[test]
611    fn init_with_no_agent_skips_setup() {
612        let dir = TempDir::new().unwrap();
613        let mut args = default_args();
614        args.no_agent = true;
615        let result = cmd_init(Some(dir.path()), args);
616
617        assert!(result.is_ok());
618
619        let config = Config::load(&dir.path().join(".beans")).unwrap();
620        assert_eq!(config.run, None);
621        assert_eq!(config.plan, None);
622    }
623
624    #[test]
625    fn init_setup_on_existing_reconfigures() {
626        let dir = TempDir::new().unwrap();
627
628        // First init — no agent
629        let mut args1 = default_args();
630        args1.project_name = Some("my-project".to_string());
631        cmd_init(Some(dir.path()), args1).unwrap();
632
633        let config1 = Config::load(&dir.path().join(".beans")).unwrap();
634        assert_eq!(config1.run, None);
635
636        // Bump next_id to simulate usage
637        let mut config_modified = config1;
638        config_modified.next_id = 5;
639        config_modified.save(&dir.path().join(".beans")).unwrap();
640
641        // Re-init with --setup --agent pi
642        let mut args2 = default_args();
643        args2.setup = true;
644        args2.agent = Some("pi".to_string());
645        args2.no_agent = false;
646        cmd_init(Some(dir.path()), args2).unwrap();
647
648        let config2 = Config::load(&dir.path().join(".beans")).unwrap();
649        // Agent configured
650        assert!(config2.run.is_some());
651        assert!(config2.run.unwrap().contains("pi"));
652        // Preserved
653        assert_eq!(config2.project, "my-project");
654        assert_eq!(config2.next_id, 5);
655    }
656
657    #[test]
658    fn reinit_without_setup_shows_config() {
659        let dir = TempDir::new().unwrap();
660
661        // First init
662        let mut args1 = default_args();
663        args1.project_name = Some("show-test".to_string());
664        cmd_init(Some(dir.path()), args1).unwrap();
665
666        // Second init without --setup (no flags that would trigger re-write)
667        let args2 = default_args();
668        let result = cmd_init(Some(dir.path()), args2);
669        assert!(result.is_ok());
670
671        // Config unchanged
672        let config = Config::load(&dir.path().join(".beans")).unwrap();
673        assert_eq!(config.project, "show-test");
674    }
675
676    #[test]
677    fn find_preset_is_case_insensitive() {
678        assert!(find_preset("Pi").is_some());
679        assert!(find_preset("PI").is_some());
680        assert!(find_preset("pi").is_some());
681        assert!(find_preset("Claude").is_some());
682        assert!(find_preset("AIDER").is_some());
683        assert!(find_preset("unknown").is_none());
684    }
685
686    #[test]
687    fn detect_agents_returns_all_presets() {
688        let agents = detect_agents();
689        assert_eq!(agents.len(), PRESETS.len());
690        // Each entry maps to a known preset
691        for (preset, _) in &agents {
692            assert!(PRESETS.iter().any(|p| p.name == preset.name));
693        }
694    }
695
696    #[test]
697    fn init_creates_rules_md_stub() {
698        let dir = TempDir::new().unwrap();
699        cmd_init(Some(dir.path()), default_args()).unwrap();
700
701        let rules_path = dir.path().join(".beans").join("RULES.md");
702        assert!(rules_path.exists(), "RULES.md should be created by init");
703
704        let content = fs::read_to_string(&rules_path).unwrap();
705        assert!(content.contains("# Project Rules"));
706    }
707
708    #[test]
709    fn init_does_not_overwrite_existing_rules_md() {
710        let dir = TempDir::new().unwrap();
711        cmd_init(Some(dir.path()), default_args()).unwrap();
712
713        // Overwrite RULES.md with custom content
714        let rules_path = dir.path().join(".beans").join("RULES.md");
715        fs::write(&rules_path, "# Custom rules\nNo panics allowed.").unwrap();
716
717        // Re-init with --setup
718        let mut args = default_args();
719        args.setup = true;
720        cmd_init(Some(dir.path()), args).unwrap();
721
722        // Custom content preserved
723        let content = fs::read_to_string(&rules_path).unwrap();
724        assert!(content.contains("No panics allowed."));
725    }
726
727    #[test]
728    fn init_preserves_next_id_on_setup() {
729        let dir = TempDir::new().unwrap();
730
731        // Create initial config with bumped next_id
732        let mut args1 = default_args();
733        args1.project_name = Some("preserve-test".to_string());
734        cmd_init(Some(dir.path()), args1).unwrap();
735
736        let beans_dir = dir.path().join(".beans");
737        let mut config = Config::load(&beans_dir).unwrap();
738        config.next_id = 42;
739        config.save(&beans_dir).unwrap();
740
741        // Re-init with --setup
742        let mut args2 = default_args();
743        args2.setup = true;
744        cmd_init(Some(dir.path()), args2).unwrap();
745
746        let config2 = Config::load(&beans_dir).unwrap();
747        assert_eq!(config2.next_id, 42);
748    }
749}