1use anyhow::{Context, Result};
6use chrono::Local;
7use serde_json::{json, Value};
8use std::fs;
9use std::io::{self, Write as IoWrite};
10use std::path::{Path, PathBuf};
11
12use crate::scanner::{Scanner, ScannerConfig};
13use crate::TreeStats;
14
15const VALID_HOOK_KEYS: &[&str] = &[
17 "SessionStart",
18 "UserPromptSubmit",
19 "PreToolUse",
20 "PermissionRequest",
21 "PostToolUse",
22 "PostToolUseFailure",
23 "SubagentStart",
24 "SubagentStop",
25 "Stop",
26 "PreCompact",
27 "SessionEnd",
28 "Notification",
29 "Setup",
30];
31
32fn confirm_overwrite(path: &Path) -> bool {
34 print!(" ā ļø {} exists. Overwrite? [y/N]: ", path.display());
35 io::stdout().flush().unwrap();
36
37 let mut input = String::new();
38 if io::stdin().read_line(&mut input).is_ok() {
39 let response = input.trim().to_lowercase();
40 return response == "y" || response == "yes";
41 }
42 false
43}
44
45pub fn validate_settings(path: &Path) -> Result<Option<String>> {
48 if !path.exists() {
49 return Ok(None);
50 }
51
52 let content = fs::read_to_string(path)?;
53 let parsed: Result<Value, _> = serde_json::from_str(&content);
54
55 match parsed {
56 Err(e) => Ok(Some(format!("Invalid JSON: {}", e))),
57 Ok(json) => {
58 if let Some(hooks) = json.get("hooks") {
59 if let Some(obj) = hooks.as_object() {
60 for key in obj.keys() {
61 if !VALID_HOOK_KEYS.contains(&key.as_str()) {
62 return Ok(Some(format!(
63 "Invalid hook key '{}'. Valid: {}",
64 key,
65 VALID_HOOK_KEYS.join(", ")
66 )));
67 }
68 }
69 }
70 }
71 Ok(None)
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub enum ProjectType {
79 Rust,
80 Python,
81 JavaScript,
82 TypeScript,
83 Mixed,
84 Unknown,
85}
86
87pub struct ClaudeInit {
89 project_path: PathBuf,
90 project_type: ProjectType,
91 stats: TreeStats,
92}
93
94impl ClaudeInit {
95 pub fn new(project_path: PathBuf) -> Result<Self> {
97 let config = ScannerConfig {
99 max_depth: 3,
100 show_hidden: false,
101 follow_symlinks: false,
102 ..Default::default()
103 };
104
105 let scanner = Scanner::new(&project_path, config)?;
106 let (nodes, stats) = scanner.scan()?;
107
108 let project_type = Self::detect_project_type(&nodes, &stats);
110
111 Ok(Self {
112 project_path,
113 project_type,
114 stats,
115 })
116 }
117
118 fn detect_project_type(nodes: &[crate::FileNode], _stats: &TreeStats) -> ProjectType {
120 let mut rust_score = 0;
121 let mut python_score = 0;
122 let mut js_score = 0;
123 let mut ts_score = 0;
124
125 for node in nodes {
127 let path_str = node.path.to_string_lossy();
128
129 if path_str.contains("Cargo.toml") {
131 rust_score += 100;
132 }
133 if path_str.contains("package.json") {
134 js_score += 50;
135 ts_score += 30;
136 }
137 if path_str.contains("pyproject.toml") || path_str.contains("requirements.txt") {
138 python_score += 100;
139 }
140 if path_str.contains("tsconfig.json") {
141 ts_score += 100;
142 }
143
144 if path_str.ends_with(".rs") {
146 rust_score += 1;
147 }
148 if path_str.ends_with(".py") {
149 python_score += 1;
150 }
151 if path_str.ends_with(".js") || path_str.ends_with(".jsx") {
152 js_score += 1;
153 }
154 if path_str.ends_with(".ts") || path_str.ends_with(".tsx") {
155 ts_score += 1;
156 }
157 }
158
159 let max_score = rust_score.max(python_score).max(js_score).max(ts_score);
161
162 if max_score == 0 {
163 ProjectType::Unknown
164 } else if rust_score == max_score {
165 ProjectType::Rust
166 } else if python_score == max_score {
167 ProjectType::Python
168 } else if ts_score == max_score {
169 ProjectType::TypeScript
170 } else if js_score == max_score {
171 ProjectType::JavaScript
172 } else {
173 ProjectType::Mixed
174 }
175 }
176
177 pub fn setup(&self) -> Result<()> {
179 let claude_dir = self.project_path.join(".claude");
180
181 if claude_dir.exists() {
182 self.update_existing(&claude_dir)
184 } else {
185 self.init_new(&claude_dir)
187 }
188 }
189
190 fn init_new(&self, claude_dir: &Path) -> Result<()> {
192 fs::create_dir_all(claude_dir).context("Failed to create .claude directory")?;
194
195 self.create_settings_json(claude_dir, true)?;
197
198 self.create_claude_md(claude_dir, true)?;
200
201 println!(
202 "⨠Claude integration initialized for {:?} project!",
203 self.project_type
204 );
205 println!("š Created .claude/ directory with:");
206 println!(" ⢠settings.json - Smart hooks configured");
207 println!(" ⢠CLAUDE.md - Project-specific AI guidance");
208 println!("\nš” Tip: Run 'st --setup-claude' anytime to update");
209
210 Ok(())
211 }
212
213 fn update_existing(&self, claude_dir: &Path) -> Result<()> {
215 println!("š Checking existing Claude integration...");
216
217 let settings_path = claude_dir.join("settings.json");
218 let claude_md_path = claude_dir.join("CLAUDE.md");
219
220 let mut updated = false;
221
222 if settings_path.exists() {
224 if let Some(error) = validate_settings(&settings_path)? {
225 println!(" ā ļø settings.json has issues: {}", error);
226 println!(" š” Suggested fix:");
227 self.show_suggested()?;
228 return Ok(());
229 }
230
231 let existing: Value = serde_json::from_str(&fs::read_to_string(&settings_path)?)?;
233 let is_auto = existing
234 .get("smart_tree")
235 .and_then(|st| st.get("auto_configured"))
236 .and_then(|v| v.as_bool())
237 .unwrap_or(false);
238
239 if is_auto {
240 if self.create_settings_json(claude_dir, true)? {
242 println!(" ā
Updated settings.json");
243 updated = true;
244 }
245 } else {
246 println!(" ā¹ļø settings.json has manual configuration");
248 if self.create_settings_json(claude_dir, false)? {
249 println!(" ā
Updated settings.json");
250 updated = true;
251 } else {
252 println!(" āļø Skipped settings.json");
253 }
254 }
255 } else if self.create_settings_json(claude_dir, true)? {
256 println!(" ā
Created settings.json");
257 updated = true;
258 }
259
260 if claude_md_path.exists() {
262 if self.create_claude_md(claude_dir, false)? {
263 println!(" ā
Updated CLAUDE.md");
264 updated = true;
265 } else {
266 println!(" āļø Skipped CLAUDE.md");
267 }
268 } else if self.create_claude_md(claude_dir, true)? {
269 println!(" ā
Created CLAUDE.md");
270 updated = true;
271 }
272
273 if updated {
274 println!(
275 "\nš Claude integration updated for {:?} project!",
276 self.project_type
277 );
278 } else {
279 println!("\n⨠No changes made. Use --force to overwrite.");
280 }
281
282 Ok(())
283 }
284
285 fn create_settings_json(&self, claude_dir: &Path, force: bool) -> Result<bool> {
288 let settings_path = claude_dir.join("settings.json");
289
290 if settings_path.exists() && !force {
292 if !confirm_overwrite(&settings_path) {
293 return Ok(false);
294 }
295 let backup = settings_path.with_extension("json.bak");
297 fs::copy(&settings_path, &backup)?;
298 }
299
300 let hooks = match self.project_type {
304 ProjectType::Rust => {
305 json!({
306 "SessionStart": [{
307 "matcher": "",
308 "hooks": [{
309 "type": "command",
310 "command": "st --claude-restore"
311 }]
312 }],
313 "SessionEnd": [{
314 "matcher": "",
315 "hooks": [{
316 "type": "command",
317 "command": "st --claude-save"
318 }]
319 }],
320 "PreToolUse": [{
321 "matcher": "cargo (build|test|run)",
322 "hooks": [{
323 "type": "command",
324 "command": "st -m summary --depth 3 ."
325 }]
326 }]
327 })
328 }
329 ProjectType::Python => {
330 json!({
331 "SessionStart": [{
332 "matcher": "",
333 "hooks": [{
334 "type": "command",
335 "command": "st --claude-restore"
336 }]
337 }],
338 "SessionEnd": [{
339 "matcher": "",
340 "hooks": [{
341 "type": "command",
342 "command": "st --claude-save"
343 }]
344 }],
345 "PreToolUse": [{
346 "matcher": "pytest|python.*test",
347 "hooks": [{
348 "type": "command",
349 "command": "st -m summary --depth 3 ."
350 }]
351 }]
352 })
353 }
354 ProjectType::JavaScript | ProjectType::TypeScript => {
355 json!({
356 "SessionStart": [{
357 "matcher": "",
358 "hooks": [{
359 "type": "command",
360 "command": "st --claude-restore"
361 }]
362 }],
363 "SessionEnd": [{
364 "matcher": "",
365 "hooks": [{
366 "type": "command",
367 "command": "st --claude-save"
368 }]
369 }],
370 "PreToolUse": [{
371 "matcher": "npm (test|build|run)",
372 "hooks": [{
373 "type": "command",
374 "command": "st -m summary --depth 3 ."
375 }]
376 }]
377 })
378 }
379 _ => {
380 json!({
382 "SessionStart": [{
383 "matcher": "",
384 "hooks": [{
385 "type": "command",
386 "command": "st --claude-restore"
387 }]
388 }],
389 "SessionEnd": [{
390 "matcher": "",
391 "hooks": [{
392 "type": "command",
393 "command": "st --claude-save"
394 }]
395 }]
396 })
397 }
398 };
399
400 let settings = json!({
401 "hooks": hooks,
402 "smart_tree": {
403 "version": env!("CARGO_PKG_VERSION"),
404 "project_type": format!("{:?}", self.project_type),
405 "auto_configured": true,
406 "stats": {
407 "files": self.stats.total_files,
408 "directories": self.stats.total_dirs,
409 "size": self.stats.total_size
410 }
411 }
412 });
413
414 let content = serde_json::to_string_pretty(&settings)?;
415 fs::write(&settings_path, &content)?;
416
417 if let Some(error) = validate_settings(&settings_path)? {
419 let backup = settings_path.with_extension("json.bak");
421 if backup.exists() {
422 fs::copy(&backup, &settings_path)?;
423 fs::remove_file(&backup)?;
424 }
425 anyhow::bail!("Validation failed, reverted: {}", error);
426 }
427
428 Ok(true)
429 }
430
431 fn create_claude_md(&self, claude_dir: &Path, force: bool) -> Result<bool> {
434 let claude_md_path = claude_dir.join("CLAUDE.md");
435
436 if claude_md_path.exists() && !force && !confirm_overwrite(&claude_md_path) {
438 return Ok(false);
439 }
440
441 let content = match self.project_type {
442 ProjectType::Rust => {
443 format!(
444 r#"# CLAUDE.md
445
446This Rust project uses Smart Tree for optimal AI context management.
447
448## Project Stats
449- Files: {}
450- Directories: {}
451- Total size: {} bytes
452
453## Essential Commands
454
455```bash
456# Build & Test
457cargo build --release
458cargo test -- --nocapture
459cargo clippy -- -D warnings
460
461# Smart Tree context
462st -m context . # Full context with git info
463st -m quantum . # Compressed for large contexts
464st -m relations --focus main.rs # Code relationships
465```
466
467## Key Patterns
468- Always use `Result<T>` for error handling
469- Prefer `&str` over `String` for function parameters
470- Use `anyhow` for error context
471- Run clippy before commits
472
473## Smart Tree Integration
474This project has hooks configured to automatically provide context.
475The quantum-semantic mode is used for optimal token efficiency.
476"#,
477 self.stats.total_files, self.stats.total_dirs, self.stats.total_size
478 )
479 }
480 ProjectType::Python => {
481 format!(
482 r#"# CLAUDE.md
483
484This Python project uses Smart Tree for optimal AI context management.
485
486## Project Stats
487- Files: {}
488- Directories: {}
489- Total size: {} bytes
490
491## Essential Commands
492
493```bash
494# Environment & Testing
495uv sync # Install dependencies with uv
496pytest -v # Run tests
497ruff check . # Lint code
498mypy . # Type checking
499
500# Smart Tree context
501st -m context . # Full context with git info
502st -m quantum . # Compressed for large contexts
503```
504
505## Key Patterns
506- Use type hints for all functions
507- Prefer uv over pip for package management
508- Follow PEP 8 style guide
509- Write docstrings for all public functions
510
511## Smart Tree Integration
512Hooks provide automatic context on prompt submission.
513Test runs trigger summary of test directories.
514"#,
515 self.stats.total_files, self.stats.total_dirs, self.stats.total_size
516 )
517 }
518 ProjectType::TypeScript | ProjectType::JavaScript => {
519 format!(
520 r#"# CLAUDE.md
521
522This {0} project uses Smart Tree for optimal AI context management.
523
524## Project Stats
525- Files: {1}
526- Directories: {2}
527- Total size: {3} bytes
528
529## Essential Commands
530
531```bash
532# Development
533pnpm install # Install dependencies
534pnpm run dev # Start dev server
535pnpm test # Run tests
536pnpm build # Production build
537
538# Smart Tree context
539st -m context . # Full context with git info
540st -m quantum . # Compressed for large contexts
541```
542
543## Key Patterns
544- Use pnpm for package management
545- Implement proper TypeScript types
546- Follow ESLint rules
547- Component-based architecture
548
549## Smart Tree Integration
550Automatic context provision via hooks.
551Node_modules excluded from summaries.
552"#,
553 if matches!(self.project_type, ProjectType::TypeScript) {
554 "TypeScript"
555 } else {
556 "JavaScript"
557 },
558 self.stats.total_files,
559 self.stats.total_dirs,
560 self.stats.total_size
561 )
562 }
563 _ => {
564 format!(
565 r#"# CLAUDE.md
566
567This project uses Smart Tree for optimal AI context management.
568
569## Project Stats
570- Files: {}
571- Directories: {}
572- Total size: {} bytes
573- Type: {:?}
574
575## Smart Tree Commands
576
577```bash
578st -m context . # Full context with git info
579st -m quantum . # Compressed for large contexts
580st -m summary . # Human-readable summary
581st -m quantum-semantic . # Maximum compression
582```
583
584## Smart Tree Integration
585This project has been configured with automatic hooks that provide
586context to Claude on every prompt. The hook mode is optimized based
587on your project size.
588
589Use `st --help` to explore more features!
590"#,
591 self.stats.total_files,
592 self.stats.total_dirs,
593 self.stats.total_size,
594 self.project_type
595 )
596 }
597 };
598
599 fs::write(claude_md_path, content)?;
600
601 Ok(true)
602 }
603
604 pub fn show_suggested(&self) -> Result<()> {
606 println!(
607 "š Suggested Claude integration for {:?} project:\n",
608 self.project_type
609 );
610
611 let hooks = match self.project_type {
613 ProjectType::Rust => json!({
614 "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
615 "SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}],
616 "PreToolUse": [{"matcher": "cargo (build|test|run)", "hooks": [{"type": "command", "command": "st -m summary --depth 1 target/"}]}]
617 }),
618 ProjectType::Python => json!({
619 "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
620 "SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}],
621 "PreToolUse": [{"matcher": "pytest|python.*test", "hooks": [{"type": "command", "command": "st -m summary --depth 2 tests/"}]}]
622 }),
623 _ => json!({
624 "SessionStart": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-restore"}]}],
625 "SessionEnd": [{"matcher": "", "hooks": [{"type": "command", "command": "st --claude-save"}]}]
626 }),
627 };
628
629 let settings = json!({"hooks": hooks});
630 println!("āāā Add to .claude/settings.json āāā");
631 println!("{}\n", serde_json::to_string_pretty(&settings)?);
632
633 println!("š” Or run: st --setup-claude (will ask before overwriting)");
634 Ok(())
635 }
636}
637
638#[derive(Debug)]
644pub struct McpInstallResult {
645 pub success: bool,
646 pub config_path: PathBuf,
647 pub backup_path: Option<PathBuf>,
648 pub message: String,
649 pub was_update: bool,
650}
651
652pub struct McpInstaller {
655 st_binary_path: PathBuf,
657 custom_config_path: Option<PathBuf>,
659}
660
661impl McpInstaller {
662 pub fn new() -> Result<Self> {
664 let st_binary_path = Self::find_st_binary()?;
666
667 Ok(Self {
668 st_binary_path,
669 custom_config_path: None,
670 })
671 }
672
673 pub fn with_binary_path(path: PathBuf) -> Self {
675 Self {
676 st_binary_path: path,
677 custom_config_path: None,
678 }
679 }
680
681 pub fn with_config_path(mut self, path: PathBuf) -> Self {
683 self.custom_config_path = Some(path);
684 self
685 }
686
687 fn find_st_binary() -> Result<PathBuf> {
689 if let Ok(exe) = std::env::current_exe() {
691 if exe.file_name().map(|n| n == "st").unwrap_or(false) {
692 return Ok(exe);
693 }
694 }
695
696 let candidates = vec![
698 dirs::home_dir().map(|h| h.join(".cargo/bin/st")),
700 Some(PathBuf::from("/usr/local/bin/st")),
702 Some(PathBuf::from("/opt/homebrew/bin/st")),
704 ];
705
706 for candidate in candidates.into_iter().flatten() {
707 if candidate.exists() {
708 return Ok(candidate);
709 }
710 }
711
712 Ok(PathBuf::from("st"))
714 }
715
716 pub fn get_all_target_configs() -> Vec<(&'static str, PathBuf)> {
719 let mut paths = Vec::new();
720
721 #[cfg(target_os = "macos")]
723 if let Some(h) = dirs::home_dir() {
724 paths.push(("Claude Desktop", h.join("Library/Application Support/Claude/claude_desktop_config.json")));
725 }
726 #[cfg(target_os = "windows")]
727 if let Some(c) = dirs::config_dir() {
728 paths.push(("Claude Desktop", c.join("Claude/claude_desktop_config.json")));
729 }
730 #[cfg(target_os = "linux")]
731 if let Some(c) = dirs::config_dir() {
732 paths.push(("Claude Desktop", c.join("Claude/claude_desktop_config.json")));
733 }
734
735 if let Some(h) = dirs::home_dir() {
737 paths.push(("Antigravity", h.join(".gemini/antigravity/mcp_config.json")));
738 paths.push(("Gemini", h.join(".gemini/mcp_config.json")));
739 }
740
741 paths
742 }
743
744 pub fn install_all(&self) -> Result<Vec<McpInstallResult>> {
746 let targets = if let Some(custom) = &self.custom_config_path {
747 vec![("Custom", custom.clone())]
748 } else {
749 Self::get_all_target_configs()
750 };
751
752 if targets.is_empty() {
753 anyhow::bail!("No supported agent configurations found for this OS.");
754 }
755
756 let mut results = Vec::new();
757
758 for (agent_name, config_path) in targets {
759 if let Some(parent) = config_path.parent() {
761 if fs::create_dir_all(parent).is_err() {
762 continue; }
764 }
765
766 let (mut config, was_update) = if config_path.exists() {
768 if let Ok(content) = fs::read_to_string(&config_path) {
769 if let Ok(json_val) = serde_json::from_str::<Value>(&content) {
770 (json_val, true)
771 } else {
772 (json!({}), false)
773 }
774 } else {
775 (json!({}), false)
776 }
777 } else {
778 (json!({}), false)
779 };
780
781 let backup_path = if was_update {
783 let backup = config_path.with_extension(format!(
784 "json.backup.{}",
785 Local::now().format("%Y%m%d_%H%M%S")
786 ));
787 let _ = fs::copy(&config_path, &backup);
788 Some(backup)
789 } else {
790 None
791 };
792
793 let st_config = json!({
795 "command": self.st_binary_path.to_string_lossy(),
796 "args": ["--mcp"],
797 "env": {}
798 });
799
800 if config.get("mcpServers").is_none() {
802 config["mcpServers"] = json!({});
803 }
804
805 let already_installed = config["mcpServers"].get("smart-tree").is_some();
807
808 config["mcpServers"]["smart-tree"] = st_config;
810
811 if let Ok(formatted) = serde_json::to_string_pretty(&config) {
813 if fs::write(&config_path, formatted).is_err() {
814 continue; }
816 }
817
818 let message = if already_installed {
819 format!(
820 "⨠Updated Smart Tree MCP server in {}!\n\
821 š Config: {}\n\
822 š§ Binary: {}",
823 agent_name,
824 config_path.display(),
825 self.st_binary_path.display()
826 )
827 } else {
828 format!(
829 "š Smart Tree MCP server installed to {}!\n\
830 š Config: {}\n\
831 š§ Binary: {}",
832 agent_name,
833 config_path.display(),
834 self.st_binary_path.display()
835 )
836 };
837
838 results.push(McpInstallResult {
839 success: true,
840 config_path,
841 backup_path,
842 message,
843 was_update: already_installed,
844 });
845 }
846
847 Ok(results)
848 }
849
850 pub fn uninstall_all(&self) -> Result<Vec<McpInstallResult>> {
852 let targets = if let Some(custom) = &self.custom_config_path {
853 vec![("Custom", custom.clone())]
854 } else {
855 Self::get_all_target_configs()
856 };
857
858 let mut results = Vec::new();
859
860 for (agent_name, config_path) in targets {
861 if !config_path.exists() {
862 continue;
863 }
864
865 let content = match fs::read_to_string(&config_path) {
866 Ok(c) => c,
867 Err(_) => continue,
868 };
869
870 let mut config: Value = match serde_json::from_str(&content) {
871 Ok(c) => c,
872 Err(_) => continue,
873 };
874
875 let was_removed = if let Some(servers) = config.get_mut("mcpServers").and_then(|s| s.as_object_mut()) {
876 servers.remove("smart-tree").is_some()
877 } else {
878 false
879 };
880
881 if was_removed {
882 let backup = config_path.with_extension(format!(
883 "json.backup.{}",
884 Local::now().format("%Y%m%d_%H%M%S")
885 ));
886 let _ = fs::copy(&config_path, &backup);
887
888 if let Ok(formatted) = serde_json::to_string_pretty(&config) {
889 let _ = fs::write(&config_path, formatted);
890 }
891
892 results.push(McpInstallResult {
893 success: true,
894 config_path: config_path.clone(),
895 backup_path: Some(backup),
896 message: format!(
897 "šļø Removed Smart Tree MCP server from {}.\n\
898 š Config: {}",
899 agent_name,
900 config_path.display()
901 ),
902 was_update: true,
903 });
904 }
905 }
906
907 Ok(results)
908 }
909
910 pub fn is_installed(&self) -> Result<bool> {
912 let targets = if let Some(custom) = &self.custom_config_path {
913 vec![("Custom", custom.clone())]
914 } else {
915 Self::get_all_target_configs()
916 };
917
918 for (_, path) in targets {
919 if path.exists() {
920 if let Ok(content) = fs::read_to_string(&path) {
921 if let Ok(config) = serde_json::from_str::<Value>(&content) {
922 if config["mcpServers"].get("smart-tree").is_some() {
923 return Ok(true);
924 }
925 }
926 }
927 }
928 }
929
930 Ok(false)
931 }
932
933 pub fn status(&self) -> Result<Value> {
935 let targets = Self::get_all_target_configs();
936 let is_installed = self.is_installed().unwrap_or(false);
937
938 let paths: Vec<String> = targets.into_iter()
939 .map(|(_, p)| p.display().to_string())
940 .collect();
941
942 Ok(json!({
943 "installed": is_installed,
944 "config_paths": paths,
945 "binary_path": self.st_binary_path.display().to_string(),
946 "binary_exists": self.st_binary_path.exists(),
947 }))
948 }
949}
950
951impl Default for McpInstaller {
952 fn default() -> Self {
953 Self::new().unwrap_or_else(|_| Self {
954 st_binary_path: PathBuf::from("st"),
955 custom_config_path: None,
956 })
957 }
958}
959
960pub fn install_mcp_to_desktop() -> Result<String> {
963 let installer = McpInstaller::new()?;
964 let results = installer.install_all()?;
965 let msg = results.into_iter()
966 .filter(|r| r.success)
967 .map(|r| r.message)
968 .collect::<Vec<_>>()
969 .join("\n\n");
970 if msg.is_empty() {
971 Ok("Nothing to install or update.".to_string())
972 } else {
973 Ok(msg)
974 }
975}
976
977pub fn uninstall_mcp_from_desktop() -> Result<String> {
979 let installer = McpInstaller::new()?;
980 let results = installer.uninstall_all()?;
981 let msg = results.into_iter()
982 .filter(|r| r.success)
983 .map(|r| r.message)
984 .collect::<Vec<_>>()
985 .join("\n\n");
986 if msg.is_empty() {
987 Ok("No installations found to remove.".to_string())
988 } else {
989 Ok(msg)
990 }
991}
992
993pub fn check_mcp_installation_status() -> Result<String> {
995 let installer = McpInstaller::new()?;
996 let status = installer.status()?;
997
998 let installed = status["installed"].as_bool().unwrap_or(false);
999 let config_paths = status["config_paths"].as_array();
1000
1001 if installed {
1002 Ok(format!(
1003 "ā
Smart Tree MCP server is installed!\n\
1004 š Configs: {:?}\n\
1005 š§ Binary: {}",
1006 config_paths,
1007 status["binary_path"].as_str().unwrap_or("st")
1008 ))
1009 } else {
1010 Ok(format!(
1011 "ā Smart Tree MCP server is NOT installed.\n\
1012 š Expected configs: {:?}\n\
1013 š” Run 'st --mcp-install' to install",
1014 config_paths
1015 ))
1016 }
1017}