Skip to main content

lean_ctx/cli/
shell_init.rs

1macro_rules! qprintln {
2    ($($t:tt)*) => {
3        if !super::quiet_enabled() {
4            println!($($t)*);
5        }
6    };
7}
8
9fn backup_shell_config(path: &std::path::Path) {
10    if !path.exists() {
11        return;
12    }
13    let bak = path.with_extension("lean-ctx.bak");
14    if std::fs::copy(path, &bak).is_ok() {
15        qprintln!(
16            "  Backup: {}",
17            bak.file_name()
18                .map(|n| format!("~/{}", n.to_string_lossy()))
19                .unwrap_or_else(|| bak.display().to_string())
20        );
21    }
22}
23
24pub fn init_powershell(binary: &str) {
25    let profile_dir = dirs::home_dir().map(|h| h.join("Documents").join("PowerShell"));
26    let profile_path = match profile_dir {
27        Some(dir) => {
28            let _ = std::fs::create_dir_all(&dir);
29            dir.join("Microsoft.PowerShell_profile.ps1")
30        }
31        None => {
32            eprintln!("Could not resolve PowerShell profile directory");
33            return;
34        }
35    };
36
37    let binary_escaped = binary.replace('\\', "\\\\");
38    let functions = format!(
39        r#"
40# lean-ctx shell hook — transparent CLI compression (90+ patterns)
41if (-not $env:LEAN_CTX_ACTIVE -and -not $env:LEAN_CTX_DISABLED) {{
42  $LeanCtxBin = "{binary_escaped}"
43  function _lc {{
44    if ($env:LEAN_CTX_DISABLED -or [Console]::IsOutputRedirected) {{ & @args; return }}
45    & $LeanCtxBin -c @args
46    if ($LASTEXITCODE -eq 127 -or $LASTEXITCODE -eq 126) {{
47      & @args
48    }}
49  }}
50  function lean-ctx-raw {{ $env:LEAN_CTX_RAW = '1'; & @args; Remove-Item Env:LEAN_CTX_RAW -ErrorAction SilentlyContinue }}
51  if (Get-Command lean-ctx -ErrorAction SilentlyContinue) {{
52    function git {{ _lc git @args }}
53    function cargo {{ _lc cargo @args }}
54    function docker {{ _lc docker @args }}
55    function kubectl {{ _lc kubectl @args }}
56    function gh {{ _lc gh @args }}
57    function pip {{ _lc pip @args }}
58    function pip3 {{ _lc pip3 @args }}
59    function ruff {{ _lc ruff @args }}
60    function go {{ _lc go @args }}
61    function curl {{ _lc curl @args }}
62    function wget {{ _lc wget @args }}
63    foreach ($c in @('npm','pnpm','yarn','eslint','prettier','tsc')) {{
64      if (Get-Command $c -CommandType Application -ErrorAction SilentlyContinue) {{
65        New-Item -Path "function:$c" -Value ([scriptblock]::Create("_lc $c @args")) -Force | Out-Null
66      }}
67    }}
68  }}
69}}
70"#
71    );
72
73    backup_shell_config(&profile_path);
74
75    if let Ok(existing) = std::fs::read_to_string(&profile_path) {
76        if existing.contains("lean-ctx shell hook") {
77            let cleaned = remove_lean_ctx_block_ps(&existing);
78            match std::fs::write(&profile_path, format!("{cleaned}{functions}")) {
79                Ok(()) => {
80                    qprintln!("Updated lean-ctx functions in {}", profile_path.display());
81                    qprintln!("  Binary: {binary}");
82                    return;
83                }
84                Err(e) => {
85                    eprintln!("Error updating {}: {e}", profile_path.display());
86                    return;
87                }
88            }
89        }
90    }
91
92    match std::fs::OpenOptions::new()
93        .append(true)
94        .create(true)
95        .open(&profile_path)
96    {
97        Ok(mut f) => {
98            use std::io::Write;
99            let _ = f.write_all(functions.as_bytes());
100            qprintln!("Added lean-ctx functions to {}", profile_path.display());
101            qprintln!("  Binary: {binary}");
102        }
103        Err(e) => eprintln!("Error writing {}: {e}", profile_path.display()),
104    }
105}
106
107pub fn remove_lean_ctx_block_ps(content: &str) -> String {
108    let mut result = String::new();
109    let mut in_block = false;
110    let mut brace_depth = 0i32;
111
112    for line in content.lines() {
113        if line.contains("lean-ctx shell hook") {
114            in_block = true;
115            continue;
116        }
117        if in_block {
118            brace_depth += line.matches('{').count() as i32;
119            brace_depth -= line.matches('}').count() as i32;
120            if brace_depth <= 0 && (line.trim() == "}" || line.trim().is_empty()) {
121                if line.trim() == "}" {
122                    in_block = false;
123                    brace_depth = 0;
124                }
125                continue;
126            }
127            continue;
128        }
129        result.push_str(line);
130        result.push('\n');
131    }
132    result
133}
134
135pub fn init_fish(binary: &str) {
136    let config = dirs::home_dir()
137        .map(|h| h.join(".config/fish/config.fish"))
138        .unwrap_or_default();
139
140    let alias_list = crate::rewrite_registry::shell_alias_list();
141    let aliases = format!(
142        "\n# lean-ctx shell hook — smart shell mode (track-by-default)\n\
143        set -g _lean_ctx_cmds {alias_list}\n\
144        \n\
145        function _lc\n\
146        \tif set -q LEAN_CTX_DISABLED; or not isatty stdout\n\
147        \t\tcommand $argv\n\
148        \t\treturn\n\
149        \tend\n\
150        \t'{binary}' -t $argv\n\
151        \tset -l _lc_rc $status\n\
152        \tif test $_lc_rc -eq 127 -o $_lc_rc -eq 126\n\
153        \t\tcommand $argv\n\
154        \telse\n\
155        \t\treturn $_lc_rc\n\
156        \tend\n\
157        end\n\
158        \n\
159        function _lc_compress\n\
160        \tif set -q LEAN_CTX_DISABLED; or not isatty stdout\n\
161        \t\tcommand $argv\n\
162        \t\treturn\n\
163        \tend\n\
164        \t'{binary}' -c $argv\n\
165        \tset -l _lc_rc $status\n\
166        \tif test $_lc_rc -eq 127 -o $_lc_rc -eq 126\n\
167        \t\tcommand $argv\n\
168        \telse\n\
169        \t\treturn $_lc_rc\n\
170        \tend\n\
171        end\n\
172        \n\
173        function lean-ctx-on\n\
174        \tfor _lc_cmd in $_lean_ctx_cmds\n\
175        \t\talias $_lc_cmd '_lc '$_lc_cmd\n\
176        \tend\n\
177        \talias k '_lc kubectl'\n\
178        \tset -gx LEAN_CTX_ENABLED 1\n\
179        \tisatty stdout; and echo 'lean-ctx: ON (track mode — full output, stats recorded)'\n\
180        end\n\
181        \n\
182        function lean-ctx-off\n\
183        \tfor _lc_cmd in $_lean_ctx_cmds\n\
184        \t\tfunctions --erase $_lc_cmd 2>/dev/null; true\n\
185        \tend\n\
186        \tfunctions --erase k 2>/dev/null; true\n\
187        \tset -e LEAN_CTX_ENABLED\n\
188        \tisatty stdout; and echo 'lean-ctx: OFF'\n\
189        end\n\
190        \n\
191        function lean-ctx-mode\n\
192        \tswitch $argv[1]\n\
193        \t\tcase compress\n\
194        \t\t\tfor _lc_cmd in $_lean_ctx_cmds\n\
195        \t\t\t\talias $_lc_cmd '_lc_compress '$_lc_cmd\n\
196        \t\t\t\tend\n\
197        \t\t\talias k '_lc_compress kubectl'\n\
198        \t\t\tset -gx LEAN_CTX_ENABLED 1\n\
199        \t\t\tisatty stdout; and echo 'lean-ctx: COMPRESS mode (all output compressed)'\n\
200        \t\tcase track\n\
201        \t\t\tlean-ctx-on\n\
202        \t\tcase off\n\
203        \t\t\tlean-ctx-off\n\
204        \t\tcase '*'\n\
205        \t\t\techo 'Usage: lean-ctx-mode <track|compress|off>'\n\
206        \t\t\techo '  track    — Full output, stats recorded (default)'\n\
207        \t\t\techo '  compress — Compressed output for all commands'\n\
208        \t\t\techo '  off      — No aliases, raw shell'\n\
209        \tend\n\
210        end\n\
211        \n\
212        function lean-ctx-raw\n\
213        \tset -lx LEAN_CTX_RAW 1\n\
214        \tcommand $argv\n\
215        end\n\
216        \n\
217        function lean-ctx-status\n\
218        \tif set -q LEAN_CTX_DISABLED\n\
219        \t\tisatty stdout; and echo 'lean-ctx: DISABLED (LEAN_CTX_DISABLED is set)'\n\
220        \telse if set -q LEAN_CTX_ENABLED\n\
221        \t\tisatty stdout; and echo 'lean-ctx: ON'\n\
222        \telse\n\
223        \t\tisatty stdout; and echo 'lean-ctx: OFF'\n\
224        \tend\n\
225        end\n\
226        \n\
227        if not set -q LEAN_CTX_ACTIVE; and not set -q LEAN_CTX_DISABLED; and test (set -q LEAN_CTX_ENABLED; and echo $LEAN_CTX_ENABLED; or echo 1) != '0'\n\
228        \tif command -q lean-ctx\n\
229        \t\tlean-ctx-on\n\
230        \tend\n\
231        end\n\
232        # lean-ctx shell hook — end\n"
233    );
234
235    backup_shell_config(&config);
236
237    if let Ok(existing) = std::fs::read_to_string(&config) {
238        if existing.contains("lean-ctx shell hook") {
239            let cleaned = remove_lean_ctx_block(&existing);
240            match std::fs::write(&config, format!("{cleaned}{aliases}")) {
241                Ok(()) => {
242                    qprintln!("Updated lean-ctx aliases in {}", config.display());
243                    qprintln!("  Binary: {binary}");
244                    return;
245                }
246                Err(e) => {
247                    eprintln!("Error updating {}: {e}", config.display());
248                    return;
249                }
250            }
251        }
252    }
253
254    match std::fs::OpenOptions::new()
255        .append(true)
256        .create(true)
257        .open(&config)
258    {
259        Ok(mut f) => {
260            use std::io::Write;
261            let _ = f.write_all(aliases.as_bytes());
262            qprintln!("Added lean-ctx aliases to {}", config.display());
263            qprintln!("  Binary: {binary}");
264        }
265        Err(e) => eprintln!("Error writing {}: {e}", config.display()),
266    }
267}
268
269pub fn init_posix(is_zsh: bool, binary: &str) {
270    let rc_file = if is_zsh {
271        dirs::home_dir()
272            .map(|h| h.join(".zshrc"))
273            .unwrap_or_default()
274    } else {
275        dirs::home_dir()
276            .map(|h| h.join(".bashrc"))
277            .unwrap_or_default()
278    };
279
280    let alias_list = crate::rewrite_registry::shell_alias_list();
281    let aliases = format!(
282        r#"
283# lean-ctx shell hook — smart shell mode (track-by-default)
284_lean_ctx_cmds=({alias_list})
285
286_lc() {{
287    if [ -n "${{LEAN_CTX_DISABLED:-}}" ] || [ ! -t 1 ]; then
288        command "$@"
289        return
290    fi
291    '{binary}' -t "$@"
292    local _lc_rc=$?
293    if [ "$_lc_rc" -eq 127 ] || [ "$_lc_rc" -eq 126 ]; then
294        command "$@"
295    else
296        return "$_lc_rc"
297    fi
298}}
299
300_lc_compress() {{
301    if [ -n "${{LEAN_CTX_DISABLED:-}}" ] || [ ! -t 1 ]; then
302        command "$@"
303        return
304    fi
305    '{binary}' -c "$@"
306    local _lc_rc=$?
307    if [ "$_lc_rc" -eq 127 ] || [ "$_lc_rc" -eq 126 ]; then
308        command "$@"
309    else
310        return "$_lc_rc"
311    fi
312}}
313
314lean-ctx-on() {{
315    for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
316        # shellcheck disable=SC2139
317        alias "$_lc_cmd"='_lc '"$_lc_cmd"
318    done
319    alias k='_lc kubectl'
320    export LEAN_CTX_ENABLED=1
321    [ -t 1 ] && echo "lean-ctx: ON (track mode — full output, stats recorded)"
322}}
323
324lean-ctx-off() {{
325    for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
326        unalias "$_lc_cmd" 2>/dev/null || true
327    done
328    unalias k 2>/dev/null || true
329    unset LEAN_CTX_ENABLED
330    [ -t 1 ] && echo "lean-ctx: OFF"
331}}
332
333lean-ctx-mode() {{
334    case "${{1:-}}" in
335        compress)
336            for _lc_cmd in "${{_lean_ctx_cmds[@]}}"; do
337                # shellcheck disable=SC2139
338                alias "$_lc_cmd"='_lc_compress '"$_lc_cmd"
339            done
340            alias k='_lc_compress kubectl'
341            export LEAN_CTX_ENABLED=1
342            [ -t 1 ] && echo "lean-ctx: COMPRESS mode (all output compressed)"
343            ;;
344        track)
345            lean-ctx-on
346            ;;
347        off)
348            lean-ctx-off
349            ;;
350        *)
351            echo "Usage: lean-ctx-mode <track|compress|off>"
352            echo "  track    — Full output, stats recorded (default)"
353            echo "  compress — Compressed output for all commands"
354            echo "  off      — No aliases, raw shell"
355            ;;
356    esac
357}}
358
359lean-ctx-raw() {{
360    LEAN_CTX_RAW=1 command "$@"
361}}
362
363lean-ctx-status() {{
364    if [ -n "${{LEAN_CTX_DISABLED:-}}" ]; then
365        [ -t 1 ] && echo "lean-ctx: DISABLED (LEAN_CTX_DISABLED is set)"
366    elif [ -n "${{LEAN_CTX_ENABLED:-}}" ]; then
367        [ -t 1 ] && echo "lean-ctx: ON"
368    else
369        [ -t 1 ] && echo "lean-ctx: OFF"
370    fi
371}}
372
373if [ -z "${{LEAN_CTX_ACTIVE:-}}" ] && [ -z "${{LEAN_CTX_DISABLED:-}}" ] && [ "${{LEAN_CTX_ENABLED:-1}}" != "0" ]; then
374    command -v lean-ctx >/dev/null 2>&1 && lean-ctx-on
375fi
376# lean-ctx shell hook — end
377"#
378    );
379
380    backup_shell_config(&rc_file);
381
382    if let Ok(existing) = std::fs::read_to_string(&rc_file) {
383        if existing.contains("lean-ctx shell hook") {
384            let cleaned = remove_lean_ctx_block(&existing);
385            match std::fs::write(&rc_file, format!("{cleaned}{aliases}")) {
386                Ok(()) => {
387                    qprintln!("Updated lean-ctx aliases in {}", rc_file.display());
388                    qprintln!("  Binary: {binary}");
389                    return;
390                }
391                Err(e) => {
392                    eprintln!("Error updating {}: {e}", rc_file.display());
393                    return;
394                }
395            }
396        }
397    }
398
399    match std::fs::OpenOptions::new()
400        .append(true)
401        .create(true)
402        .open(&rc_file)
403    {
404        Ok(mut f) => {
405            use std::io::Write;
406            let _ = f.write_all(aliases.as_bytes());
407            qprintln!("Added lean-ctx aliases to {}", rc_file.display());
408            qprintln!("  Binary: {binary}");
409        }
410        Err(e) => eprintln!("Error writing {}: {e}", rc_file.display()),
411    }
412
413    write_env_sh_for_containers(&aliases);
414    print_docker_env_hints(is_zsh);
415}
416
417pub fn write_env_sh_for_containers(aliases: &str) {
418    let env_sh = match crate::core::data_dir::lean_ctx_data_dir() {
419        Ok(d) => d.join("env.sh"),
420        Err(_) => return,
421    };
422    if let Some(parent) = env_sh.parent() {
423        let _ = std::fs::create_dir_all(parent);
424    }
425    let sanitized_aliases = crate::core::sanitize::neutralize_shell_content(aliases);
426    let mut content = sanitized_aliases;
427    content.push_str(
428        r#"
429
430# lean-ctx docker self-heal: re-inject Claude MCP config if Claude overwrote ~/.claude.json
431if command -v claude >/dev/null 2>&1 && command -v lean-ctx >/dev/null 2>&1; then
432  if ! claude mcp list 2>/dev/null | grep -q "lean-ctx"; then
433    LEAN_CTX_QUIET=1 lean-ctx init --agent claude >/dev/null 2>&1
434  fi
435fi
436"#,
437    );
438    match std::fs::write(&env_sh, content) {
439        Ok(()) => qprintln!("  env.sh: {}", env_sh.display()),
440        Err(e) => eprintln!("  Warning: could not write {}: {e}", env_sh.display()),
441    }
442}
443
444fn print_docker_env_hints(is_zsh: bool) {
445    if is_zsh || !crate::shell::is_container() {
446        return;
447    }
448    let env_sh = crate::core::data_dir::lean_ctx_data_dir()
449        .map(|d| d.join("env.sh").to_string_lossy().to_string())
450        .unwrap_or_else(|_| "/root/.lean-ctx/env.sh".to_string());
451
452    let has_bash_env = std::env::var("BASH_ENV").is_ok();
453    let has_claude_env = std::env::var("CLAUDE_ENV_FILE").is_ok();
454
455    if has_bash_env && has_claude_env {
456        return;
457    }
458
459    eprintln!();
460    eprintln!("  \x1b[33m⚠  Docker detected — environment hints:\x1b[0m");
461
462    if !has_bash_env {
463        eprintln!("  For generic bash -c usage (non-interactive shells):");
464        eprintln!("    \x1b[1mENV BASH_ENV=\"{env_sh}\"\x1b[0m");
465    }
466    if !has_claude_env {
467        eprintln!("  For Claude Code (sources before each command):");
468        eprintln!("    \x1b[1mENV CLAUDE_ENV_FILE=\"{env_sh}\"\x1b[0m");
469    }
470    eprintln!();
471}
472
473pub fn remove_lean_ctx_block(content: &str) -> String {
474    if content.contains("# lean-ctx shell hook — end") {
475        return remove_lean_ctx_block_by_marker(content);
476    }
477    remove_lean_ctx_block_legacy(content)
478}
479
480fn remove_lean_ctx_block_by_marker(content: &str) -> String {
481    let mut result = String::new();
482    let mut in_block = false;
483
484    for line in content.lines() {
485        if !in_block && line.contains("lean-ctx shell hook") && !line.contains("end") {
486            in_block = true;
487            continue;
488        }
489        if in_block {
490            if line.trim() == "# lean-ctx shell hook — end" {
491                in_block = false;
492            }
493            continue;
494        }
495        result.push_str(line);
496        result.push('\n');
497    }
498    result
499}
500
501fn remove_lean_ctx_block_legacy(content: &str) -> String {
502    let mut result = String::new();
503    let mut in_block = false;
504
505    for line in content.lines() {
506        if line.contains("lean-ctx shell hook") {
507            in_block = true;
508            continue;
509        }
510        if in_block {
511            if line.trim() == "fi" || line.trim() == "end" || line.trim().is_empty() {
512                if line.trim() == "fi" || line.trim() == "end" {
513                    in_block = false;
514                }
515                continue;
516            }
517            if !line.starts_with("alias ") && !line.starts_with('\t') && !line.starts_with("if ") {
518                in_block = false;
519                result.push_str(line);
520                result.push('\n');
521            }
522            continue;
523        }
524        result.push_str(line);
525        result.push('\n');
526    }
527    result
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    #[test]
535    fn test_remove_lean_ctx_block_posix() {
536        let input = r#"# existing config
537export PATH="$HOME/bin:$PATH"
538
539# lean-ctx shell hook — transparent CLI compression (90+ patterns)
540if [ -z "$LEAN_CTX_ACTIVE" ]; then
541alias git='lean-ctx -c git'
542alias npm='lean-ctx -c npm'
543fi
544
545# other stuff
546export EDITOR=vim
547"#;
548        let result = remove_lean_ctx_block(input);
549        assert!(!result.contains("lean-ctx"), "block should be removed");
550        assert!(result.contains("export PATH"), "other content preserved");
551        assert!(
552            result.contains("export EDITOR"),
553            "trailing content preserved"
554        );
555    }
556
557    #[test]
558    fn test_remove_lean_ctx_block_fish() {
559        let input = "# other fish config\nset -x FOO bar\n\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\nif not set -q LEAN_CTX_ACTIVE\n\talias git 'lean-ctx -c git'\n\talias npm 'lean-ctx -c npm'\nend\n\n# more config\nset -x BAZ qux\n";
560        let result = remove_lean_ctx_block(input);
561        assert!(!result.contains("lean-ctx"), "block should be removed");
562        assert!(result.contains("set -x FOO"), "other content preserved");
563        assert!(result.contains("set -x BAZ"), "trailing content preserved");
564    }
565
566    #[test]
567    fn test_remove_lean_ctx_block_ps() {
568        let input = "# PowerShell profile\n$env:FOO = 'bar'\n\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\nif (-not $env:LEAN_CTX_ACTIVE) {\n  $LeanCtxBin = \"C:\\\\bin\\\\lean-ctx.exe\"\n  function git { & $LeanCtxBin -c \"git $($args -join ' ')\" }\n}\n\n# other stuff\n$env:EDITOR = 'vim'\n";
569        let result = remove_lean_ctx_block_ps(input);
570        assert!(
571            !result.contains("lean-ctx shell hook"),
572            "block should be removed"
573        );
574        assert!(result.contains("$env:FOO"), "other content preserved");
575        assert!(result.contains("$env:EDITOR"), "trailing content preserved");
576    }
577
578    #[test]
579    fn test_remove_lean_ctx_block_ps_nested() {
580        let input = "# PowerShell profile\n$env:FOO = 'bar'\n\n# lean-ctx shell hook — transparent CLI compression (90+ patterns)\nif (-not $env:LEAN_CTX_ACTIVE) {\n  $LeanCtxBin = \"lean-ctx\"\n  function _lc {\n    & $LeanCtxBin -c \"$($args -join ' ')\"\n  }\n  if (Get-Command lean-ctx -ErrorAction SilentlyContinue) {\n    function git { _lc git @args }\n    foreach ($c in @('npm','pnpm')) {\n      if ($a) {\n        Set-Variable -Name \"_lc_$c\" -Value $a.Source -Scope Script\n      }\n    }\n  }\n}\n\n# other stuff\n$env:EDITOR = 'vim'\n";
581        let result = remove_lean_ctx_block_ps(input);
582        assert!(
583            !result.contains("lean-ctx shell hook"),
584            "block should be removed"
585        );
586        assert!(!result.contains("_lc"), "function should be removed");
587        assert!(result.contains("$env:FOO"), "other content preserved");
588        assert!(result.contains("$env:EDITOR"), "trailing content preserved");
589    }
590
591    #[test]
592    fn test_remove_block_no_lean_ctx() {
593        let input = "# normal bashrc\nexport PATH=\"$HOME/bin:$PATH\"\n";
594        let result = remove_lean_ctx_block(input);
595        assert!(result.contains("export PATH"), "content unchanged");
596    }
597
598    #[test]
599    fn test_bash_hook_contains_pipe_guard() {
600        let binary = "/usr/local/bin/lean-ctx";
601        let hook = format!(
602            r#"_lc() {{
603    if [ -n "${{LEAN_CTX_DISABLED:-}}" ] || [ ! -t 1 ]; then
604        command "$@"
605        return
606    fi
607    '{binary}' -t "$@"
608}}"#
609        );
610        assert!(
611            hook.contains("! -t 1"),
612            "bash/zsh hook must contain pipe guard [ ! -t 1 ]"
613        );
614        assert!(
615            hook.contains("LEAN_CTX_DISABLED") && hook.contains("! -t 1"),
616            "pipe guard must be in the same conditional as LEAN_CTX_DISABLED"
617        );
618    }
619
620    #[test]
621    fn test_lc_uses_track_mode_by_default() {
622        let binary = "/usr/local/bin/lean-ctx";
623        let alias_list = crate::rewrite_registry::shell_alias_list();
624        let aliases = format!(
625            r#"_lc() {{
626    '{binary}' -t "$@"
627}}
628_lc_compress() {{
629    '{binary}' -c "$@"
630}}"#
631        );
632        assert!(
633            aliases.contains("-t \"$@\""),
634            "_lc must use -t (track mode) by default"
635        );
636        assert!(
637            aliases.contains("-c \"$@\""),
638            "_lc_compress must use -c (compress mode)"
639        );
640        let _ = alias_list;
641    }
642
643    #[test]
644    fn test_posix_shell_has_lean_ctx_mode() {
645        let alias_list = crate::rewrite_registry::shell_alias_list();
646        let aliases = r#"
647lean-ctx-mode() {{
648    case "${{1:-}}" in
649        compress) echo compress ;;
650        track) echo track ;;
651        off) echo off ;;
652    esac
653}}
654"#
655        .to_string();
656        assert!(
657            aliases.contains("lean-ctx-mode()"),
658            "lean-ctx-mode function must exist"
659        );
660        assert!(
661            aliases.contains("compress"),
662            "compress mode must be available"
663        );
664        assert!(aliases.contains("track"), "track mode must be available");
665        let _ = alias_list;
666    }
667
668    #[test]
669    fn test_fish_hook_contains_pipe_guard() {
670        let hook = "function _lc\n\tif set -q LEAN_CTX_DISABLED; or not isatty stdout\n\t\tcommand $argv\n\t\treturn\n\tend\nend";
671        assert!(
672            hook.contains("isatty stdout"),
673            "fish hook must contain pipe guard (isatty stdout)"
674        );
675    }
676
677    #[test]
678    fn test_powershell_hook_contains_pipe_guard() {
679        let hook = "function _lc { if ($env:LEAN_CTX_DISABLED -or [Console]::IsOutputRedirected) { & @args; return } }";
680        assert!(
681            hook.contains("IsOutputRedirected"),
682            "PowerShell hook must contain pipe guard ([Console]::IsOutputRedirected)"
683        );
684    }
685
686    #[test]
687    fn test_remove_lean_ctx_block_new_format_with_end_marker() {
688        let input = r#"# existing config
689export PATH="$HOME/bin:$PATH"
690
691# lean-ctx shell hook — transparent CLI compression (90+ patterns)
692_lean_ctx_cmds=(git npm pnpm)
693
694lean-ctx-on() {
695    for _lc_cmd in "${_lean_ctx_cmds[@]}"; do
696        alias "$_lc_cmd"='lean-ctx -c '"$_lc_cmd"
697    done
698    export LEAN_CTX_ENABLED=1
699    [ -t 1 ] && echo "lean-ctx: ON"
700}
701
702lean-ctx-off() {
703    unset LEAN_CTX_ENABLED
704    [ -t 1 ] && echo "lean-ctx: OFF"
705}
706
707if [ -z "${LEAN_CTX_ACTIVE:-}" ] && [ "${LEAN_CTX_ENABLED:-1}" != "0" ]; then
708    lean-ctx-on
709fi
710# lean-ctx shell hook — end
711
712# other stuff
713export EDITOR=vim
714"#;
715        let result = remove_lean_ctx_block(input);
716        assert!(!result.contains("lean-ctx-on"), "block should be removed");
717        assert!(!result.contains("lean-ctx shell hook"), "marker removed");
718        assert!(result.contains("export PATH"), "other content preserved");
719        assert!(
720            result.contains("export EDITOR"),
721            "trailing content preserved"
722        );
723    }
724
725    #[test]
726    fn env_sh_for_containers_includes_self_heal() {
727        let _g = crate::core::data_dir::test_env_lock();
728        let tmp = tempfile::tempdir().expect("tempdir");
729        let data_dir = tmp.path().join("data");
730        std::fs::create_dir_all(&data_dir).expect("mkdir data");
731        std::env::set_var("LEAN_CTX_DATA_DIR", &data_dir);
732
733        write_env_sh_for_containers("alias git='lean-ctx -c git'\n");
734        let env_sh = data_dir.join("env.sh");
735        let content = std::fs::read_to_string(&env_sh).expect("env.sh exists");
736        assert!(content.contains("lean-ctx docker self-heal"));
737        assert!(content.contains("claude mcp list"));
738        assert!(content.contains("lean-ctx init --agent claude"));
739
740        std::env::remove_var("LEAN_CTX_DATA_DIR");
741    }
742}