ralph_workflow/platform/
binary_guidance.rs

1//! Binary-specific installation guidance
2//!
3//! Provides installation instructions for known AI coding tools.
4
5use super::{known_binaries, Platform};
6
7/// Installation guidance for a missing binary
8#[derive(Debug)]
9pub struct InstallGuidance {
10    /// The binary that was not found
11    pub(crate) binary: String,
12    /// Primary suggested command to install it
13    pub(crate) install_cmd: Option<String>,
14    /// Alternative installation method
15    pub(crate) alternative: Option<String>,
16    /// Additional helpful context
17    pub(crate) notes: Vec<String>,
18}
19
20impl InstallGuidance {
21    /// Generate installation guidance for a missing binary on the current platform
22    pub(crate) fn for_binary(binary: &str) -> Self {
23        let platform = Platform::detect();
24        Self::for_binary_on_platform(binary, platform)
25    }
26
27    /// Generate installation guidance for a specific platform
28    pub(crate) fn for_binary_on_platform(binary: &str, platform: Platform) -> Self {
29        let mut guidance = Self {
30            binary: binary.to_string(),
31            install_cmd: None,
32            alternative: None,
33            notes: Vec::new(),
34        };
35
36        // Check if this is a known binary with specific guidance
37        if !known_binaries::add_known_binary_guidance(&mut guidance, binary, platform) {
38            // Generic binary - provide platform-specific package manager hints
39            add_generic_guidance(&mut guidance, binary, platform);
40        }
41
42        guidance
43    }
44
45    /// Format the guidance as a user-friendly message
46    pub(crate) fn format(&self) -> String {
47        let mut lines = Vec::new();
48
49        lines.push(format!("Binary '{}' not found in PATH.", self.binary));
50        lines.push(String::new());
51
52        for note in &self.notes {
53            lines.push(format!("  {note}"));
54        }
55
56        if let Some(ref cmd) = self.install_cmd {
57            lines.push(String::new());
58            lines.push("  To install:".to_string());
59            lines.push(format!("    {cmd}"));
60        }
61
62        if let Some(ref alt) = self.alternative {
63            lines.push(format!("  Or: {alt}"));
64        }
65
66        lines.join("\n")
67    }
68}
69
70/// Add generic platform-specific installation guidance for unknown binaries
71fn add_generic_guidance(guidance: &mut InstallGuidance, binary: &str, platform: Platform) {
72    match platform {
73        Platform::MacWithBrew => {
74            guidance.install_cmd = Some(format!("brew install {binary}"));
75            guidance
76                .notes
77                .push("Or check if available via npm/pip".to_string());
78        }
79        Platform::MacWithoutBrew => {
80            guidance
81                .notes
82                .push("Consider installing Homebrew first:".to_string());
83            guidance.install_cmd = Some(
84                "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"".to_string()
85            );
86            guidance.alternative = Some(format!("Then: brew install {binary}"));
87        }
88        Platform::DebianLinux => {
89            guidance.install_cmd = Some(format!("sudo apt-get install {binary}"));
90            guidance
91                .notes
92                .push("Or check if available via npm/pip".to_string());
93        }
94        Platform::RhelLinux => {
95            guidance.install_cmd = Some(format!("sudo dnf install {binary}"));
96            guidance
97                .notes
98                .push("Or check if available via npm/pip".to_string());
99        }
100        Platform::ArchLinux => {
101            guidance.install_cmd = Some(format!("sudo pacman -S {binary}"));
102            guidance
103                .notes
104                .push("Or check the AUR if not in official repos".to_string());
105        }
106        Platform::GenericLinux => {
107            guidance
108                .notes
109                .push("Check your distribution's package manager".to_string());
110            guidance
111                .notes
112                .push("Or try: npm/pip/cargo install".to_string());
113        }
114        Platform::Windows => {
115            guidance.install_cmd = Some(format!("winget install {binary}"));
116            guidance.alternative = Some("Or use Chocolatey: choco install ...".to_string());
117        }
118        Platform::Unknown => {
119            guidance
120                .notes
121                .push("Check the tool's documentation for installation instructions".to_string());
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_install_guidance_for_claude() {
132        let guidance = InstallGuidance::for_binary_on_platform("claude", Platform::MacWithBrew);
133        assert_eq!(guidance.binary, "claude");
134        assert!(guidance.install_cmd.is_some());
135        assert!(guidance.install_cmd.as_ref().unwrap().contains("npm"));
136        assert!(!guidance.notes.is_empty());
137    }
138
139    #[test]
140    fn test_install_guidance_for_aider_mac() {
141        let guidance = InstallGuidance::for_binary_on_platform("aider", Platform::MacWithBrew);
142        assert!(guidance.install_cmd.as_ref().unwrap().contains("brew"));
143    }
144
145    #[test]
146    fn test_install_guidance_for_aider_linux() {
147        let guidance = InstallGuidance::for_binary_on_platform("aider", Platform::DebianLinux);
148        assert!(guidance.install_cmd.as_ref().unwrap().contains("pip"));
149    }
150
151    #[test]
152    fn test_install_guidance_for_unknown_binary_mac_with_brew() {
153        let guidance =
154            InstallGuidance::for_binary_on_platform("unknown-tool", Platform::MacWithBrew);
155        assert!(guidance
156            .install_cmd
157            .as_ref()
158            .unwrap()
159            .contains("brew install unknown-tool"));
160    }
161
162    #[test]
163    fn test_install_guidance_for_unknown_binary_debian() {
164        let guidance =
165            InstallGuidance::for_binary_on_platform("unknown-tool", Platform::DebianLinux);
166        assert!(guidance.install_cmd.as_ref().unwrap().contains("apt-get"));
167    }
168
169    #[test]
170    fn test_install_guidance_mac_without_brew() {
171        let guidance =
172            InstallGuidance::for_binary_on_platform("unknown-tool", Platform::MacWithoutBrew);
173        // Should suggest installing Homebrew first
174        assert!(guidance.install_cmd.as_ref().unwrap().contains("Homebrew"));
175    }
176
177    #[test]
178    fn test_install_guidance_format() {
179        let guidance = InstallGuidance::for_binary_on_platform("claude", Platform::MacWithBrew);
180        let formatted = guidance.format();
181        assert!(formatted.contains("claude"));
182        assert!(formatted.contains("not found"));
183        assert!(formatted.contains("install"));
184    }
185}