llman 0.0.59

A tool for managing LLM application rules(prompts) ...
Documentation
use crate::env_safety::{find_dangerous_env_keys, is_valid_env_key};
use crate::x::claude_code::config::ConfigGroup;
use anyhow::{Result, bail};
use rust_i18n::t;

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum EnvSyntax {
    PosixExport,
    PowerShell,
}

pub fn env_syntax_for_current_platform() -> EnvSyntax {
    if cfg!(windows) {
        EnvSyntax::PowerShell
    } else {
        EnvSyntax::PosixExport
    }
}

pub fn render_env_injection_lines(group: &ConfigGroup, syntax: EnvSyntax) -> Result<Vec<String>> {
    let mut invalid_keys: Vec<&str> = group
        .keys()
        .filter(|key| !is_valid_env_key(key))
        .map(String::as_str)
        .collect();
    invalid_keys.sort_unstable();

    if !invalid_keys.is_empty() {
        bail!(t!(
            "claude_code.account.env_invalid_keys",
            keys = invalid_keys.join(", ")
        ));
    }

    let mut keys: Vec<&str> = group.keys().map(String::as_str).collect();
    keys.sort_unstable();

    let mut lines = Vec::with_capacity(keys.len());
    for key in keys {
        let value = group
            .get(key)
            .expect("key collected from group keys must exist");

        let quoted_value = match syntax {
            EnvSyntax::PosixExport => quote_posix_single(value),
            EnvSyntax::PowerShell => quote_powershell_single(value),
        };

        let line = match syntax {
            EnvSyntax::PosixExport => format!("export {key}={quoted_value}"),
            EnvSyntax::PowerShell => format!("$env:{key}={quoted_value}"),
        };

        lines.push(line);
    }

    Ok(lines)
}

pub fn prepare_env_injection(group: &ConfigGroup) -> Result<()> {
    let mut invalid_keys: Vec<&str> = group
        .keys()
        .filter(|key| !is_valid_env_key(key))
        .map(String::as_str)
        .collect();
    invalid_keys.sort_unstable();
    if !invalid_keys.is_empty() {
        bail!(t!(
            "claude_code.account.env_invalid_keys",
            keys = invalid_keys.join(", ")
        ));
    }

    let dangerous = find_dangerous_env_keys(group.keys().map(String::as_str));
    if !dangerous.is_empty() {
        bail!(t!(
            "claude_code.account.env_dangerous_keys",
            keys = dangerous.join(", ")
        ));
    }
    Ok(())
}

fn quote_posix_single(value: &str) -> String {
    format!("'{}'", value.replace('\'', "'\\''"))
}

fn quote_powershell_single(value: &str) -> String {
    format!("'{}'", value.replace('\'', "''"))
}

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

    #[test]
    fn quote_posix_single_escapes_single_quote() {
        assert_eq!(quote_posix_single("a'b"), "'a'\\''b'");
    }

    #[test]
    fn quote_posix_single_handles_empty() {
        assert_eq!(quote_posix_single(""), "''");
    }

    #[test]
    fn quote_posix_single_preserves_spaces() {
        assert_eq!(quote_posix_single("hello world"), "'hello world'");
    }

    #[test]
    fn quote_powershell_single_escapes_single_quote() {
        assert_eq!(quote_powershell_single("a'b"), "'a''b'");
    }

    #[test]
    fn quote_powershell_single_handles_empty() {
        assert_eq!(quote_powershell_single(""), "''");
    }

    #[test]
    fn quote_powershell_single_preserves_spaces() {
        assert_eq!(quote_powershell_single("hello world"), "'hello world'");
    }

    #[test]
    fn render_env_injection_lines_sorts_keys() {
        let group: ConfigGroup = HashMap::from([
            ("B".to_string(), "2".to_string()),
            ("A".to_string(), "1".to_string()),
        ]);

        let lines = render_env_injection_lines(&group, EnvSyntax::PosixExport).expect("lines");
        assert_eq!(lines, vec!["export A='1'", "export B='2'"]);
    }

    #[test]
    fn render_env_injection_lines_rejects_invalid_keys() {
        let group: ConfigGroup = HashMap::from([("BAD-KEY".to_string(), "1".to_string())]);
        let err =
            render_env_injection_lines(&group, EnvSyntax::PosixExport).expect_err("should fail");
        assert!(
            err.to_string().contains("Invalid environment variable key"),
            "unexpected error: {err}"
        );
    }

    #[test]
    fn prepare_env_injection_rejects_dangerous_keys() {
        let group: ConfigGroup = HashMap::from([("LD_PRELOAD".to_string(), "/tmp/x".to_string())]);
        let err = prepare_env_injection(&group).expect_err("should fail");
        assert!(
            err.to_string().contains("dangerous"),
            "unexpected error: {err}"
        );
    }
}