lean_ctx/cli/
shell_init.rs1macro_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}