ironclaw 0.22.0

Secure personal AI assistant that protects your data and expands its capabilities on the fly
Documentation
use clap::{CommandFactory, Parser};
use clap_complete::{Shell, generate};
use std::io::{self, Write};

/// Generate shell completion scripts for ironclaw
#[derive(Parser, Debug)]
pub struct Completion {
    /// The shell to generate completions for
    #[arg(value_enum, long)]
    pub shell: Shell,
}

impl Completion {
    pub fn run(&self) -> anyhow::Result<()> {
        let mut cmd = crate::cli::Cli::command();
        let bin_name = cmd.get_name().to_string();

        if self.shell == Shell::Zsh {
            // Generate to buffer so we can patch the compdef call.
            // clap_complete emits bare `compdef _ironclaw ironclaw` which
            // errors if sourced before compinit. Guard it so the script
            // works in all sourcing contexts.
            let mut buf = Vec::new();
            generate(self.shell, &mut cmd, bin_name.clone(), &mut buf);
            let script = String::from_utf8(buf)?;

            let bare = format!("compdef _{0} {0}", bin_name);
            let guarded = format!("(( $+functions[compdef] )) && compdef _{0} {0}", bin_name);
            let patched = script.replace(&bare, &guarded);

            io::stdout().write_all(patched.as_bytes())?;
        } else {
            generate(self.shell, &mut cmd, bin_name, &mut io::stdout());
        }

        Ok(())
    }
}

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

    #[test]
    fn test_run_generates_output() {
        let completion = Completion { shell: Shell::Zsh };
        let mut cmd = crate::cli::Cli::command();
        let bin_name = cmd.get_name().to_string();
        let mut buf = Vec::new();
        generate(completion.shell, &mut cmd, bin_name, &mut buf);
        assert!(!buf.is_empty(), "generate() should produce output");
    }

    #[test]
    fn test_zsh_compdef_guard_applied() {
        let mut cmd = crate::cli::Cli::command();
        let bin_name = cmd.get_name().to_string();
        let mut buf = Vec::new();
        generate(Shell::Zsh, &mut cmd, bin_name.clone(), &mut buf);
        let raw = String::from_utf8(buf).unwrap();

        // Apply the same patching logic as run()
        let bare = format!("compdef _{0} {0}", bin_name);
        let guarded = format!("(( $+functions[compdef] )) && compdef _{0} {0}", bin_name);
        let patched = raw.replace(&bare, &guarded);

        let bare_compdef = format!("    compdef _{0} {0}\n", bin_name);
        assert!(
            !patched.contains(&bare_compdef),
            "bare compdef should not appear after patching"
        );
        assert!(
            patched.contains("$+functions[compdef]"),
            "patched output should contain compdef guard"
        );
    }
}