Skip to main content

cargo_mate/captain/
shell_integration.rs

1use anyhow::{Context, Result};
2use colored::*;
3use std::fs;
4use std::path::{Path, PathBuf};
5pub struct ShellIntegration;
6impl ShellIntegration {
7    pub fn install() -> Result<()> {
8        println!("Installing cargo-mate...");
9        let shell = Self::detect_shell()?;
10        let rc_file = Self::get_rc_file(&shell)?;
11        println!("[SEARCH] Detected shell: {}", shell.cyan());
12        println!("[FILE] RC file: {}", rc_file.display());
13        Self::backup_rc_file(&rc_file)?;
14        Self::ensure_shipwreck_bin_exists()?;
15        let integration_code = Self::generate_integration_code(&shell);
16        Self::add_to_rc_file(&rc_file, &integration_code)?;
17        Self::create_completion_script(&shell)?;
18        println!("[RELOAD] {}", "To activate immediately, run one of these:".yellow());
19        println!("   {} {}", "source".green(), format!("{}", rc_file.display()) .cyan());
20        println!("   {} {}", "cm".green(), "activate".cyan());
21        println!("   {}", "Or restart your terminal".dimmed());
22        println!();
23        println!("[DOCS] {}", "Available commands after activation:".yellow());
24        println!(" Run cm commands in cargo and run cargo commands in cm");
25        println!("   {} - Direct cargo-mate access", "cm".cyan());
26        println!();
27        Ok(())
28    }
29    pub fn uninstall() -> Result<()> {
30        println!("[TRASH] Removing cargo-mate shell integration...");
31        let shell = Self::detect_shell()?;
32        let rc_file = Self::get_rc_file(&shell)?;
33        if !rc_file.exists() {
34            println!("[WARN] RC file not found: {}", rc_file.display());
35            return Ok(());
36        }
37        let content = fs::read_to_string(&rc_file)?;
38        let cleaned = Self::remove_integration_code(&content);
39        fs::write(&rc_file, cleaned)?;
40        println!("[RELOAD] Please restart your terminal");
41        Ok(())
42    }
43    fn ensure_shipwreck_bin_exists() -> Result<()> {
44        let shipwreck_bin = dirs::home_dir()
45            .context("Could not find home directory")?
46            .join(".shipwreck")
47            .join("bin");
48        if !shipwreck_bin.exists() {
49            fs::create_dir_all(&shipwreck_bin)?;
50            println!("[DIR] Created directory: {}", shipwreck_bin.display());
51        }
52        let cm_binary = shipwreck_bin.join("cm");
53        if cm_binary.exists() {
54            println!("[OK] Found cm binary in {}", shipwreck_bin.display());
55            #[cfg(unix)]
56            {
57                use std::os::unix::fs::PermissionsExt;
58                if let Ok(metadata) = std::fs::metadata(&cm_binary) {
59                    let perms = metadata.permissions();
60                    if perms.mode() & 0o111 == 0 {
61                        println!("[WARN] cm binary is not executable, fixing...");
62                        let current_mode = perms.mode();
63                        let mut new_perms = perms;
64                        new_perms.set_mode(current_mode | 0o755);
65                        if let Err(e) = std::fs::set_permissions(&cm_binary, new_perms) {
66                            println!("[ERROR] Failed to make cm executable: {}", e);
67                        }
68                    }
69                }
70            }
71        } else {
72            let mut found_binary = false;
73            for entry in std::fs::read_dir(&shipwreck_bin)
74                .unwrap_or_else(|_| std::fs::read_dir(".").unwrap())
75            {
76                if let Ok(entry) = entry {
77                    let file_name = entry.file_name();
78                    let name_str = file_name.to_string_lossy();
79                    if name_str.starts_with("cargo-mate")
80                        && name_str.ends_with(".protected")
81                    {
82                        println!("[WARN] Found incorrectly named binary: {}", name_str);
83                        println!(
84                            "[FIX] This should be renamed to 'cm' for proper cargo integration"
85                        );
86                        found_binary = true;
87                        break;
88                    }
89                }
90            }
91            if !found_binary {
92                println!("[INFO] No cm binary found in {}", shipwreck_bin.display());
93                println!(
94                    "[INFO] The protected binary will be installed when you first run 'cm'"
95                );
96            }
97        }
98        Ok(())
99    }
100    pub fn detect_shell() -> Result<String> {
101        let os_type = std::env::consts::OS;
102        let is_windows = os_type == "windows";
103        if is_windows {
104            if std::env::var("PSModulePath").is_ok() {
105                return Ok("powershell".to_string());
106            } else {
107                return Ok("cmd".to_string());
108            }
109        } else {
110            if let Ok(shell_path) = std::env::var("SHELL") {
111                let shell_name = std::path::Path::new(&shell_path)
112                    .file_name()
113                    .and_then(|n| n.to_str())
114                    .unwrap_or("");
115                match shell_name {
116                    "zsh" => return Ok("zsh".to_string()),
117                    "bash" => return Ok("bash".to_string()),
118                    "fish" => return Ok("fish".to_string()),
119                    "ash" | "dash" => return Ok("ash".to_string()),
120                    "sh" => {
121                        if os_type == "macos" {
122                            return Ok("bash".to_string());
123                        } else {
124                            return Ok("ash".to_string());
125                        }
126                    }
127                    _ => {
128                        if shell_path.contains("zsh") {
129                            return Ok("zsh".to_string());
130                        } else if shell_path.contains("bash") {
131                            return Ok("bash".to_string());
132                        } else if shell_path.contains("fish") {
133                            return Ok("fish".to_string());
134                        }
135                    }
136                }
137            }
138            if os_type == "macos" {
139                if std::process::Command::new("zsh").arg("--version").output().is_ok() {
140                    return Ok("zsh".to_string());
141                }
142            }
143        }
144        match os_type {
145            "macos" => Ok("zsh".to_string()),
146            "linux" => Ok("bash".to_string()),
147            _ => Ok("bash".to_string()),
148        }
149    }
150    pub fn get_rc_file(shell: &str) -> Result<PathBuf> {
151        let home = dirs::home_dir().context("Could not find home directory")?;
152        let os_type = std::env::consts::OS;
153        let is_windows = os_type == "windows";
154        let is_macos = os_type == "macos";
155        let rc_file = match shell {
156            "powershell" => {
157                if is_windows {
158                    let profile_paths = vec![
159                        home.join("Documents").join("PowerShell")
160                        .join("Microsoft.PowerShell_profile.ps1"), home.join("Documents")
161                        .join("WindowsPowerShell")
162                        .join("Microsoft.PowerShell_profile.ps1"),
163                    ];
164                    for path in &profile_paths {
165                        if path.exists() {
166                            return Ok(path.clone());
167                        }
168                    }
169                    profile_paths[0].clone()
170                } else {
171                    home.join(".profile")
172                }
173            }
174            "cmd" => {
175                if is_windows {
176                    let autoexec = PathBuf::from("C:\\autoexec.bat");
177                    if autoexec.exists() {
178                        autoexec
179                    } else {
180                        home.join("cargo-mate-profile.cmd")
181                    }
182                } else {
183                    home.join(".profile")
184                }
185            }
186            "zsh" => {
187                let zshrc = home.join(".zshrc");
188                if zshrc.exists() || is_macos { zshrc } else { home.join(".profile") }
189            }
190            "bash" => {
191                let bashrc = home.join(".bashrc");
192                let bash_profile = home.join(".bash_profile");
193                if bashrc.exists() {
194                    bashrc
195                } else if bash_profile.exists() {
196                    bash_profile
197                } else {
198                    bashrc
199                }
200            }
201            "fish" => {
202                let config_dir = if let Ok(xdg_config) = std::env::var(
203                    "XDG_CONFIG_HOME",
204                ) {
205                    PathBuf::from(xdg_config)
206                } else {
207                    home.join(".config")
208                };
209                let fish_config = config_dir.join("fish").join("config.fish");
210                if let Some(parent) = fish_config.parent() {
211                    let _ = fs::create_dir_all(parent);
212                }
213                fish_config
214            }
215            "ash" => {
216                let profile = home.join(".profile");
217                if profile.exists() { profile } else { profile }
218            }
219            _ => {
220                let profile = home.join(".profile");
221                if profile.exists() { profile } else { profile }
222            }
223        };
224        Ok(rc_file)
225    }
226    fn backup_rc_file(rc_file: &Path) -> Result<()> {
227        if rc_file.exists() {
228            let backup = rc_file.with_extension("bak.cargo-mate");
229            fs::copy(rc_file, &backup)?;
230            println!("[BACKUP] Backed up to: {}", backup.display());
231        }
232        Ok(())
233    }
234    fn generate_integration_code(shell: &str) -> String {
235        let home = dirs::home_dir()
236            .map(|p| p.to_string_lossy().to_string())
237            .unwrap_or_else(|| "$HOME".to_string());
238        let shipwreck_bin = format!("{}/.shipwreck/bin", home);
239        let os_type = std::env::consts::OS;
240        let is_windows = os_type == "windows";
241        match shell {
242            "powershell" => {
243                if is_windows {
244                    r#"
245# === Cargo Mate (cm) Integration for PowerShell ===
246# This section was automatically added by cargo-mate
247
248# Add Cargo Mate to PATH
249$cmBinPath = "$env:USERPROFILE\.shipwreck\bin"
250if ($env:PATH -notlike "*$cmBinPath*") {
251    $env:PATH = "$cmBinPath;$env:PATH"
252}
253
254# Function to intercept cargo commands
255function cargo {
256    if (Get-Command cm -ErrorAction SilentlyContinue) {
257        # Store original cargo path to avoid infinite loops
258        $env:CARGO_BIN_PATH = (Get-Command cargo -ErrorAction SilentlyContinue).Source
259        # Call cargo-mate instead
260        & "$env:USERPROFILE\.shipwreck\bin\cm.exe" exec @args
261    } else {
262        # Fallback to original cargo if cm not found
263        if ($env:CARGO_BIN_PATH) {
264            & $env:CARGO_BIN_PATH @args
265        } else {
266            Write-Host "cargo command not found" -ForegroundColor Red
267        }
268    }
269}
270
271# Alias for quick access
272Set-Alias cg cm
273
274# Function to load project config
275function cm_load_config {
276    if (Test-Path .cg) {
277        $env:CM_PROJECT_CONFIG = ".cg"
278        # Uncomment below to show config loading
279        # Write-Host "[ANCHOR] Loaded project config: .cg" -ForegroundColor Green
280    }
281}
282
283# Load config when entering directory (PowerShell 6+)
284if ($PSVersionTable.PSVersion.Major -ge 6) {
285    Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {
286        cm_load_config
287    } | Out-Null
288}
289
290# Nautical theme
291$env:CM_THEME = "nautical"
292
293# === End Cargo Mate Integration ===
294"#
295                        .to_string()
296                } else {
297                    r#"
298# PowerShell integration not supported on this platform
299# Please use bash/zsh/fish integration instead
300"#
301                        .to_string()
302                }
303            }
304            "cmd" => {
305                if is_windows {
306                    r#"
307@echo off
308REM === Cargo Mate (cm) Integration for CMD ===
309REM This section was automatically added by cargo-mate
310
311REM Add Cargo Mate to PATH
312set CM_BIN_PATH=%USERPROFILE%\.shipwreck\bin
313if "%PATH%"=="%PATH:%CM_BIN_PATH%=%" (
314    set PATH=%CM_BIN_PATH%;%PATH%
315)
316
317REM Function to intercept cargo commands (via alias)
318REM Note: CMD has limited function support, using alias instead
319doskey cargo=cm exec $*
320
321REM Alias for quick access
322doskey cg=cm
323
324REM Nautical theme
325set CM_THEME=nautical
326
327REM === End Cargo Mate Integration ===
328"#
329                        .to_string()
330                } else {
331                    r#"
332# CMD integration not supported on this platform
333# Please use bash/zsh/fish integration instead
334"#
335                        .to_string()
336                }
337            }
338            "zsh" | "bash" => {
339                r#"
340# === Cargo Mate (cm) Integration ===
341# This section was automatically added by cargo-mate
342
343# Add ~/.shipwreck/bin to PATH for cargo-mate commands
344if [[ ":$PATH:" != *":$HOME/.shipwreck/bin:"* ]]; then
345    export PATH="$HOME/.shipwreck/bin:$PATH"
346fi
347
348# Function to check if cm command exists (more robust than command -v)
349cm_exists() {
350    # Try multiple ways to find cm
351    if command -v cm &> /dev/null; then
352        return 0
353    elif [ -x "$HOME/.shipwreck/bin/cm" ]; then
354        return 0
355    elif [ -f "$HOME/.shipwreck/bin/cm" ]; then
356        return 0
357    fi
358    return 1
359}
360
361# Function to intercept cargo commands
362cargo() {
363    if cm_exists; then
364        # Set the path to the real cargo binary to avoid infinite loops
365        export CARGO_BIN_PATH="$(command -v cargo 2>/dev/null || which cargo 2>/dev/null || echo 'cargo')"
366        # Use the protected binary from .shipwreck/bin
367        "$HOME/.shipwreck/bin/cm" exec "$@"
368    else
369        command cargo "$@"
370    fi
371}
372
373# Alias for quick access
374alias cg='cm'
375
376# Auto-complete for cm
377if [ -f ~/.shipwreck/completions/cm.bash ]; then
378    source ~/.shipwreck/completions/cm.bash
379fi
380
381# Project-specific config loader
382cm_load_config() {
383    if [ -f .cg ]; then
384        export CM_PROJECT_CONFIG=".cg"
385        # Silent config loading (remove # to show once per session)
386        # if [ -z "$CM_CONFIG_LOADED" ]; then
387        #     echo "⚓ Loaded project config: .cg"
388        #     export CM_CONFIG_LOADED="1"
389        # fi
390    fi
391}
392
393# Auto-load config when entering directory
394if [[ "$SHELL" == *"zsh"* ]]; then
395    autoload -U add-zsh-hook
396    add-zsh-hook chpwd cm_load_config
397elif [[ "$SHELL" == *"bash"* ]]; then
398    PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;} cm_load_config"
399fi
400
401# Nautical prompt enhancement (optional)
402export CM_THEME="nautical"
403
404# === End Cargo Mate Integration ===
405"#
406                    .to_string()
407            }
408            "fish" => {
409                r#"
410# === Cargo Mate (cm) Integration ===
411# This section was automatically added by cargo-mate
412
413# Add ~/.shipwreck/bin to PATH for cargo-mate commands
414if not contains $HOME/.shipwreck/bin $fish_user_paths
415    set -U fish_user_paths $HOME/.shipwreck/bin $fish_user_paths
416end
417
418# Function to intercept cargo commands
419function cargo
420    if command -v cm > /dev/null
421        # Set the path to the real cargo binary to avoid infinite loops
422        set -x CARGO_BIN_PATH (command -v cargo)
423        cm exec $argv
424    else
425        command cargo $argv
426    end
427end
428
429# Aliases
430alias cg='cm'
431
432# Auto-complete
433if test -f ~/.shipwreck/completions/cm.fish
434    source ~/.shipwreck/completions/cm.fish
435end
436
437# Project config loader
438function cm_load_config --on-variable PWD
439    if test -f .cg
440        set -x CM_PROJECT_CONFIG ".cg"
441        echo "[ANCHOR] Loaded project config: .cg"
442    end
443end
444
445# === End Cargo Mate Integration ===
446"#
447                    .to_string()
448            }
449            _ => String::new(),
450        }
451    }
452    fn add_to_rc_file(rc_file: &Path, integration_code: &str) -> Result<()> {
453        if let Some(parent) = rc_file.parent() {
454            fs::create_dir_all(parent)
455                .with_context(|| {
456                    format!("Failed to create directory: {}", parent.display())
457                })?;
458        }
459        let mut content = if rc_file.exists() {
460            fs::read_to_string(rc_file)
461                .with_context(|| {
462                    format!("Failed to read RC file: {}", rc_file.display())
463                })?
464        } else {
465            String::new()
466        };
467        let has_old_integration = content.contains("=== Cargo Mate");
468        let has_new_integration = content.contains("Cargo Mate Integration");
469        if has_old_integration || has_new_integration {
470            println!("[WARN] Integration already exists, updating...");
471            content = Self::remove_integration_code(&content);
472        }
473        if !content.is_empty() && !content.ends_with('\n') {
474            content.push('\n');
475        }
476        if !content.ends_with("\n\n") {
477            content.push('\n');
478        }
479        content.push_str(integration_code);
480        if !content.ends_with('\n') {
481            content.push('\n');
482        }
483        Self::backup_rc_file(rc_file)?;
484        fs::write(rc_file, &content)
485            .with_context(|| format!("Failed to write RC file: {}", rc_file.display()))?;
486        #[cfg(unix)]
487        {
488            use std::os::unix::fs::PermissionsExt;
489            if let Ok(metadata) = fs::metadata(rc_file) {
490                let mut perms = metadata.permissions();
491                let current_mode = perms.mode();
492                perms.set_mode(current_mode | 0o600);
493                let _ = fs::set_permissions(rc_file, perms);
494            }
495        }
496        Ok(())
497    }
498    fn remove_integration_code(content: &str) -> String {
499        let lines: Vec<&str> = content.lines().collect();
500        let mut result = Vec::new();
501        let mut in_section = false;
502        for line in lines {
503            if line.contains("=== Cargo Mate") && line.contains("Integration ===") {
504                in_section = !in_section;
505                continue;
506            }
507            if !in_section {
508                result.push(line);
509            }
510        }
511        result.join("\n")
512    }
513    fn create_completion_script(shell: &str) -> Result<()> {
514        let completions_dir = dirs::home_dir()
515            .context("Could not find home directory")?
516            .join(".shipwreck")
517            .join("completions");
518        fs::create_dir_all(&completions_dir)?;
519        let os_type = std::env::consts::OS;
520        let is_windows = os_type == "windows";
521        match shell {
522            "powershell" => {
523                if is_windows {
524                    let script = r#"
525# PowerShell completion for cm (cargo-mate)
526
527using namespace System.Management.Automation
528using namespace System.Management.Automation.Language
529
530function CmCompletion {
531    param($wordToComplete, $commandAst, $cursorPosition)
532
533    $commands = @(
534        "build", "test", "run", "check", "clean", "doc", "fmt", "clippy",
535        "init", "help", "journey", "anchor", "log", "tide", "map",
536        "mutiny", "config", "version", "view", "optimize", "test",
537        "checklist", "history", "install", "activate", "register", "idea", "wtf", "user"
538    )
539
540    $completions = $commands | Where-Object { $_ -like "$wordToComplete*" }
541
542    foreach ($completion in $completions) {
543        [CompletionResult]::new($completion, $completion, 'ParameterValue', $completion)
544    }
545}
546
547Register-ArgumentCompleter -CommandName cm -ScriptBlock ${function:CmCompletion}
548Register-ArgumentCompleter -CommandName cg -ScriptBlock ${function:CmCompletion}
549"#;
550                    let completion_file = completions_dir.join("cm.ps1");
551                    fs::write(&completion_file, script)?;
552                }
553            }
554            "cmd" => {
555                if is_windows {
556                    let script = r#"
557REM CMD completion helper for cm (cargo-mate)
558REM This file provides command information for manual completion
559
560REM Available commands:
561REM build test run check clean doc fmt clippy
562REM init help journey anchor log tide map
563REM mutiny config version view optimize test
564REM checklist history install activate register idea wtf user
565"#;
566                    let completion_file = completions_dir.join("cm-help.cmd");
567                    fs::write(&completion_file, script)?;
568                }
569            }
570            "bash" | "zsh" => {
571                let script = r#"#!/bin/bash
572# Bash completion for cm (cargo-mate)
573
574_cm_completions() {
575    local cur prev opts
576    cur="${COMP_WORDS[COMP_CWORD]}"
577    prev="${COMP_WORDS[COMP_CWORD-1]}"
578    
579    # Main commands
580    opts="build test run check clean doc fmt clippy init help \
581          journey anchor log tide map mutiny config checklist history"
582    
583    # Sub-commands
584    case "${prev}" in
585        journey)
586            opts="record play list export import"
587            ;;
588        anchor)
589            opts="save restore list show diff"
590            ;;
591        log)
592            opts="add search timeline export analyze"
593            ;;
594        tide)
595            opts="show analyze export"
596            ;;
597        map)
598            opts="deps show analyze export"
599            ;;
600        mutiny)
601            opts="activate deactivate allow-warnings skip-tests force yolo status"
602            ;;
603        config)
604            opts="set get list init add-shortcut add-hook"
605            ;;
606        *)
607            # Include cargo commands too
608            if command -v cargo &> /dev/null; then
609                cargo_opts=$(cargo --list 2>/dev/null | awk '{print $1}')
610                opts="$opts $cargo_opts"
611            fi
612            ;;
613    esac
614    
615    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
616}
617
618complete -F _cm_completions cm
619complete -F _cm_completions cg
620"#;
621                let completion_file = completions_dir.join("cm.bash");
622                fs::write(&completion_file, script)?;
623                #[cfg(unix)]
624                {
625                    use std::os::unix::fs::PermissionsExt;
626                    let mut perms = fs::metadata(&completion_file)?.permissions();
627                    perms.set_mode(0o755);
628                    fs::set_permissions(&completion_file, perms)?;
629                }
630            }
631            "fish" => {
632                let script = r#"# Fish completion for cm (cargo-mate)
633
634complete -c cm -f
635
636# Main commands
637complete -c cm -n "__fish_use_subcommand" -a "build" -d "Compile the current package"
638complete -c cm -n "__fish_use_subcommand" -a "test" -d "Run tests"
639complete -c cm -n "__fish_use_subcommand" -a "run" -d "Run a binary"
640complete -c cm -n "__fish_use_subcommand" -a "check" -d "Check code without building"
641complete -c cm -n "__fish_use_subcommand" -a "journey" -d "Journey recording and playback"
642complete -c cm -n "__fish_use_subcommand" -a "anchor" -d "Save and restore project states"
643complete -c cm -n "__fish_use_subcommand" -a "log" -d "Captain's log"
644complete -c cm -n "__fish_use_subcommand" -a "tide" -d "Performance tracking"
645complete -c cm -n "__fish_use_subcommand" -a "map" -d "Dependency visualization"
646complete -c cm -n "__fish_use_subcommand" -a "mutiny" -d "Override cargo restrictions"
647complete -c cm -n "__fish_use_subcommand" -a "config" -d "Configuration management"
648
649# Journey subcommands
650complete -c cm -n "__fish_seen_subcommand_from journey" -a "record play list export import"
651
652# Anchor subcommands
653complete -c cm -n "__fish_seen_subcommand_from anchor" -a "save restore list show diff"
654
655# Copy cg alias
656complete -c cg -w cm
657"#;
658                let completion_file = completions_dir.join("cm.fish");
659                fs::write(&completion_file, script)?;
660            }
661            _ => {}
662        }
663        Ok(())
664    }
665    pub fn show_status() {
666        println!("{}", "=== Shell Integration Status ===".blue().bold());
667        let shell = Self::detect_shell().unwrap_or_else(|_| "unknown".to_string());
668        let os_type = std::env::consts::OS;
669        let is_windows = os_type == "windows";
670        println!("[SHELL] Current shell: {}", shell.cyan());
671        println!("[OS] Operating system: {}", os_type.cyan());
672        if let Ok(rc_file) = Self::get_rc_file(&shell) {
673            if rc_file.exists() {
674                let content = fs::read_to_string(&rc_file).unwrap_or_default();
675                if content.contains("=== Cargo Mate")
676                    || content.contains("Cargo Mate Integration")
677                {
678                    println!("[OK] Integration: {}", "Installed".green());
679                    println!(
680                        "   Config file: {}", rc_file.display().to_string().dimmed()
681                    );
682                } else {
683                    println!("[X] Integration: {}", "Not installed".red());
684                }
685            } else {
686                println!("[X] Integration: {}", "Not installed".red());
687                println!(
688                    "   Config file location: {}", rc_file.display().to_string().dimmed()
689                );
690            }
691        }
692        if let Ok(path) = std::env::var("PATH") {
693            let separator = if is_windows { ';' } else { ':' };
694            let cm_in_path = path
695                .split(separator)
696                .any(|p| {
697                    let cm_path = Path::new(p)
698                        .join(if is_windows { "cm.exe" } else { "cm" });
699                    cm_path.exists()
700                });
701            if cm_in_path {
702                println!("[OK] Binary in PATH: {}", "Yes".green());
703            } else {
704                println!("[WARN] Binary in PATH: {}", "No".yellow());
705            }
706        }
707        let completions_dir = dirs::home_dir()
708            .map(|h| h.join(".shipwreck").join("completions"))
709            .unwrap_or_default();
710        if completions_dir.exists() {
711            println!("[OK] Completions: {}", "Installed".green());
712            let extensions = if is_windows {
713                vec!["ps1", "cmd"]
714            } else {
715                vec!["bash", "fish"]
716            };
717            for ext in extensions {
718                let completion_file = completions_dir.join(format!("cm.{}", ext));
719                if completion_file.exists() {
720                    println!(
721                        "   {} {} completion", "•".dimmed(), ext.to_uppercase()
722                        .dimmed()
723                    );
724                }
725            }
726        } else {
727            println!("[X] Completions: {}", "Not installed".red());
728        }
729        if is_windows {
730            println!();
731            println!("[WINDOWS] Windows-specific notes:");
732            println!("   • Use PowerShell for best experience");
733            println!("   • CMD support is limited");
734            println!("   • Admin privileges may be required for installation");
735        }
736    }
737    pub fn add_shell_integration(rc_file: &PathBuf, shell: &str) -> Result<()> {
738        use std::fs::OpenOptions;
739        use std::io::Write;
740        if rc_file.exists() {
741            let backup = rc_file.with_extension("bak.cargo-mate");
742            std::fs::copy(rc_file, &backup)?;
743            println!("📋 Backed up {} to {}", rc_file.display(), backup.display());
744        }
745        let integration_code = match shell {
746            "fish" => {
747                r#"
748# === Cargo Mate (cm) Integration ===
749function cargo
750    cm exec $argv
751end
752
753# Note: cm binary should be in PATH
754alias cg='cm'
755# === End Cargo Mate Integration ===
756"#
757            }
758            _ => {
759                r#"
760# === Cargo Mate (cm) Integration ===
761cargo() {
762    cm exec "$@"
763}
764# Note: cm binary should be in PATH
765alias cg='cm'
766# === End Cargo Mate Integration ===
767"#
768            }
769        };
770        let mut file = OpenOptions::new().create(true).append(true).open(rc_file)?;
771        writeln!(file, "{}", integration_code)?;
772        println!("✅ Shell integration added to {}", rc_file.display());
773        Ok(())
774    }
775}