pub mod context;
pub mod headless;
pub mod prompt;
pub mod session;
pub mod skill_drop;
use crate::agents::Agent;
#[derive(Debug, Clone)]
pub struct WizardOpts {
pub agent_override: Option<String>,
pub skill_only: bool,
pub apply: bool,
pub output_format: String,
pub config_file: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WizardMode {
Headless { agent: Agent, cli_command: String },
SkillDrop { agent: Agent },
QuickstartFallback,
}
pub fn pick_mode(
opts: &WizardOpts,
installed_agents: &[Agent],
cli_on_path: impl Fn(Agent) -> bool,
) -> WizardMode {
if let Some(slug) = opts.agent_override.as_deref() {
if let Some(agent) = Agent::from_slug(slug) {
if !opts.skill_only
&& let Some(cmd) = agent_cli_command(agent)
&& cli_on_path(agent)
{
return WizardMode::Headless {
agent,
cli_command: cmd.into(),
};
}
return WizardMode::SkillDrop { agent };
}
}
if !opts.skill_only {
for &agent in installed_agents {
if let Some(cmd) = agent_cli_command(agent)
&& cli_on_path(agent)
{
return WizardMode::Headless {
agent,
cli_command: cmd.into(),
};
}
}
}
if let Some(&agent) = installed_agents.first() {
return WizardMode::SkillDrop { agent };
}
WizardMode::QuickstartFallback
}
pub fn agent_cli_command(agent: Agent) -> Option<&'static str> {
match agent {
Agent::ClaudeCode => Some("claude"),
Agent::Codex => Some("codex"),
Agent::Cursor | Agent::Windsurf | Agent::Cline | Agent::Continue => None,
}
}
pub fn detect_installed_agents() -> Vec<Agent> {
Agent::ALL
.iter()
.copied()
.filter(|a| a.is_installed())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn opts() -> WizardOpts {
WizardOpts {
agent_override: None,
skill_only: false,
apply: false,
output_format: "pretty".into(),
config_file: "./jarvy.toml".into(),
}
}
#[test]
fn pick_quickstart_when_no_agents_installed() {
let mode = pick_mode(&opts(), &[], |_| false);
assert_eq!(mode, WizardMode::QuickstartFallback);
}
#[test]
fn pick_headless_when_claude_installed_and_on_path() {
let mode = pick_mode(&opts(), &[Agent::ClaudeCode], |a| a == Agent::ClaudeCode);
assert_eq!(
mode,
WizardMode::Headless {
agent: Agent::ClaudeCode,
cli_command: "claude".into(),
}
);
}
#[test]
fn pick_skill_drop_when_claude_installed_but_cli_not_on_path() {
let mode = pick_mode(&opts(), &[Agent::ClaudeCode], |_| false);
assert_eq!(
mode,
WizardMode::SkillDrop {
agent: Agent::ClaudeCode
}
);
}
#[test]
fn pick_skill_drop_when_only_gui_agent_installed() {
let mode = pick_mode(&opts(), &[Agent::Cursor], |_| false);
assert_eq!(
mode,
WizardMode::SkillDrop {
agent: Agent::Cursor
}
);
}
#[test]
fn pick_headless_prefers_claude_over_codex_when_both_present() {
let mode = pick_mode(&opts(), &[Agent::ClaudeCode, Agent::Codex], |a| {
matches!(a, Agent::ClaudeCode | Agent::Codex)
});
match mode {
WizardMode::Headless { agent, .. } => assert_eq!(agent, Agent::ClaudeCode),
other => panic!("expected headless claude, got {other:?}"),
}
}
#[test]
fn skill_only_forces_skill_drop_even_with_claude_on_path() {
let mut o = opts();
o.skill_only = true;
let mode = pick_mode(&o, &[Agent::ClaudeCode], |_| true);
assert_eq!(
mode,
WizardMode::SkillDrop {
agent: Agent::ClaudeCode
}
);
}
#[test]
fn agent_override_wins_over_first_installed() {
let mut o = opts();
o.agent_override = Some("cursor".into());
let mode = pick_mode(&o, &[Agent::ClaudeCode, Agent::Cursor], |_| false);
assert_eq!(
mode,
WizardMode::SkillDrop {
agent: Agent::Cursor
}
);
}
#[test]
fn unknown_override_falls_through_to_auto_detect() {
let mut o = opts();
o.agent_override = Some("notarealagent".into());
let mode = pick_mode(&o, &[Agent::ClaudeCode], |a| a == Agent::ClaudeCode);
match mode {
WizardMode::Headless { agent, .. } => assert_eq!(agent, Agent::ClaudeCode),
other => panic!("expected headless claude on fallthrough, got {other:?}"),
}
}
#[test]
fn agent_cli_command_only_returns_for_headless_capable() {
assert_eq!(agent_cli_command(Agent::ClaudeCode), Some("claude"));
assert_eq!(agent_cli_command(Agent::Codex), Some("codex"));
assert_eq!(agent_cli_command(Agent::Cursor), None);
assert_eq!(agent_cli_command(Agent::Windsurf), None);
assert_eq!(agent_cli_command(Agent::Cline), None);
assert_eq!(agent_cli_command(Agent::Continue), None);
}
}