Skip to main content

lean_ctx/
shell_hook.rs

1use crate::marked_block;
2
3const MARKER_START: &str = "# >>> lean-ctx shell hook >>>";
4const MARKER_END: &str = "# <<< lean-ctx shell hook <<<";
5const ALIAS_START: &str = "# >>> lean-ctx agent aliases >>>";
6const ALIAS_END: &str = "# <<< lean-ctx agent aliases <<<";
7
8const KNOWN_AGENT_ENV_VARS: &[&str] = &[
9    "LEAN_CTX_AGENT",
10    "CLAUDECODE",
11    "CODEX_CLI_SESSION",
12    "GEMINI_SESSION",
13];
14
15const AGENT_ALIASES: &[(&str, &str)] = &[
16    ("claude", "claude"),
17    ("codex", "codex"),
18    ("gemini", "gemini"),
19];
20
21pub fn install_all(quiet: bool) {
22    let home = match dirs::home_dir() {
23        Some(h) => h,
24        None => {
25            eprintln!("Cannot resolve home directory");
26            return;
27        }
28    };
29
30    install_zshenv(&home, quiet);
31    install_bashenv(&home, quiet);
32    install_aliases(&home, quiet);
33}
34
35pub fn uninstall_all(quiet: bool) {
36    let home = match dirs::home_dir() {
37        Some(h) => h,
38        None => return,
39    };
40
41    marked_block::remove_from_file(
42        &home.join(".zshenv"),
43        MARKER_START,
44        MARKER_END,
45        quiet,
46        "shell hook from ~/.zshenv",
47    );
48    marked_block::remove_from_file(
49        &home.join(".bashenv"),
50        MARKER_START,
51        MARKER_END,
52        quiet,
53        "shell hook from ~/.bashenv",
54    );
55    marked_block::remove_from_file(
56        &home.join(".zshrc"),
57        ALIAS_START,
58        ALIAS_END,
59        quiet,
60        "agent aliases from ~/.zshrc",
61    );
62    marked_block::remove_from_file(
63        &home.join(".bashrc"),
64        ALIAS_START,
65        ALIAS_END,
66        quiet,
67        "agent aliases from ~/.bashrc",
68    );
69}
70
71fn install_zshenv(home: &std::path::Path, quiet: bool) {
72    let path = home.join(".zshenv");
73    let env_check = build_env_check();
74
75    let hook = format!(
76        r#"{MARKER_START}
77if [[ -n "$ZSH_EXECUTION_STRING" ]] && command -v lean-ctx &>/dev/null; then
78  if {env_check}; then
79    exec lean-ctx -c "$ZSH_EXECUTION_STRING"
80  fi
81fi
82{MARKER_END}"#
83    );
84
85    marked_block::upsert(
86        &path,
87        MARKER_START,
88        MARKER_END,
89        &hook,
90        quiet,
91        "shell hook in ~/.zshenv",
92    );
93}
94
95fn install_bashenv(home: &std::path::Path, quiet: bool) {
96    let path = home.join(".bashenv");
97    let env_check = build_env_check();
98
99    let hook = format!(
100        r#"{MARKER_START}
101if [[ -n "$BASH_EXECUTION_STRING" ]] && command -v lean-ctx &>/dev/null; then
102  if {env_check}; then
103    exec lean-ctx -c "$BASH_EXECUTION_STRING"
104  fi
105fi
106{MARKER_END}"#
107    );
108
109    marked_block::upsert(
110        &path,
111        MARKER_START,
112        MARKER_END,
113        &hook,
114        quiet,
115        "shell hook in ~/.bashenv",
116    );
117}
118
119fn install_aliases(home: &std::path::Path, quiet: bool) {
120    let mut lines = Vec::new();
121    lines.push(ALIAS_START.to_string());
122    for (alias_name, bin_name) in AGENT_ALIASES {
123        lines.push(format!(
124            "alias {alias_name}='LEAN_CTX_AGENT=1 BASH_ENV=\"$HOME/.bashenv\" {bin_name}'"
125        ));
126    }
127    lines.push(ALIAS_END.to_string());
128    let block = lines.join("\n");
129
130    for rc in &[home.join(".zshrc"), home.join(".bashrc")] {
131        if rc.exists() {
132            let label = format!(
133                "agent aliases in ~/{}",
134                rc.file_name().unwrap_or_default().to_string_lossy()
135            );
136            marked_block::upsert(rc, ALIAS_START, ALIAS_END, &block, quiet, &label);
137        }
138    }
139}
140
141fn build_env_check() -> String {
142    let checks: Vec<String> = KNOWN_AGENT_ENV_VARS
143        .iter()
144        .map(|v| format!("-n \"${v}\""))
145        .collect();
146    format!("[[ {} ]]", checks.join(" || "))
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn env_check_format() {
155        let check = build_env_check();
156        assert!(check.contains("LEAN_CTX_AGENT"));
157        assert!(check.contains("CLAUDECODE"));
158        assert!(check.contains("||"));
159    }
160}