1pub mod catalog;
34pub mod import;
35pub mod input;
36pub mod plan;
37pub mod render;
38pub mod state;
39pub mod verify;
40
41use std::path::{Path, PathBuf};
42use std::time::Duration;
43
44use clap::Args;
45use ratatui::Terminal;
46use tokio::sync::mpsc;
47
48use crate::config::Config;
49use crate::tui::{EventSource, TerminalSetup};
50use crossterm::event::{Event, KeyEventKind};
51use state::{VerifyReply, VerifyRequest, Wizard};
52use verify::ProviderVerifier;
53
54#[derive(Args)]
55pub struct SetupArgs {
56 #[arg(long)]
58 pub non_interactive: bool,
59
60 #[arg(long)]
62 pub no_verify: bool,
63
64 #[arg(long)]
66 pub anthropic_key: Option<String>,
67
68 #[arg(long)]
70 pub openai_key: Option<String>,
71
72 #[arg(long)]
74 pub google_key: Option<String>,
75
76 #[arg(long)]
78 pub openrouter_key: Option<String>,
79
80 #[arg(long)]
82 pub ollama_url: Option<String>,
83
84 #[arg(long)]
86 pub default_model: Option<String>,
87
88 #[arg(long)]
92 pub claude_code: Option<bool>,
93
94 #[arg(long)]
97 pub claude_code_effort: Option<String>,
98
99 #[arg(long)]
101 pub install_agents: bool,
102}
103
104pub type EnvLookup = Box<dyn Fn(&str) -> Option<String> + Send + Sync>;
107
108pub struct SetupEnv {
111 pub config_path: PathBuf,
112 pub agents_dir: PathBuf,
113 pub roots: import::Roots,
115 pub env_lookup: EnvLookup,
117 pub opener: leviath_mcp::BrowserOpener,
119}
120
121pub fn run_non_interactive(args: &SetupArgs, env: &SetupEnv) -> anyhow::Result<()> {
130 let mut config = Config::load_from_path_public(&env.config_path).unwrap_or_default();
131 apply_flags(&mut config, args);
132
133 let agents = if args.install_agents {
134 crate::bundled::plan_agent_actions(&env.agents_dir)
135 .into_iter()
136 .filter(|(_, action)| action.is_change())
137 .map(|(agent, _)| agent)
138 .collect()
139 } else {
140 Vec::new()
141 };
142
143 let applied = plan::apply(
144 &plan::SetupPlan { config, agents },
145 &env.config_path,
146 &env.agents_dir,
147 )?;
148 report(&applied);
149 Ok(())
150}
151
152fn report(applied: &plan::Applied) {
155 println!("Config saved to {}", applied.config_path.display());
156 if !applied.agents_installed.is_empty() {
157 println!(
158 "Installed {} agent(s): {}",
159 applied.agents_installed.len(),
160 applied.agents_installed.join(", ")
161 );
162 }
163 for warning in &applied.warnings {
164 println!(" Warning: {warning}");
165 }
166}
167
168fn apply_flags(config: &mut Config, args: &SetupArgs) {
170 if let Some(ref k) = args.anthropic_key {
171 config.providers.anthropic_api_key = Some(k.clone());
172 }
173 if let Some(ref k) = args.openai_key {
174 config.providers.openai_api_key = Some(k.clone());
175 }
176 if let Some(ref k) = args.google_key {
177 config.providers.google_api_key = Some(k.clone());
178 }
179 if let Some(ref k) = args.openrouter_key {
180 config.openrouter_api_key = Some(k.clone());
181 }
182 if let Some(ref u) = args.ollama_url {
183 config.ollama_base_url = Some(u.clone());
184 }
185 if let Some(ref m) = args.default_model {
186 config.default_model = Some(m.clone());
187 }
188 if let Some(enabled) = args.claude_code {
189 config.providers.claude_code_enabled = enabled;
190 }
191 if let Some(ref e) = args.claude_code_effort {
192 config.providers.claude_code_effort = Some(e.clone());
193 }
194}
195
196pub fn build_wizard(env: &SetupEnv) -> Wizard {
204 let base = Config::load_from_path_public(&env.config_path).unwrap_or_default();
205 let (candidates, errors) = state::candidates_from_scans(import::scan(&env.roots));
206 Wizard::new(
207 base,
208 &env.env_lookup,
209 candidates,
210 errors,
211 &env.agents_dir,
212 env.opener.clone(),
213 )
214}
215
216pub async fn verification_loop<V: ProviderVerifier>(
223 verifier: V,
224 mut requests: mpsc::UnboundedReceiver<VerifyRequest>,
225 replies: mpsc::UnboundedSender<VerifyReply>,
226) {
227 while let Some(request) = requests.recv().await {
228 let outcome = verifier.verify(&request.creds).await;
229 if replies
231 .send(VerifyReply {
232 provider_id: request.provider_id,
233 outcome,
234 })
235 .is_err()
236 {
237 return;
238 }
239 }
240}
241
242pub async fn run_wizard_loop<B: ratatui::backend::Backend>(
248 wizard: &mut Wizard,
249 terminal: &mut Terminal<B>,
250 events: &mut impl EventSource,
251 tick_rate: Duration,
252) -> anyhow::Result<Option<plan::SetupPlan>> {
253 loop {
254 wizard.ticks += 1;
255 wizard.drain_verifications();
256 terminal
257 .draw(|frame| render::draw(frame, wizard))
258 .map_err(|e| anyhow::anyhow!("terminal draw failed: {e}"))?;
261
262 if let Some(Event::Key(key)) = events.poll_event(tick_rate)?
263 && key.kind == KeyEventKind::Press
264 && wizard.handle_key(key) == input::Action::Save
265 {
266 wizard.finished = true;
267 }
268
269 if wizard.finished {
270 return Ok(Some(wizard.build_plan()));
271 }
272 if wizard.should_quit {
273 return Ok(None);
274 }
275 }
276}
277
278pub async fn execute_core<S: TerminalSetup, E: EventSource>(
283 wizard: &mut Wizard,
284 env: &SetupEnv,
285 setup: &mut S,
286 events: &mut E,
287) -> anyhow::Result<()> {
288 setup.enable()?;
289 let mut terminal = setup.create_terminal()?;
290 let result = run_wizard_loop(wizard, &mut terminal, events, Duration::from_millis(120)).await;
291 setup.disable();
292
293 match result? {
294 Some(plan) => {
295 let applied = plan::apply(&plan, &env.config_path, &env.agents_dir)?;
296 report(&applied);
297 print_next_steps(&applied);
298 }
299 None => println!("Setup cancelled. Nothing was written."),
300 }
301 Ok(())
302}
303
304fn print_next_steps(applied: &plan::Applied) {
306 println!();
307 match applied.agents_installed.first() {
308 Some(agent) => println!("Try it: lev run {agent} --task \"...\""),
309 None => println!("Install an agent with `lev setup`, then `lev run <agent>`."),
310 }
311}
312
313pub async fn execute_with<S: TerminalSetup, E: EventSource>(
319 args: &SetupArgs,
320 env: &SetupEnv,
321 setup: &mut S,
322 events: &mut E,
323 is_terminal: bool,
324) -> anyhow::Result<()> {
325 if args.non_interactive {
326 return run_non_interactive(args, env);
327 }
328 if !is_terminal {
329 anyhow::bail!(
330 "lev setup needs a terminal. For scripted use:\n \
331 lev setup --non-interactive --anthropic-key sk-ant-... --install-agents"
332 );
333 }
334 let mut wizard = build_wizard(env);
335 execute_core(&mut wizard, env, setup, events).await
336}
337
338pub fn real_agents_dir(home: Option<&Path>) -> PathBuf {
340 home.unwrap_or(Path::new(""))
341 .join(".leviath")
342 .join("agents")
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use crate::bundled::BUNDLED_AGENTS;
349 use crate::tui::{TestEventSource, TestSetup, key, key_with, test_terminal};
350 use crossterm::event::{KeyCode, KeyModifiers};
351
352 fn args() -> SetupArgs {
354 SetupArgs {
355 non_interactive: false,
356 no_verify: false,
357 anthropic_key: None,
358 openai_key: None,
359 google_key: None,
360 openrouter_key: None,
361 ollama_url: None,
362 default_model: None,
363 claude_code: None,
364 claude_code_effort: None,
365 install_agents: false,
366 }
367 }
368
369 fn env_in(dir: &Path) -> SetupEnv {
372 SetupEnv {
373 config_path: dir.join("config.toml"),
374 agents_dir: dir.join("agents"),
375 roots: import::Roots {
376 home: dir.join("home"),
377 os_config: dir.join("os-config"),
378 xdg_config: dir.join("home").join(".config"),
379 cwd: dir.join("cwd"),
380 },
381 env_lookup: Box::new(|_| None),
382 opener: std::sync::Arc::new(|_| true),
383 }
384 }
385
386 #[test]
389 fn flags_are_written_to_the_config() {
390 let dir = tempfile::tempdir().unwrap();
391 let env = env_in(dir.path());
392 let args = SetupArgs {
393 non_interactive: true,
394 anthropic_key: Some("sk-ant-x".to_string()),
395 openai_key: Some("sk-oai".to_string()),
396 google_key: Some("goog".to_string()),
397 openrouter_key: Some("sk-or".to_string()),
398 ollama_url: Some("http://box:11434".to_string()),
399 default_model: Some("m".to_string()),
400 claude_code: Some(true),
401 claude_code_effort: Some("xhigh".to_string()),
402 ..args()
403 };
404
405 run_non_interactive(&args, &env).unwrap();
406
407 let written = Config::load_from_path_public(&env.config_path).unwrap();
408 assert_eq!(
409 written.providers.anthropic_api_key.as_deref(),
410 Some("sk-ant-x")
411 );
412 assert_eq!(written.providers.openai_api_key.as_deref(), Some("sk-oai"));
413 assert_eq!(written.providers.google_api_key.as_deref(), Some("goog"));
414 assert_eq!(written.openrouter_api_key.as_deref(), Some("sk-or"));
415 assert_eq!(written.ollama_base_url.as_deref(), Some("http://box:11434"));
416 assert_eq!(written.default_model.as_deref(), Some("m"));
417 assert!(written.providers.claude_code_enabled);
418 assert_eq!(
419 written.providers.claude_code_effort.as_deref(),
420 Some("xhigh")
421 );
422 }
423
424 #[test]
425 fn the_non_interactive_path_installs_agents_only_when_asked() {
426 let dir = tempfile::tempdir().unwrap();
427 let env = env_in(dir.path());
428
429 run_non_interactive(&args(), &env).unwrap();
430 assert!(!env.agents_dir.exists(), "nothing was asked for");
431
432 run_non_interactive(
433 &SetupArgs {
434 install_agents: true,
435 ..args()
436 },
437 &env,
438 )
439 .unwrap();
440 assert!(
441 env.agents_dir.join(BUNDLED_AGENTS[0].name).exists(),
442 "every bundled blueprint should land"
443 );
444
445 run_non_interactive(
447 &SetupArgs {
448 install_agents: true,
449 ..args()
450 },
451 &env,
452 )
453 .unwrap();
454 assert!(env.agents_dir.join(BUNDLED_AGENTS[0].name).exists());
455 }
456
457 #[test]
458 fn the_non_interactive_path_keeps_settings_it_was_not_given() {
459 let dir = tempfile::tempdir().unwrap();
460 let env = env_in(dir.path());
461 run_non_interactive(
462 &SetupArgs {
463 anthropic_key: Some("sk-ant-first".to_string()),
464 ..args()
465 },
466 &env,
467 )
468 .unwrap();
469
470 run_non_interactive(
471 &SetupArgs {
472 openai_key: Some("sk-oai".to_string()),
473 ..args()
474 },
475 &env,
476 )
477 .unwrap();
478
479 let written = Config::load_from_path_public(&env.config_path).unwrap();
480 assert_eq!(
481 written.providers.anthropic_api_key.as_deref(),
482 Some("sk-ant-first")
483 );
484 assert_eq!(written.providers.openai_api_key.as_deref(), Some("sk-oai"));
485 }
486
487 #[test]
488 fn a_config_that_cannot_be_written_is_an_error() {
489 let dir = tempfile::tempdir().unwrap();
490 let blocked = dir.path().join("not-a-dir");
491 std::fs::write(&blocked, "").unwrap();
492 let mut env = env_in(dir.path());
493 env.config_path = blocked.join("config.toml");
494
495 assert!(run_non_interactive(&args(), &env).is_err());
496 }
497
498 #[test]
501 fn the_wizard_reads_the_config_file_and_scans_for_harnesses() {
502 let dir = tempfile::tempdir().unwrap();
503 let env = env_in(dir.path());
504 std::fs::create_dir_all(&env.roots.home).unwrap();
505 std::fs::write(
506 env.roots.home.join(".claude.json"),
507 r#"{"mcpServers":{"fs":{"command":"npx"}}}"#,
508 )
509 .unwrap();
510 run_non_interactive(
511 &SetupArgs {
512 anthropic_key: Some("sk-ant-stored".to_string()),
513 ..args()
514 },
515 &env,
516 )
517 .unwrap();
518
519 let wizard = build_wizard(&env);
520
521 assert_eq!(
522 wizard.base.providers.anthropic_api_key.as_deref(),
523 Some("sk-ant-stored")
524 );
525 assert_eq!(wizard.mcp.len(), 1);
526 assert_eq!(wizard.mcp[0].candidate.config.name, "fs");
527 }
528
529 #[test]
530 fn a_missing_config_file_starts_from_defaults() {
531 let dir = tempfile::tempdir().unwrap();
532
533 let wizard = build_wizard(&env_in(dir.path()));
534
535 assert_eq!(
536 wizard.base.default_provider,
537 Config::default().default_provider
538 );
539 }
540
541 #[tokio::test]
544 async fn the_verification_loop_answers_every_request_then_stops() {
545 let dir = tempfile::tempdir().unwrap();
546 let mut wizard = build_wizard(&env_in(dir.path()));
547 let (requests, replies) = wizard.take_verify_ends().expect("first take");
548 wizard.providers[0].selected = true;
549 wizard.providers[0].value = "sk-ant".to_string();
550 wizard.request_verification(0);
551
552 let handle = tokio::spawn(verification_loop(verify::SkipVerifier, requests, replies));
553 let sender = wizard.verify_tx.clone();
555 drop(sender);
556
557 for _ in 0..50 {
559 wizard.drain_verifications();
560 if !wizard.providers[0].checking {
561 break;
562 }
563 tokio::time::sleep(Duration::from_millis(2)).await;
564 }
565 assert!(!wizard.providers[0].checking);
566 assert_eq!(wizard.providers[0].outcome, verify::Outcome::Skipped);
567
568 drop(wizard);
569 handle.await.expect("the loop exits cleanly");
570 }
571
572 #[tokio::test]
573 async fn the_verification_loop_stops_when_nobody_is_listening() {
574 let (request_tx, request_rx) = mpsc::unbounded_channel();
576 let (reply_tx, reply_rx) = mpsc::unbounded_channel::<VerifyReply>();
577 request_tx
578 .send(VerifyRequest {
579 provider_id: "anthropic".to_string(),
580 creds: leviath_runtime::provider_creds::ProviderCreds {
581 name: "anthropic".to_string(),
582 api_key: Some("sk-ant".to_string()),
583 base_url: None,
584 model_capabilities: std::collections::HashMap::new(),
585 request_timeout_secs: Some(1),
586 rate_limit: None,
587 options: std::collections::HashMap::new(),
588 },
589 })
590 .unwrap();
591 drop(reply_rx);
592
593 verification_loop(verify::SkipVerifier, request_rx, reply_tx).await;
594 }
595
596 #[tokio::test]
599 async fn quitting_returns_no_plan() {
600 let dir = tempfile::tempdir().unwrap();
601 let mut wizard = build_wizard(&env_in(dir.path()));
602 let mut terminal = test_terminal();
603 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
604
605 let plan = run_wizard_loop(
606 &mut wizard,
607 &mut terminal,
608 &mut events,
609 Duration::from_millis(1),
610 )
611 .await
612 .unwrap();
613
614 assert!(plan.is_none());
615 }
616
617 #[tokio::test]
618 async fn saving_returns_the_plan_the_wizard_describes() {
619 let dir = tempfile::tempdir().unwrap();
620 let mut wizard = build_wizard(&env_in(dir.path()));
621 let mut terminal = test_terminal();
622 let mut events = TestEventSource::new_with_nones(vec![
624 None,
625 Some(key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)),
626 ]);
627
628 let plan = run_wizard_loop(
629 &mut wizard,
630 &mut terminal,
631 &mut events,
632 Duration::from_millis(1),
633 )
634 .await
635 .unwrap()
636 .expect("a plan was produced");
637
638 assert_eq!(plan.agents.len(), BUNDLED_AGENTS.len());
639 }
640
641 #[tokio::test]
642 async fn non_press_and_non_key_events_are_ignored() {
643 let dir = tempfile::tempdir().unwrap();
644 let mut wizard = build_wizard(&env_in(dir.path()));
645 let mut terminal = test_terminal();
646 let release = crossterm::event::Event::Key(crossterm::event::KeyEvent::new_with_kind(
647 KeyCode::Char('q'),
648 KeyModifiers::empty(),
649 KeyEventKind::Release,
650 ));
651 let mut events = TestEventSource::new(vec![
652 release,
653 crossterm::event::Event::FocusGained,
654 crossterm::event::Event::Resize(80, 24),
655 key(KeyCode::Char('q')),
656 ]);
657
658 let plan = run_wizard_loop(
659 &mut wizard,
660 &mut terminal,
661 &mut events,
662 Duration::from_millis(1),
663 )
664 .await
665 .unwrap();
666
667 assert!(plan.is_none(), "only the real press quit");
668 }
669
670 #[tokio::test]
671 async fn a_draw_failure_propagates() {
672 let dir = tempfile::tempdir().unwrap();
673 let mut wizard = build_wizard(&env_in(dir.path()));
674 let mut terminal =
675 ratatui::Terminal::new(crate::tui::TestBackendHarness::failing(80, 24)).unwrap();
676 let mut events = TestEventSource::new(vec![]);
677
678 let result = run_wizard_loop(
679 &mut wizard,
680 &mut terminal,
681 &mut events,
682 Duration::from_millis(1),
683 )
684 .await;
685
686 assert!(result.is_err());
687 }
688
689 #[tokio::test]
690 async fn an_event_source_failure_propagates() {
691 let dir = tempfile::tempdir().unwrap();
692 let mut wizard = build_wizard(&env_in(dir.path()));
693 let mut terminal = test_terminal();
694 let mut events = TestEventSource::failing();
695
696 let result = run_wizard_loop(
697 &mut wizard,
698 &mut terminal,
699 &mut events,
700 Duration::from_millis(1),
701 )
702 .await;
703
704 assert!(result.is_err());
705 }
706
707 #[tokio::test]
710 async fn saving_writes_the_config_and_installs_the_agents() {
711 let dir = tempfile::tempdir().unwrap();
712 let env = env_in(dir.path());
713 let mut wizard = build_wizard(&env);
714 let mut setup = TestSetup::new();
715 let mut events =
716 TestEventSource::new(vec![key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
717
718 execute_core(&mut wizard, &env, &mut setup, &mut events)
719 .await
720 .unwrap();
721
722 assert!(env.config_path.exists());
723 assert!(env.agents_dir.join(BUNDLED_AGENTS[0].name).exists());
724 }
725
726 #[tokio::test]
727 async fn quitting_writes_nothing() {
728 let dir = tempfile::tempdir().unwrap();
729 let env = env_in(dir.path());
730 let mut wizard = build_wizard(&env);
731 let mut setup = TestSetup::new();
732 let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
733
734 execute_core(&mut wizard, &env, &mut setup, &mut events)
735 .await
736 .unwrap();
737
738 assert!(
739 !env.config_path.exists(),
740 "nothing should have been written"
741 );
742 assert!(!env.agents_dir.exists());
743 }
744
745 #[tokio::test]
746 async fn a_terminal_that_will_not_start_is_an_error() {
747 let dir = tempfile::tempdir().unwrap();
748 let env = env_in(dir.path());
749 let mut wizard = build_wizard(&env);
750 let mut events = TestEventSource::new(vec![]);
751
752 let mut enable_fails = TestSetup {
753 enable_should_fail: true,
754 create_should_fail: false,
755 };
756 assert!(
757 execute_core(&mut wizard, &env, &mut enable_fails, &mut events)
758 .await
759 .is_err()
760 );
761
762 let mut create_fails = TestSetup {
763 enable_should_fail: false,
764 create_should_fail: true,
765 };
766 assert!(
767 execute_core(&mut wizard, &env, &mut create_fails, &mut events)
768 .await
769 .is_err()
770 );
771 }
772
773 #[tokio::test]
774 async fn a_loop_failure_is_surfaced_after_the_terminal_is_restored() {
775 let dir = tempfile::tempdir().unwrap();
776 let env = env_in(dir.path());
777 let mut wizard = build_wizard(&env);
778 let mut setup = TestSetup::new();
779 let mut events = TestEventSource::failing();
780
781 let result = execute_core(&mut wizard, &env, &mut setup, &mut events).await;
782
783 assert!(result.is_err());
784 assert!(!env.config_path.exists());
785 }
786
787 #[tokio::test]
788 async fn a_write_failure_after_the_wizard_is_surfaced() {
789 let dir = tempfile::tempdir().unwrap();
792 let mut env = env_in(dir.path());
793 let blocked = dir.path().join("not-a-dir");
794 std::fs::write(&blocked, "").unwrap();
795 let mut wizard = build_wizard(&env);
796 env.config_path = blocked.join("config.toml");
797 let mut setup = TestSetup::new();
798 let mut events =
799 TestEventSource::new(vec![key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
800
801 let result = execute_core(&mut wizard, &env, &mut setup, &mut events).await;
802
803 assert!(result.is_err());
804 }
805
806 #[tokio::test]
807 async fn execute_with_routes_to_the_flags_path() {
808 let dir = tempfile::tempdir().unwrap();
809 let env = env_in(dir.path());
810 let mut setup = TestSetup::new();
811 let mut events = TestEventSource::new(vec![]);
812
813 execute_with(
814 &SetupArgs {
815 non_interactive: true,
816 anthropic_key: Some("sk-ant-x".to_string()),
817 ..args()
818 },
819 &env,
820 &mut setup,
821 &mut events,
822 false,
823 )
824 .await
825 .unwrap();
826
827 let written = Config::load_from_path_public(&env.config_path).unwrap();
828 assert_eq!(
829 written.providers.anthropic_api_key.as_deref(),
830 Some("sk-ant-x")
831 );
832 }
833
834 #[tokio::test]
835 async fn without_a_terminal_the_wizard_refuses_and_says_what_to_run_instead() {
836 let dir = tempfile::tempdir().unwrap();
839 let env = env_in(dir.path());
840 let mut setup = TestSetup::new();
841 let mut events = TestEventSource::new(vec![]);
842
843 let error = execute_with(&args(), &env, &mut setup, &mut events, false)
844 .await
845 .expect_err("a pipe is not a terminal");
846
847 let message = error.to_string();
848 assert!(message.contains("needs a terminal"), "{message}");
849 assert!(message.contains("--non-interactive"), "{message}");
850 assert!(!env.config_path.exists());
851 }
852
853 #[tokio::test]
854 async fn with_a_terminal_execute_with_runs_the_wizard() {
855 let dir = tempfile::tempdir().unwrap();
856 let env = env_in(dir.path());
857 let mut setup = TestSetup::new();
858 let mut events =
859 TestEventSource::new(vec![key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
860
861 execute_with(&args(), &env, &mut setup, &mut events, true)
862 .await
863 .unwrap();
864
865 assert!(env.config_path.exists());
866 }
867
868 #[test]
871 fn the_summary_covers_agents_warnings_and_the_empty_case() {
872 report(&plan::Applied {
873 config_path: PathBuf::from("/tmp/config.toml"),
874 agents_installed: vec!["coder".to_string()],
875 warnings: vec!["could not install x".to_string()],
876 });
877 report(&plan::Applied {
878 config_path: PathBuf::from("/tmp/config.toml"),
879 agents_installed: Vec::new(),
880 warnings: Vec::new(),
881 });
882 }
883
884 #[test]
885 fn the_next_step_names_an_installed_agent_when_there_is_one() {
886 print_next_steps(&plan::Applied {
887 config_path: PathBuf::from("/tmp/config.toml"),
888 agents_installed: vec!["coder".to_string()],
889 warnings: Vec::new(),
890 });
891 print_next_steps(&plan::Applied {
892 config_path: PathBuf::from("/tmp/config.toml"),
893 agents_installed: Vec::new(),
894 warnings: Vec::new(),
895 });
896 }
897
898 #[test]
899 fn the_real_agents_directory_sits_under_the_leviath_home() {
900 assert_eq!(
901 real_agents_dir(Some(Path::new("/home/u"))),
902 PathBuf::from("/home/u/.leviath/agents")
903 );
904 assert_eq!(real_agents_dir(None), PathBuf::from(".leviath/agents"));
906 }
907}