agentry-tui 0.2.2

Multi-Agent Prompt Manager TUI
use std::path::Path;

use agentry_core::models::{DetectedAgent, InstallMethod};
use agentry_harness::context::HarnessConfig;

pub struct InstallOffer {
    pub agent_id: String,
    pub agent_name: String,
    pub method: InstallMethod,
    pub command: String,
}

pub struct AuditorSetupReport {
    pub config_written: bool,
    pub prompt_written: bool,
    pub collection_adopted: bool,
}

pub fn select_install_offers(agents: &[DetectedAgent]) -> Vec<InstallOffer> {
    agents
        .iter()
        .filter(|agent| !agent.installed)
        .filter_map(|agent| {
            let method = agent.spec.install_methods.iter().find(|method| {
                method.available_on_os()
                    && !matches!(method, InstallMethod::BuiltIn)
                    && method.install_command(None) != "echo 'No automatic install available'"
            })?;
            Some(InstallOffer {
                agent_id: agent.spec.id.clone(),
                agent_name: agent.spec.name.clone(),
                method: method.clone(),
                command: method.install_command(None),
            })
        })
        .collect()
}

pub fn default_harness_config(agents: &[DetectedAgent]) -> HarnessConfig {
    let mut config = HarnessConfig::default();
    config.harness.enabled_agents = agents
        .iter()
        .filter(|agent| agent.installed)
        .map(|agent| agent.spec.id.clone())
        .collect();
    config.local.runtime = Some("ollama".to_string());
    config.local.model = Some("qwen2.5-coder:7b".to_string());
    config.onboarding.setup_completed_at = Some(chrono::Utc::now().to_rfc3339());
    config
}

pub fn run_auditor_setup(home_dir: &Path) -> Result<AuditorSetupReport, String> {
    let config = agentry_auditor::config::load_config(home_dir);
    let mut config_written = false;
    if config.host_cli.is_none() {
        agentry_auditor::config::write_config(home_dir, &config)?;
        config_written = true;
    }
    let prompt_written = agentry_auditor::config::write_canonical_prompt_if_absent(home_dir)?;
    let collection_adopted = agentry_auditor::config::adopt_orphaned_collection(home_dir)?;
    Ok(AuditorSetupReport {
        config_written,
        prompt_written,
        collection_adopted,
    })
}

pub fn confirm(prompt: &str) -> bool {
    use std::io::Write;
    print!("{prompt} ");
    let _ = std::io::stdout().flush();
    let mut line = String::new();
    match std::io::stdin().read_line(&mut line) {
        Ok(_) => matches!(line.trim().to_ascii_lowercase().as_str(), "y" | "yes"),
        Err(_) => false,
    }
}

pub fn run_install_command(command: &str) -> Result<bool, String> {
    let status = std::process::Command::new("sh")
        .arg("-c")
        .arg(command)
        .status()
        .map_err(|err| format!("failed to run command: {err}"))?;
    Ok(status.success())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn temp_home(prefix: &str) -> std::path::PathBuf {
        let path = std::env::temp_dir().join(format!("{}_{}", prefix, std::process::id()));
        std::fs::create_dir_all(&path).unwrap();
        path
    }

    fn detected_agent(
        id: &str,
        name: &str,
        installed: bool,
        methods: Vec<InstallMethod>,
    ) -> DetectedAgent {
        DetectedAgent {
            spec: agentry_core::models::AgentSpec {
                id: id.to_string(),
                name: name.to_string(),
                cli_binary: id.to_string(),
                config_dir: format!(".{id}"),
                prompt_filename: "AGENTS.md".to_string(),
                prompt_format: agentry_core::models::PromptFormat::PlainMd,
                skills_dir_name: None,
                max_size: None,
                install_methods: methods,
            },
            installed,
            version: None,
            config_dir_exists: false,
            prompt_file_exists: false,
            skills_dir: None,
            skills_symlink_pattern: None,
            installed_skills: Vec::new(),
            detected_methods: Vec::new(),
        }
    }

    fn other_method(cmd: &str) -> InstallMethod {
        InstallMethod::Other {
            description: "test".to_string(),
            install_cmd: cmd.to_string(),
        }
    }

    #[test]
    fn select_install_offers_skips_installed_agents() {
        let agents = vec![
            detected_agent(
                "installed-a",
                "Installed A",
                true,
                vec![other_method("true")],
            ),
            detected_agent("missing-b", "Missing B", false, vec![other_method("true")]),
        ];
        let offers = select_install_offers(&agents);
        assert_eq!(offers.len(), 1);
        assert_eq!(offers[0].agent_id, "missing-b");
        assert_eq!(offers[0].command, "true");
    }

    #[test]
    fn select_install_offers_picks_first_available_method() {
        let agents = vec![detected_agent(
            "missing-a",
            "Missing A",
            false,
            vec![other_method("first"), other_method("second")],
        )];
        let offers = select_install_offers(&agents);
        assert_eq!(offers.len(), 1);
        assert_eq!(offers[0].command, "first");
    }

    #[test]
    fn select_install_offers_skips_agents_without_methods() {
        let agents = vec![detected_agent("missing-a", "Missing A", false, Vec::new())];
        assert!(select_install_offers(&agents).is_empty());
    }

    #[test]
    fn select_install_offers_skips_builtin_methods() {
        let agents = vec![detected_agent(
            "missing-a",
            "Missing A",
            false,
            vec![InstallMethod::BuiltIn],
        )];
        assert!(select_install_offers(&agents).is_empty());
    }

    #[test]
    fn select_install_offers_skips_noop_commands() {
        let agents = vec![detected_agent(
            "missing-a",
            "Missing A",
            false,
            vec![InstallMethod::JetBrainsPlugin {
                plugin_id: "com.example".to_string(),
            }],
        )];
        assert!(select_install_offers(&agents).is_empty());
    }

    #[test]
    fn default_harness_config_enables_installed_agents() {
        let agents = vec![
            detected_agent("installed-a", "Installed A", true, Vec::new()),
            detected_agent("missing-b", "Missing B", false, Vec::new()),
        ];
        let config = default_harness_config(&agents);
        assert_eq!(config.harness.enabled_agents, vec!["installed-a"]);
        assert_eq!(config.local.runtime.as_deref(), Some("ollama"));
        assert_eq!(config.local.model.as_deref(), Some("qwen2.5-coder:7b"));
        assert!(config.onboarding.setup_completed_at.is_some());
    }

    #[test]
    fn run_auditor_setup_is_idempotent() {
        let home = temp_home("agentry_test_onb_auditor");
        let first = run_auditor_setup(&home).unwrap();
        assert!(first.config_written);
        assert!(first.prompt_written);
        assert!(!first.collection_adopted);
        let path = agentry_harness::context::config_path(&home);
        let content_before = std::fs::read_to_string(&path).unwrap();
        let second = run_auditor_setup(&home).unwrap();
        assert!(!second.prompt_written);
        assert!(!second.collection_adopted);
        let content_after = std::fs::read_to_string(&path).unwrap();
        assert_eq!(content_before, content_after);
        std::fs::remove_dir_all(&home).unwrap();
    }

    #[test]
    fn run_auditor_setup_preserves_existing_auditor_config() {
        let home = temp_home("agentry_test_onb_audcfg");
        let config = agentry_auditor::config::AuditorConfig {
            host_cli: Some("codex".to_string()),
            ..Default::default()
        };
        agentry_auditor::config::write_config(&home, &config).unwrap();
        let report = run_auditor_setup(&home).unwrap();
        assert!(!report.config_written);
        assert!(report.prompt_written);
        std::fs::remove_dir_all(&home).unwrap();
    }
}