1use std::path::PathBuf;
2
3fn resolve_binary_path() -> String {
4 std::env::current_exe()
5 .map(|p| p.to_string_lossy().to_string())
6 .unwrap_or_else(|_| "lean-ctx".to_string())
7}
8
9fn resolve_binary_path_for_bash() -> String {
10 let path = resolve_binary_path();
11 to_bash_compatible_path(&path)
12}
13
14pub fn to_bash_compatible_path(path: &str) -> String {
15 let path = path.replace('\\', "/");
16 if path.len() >= 2 && path.as_bytes()[1] == b':' {
17 let drive = (path.as_bytes()[0] as char).to_ascii_lowercase();
18 format!("/{drive}{}", &path[2..])
19 } else {
20 path
21 }
22}
23
24pub fn install_agent_hook(agent: &str, global: bool) {
25 match agent {
26 "claude" | "claude-code" => install_claude_hook(global),
27 "cursor" => install_cursor_hook(global),
28 "gemini" => install_gemini_hook(),
29 "codex" => install_codex_hook(),
30 "windsurf" => install_windsurf_rules(global),
31 "cline" | "roo" => install_cline_rules(global),
32 "copilot" => install_copilot_hook(global),
33 "pi" => install_pi_hook(global),
34 "qwen" => install_mcp_json_agent(
35 "Qwen Code",
36 "~/.qwen/mcp.json",
37 &dirs::home_dir().unwrap_or_default().join(".qwen/mcp.json"),
38 ),
39 "trae" => install_mcp_json_agent(
40 "Trae",
41 "~/.trae/mcp.json",
42 &dirs::home_dir().unwrap_or_default().join(".trae/mcp.json"),
43 ),
44 "amazonq" => install_mcp_json_agent(
45 "Amazon Q Developer",
46 "~/.aws/amazonq/mcp.json",
47 &dirs::home_dir()
48 .unwrap_or_default()
49 .join(".aws/amazonq/mcp.json"),
50 ),
51 "jetbrains" => install_mcp_json_agent(
52 "JetBrains IDEs",
53 "~/.jb-mcp.json",
54 &dirs::home_dir().unwrap_or_default().join(".jb-mcp.json"),
55 ),
56 _ => {
57 eprintln!("Unknown agent: {agent}");
58 eprintln!(" Supported: claude, cursor, gemini, codex, windsurf, cline, roo, copilot, pi, qwen, trae, amazonq, jetbrains");
59 std::process::exit(1);
60 }
61 }
62}
63
64fn install_claude_hook(global: bool) {
65 let home = match dirs::home_dir() {
66 Some(h) => h,
67 None => {
68 eprintln!("Cannot resolve home directory");
69 return;
70 }
71 };
72
73 let hooks_dir = home.join(".claude").join("hooks");
74 let _ = std::fs::create_dir_all(&hooks_dir);
75
76 let script_path = hooks_dir.join("lean-ctx-rewrite.sh");
77 let binary = resolve_binary_path_for_bash();
78 let script = format!(
79 r#"#!/usr/bin/env bash
80# lean-ctx PreToolUse hook — rewrites bash commands to lean-ctx equivalents
81set -euo pipefail
82
83LEAN_CTX_BIN="{binary}"
84
85INPUT=$(cat)
86TOOL=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4)
87
88if [ "$TOOL" != "Bash" ] && [ "$TOOL" != "bash" ]; then
89 exit 0
90fi
91
92CMD=$(echo "$INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4)
93
94if echo "$CMD" | grep -qE "^(lean-ctx |$LEAN_CTX_BIN )"; then
95 exit 0
96fi
97
98REWRITE=""
99case "$CMD" in
100 git\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
101 gh\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
102 cargo\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
103 npm\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
104 pnpm\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
105 yarn\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
106 docker\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
107 kubectl\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
108 pip\ *|pip3\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
109 ruff\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
110 go\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
111 curl\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
112 grep\ *|rg\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
113 find\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
114 cat\ *|head\ *|tail\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
115 ls\ *|ls) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
116 eslint*|prettier*|tsc*) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
117 pytest*|ruff\ *|mypy*) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
118 aws\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
119 helm\ *) REWRITE="$LEAN_CTX_BIN -c $CMD" ;;
120 *) exit 0 ;;
121esac
122
123if [ -n "$REWRITE" ]; then
124 echo "{{\"command\":\"$REWRITE\"}}"
125fi
126"#
127 );
128
129 write_file(&script_path, &script);
130 make_executable(&script_path);
131
132 let settings_path = home.join(".claude").join("settings.json");
133 let settings_content = if settings_path.exists() {
134 std::fs::read_to_string(&settings_path).unwrap_or_default()
135 } else {
136 String::new()
137 };
138
139 if settings_content.contains("lean-ctx-rewrite") {
140 println!("Claude Code hook already configured.");
141 } else {
142 let hook_entry = serde_json::json!({
143 "hooks": {
144 "PreToolUse": [{
145 "matcher": "Bash|bash",
146 "hooks": [{
147 "type": "command",
148 "command": script_path.to_string_lossy()
149 }]
150 }]
151 }
152 });
153
154 if settings_content.is_empty() {
155 write_file(
156 &settings_path,
157 &serde_json::to_string_pretty(&hook_entry).unwrap(),
158 );
159 } else if let Ok(mut existing) =
160 serde_json::from_str::<serde_json::Value>(&settings_content)
161 {
162 if let Some(obj) = existing.as_object_mut() {
163 obj.insert("hooks".to_string(), hook_entry["hooks"].clone());
164 write_file(
165 &settings_path,
166 &serde_json::to_string_pretty(&existing).unwrap(),
167 );
168 }
169 }
170 println!(
171 "Installed Claude Code PreToolUse hook at {}",
172 script_path.display()
173 );
174 }
175
176 if !global {
177 let claude_md = PathBuf::from("CLAUDE.md");
178 if !claude_md.exists()
179 || !std::fs::read_to_string(&claude_md)
180 .unwrap_or_default()
181 .contains("lean-ctx")
182 {
183 let content = include_str!("templates/CLAUDE.md");
184 write_file(&claude_md, content);
185 println!("Created CLAUDE.md in current project directory.");
186 } else {
187 println!("CLAUDE.md already configured.");
188 }
189 } else {
190 println!(
191 "Global mode: skipping project-local CLAUDE.md (use without --global in a project)."
192 );
193 }
194}
195
196fn install_cursor_hook(global: bool) {
197 let home = match dirs::home_dir() {
198 Some(h) => h,
199 None => {
200 eprintln!("Cannot resolve home directory");
201 return;
202 }
203 };
204
205 let hooks_dir = home.join(".cursor").join("hooks");
206 let _ = std::fs::create_dir_all(&hooks_dir);
207
208 let script_path = hooks_dir.join("lean-ctx-rewrite.sh");
209 let binary = resolve_binary_path_for_bash();
210 let script = format!(
211 r#"#!/usr/bin/env bash
212# lean-ctx Cursor hook — rewrites shell commands
213set -euo pipefail
214LEAN_CTX_BIN="{binary}"
215INPUT=$(cat)
216CMD=$(echo "$INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
217if [ -z "$CMD" ] || echo "$CMD" | grep -qE "^(lean-ctx |$LEAN_CTX_BIN )"; then exit 0; fi
218case "$CMD" in
219 git\ *|gh\ *|cargo\ *|npm\ *|pnpm\ *|docker\ *|kubectl\ *|pip\ *|ruff\ *|go\ *|curl\ *|grep\ *|rg\ *|find\ *|ls\ *|ls|cat\ *|aws\ *|helm\ *)
220 echo "{{\"command\":\"$LEAN_CTX_BIN -c $CMD\"}}" ;;
221 *) exit 0 ;;
222esac
223"#
224 );
225
226 write_file(&script_path, &script);
227 make_executable(&script_path);
228
229 let hooks_json = home.join(".cursor").join("hooks.json");
230 let hook_config = serde_json::json!({
231 "hooks": [{
232 "event": "preToolUse",
233 "matcher": {
234 "tool": "terminal_command"
235 },
236 "command": script_path.to_string_lossy()
237 }]
238 });
239
240 let content = if hooks_json.exists() {
241 std::fs::read_to_string(&hooks_json).unwrap_or_default()
242 } else {
243 String::new()
244 };
245
246 if content.contains("lean-ctx-rewrite") {
247 println!("Cursor hook already configured.");
248 } else {
249 write_file(
250 &hooks_json,
251 &serde_json::to_string_pretty(&hook_config).unwrap(),
252 );
253 println!("Installed Cursor hook at {}", hooks_json.display());
254 }
255
256 if !global {
257 let rules_dir = PathBuf::from(".cursor").join("rules");
258 let _ = std::fs::create_dir_all(&rules_dir);
259 let rule_path = rules_dir.join("lean-ctx.mdc");
260 if !rule_path.exists() {
261 let rule_content = include_str!("templates/lean-ctx.mdc");
262 write_file(&rule_path, rule_content);
263 println!("Created .cursor/rules/lean-ctx.mdc in current project.");
264 } else {
265 println!("Cursor rule already exists.");
266 }
267 } else {
268 println!("Global mode: skipping project-local .cursor/rules/ (use without --global in a project).");
269 }
270
271 println!("Restart Cursor to activate.");
272}
273
274fn install_gemini_hook() {
275 let home = match dirs::home_dir() {
276 Some(h) => h,
277 None => {
278 eprintln!("Cannot resolve home directory");
279 return;
280 }
281 };
282
283 let hooks_dir = home.join(".gemini").join("hooks");
284 let _ = std::fs::create_dir_all(&hooks_dir);
285
286 let script_path = hooks_dir.join("lean-ctx-hook-gemini.sh");
287 let binary = resolve_binary_path_for_bash();
288 let script = format!(
289 r#"#!/usr/bin/env bash
290# lean-ctx Gemini CLI BeforeTool hook
291set -euo pipefail
292LEAN_CTX_BIN="{binary}"
293INPUT=$(cat)
294CMD=$(echo "$INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
295if [ -z "$CMD" ] || echo "$CMD" | grep -qE "^(lean-ctx |$LEAN_CTX_BIN )"; then exit 0; fi
296case "$CMD" in
297 git\ *|gh\ *|cargo\ *|npm\ *|pnpm\ *|docker\ *|kubectl\ *|pip\ *|ruff\ *|go\ *|curl\ *|grep\ *|rg\ *|find\ *|ls\ *|ls|cat\ *|aws\ *|helm\ *)
298 echo "{{\"command\":\"$LEAN_CTX_BIN -c $CMD\"}}" ;;
299 *) exit 0 ;;
300esac
301"#
302 );
303
304 write_file(&script_path, &script);
305 make_executable(&script_path);
306
307 let settings_path = home.join(".gemini").join("settings.json");
308 let settings_content = if settings_path.exists() {
309 std::fs::read_to_string(&settings_path).unwrap_or_default()
310 } else {
311 String::new()
312 };
313
314 if settings_content.contains("lean-ctx") {
315 println!("Gemini CLI hook already configured.");
316 } else {
317 let hook_config = serde_json::json!({
318 "hooks": {
319 "BeforeTool": [{
320 "command": script_path.to_string_lossy()
321 }]
322 }
323 });
324
325 if settings_content.is_empty() {
326 write_file(
327 &settings_path,
328 &serde_json::to_string_pretty(&hook_config).unwrap(),
329 );
330 } else if let Ok(mut existing) =
331 serde_json::from_str::<serde_json::Value>(&settings_content)
332 {
333 if let Some(obj) = existing.as_object_mut() {
334 obj.insert("hooks".to_string(), hook_config["hooks"].clone());
335 write_file(
336 &settings_path,
337 &serde_json::to_string_pretty(&existing).unwrap(),
338 );
339 }
340 }
341 println!("Installed Gemini CLI hook at {}", script_path.display());
342 }
343}
344
345fn install_codex_hook() {
346 let home = match dirs::home_dir() {
347 Some(h) => h,
348 None => {
349 eprintln!("Cannot resolve home directory");
350 return;
351 }
352 };
353
354 let codex_dir = home.join(".codex");
355 let _ = std::fs::create_dir_all(&codex_dir);
356
357 let agents_path = codex_dir.join("AGENTS.md");
358 let agents_content = "# Global Agent Instructions\n\n@LEAN-CTX.md\n";
359
360 let lean_ctx_md = codex_dir.join("LEAN-CTX.md");
361 let binary = resolve_binary_path();
362 let lean_ctx_content = format!(
363 r#"# lean-ctx — Token Optimization
364
365Prefix all shell commands with `{binary} -c` for compressed output:
366
367```bash
368{binary} -c git status # instead of: git status
369{binary} -c cargo test # instead of: cargo test
370{binary} -c ls src/ # instead of: ls src/
371```
372
373This saves 60-90% tokens per command. Works with: git, cargo, npm, pnpm, docker, kubectl, pip, ruff, go, curl, grep, find, ls, aws, helm, and 90+ more commands.
374"#
375 );
376
377 if agents_path.exists() {
378 let content = std::fs::read_to_string(&agents_path).unwrap_or_default();
379 if content.contains("lean-ctx") || content.contains("LEAN-CTX") {
380 println!("Codex AGENTS.md already configured.");
381 return;
382 }
383 }
384
385 write_file(&agents_path, agents_content);
386 write_file(&lean_ctx_md, &lean_ctx_content);
387 println!("Installed Codex instructions at {}", codex_dir.display());
388}
389
390fn install_windsurf_rules(global: bool) {
391 if global {
392 println!("Global mode: skipping project-local .windsurfrules (use without --global in a project).");
393 return;
394 }
395
396 let rules_path = PathBuf::from(".windsurfrules");
397 if rules_path.exists() {
398 let content = std::fs::read_to_string(&rules_path).unwrap_or_default();
399 if content.contains("lean-ctx") {
400 println!(".windsurfrules already configured.");
401 return;
402 }
403 }
404
405 let rules = include_str!("templates/windsurfrules.txt");
406 write_file(&rules_path, rules);
407 println!("Installed .windsurfrules in current project.");
408}
409
410fn install_cline_rules(global: bool) {
411 if global {
412 println!(
413 "Global mode: skipping project-local .clinerules (use without --global in a project)."
414 );
415 return;
416 }
417
418 let rules_path = PathBuf::from(".clinerules");
419 if rules_path.exists() {
420 let content = std::fs::read_to_string(&rules_path).unwrap_or_default();
421 if content.contains("lean-ctx") {
422 println!(".clinerules already configured.");
423 return;
424 }
425 }
426
427 let binary = resolve_binary_path();
428 let rules = format!(
429 r#"# lean-ctx Shell Optimization
430# Prefix all shell commands with `{binary} -c` for 60-90% token savings.
431
432When running shell commands, always prefix them with `{binary} -c` to compress output. For example:
433- `{binary} -c git status` instead of `git status`
434- `{binary} -c cargo test` instead of `cargo test`
435- `{binary} -c ls src/` instead of `ls src/`
436
437Supported commands: git, cargo, npm, pnpm, docker, kubectl, pip, ruff, go, curl, grep, find, ls, aws, helm, and 90+ more.
438"#
439 );
440
441 write_file(&rules_path, &rules);
442 println!("Installed .clinerules in current project.");
443}
444
445fn install_pi_hook(global: bool) {
446 let has_pi = std::process::Command::new("pi")
447 .arg("--version")
448 .output()
449 .is_ok();
450
451 if !has_pi {
452 println!("Pi Coding Agent not found in PATH.");
453 println!("Install Pi first: npm install -g @mariozechner/pi-coding-agent");
454 println!();
455 }
456
457 println!("Installing pi-lean-ctx Pi Package...");
458 println!();
459
460 let install_result = std::process::Command::new("pi")
461 .args(["install", "npm:pi-lean-ctx"])
462 .status();
463
464 match install_result {
465 Ok(status) if status.success() => {
466 println!("Installed pi-lean-ctx Pi Package.");
467 }
468 _ => {
469 println!("Could not auto-install pi-lean-ctx. Install manually:");
470 println!(" pi install npm:pi-lean-ctx");
471 println!();
472 }
473 }
474
475 if !global {
476 let agents_md = PathBuf::from("AGENTS.md");
477 if !agents_md.exists()
478 || !std::fs::read_to_string(&agents_md)
479 .unwrap_or_default()
480 .contains("lean-ctx")
481 {
482 let content = include_str!("templates/PI_AGENTS.md");
483 write_file(&agents_md, content);
484 println!("Created AGENTS.md in current project directory.");
485 } else {
486 println!("AGENTS.md already contains lean-ctx configuration.");
487 }
488 } else {
489 println!(
490 "Global mode: skipping project-local AGENTS.md (use without --global in a project)."
491 );
492 }
493
494 println!();
495 println!(
496 "Setup complete. All Pi tools (bash, read, grep, find, ls) now route through lean-ctx."
497 );
498 println!("Use /lean-ctx in Pi to verify the binary path.");
499}
500
501fn install_copilot_hook(global: bool) {
502 let binary = resolve_binary_path();
503
504 if global {
505 let mcp_path = copilot_global_mcp_path();
506 if mcp_path.as_os_str() == "/nonexistent" {
507 println!(" \x1b[2mVS Code not found — skipping global Copilot config\x1b[0m");
508 return;
509 }
510 write_vscode_mcp_file(&mcp_path, &binary, "global VS Code User MCP");
511 } else {
512 let vscode_dir = PathBuf::from(".vscode");
513 let _ = std::fs::create_dir_all(&vscode_dir);
514 let mcp_path = vscode_dir.join("mcp.json");
515 write_vscode_mcp_file(&mcp_path, &binary, ".vscode/mcp.json");
516 }
517}
518
519fn copilot_global_mcp_path() -> PathBuf {
520 if let Some(home) = dirs::home_dir() {
521 #[cfg(target_os = "macos")]
522 {
523 return home.join("Library/Application Support/Code/User/mcp.json");
524 }
525 #[cfg(target_os = "linux")]
526 {
527 return home.join(".config/Code/User/mcp.json");
528 }
529 #[cfg(target_os = "windows")]
530 {
531 if let Ok(appdata) = std::env::var("APPDATA") {
532 return PathBuf::from(appdata).join("Code/User/mcp.json");
533 }
534 }
535 #[allow(unreachable_code)]
536 home.join(".config/Code/User/mcp.json")
537 } else {
538 PathBuf::from("/nonexistent")
539 }
540}
541
542fn write_vscode_mcp_file(mcp_path: &PathBuf, binary: &str, label: &str) {
543 if mcp_path.exists() {
544 let content = std::fs::read_to_string(mcp_path).unwrap_or_default();
545 if content.contains("lean-ctx") {
546 println!(" \x1b[32m✓\x1b[0m Copilot already configured in {label}");
547 return;
548 }
549
550 if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
551 if let Some(obj) = json.as_object_mut() {
552 let servers = obj
553 .entry("servers")
554 .or_insert_with(|| serde_json::json!({}));
555 if let Some(servers_obj) = servers.as_object_mut() {
556 servers_obj.insert(
557 "lean-ctx".to_string(),
558 serde_json::json!({ "command": binary, "args": [] }),
559 );
560 }
561 write_file(
562 mcp_path,
563 &serde_json::to_string_pretty(&json).unwrap_or_default(),
564 );
565 println!(" \x1b[32m✓\x1b[0m Added lean-ctx to {label}");
566 return;
567 }
568 }
569 }
570
571 if let Some(parent) = mcp_path.parent() {
572 let _ = std::fs::create_dir_all(parent);
573 }
574
575 let config = serde_json::json!({
576 "servers": {
577 "lean-ctx": {
578 "command": binary,
579 "args": []
580 }
581 }
582 });
583
584 write_file(
585 mcp_path,
586 &serde_json::to_string_pretty(&config).unwrap_or_default(),
587 );
588 println!(" \x1b[32m✓\x1b[0m Created {label} with lean-ctx MCP server");
589}
590
591fn write_file(path: &PathBuf, content: &str) {
592 if let Err(e) = std::fs::write(path, content) {
593 eprintln!("Error writing {}: {e}", path.display());
594 }
595}
596
597#[cfg(unix)]
598fn make_executable(path: &PathBuf) {
599 use std::os::unix::fs::PermissionsExt;
600 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755));
601}
602
603#[cfg(not(unix))]
604fn make_executable(_path: &PathBuf) {}
605
606fn install_mcp_json_agent(name: &str, display_path: &str, config_path: &std::path::Path) {
607 let binary = resolve_binary_path();
608
609 if let Some(parent) = config_path.parent() {
610 let _ = std::fs::create_dir_all(parent);
611 }
612
613 if config_path.exists() {
614 let content = std::fs::read_to_string(config_path).unwrap_or_default();
615 if content.contains("lean-ctx") {
616 println!("{name} MCP already configured at {display_path}");
617 return;
618 }
619
620 if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
621 if let Some(obj) = json.as_object_mut() {
622 let servers = obj
623 .entry("mcpServers")
624 .or_insert_with(|| serde_json::json!({}));
625 if let Some(servers_obj) = servers.as_object_mut() {
626 servers_obj.insert(
627 "lean-ctx".to_string(),
628 serde_json::json!({ "command": binary }),
629 );
630 }
631 if let Ok(formatted) = serde_json::to_string_pretty(&json) {
632 let _ = std::fs::write(config_path, formatted);
633 println!(" \x1b[32m✓\x1b[0m {name} MCP configured at {display_path}");
634 return;
635 }
636 }
637 }
638 }
639
640 let content = serde_json::to_string_pretty(&serde_json::json!({
641 "mcpServers": {
642 "lean-ctx": {
643 "command": binary
644 }
645 }
646 }));
647
648 if let Ok(json_str) = content {
649 let _ = std::fs::write(config_path, json_str);
650 println!(" \x1b[32m✓\x1b[0m {name} MCP configured at {display_path}");
651 } else {
652 eprintln!(" \x1b[31m✗\x1b[0m Failed to configure {name}");
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659
660 #[test]
661 fn bash_path_unix_unchanged() {
662 assert_eq!(
663 to_bash_compatible_path("/usr/local/bin/lean-ctx"),
664 "/usr/local/bin/lean-ctx"
665 );
666 }
667
668 #[test]
669 fn bash_path_home_unchanged() {
670 assert_eq!(
671 to_bash_compatible_path("/home/user/.cargo/bin/lean-ctx"),
672 "/home/user/.cargo/bin/lean-ctx"
673 );
674 }
675
676 #[test]
677 fn bash_path_windows_drive_converted() {
678 assert_eq!(
679 to_bash_compatible_path("C:\\Users\\Fraser\\bin\\lean-ctx.exe"),
680 "/c/Users/Fraser/bin/lean-ctx.exe"
681 );
682 }
683
684 #[test]
685 fn bash_path_windows_lowercase_drive() {
686 assert_eq!(
687 to_bash_compatible_path("D:\\tools\\lean-ctx.exe"),
688 "/d/tools/lean-ctx.exe"
689 );
690 }
691
692 #[test]
693 fn bash_path_windows_forward_slashes() {
694 assert_eq!(
695 to_bash_compatible_path("C:/Users/Fraser/bin/lean-ctx.exe"),
696 "/c/Users/Fraser/bin/lean-ctx.exe"
697 );
698 }
699
700 #[test]
701 fn bash_path_bare_name_unchanged() {
702 assert_eq!(to_bash_compatible_path("lean-ctx"), "lean-ctx");
703 }
704}