1pub mod commands;
3pub mod config;
4pub mod output;
5pub mod platform;
6
7use anyhow::Result;
8use dialoguer::Select;
9use std::path::{Path, PathBuf};
10
11use crate::platform::Platform;
12
13#[allow(async_fn_in_trait)]
15pub trait Execute {
16 async fn execute(&self) -> Result<()>;
17}
18
19pub fn command_exists(cmd: &str) -> bool {
21 which::which(cmd).is_ok()
22}
23
24pub fn home_dir() -> Result<PathBuf> {
26 directories::BaseDirs::new()
27 .map(|dirs| dirs.home_dir().to_path_buf())
28 .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))
29}
30
31pub fn config_dir() -> Result<PathBuf> {
33 let home = home_dir()?;
34 Ok(home.join(".kindlyguard"))
35}
36
37pub fn ensure_dir(path: &Path) -> Result<()> {
39 if !path.exists() {
40 std::fs::create_dir_all(path)?;
41 }
42 Ok(())
43}
44
45pub fn get_mcp_config_path() -> Result<PathBuf> {
47 let home = home_dir()?;
48
49 let candidates = vec![home.join(".mcp.json"), home.join(".config/claude/mcp.json")];
51
52 for path in &candidates {
53 if path.exists() {
54 return Ok(path.clone());
55 }
56 }
57
58 Ok(home.join(".mcp.json"))
60}
61
62pub async fn download_file(url: &str, dest: &Path) -> Result<()> {
64 use futures_util::StreamExt;
65 use indicatif::{ProgressBar, ProgressStyle};
66 use std::io::Write;
67
68 let client = reqwest::Client::new();
70
71 let response = client
73 .get(url)
74 .send()
75 .await?;
76
77 if !response.status().is_success() {
79 anyhow::bail!("Failed to download: HTTP {}", response.status());
80 }
81
82 let total_size = response
84 .content_length()
85 .unwrap_or(0);
86
87 let pb = ProgressBar::new(total_size);
89 pb.set_style(
90 ProgressStyle::default_bar()
91 .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")?
92 .progress_chars("#>-")
93 );
94 pb.set_message(format!("Downloading {}", dest.file_name().unwrap_or_default().to_string_lossy()));
95
96 let mut file = std::fs::File::create(dest)?;
98
99 let mut downloaded = 0u64;
101 let mut stream = response.bytes_stream();
102
103 while let Some(chunk) = stream.next().await {
104 let chunk = chunk?;
105 file.write_all(&chunk)?;
106 downloaded += chunk.len() as u64;
107 pb.set_position(downloaded);
108 }
109
110 pb.finish_with_message("Download complete");
111
112 Ok(())
113}
114
115pub fn get_kindlyguard_download_url(version: &str, platform: &Platform) -> String {
117 let version_tag = if version == "latest" {
118 "latest".to_string()
119 } else {
120 format!("v{}", version.trim_start_matches('v'))
121 };
122
123 let (os, arch, ext) = match platform {
124 Platform::Windows => ("pc-windows-msvc", "x86_64", "exe"),
125 Platform::MacOS => {
126 if std::env::consts::ARCH == "aarch64" {
127 ("apple-darwin", "aarch64", "")
128 } else {
129 ("apple-darwin", "x86_64", "")
130 }
131 },
132 Platform::Linux => ("unknown-linux-gnu", "x86_64", ""),
133 Platform::Unknown => {
134 return format!("https://github.com/kindly-software-inc/kindly-guard/releases/{}", version_tag);
135 }
136 };
137
138 let filename = if ext.is_empty() {
139 format!("kindlyguard-{}-{}", arch, os)
140 } else {
141 format!("kindlyguard-{}-{}.{}", arch, os, ext)
142 };
143
144 if version_tag == "latest" {
145 format!(
146 "https://github.com/kindly-software-inc/kindly-guard/releases/latest/download/{}",
147 filename
148 )
149 } else {
150 format!(
151 "https://github.com/kindly-software-inc/kindly-guard/releases/download/{}/{}",
152 version_tag, filename
153 )
154 }
155}
156
157pub async fn install_kindlyguard_from_github(version: &str, platform: &Platform) -> Result<()> {
159 use colored::Colorize;
160
161 println!("š¦ {}", "Downloading KindlyGuard from GitHub releases...".green());
162
163 let url = get_kindlyguard_download_url(version, platform);
165 println!("š URL: {}", url.cyan());
166
167 let install_dir = match platform {
169 Platform::Windows => {
170 let home = home_dir()?;
172 home.join(".kindlyguard").join("bin")
173 },
174 _ => {
175 let home = home_dir()?;
177 let local_bin = home.join(".local").join("bin");
178 if local_bin.exists() {
179 local_bin
180 } else {
181 home.join(".cargo").join("bin")
182 }
183 }
184 };
185
186 ensure_dir(&install_dir)?;
188
189 let binary_name = if platform == &Platform::Windows {
191 "kindlyguard.exe"
192 } else {
193 "kindlyguard"
194 };
195
196 let dest_path = install_dir.join(binary_name);
197 let temp_path = dest_path.with_extension("tmp");
198
199 download_file(&url, &temp_path).await?;
201
202 #[cfg(unix)]
204 {
205 use std::os::unix::fs::PermissionsExt;
206 let mut perms = std::fs::metadata(&temp_path)?.permissions();
207 perms.set_mode(0o755);
208 std::fs::set_permissions(&temp_path, perms)?;
209 }
210
211 std::fs::rename(&temp_path, &dest_path)?;
213
214 println!("\nā
{}", "Installation successful!".green().bold());
215 println!("š {}: {}", "Binary installed to".cyan(), dest_path.display());
216
217 let path_var = std::env::var("PATH").unwrap_or_default();
219 if !path_var.contains(&install_dir.to_string_lossy().to_string()) {
220 println!(
221 "\nā ļø {}",
222 "Installation directory is not in your PATH!".yellow()
223 );
224 println!("š {}", "Add this to your shell configuration:".cyan());
225
226 match platform {
227 Platform::Windows => {
228 println!(
229 " {} $env:Path += \";{}\"",
230 "$".dimmed(),
231 install_dir.display()
232 );
233 },
234 _ => {
235 println!(
236 " {} export PATH=\"$PATH:{}\"",
237 "$".dimmed(),
238 install_dir.display()
239 );
240 }
241 }
242 }
243
244 Ok(())
245}
246
247pub fn detect_environment() -> EnvironmentInfo {
249 use std::env;
250 use std::path::Path;
251
252 EnvironmentInfo {
253 is_docker: Path::new("/.dockerenv").exists(),
254 is_wsl: detect_wsl(),
255 is_ci: env::var("CI").is_ok() || env::var("CONTINUOUS_INTEGRATION").is_ok(),
256 is_ssh: env::var("SSH_CONNECTION").is_ok() || env::var("SSH_CLIENT").is_ok(),
257 has_proxy: env::var("HTTP_PROXY").is_ok()
258 || env::var("HTTPS_PROXY").is_ok()
259 || env::var("http_proxy").is_ok()
260 || env::var("https_proxy").is_ok(),
261 }
262}
263
264fn detect_wsl() -> bool {
266 #[cfg(target_os = "linux")]
267 {
268 if let Ok(content) = std::fs::read_to_string("/proc/version") {
269 return content.to_lowercase().contains("microsoft");
270 }
271 }
272 false
273}
274
275pub fn detect_linux_distro() -> LinuxDistro {
277 #[cfg(target_os = "linux")]
278 {
279 if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
281 for line in content.lines() {
282 if line.starts_with("ID=") {
283 let id = line.trim_start_matches("ID=").trim_matches('"');
284 return match id {
285 "ubuntu" => LinuxDistro::Ubuntu,
286 "debian" => LinuxDistro::Debian,
287 "fedora" => LinuxDistro::Fedora,
288 "centos" => LinuxDistro::CentOS,
289 "rhel" => LinuxDistro::RHEL,
290 "arch" => LinuxDistro::Arch,
291 "manjaro" => LinuxDistro::Manjaro,
292 "opensuse" | "opensuse-leap" | "opensuse-tumbleweed" | "suse" => {
293 LinuxDistro::OpenSUSE
294 },
295 "alpine" => LinuxDistro::Alpine,
296 "nixos" => LinuxDistro::NixOS,
297 "gentoo" => LinuxDistro::Gentoo,
298 "void" => LinuxDistro::Void,
299 "elementary" => LinuxDistro::Elementary,
300 "pop" => LinuxDistro::PopOS,
301 "mint" | "linuxmint" => LinuxDistro::Mint,
302 _ => LinuxDistro::Unknown(id.to_string()),
303 };
304 }
305 }
306 }
307
308 if Path::new("/etc/debian_version").exists() {
310 return LinuxDistro::Debian;
311 }
312 if Path::new("/etc/redhat-release").exists() {
313 return LinuxDistro::RHEL;
314 }
315 if Path::new("/etc/arch-release").exists() {
316 return LinuxDistro::Arch;
317 }
318 if Path::new("/etc/gentoo-release").exists() {
319 return LinuxDistro::Gentoo;
320 }
321 if Path::new("/etc/alpine-release").exists() {
322 return LinuxDistro::Alpine;
323 }
324 }
325
326 LinuxDistro::Unknown("generic".to_string())
327}
328
329pub fn detect_node_managers() -> NodeManagers {
331 use std::path::Path;
332
333 let home = home_dir().unwrap_or_default();
334
335 NodeManagers {
336 has_nvm: Path::new(&home.join(".nvm")).exists() || std::env::var("NVM_DIR").is_ok(),
337 has_fnm: command_exists("fnm")
338 || Path::new(&home.join(".fnm")).exists()
339 || Path::new(&home.join(".local/share/fnm")).exists(),
340 has_n: command_exists("n")
341 || Path::new("/usr/local/n").exists()
342 || Path::new("/usr/local/bin/n").exists(),
343 has_volta: command_exists("volta") || Path::new(&home.join(".volta")).exists(),
344 has_asdf: command_exists("asdf") || Path::new(&home.join(".asdf")).exists(),
345 }
346}
347
348#[derive(Debug, Clone)]
350pub struct EnvironmentInfo {
351 pub is_docker: bool,
352 pub is_wsl: bool,
353 pub is_ci: bool,
354 pub is_ssh: bool,
355 pub has_proxy: bool,
356}
357
358#[derive(Debug, Clone, PartialEq)]
360pub enum LinuxDistro {
361 Ubuntu,
362 Debian,
363 Fedora,
364 CentOS,
365 RHEL,
366 Arch,
367 Manjaro,
368 OpenSUSE,
369 Alpine,
370 NixOS,
371 Gentoo,
372 Void,
373 Elementary,
374 PopOS,
375 Mint,
376 Unknown(String),
377}
378
379impl LinuxDistro {
380 pub fn display_name(&self) -> String {
382 match self {
383 LinuxDistro::Ubuntu => "š Ubuntu".to_string(),
384 LinuxDistro::Debian => "š“ Debian".to_string(),
385 LinuxDistro::Fedora => "šµ Fedora".to_string(),
386 LinuxDistro::CentOS => "š£ CentOS".to_string(),
387 LinuxDistro::RHEL => "š“ Red Hat Enterprise Linux".to_string(),
388 LinuxDistro::Arch => "š· Arch Linux".to_string(),
389 LinuxDistro::Manjaro => "š¢ Manjaro".to_string(),
390 LinuxDistro::OpenSUSE => "š¦ openSUSE".to_string(),
391 LinuxDistro::Alpine => "šļø Alpine Linux".to_string(),
392 LinuxDistro::NixOS => "āļø NixOS".to_string(),
393 LinuxDistro::Gentoo => "š£ Gentoo".to_string(),
394 LinuxDistro::Void => "š Void Linux".to_string(),
395 LinuxDistro::Elementary => "š¦ elementary OS".to_string(),
396 LinuxDistro::PopOS => "š Pop!_OS".to_string(),
397 LinuxDistro::Mint => "šæ Linux Mint".to_string(),
398 LinuxDistro::Unknown(name) => format!("š§ Linux ({})", name),
399 }
400 }
401
402 pub fn package_manager(&self) -> &'static str {
404 match self {
405 LinuxDistro::Ubuntu
406 | LinuxDistro::Debian
407 | LinuxDistro::Elementary
408 | LinuxDistro::PopOS
409 | LinuxDistro::Mint => "apt",
410 LinuxDistro::Fedora | LinuxDistro::CentOS | LinuxDistro::RHEL => "dnf",
411 LinuxDistro::Arch | LinuxDistro::Manjaro => "pacman",
412 LinuxDistro::OpenSUSE => "zypper",
413 LinuxDistro::Alpine => "apk",
414 LinuxDistro::NixOS => "nix-env",
415 LinuxDistro::Gentoo => "emerge",
416 LinuxDistro::Void => "xbps-install",
417 LinuxDistro::Unknown(_) => "apt", }
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct NodeManagers {
425 pub has_nvm: bool,
426 pub has_fnm: bool,
427 pub has_n: bool,
428 pub has_volta: bool,
429 pub has_asdf: bool,
430}
431
432impl NodeManagers {
433 pub fn recommended(&self) -> Option<&'static str> {
435 if self.has_volta {
436 Some("volta")
437 } else if self.has_fnm {
438 Some("fnm")
439 } else if self.has_nvm {
440 Some("nvm")
441 } else if self.has_n {
442 Some("n")
443 } else if self.has_asdf {
444 Some("asdf")
445 } else {
446 None
447 }
448 }
449
450 pub fn has_any(&self) -> bool {
452 self.has_nvm || self.has_fnm || self.has_n || self.has_volta || self.has_asdf
453 }
454}
455
456#[derive(Debug, Clone, Copy, PartialEq)]
458pub enum RecoveryMethod {
459 TryWithSudo,
460 InstallToHome,
461 UseDifferentPackageManager,
462 DownloadBinary,
463 OfflineInstallation,
464 ShowDiagnostics,
465 Cancel,
466}
467
468pub fn show_recovery_menu(failed_method: &str) -> Result<RecoveryMethod> {
470 use colored::*;
471
472 println!(
473 "\nšØ {}",
474 format!("Installation failed using {}", failed_method)
475 .red()
476 .bold()
477 );
478 println!("š {}", "Let's try a different approach...".yellow());
479
480 let options = vec![
481 format!("š Try with sudo privileges"),
482 format!("š Install to home directory (~/.local)"),
483 format!("š¦ Use different package manager"),
484 format!("šæ Download binary directly"),
485 format!("š“ Offline installation"),
486 format!("š Show diagnostics"),
487 format!("ā Cancel installation"),
488 ];
489
490 let selection = Select::new()
491 .with_prompt("Select recovery method")
492 .items(&options)
493 .default(0)
494 .interact()?;
495
496 let method = match selection {
497 0 => RecoveryMethod::TryWithSudo,
498 1 => RecoveryMethod::InstallToHome,
499 2 => RecoveryMethod::UseDifferentPackageManager,
500 3 => RecoveryMethod::DownloadBinary,
501 4 => RecoveryMethod::OfflineInstallation,
502 5 => RecoveryMethod::ShowDiagnostics,
503 _ => RecoveryMethod::Cancel,
504 };
505
506 Ok(method)
507}
508
509pub async fn execute_recovery(
511 method: RecoveryMethod,
512 original_method: &str,
513 package: &str,
514 platform: &crate::platform::Platform,
515) -> Result<()> {
516 use colored::*;
517
518 match method {
519 RecoveryMethod::TryWithSudo => {
520 println!("\nš {}", "Retrying with sudo privileges...".cyan());
521 match original_method {
522 "npm" => {
523 println!("š {}", "Command:".cyan());
524 println!(
525 " {} sudo npm install -g {}",
526 "$".dimmed(),
527 package.bright_white()
528 );
529 println!(
530 "\nā ļø {}",
531 "This will install globally with root privileges".yellow()
532 );
533 },
534 "cargo" => {
535 println!("š {}", "Command:".cyan());
536 println!(
537 " {} sudo cargo install {}",
538 "$".dimmed(),
539 package.bright_white()
540 );
541 println!(
542 "\nā ļø {}",
543 "Note: Using sudo with cargo is not recommended".yellow()
544 );
545 println!(
546 "š” {}",
547 "Consider using --root ~/.local/cargo instead".yellow()
548 );
549 },
550 _ => return Err(anyhow::anyhow!("Sudo not applicable for this method")),
551 }
552 },
553 RecoveryMethod::InstallToHome => {
554 println!("\nš {}", "Installing to home directory...".cyan());
555 match original_method {
556 "npm" => {
557 println!("š {}", "Commands:".cyan());
558 println!(" {} mkdir -p ~/.local/npm", "$".dimmed());
559 println!(" {} npm config set prefix ~/.local/npm", "$".dimmed());
560 println!(
561 " {} npm install -g {}",
562 "$".dimmed(),
563 package.bright_white()
564 );
565 println!("\nš” {}", "Add to PATH:".yellow());
566 println!(" {} export PATH=$HOME/.local/npm/bin:$PATH", "$".dimmed());
567 },
568 "cargo" => {
569 println!("š {}", "Command:".cyan());
570 println!(
571 " {} cargo install --root ~/.local/cargo {}",
572 "$".dimmed(),
573 package.bright_white()
574 );
575 println!("\nš” {}", "Add to PATH:".yellow());
576 println!(
577 " {} export PATH=$HOME/.local/cargo/bin:$PATH",
578 "$".dimmed()
579 );
580 },
581 _ => {
582 println!("š {}", "Manual installation to ~/.local/bin:".cyan());
583 println!(" 1ļøā£ Download the binary");
584 println!(" 2ļøā£ {} mkdir -p ~/.local/bin", "$".dimmed());
585 println!(" 3ļøā£ {} mv kindlyguard ~/.local/bin/", "$".dimmed());
586 println!(" 4ļøā£ {} chmod +x ~/.local/bin/kindlyguard", "$".dimmed());
587 },
588 }
589 },
590 RecoveryMethod::UseDifferentPackageManager => {
591 println!("\nš¦ {}", "Alternative package managers:".cyan());
592 match platform {
593 crate::platform::Platform::MacOS => {
594 println!("šŗ {}", "Homebrew:".green());
595 println!(" {} brew tap kindly-software-inc/tap", "$".dimmed());
596 println!(" {} brew install kindlyguard", "$".dimmed());
597 println!("\nš {}", "MacPorts:".green());
598 println!(" {} sudo port install kindlyguard", "$".dimmed());
599 },
600 crate::platform::Platform::Linux => {
601 println!("š¦ {}", "Snap:".green());
602 println!(" {} sudo snap install kindlyguard", "$".dimmed());
603 println!("\nš¦ {}", "Flatpak:".green());
604 println!(
605 " {} flatpak install flathub com.kindly.guard",
606 "$".dimmed()
607 );
608 println!("\nš¦ {}", "AppImage:".green());
609 println!(" Download from releases page");
610 },
611 crate::platform::Platform::Windows => {
612 println!("š« {}", "Chocolatey:".green());
613 println!(" {} choco install kindlyguard", "$".dimmed());
614 println!("\nš· {}", "Scoop:".green());
615 println!(" {} scoop install kindlyguard", "$".dimmed());
616 println!("\nš¶ {}", "WinGet:".green());
617 println!(
618 " {} winget install KindlySoftware.KindlyGuard",
619 "$".dimmed()
620 );
621 },
622 _ => {},
623 }
624 },
625 RecoveryMethod::DownloadBinary => {
626 println!("\nšæ {}", "Direct binary download:".cyan());
627 println!("š {}", "Visit:".cyan());
628 println!(
629 " {}",
630 "https://github.com/kindly-software-inc/kindly-guard/releases"
631 .blue()
632 .underline()
633 );
634
635 let arch = crate::platform::Architecture::detect();
636 match platform {
637 crate::platform::Platform::MacOS => {
638 let arch_str = if arch == crate::platform::Architecture::Arm64 {
639 "aarch64"
640 } else {
641 "x86_64"
642 };
643 println!(
644 "\nš {}",
645 format!(
646 "Download: kindly-guard-server-{}-apple-darwin.tar.gz",
647 arch_str
648 )
649 .bright_white()
650 );
651 },
652 crate::platform::Platform::Linux => {
653 println!(
654 "\nš§ {}",
655 "Download: kindly-guard-server-x86_64-unknown-linux-gnu.tar.gz"
656 .bright_white()
657 );
658 },
659 crate::platform::Platform::Windows => {
660 println!(
661 "\nšŖ {}",
662 "Download: kindly-guard-server-x86_64-pc-windows-msvc.zip".bright_white()
663 );
664 },
665 _ => {},
666 }
667
668 println!("\nš {}", "Manual installation steps:".cyan());
669 println!(" 1ļøā£ Download the appropriate file");
670 println!(" 2ļøā£ Extract the archive");
671 println!(" 3ļøā£ Move binary to PATH location");
672 println!(" 4ļøā£ Make it executable (Unix/Linux/macOS)");
673 },
674 RecoveryMethod::OfflineInstallation => {
675 println!("\nš“ {}", "Offline installation:".cyan());
676 println!("š” {}", "For offline environments:".yellow());
677 println!("\nš {}", "Steps:".cyan());
678 println!(" 1ļøā£ Download on a connected machine:");
679 println!(" - Binary from GitHub releases");
680 println!(
681 " - Or npm package: {} npm pack kindly-guard-server",
682 "$".dimmed()
683 );
684 println!(" 2ļøā£ Transfer to target machine via USB/network");
685 println!(" 3ļøā£ Install locally:");
686 println!(" - Binary: Copy to /usr/local/bin/");
687 println!(
688 " - npm: {} npm install -g kindly-guard-server-*.tgz",
689 "$".dimmed()
690 );
691 },
692 RecoveryMethod::ShowDiagnostics => {
693 println!("\nš {}", "Running diagnostics...".cyan());
694
695 println!("\nš¾ {}", "Disk space:".yellow());
697 #[cfg(not(target_os = "windows"))]
698 {
699 if let Ok(output) = std::process::Command::new("df").args(["-h", "."]).output() {
700 println!("{}", String::from_utf8_lossy(&output.stdout));
701 }
702 }
703
704 println!("\nš {}", "Permissions:".yellow());
706 match original_method {
707 "npm" => {
708 if let Ok(output) = std::process::Command::new("npm")
709 .args(["config", "get", "prefix"])
710 .output()
711 {
712 let prefix = String::from_utf8_lossy(&output.stdout).trim().to_string();
713 println!(" npm prefix: {}", prefix);
714
715 #[cfg(not(target_os = "windows"))]
716 {
717 if let Ok(output) = std::process::Command::new("ls")
718 .args(["-ld", &prefix])
719 .output()
720 {
721 println!(" {}", String::from_utf8_lossy(&output.stdout).trim());
722 }
723 }
724 }
725 },
726 "cargo" => {
727 if let Ok(home) = home_dir() {
728 let cargo_home = home.join(".cargo");
729 println!(" CARGO_HOME: {:?}", cargo_home);
730
731 #[cfg(not(target_os = "windows"))]
732 {
733 if let Ok(output) = std::process::Command::new("ls")
734 .args(["-ld", cargo_home.to_str().unwrap_or("")])
735 .output()
736 {
737 println!(" {}", String::from_utf8_lossy(&output.stdout).trim());
738 }
739 }
740 }
741 },
742 _ => {},
743 }
744
745 println!("\nš {}", "Network connectivity:".yellow());
747 #[cfg(not(target_os = "windows"))]
748 {
749 if let Ok(output) = std::process::Command::new("ping")
750 .args(["-c", "1", "-W", "2", "8.8.8.8"])
751 .output()
752 {
753 if output.status.success() {
754 println!(" ā
Internet connection OK");
755 } else {
756 println!(" ā No internet connection");
757 }
758 }
759 }
760
761 let env_info = detect_environment();
763 println!("\nš {}", "Environment detection:".yellow());
764 if env_info.is_docker {
765 println!(" š³ Running in Docker container");
766 }
767 if env_info.is_wsl {
768 println!(" šŖ Running in WSL");
769 }
770 if env_info.is_ci {
771 println!(" š¤ Running in CI/CD environment");
772 }
773 if env_info.is_ssh {
774 println!(" š Connected via SSH");
775 }
776 if env_info.has_proxy {
777 println!(" š Proxy detected:");
778 if let Ok(http_proxy) =
779 std::env::var("HTTP_PROXY").or_else(|_| std::env::var("http_proxy"))
780 {
781 println!(" HTTP_PROXY: {}", http_proxy);
782 }
783 if let Ok(https_proxy) =
784 std::env::var("HTTPS_PROXY").or_else(|_| std::env::var("https_proxy"))
785 {
786 println!(" HTTPS_PROXY: {}", https_proxy);
787 }
788
789 match original_method {
791 "npm" => {
792 println!("\n š” {}", "Configure npm for proxy:".yellow());
793 println!(" {} npm config set proxy $HTTP_PROXY", "$".dimmed());
794 println!(
795 " {} npm config set https-proxy $HTTPS_PROXY",
796 "$".dimmed()
797 );
798 },
799 "cargo" => {
800 println!(
801 "\n š” {}",
802 "Cargo uses system proxy automatically".yellow()
803 );
804 },
805 _ => {},
806 }
807 }
808
809 if original_method == "npm" {
811 let node_managers = detect_node_managers();
812 if node_managers.has_any() {
813 println!("\nš {}", "Node.js version managers detected:".yellow());
814 if node_managers.has_nvm {
815 println!(" ā
nvm - Try: nvm install --lts");
816 }
817 if node_managers.has_fnm {
818 println!(" ā
fnm - Try: fnm install --lts");
819 }
820 if node_managers.has_n {
821 println!(" ā
n - Try: n lts");
822 }
823 if node_managers.has_volta {
824 println!(" ā
volta - Try: volta install node");
825 }
826 if node_managers.has_asdf {
827 println!(" ā
asdf - Try: asdf install nodejs latest");
828 }
829 }
830 }
831
832 println!("\nš {}", "Proxy settings:".yellow());
834 for var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"] {
835 if let Ok(value) = std::env::var(var) {
836 println!(" {}: {}", var, value);
837 }
838 }
839
840 println!("\nš” {}", "Common fixes:".cyan());
841 println!(" - Free up disk space (200MB needed)");
842 println!(" - Check file permissions");
843 println!(" - Configure proxy if behind firewall");
844 println!(" - Try different installation method");
845 },
846 RecoveryMethod::Cancel => {
847 println!("\nā {}", "Installation cancelled".red());
848 return Err(anyhow::anyhow!("Installation cancelled by user"));
849 },
850 }
851
852 Ok(())
853}
854
855pub mod dev {
856 use super::*;
857 use clap::Subcommand;
858
859 #[derive(clap::Args)]
860 pub struct DevCommand {
861 #[command(subcommand)]
862 command: DevSubcommands,
863 }
864
865 #[derive(Subcommand)]
866 enum DevSubcommands {
867 Setup {
869 #[arg(long)]
871 skip_rust: bool,
872 },
873 Audit,
875 Docs {
877 #[arg(long)]
879 open: bool,
880 },
881 }
882
883 impl Execute for DevCommand {
884 async fn execute(&self) -> Result<()> {
885 match &self.command {
886 DevSubcommands::Setup { skip_rust } => setup_dev_env(*skip_rust).await,
887 DevSubcommands::Audit => run_security_audit().await,
888 DevSubcommands::Docs { open } => generate_docs(*open).await,
889 }
890 }
891 }
892
893 async fn setup_dev_env(skip_rust: bool) -> Result<()> {
894 use crate::platform::Platform;
895 use colored::*;
896
897 println!(
898 "\nš {}",
899 "Setting up development environment...".bold().blue()
900 );
901
902 let platform = Platform::detect();
903
904 let platform_display = match platform {
906 Platform::MacOS => format!("š Platform: {}", platform),
907 Platform::Windows => format!("šŖ Platform: {}", platform),
908 Platform::Linux => format!("š§ Platform: {}", platform),
909 _ => format!("š„ļø Platform: {}", platform),
910 };
911 println!("{}", platform_display.cyan());
912
913 println!("\nš {}", "Checking system dependencies...".cyan());
915 check_basic_dev_deps(&platform)?;
916
917 if !skip_rust {
918 println!("\nš¦ {}", "Installing Rust development tools...".cyan());
919
920 let tools = [
921 ("cargo-audit", "š”ļø Security vulnerability scanner"),
922 ("cargo-geiger", "ā¢ļø Unsafe code detector"),
923 ("cargo-dist", "š¦ Distribution packaging tool"),
924 ];
925
926 for (tool, description) in &tools {
927 println!(
928 "\n {} {}: {}",
929 "ā¢".dimmed(),
930 tool.bright_white(),
931 description
932 );
933
934 if command_exists(tool) {
936 println!(" ā
{}", "Already installed".green());
937 } else {
938 println!(" ā³ {}", "Installing...".yellow());
939
940 let status = std::process::Command::new("cargo")
941 .args(["install", tool])
942 .status()?;
943
944 if status.success() {
945 println!(" ā
{}", "Installed successfully!".green());
946 } else {
947 println!(" ā {}", "Installation failed".red());
948 println!(
949 " š” {}",
950 format!("Try: cargo install {} --force", tool).yellow()
951 );
952 }
953 }
954 }
955 } else {
956 println!(
957 "\nāļø {}",
958 "Skipping Rust tools installation (--skip-rust)".dimmed()
959 );
960 }
961
962 println!(
963 "\n⨠{}",
964 "Development environment setup complete!".bold().green()
965 );
966 println!("šÆ {}", "You're ready to start developing!".green());
967
968 Ok(())
969 }
970
971 async fn run_security_audit() -> Result<()> {
972 tracing::info!("Running security audit...");
973
974 let output = std::process::Command::new("cargo")
975 .args(["audit"])
976 .output()?;
977
978 if !output.status.success() {
979 anyhow::bail!("Security audit failed");
980 }
981
982 tracing::info!("Security audit passed!");
983 Ok(())
984 }
985
986 async fn generate_docs(open: bool) -> Result<()> {
987 tracing::info!("Generating documentation...");
988
989 let mut cmd = std::process::Command::new("cargo");
990 cmd.args(["doc", "--no-deps"]);
991
992 if open {
993 cmd.arg("--open");
994 }
995
996 cmd.status()?;
997 Ok(())
998 }
999
1000 fn check_basic_dev_deps(platform: &crate::platform::Platform) -> Result<()> {
1001 use colored::*;
1002
1003 let essentials = match platform {
1004 crate::platform::Platform::Linux => vec![
1005 ("gcc", "šØ C compiler", "sudo apt install build-essential"),
1006 ("git", "šæ Version control", "sudo apt install git"),
1007 ],
1008 crate::platform::Platform::MacOS => vec![
1009 ("git", "šæ Version control", "xcode-select --install"),
1010 ("cc", "šØ C compiler", "xcode-select --install"),
1011 ],
1012 crate::platform::Platform::Windows => vec![(
1013 "git",
1014 "šæ Version control",
1015 "https://git-scm.com/download/win",
1016 )],
1017 _ => vec![],
1018 };
1019
1020 let mut missing = false;
1021
1022 for (cmd, desc, install_hint) in essentials {
1023 print!(" {} {}: ", "ā¢".dimmed(), desc);
1024 if command_exists(cmd) {
1025 println!("{}", "ā
".green());
1026 } else {
1027 println!("{}", "ā Missing".red());
1028 println!(" š” Install: {}", install_hint.yellow());
1029 missing = true;
1030 }
1031 }
1032
1033 if missing {
1034 println!("\nā ļø {}", "Some essential tools are missing!".yellow());
1035 }
1036
1037 Ok(())
1038 }
1039}
1040
1041pub mod install {
1042 use super::*;
1043 use clap::Subcommand;
1044 use std::env;
1045 use std::process::Command;
1046
1047 #[derive(clap::Args)]
1048 pub struct InstallCommand {
1049 #[command(subcommand)]
1050 command: InstallSubcommands,
1051 }
1052
1053 #[derive(Subcommand)]
1054 enum InstallSubcommands {
1055 #[command(visible_alias = "kindlyguard")]
1057 KindlyGuard {
1058 #[arg(short, long)]
1060 method: Option<String>,
1061
1062 #[arg(long)]
1064 version: Option<String>,
1065 },
1066 All,
1068 McpServers {
1070 #[arg(short, long)]
1072 server: Option<String>,
1073 },
1074 DevDeps,
1076 }
1077
1078 impl Execute for InstallCommand {
1079 async fn execute(&self) -> Result<()> {
1080 match &self.command {
1081 InstallSubcommands::KindlyGuard { method, version } => {
1082 install_kindlyguard(method.as_deref(), version.as_deref()).await
1083 },
1084 InstallSubcommands::All => install_all().await,
1085 InstallSubcommands::McpServers { server } => {
1086 install_mcp_servers(server.as_deref()).await
1087 },
1088 InstallSubcommands::DevDeps => install_dev_deps().await,
1089 }
1090 }
1091 }
1092
1093 async fn install_kindlyguard(method: Option<&str>, version: Option<&str>) -> Result<()> {
1094 use crate::platform::Platform;
1095 use colored::*;
1096
1097 println!("\nš {}", "Running pre-installation checks...".cyan());
1099 let platform = Platform::detect();
1100 let env_info = detect_environment();
1101
1102 if platform == Platform::Unknown {
1104 println!(
1105 "š¤ {}",
1106 "Hmm, couldn't detect your platform. Are you on a supported OS?".yellow()
1107 );
1108 println!(
1109 "š” {}",
1110 "Supported platforms: Linux, macOS, Windows".yellow()
1111 );
1112 println!(
1113 "š” {}",
1114 "Try specifying method manually: kindly-tools install kindlyguard --method npm"
1115 .yellow()
1116 );
1117 return Err(anyhow::anyhow!("Unsupported platform"));
1118 }
1119
1120 let version_str = if let Some(v) = version {
1122 validate_and_normalize_version(v)?
1123 } else {
1124 "latest".to_string()
1125 };
1126
1127 let install_method = if let Some(m) = method {
1129 m.to_string()
1130 } else {
1131 detect_best_install_method(&platform)?
1132 };
1133
1134 println!("\nšÆ {}", "Installation Plan".bold().blue());
1136 println!(
1137 " š¦ Package: {}",
1138 "š”ļø KindlyGuard MCP Server".bright_white()
1139 );
1140
1141 let platform_display = match platform {
1143 Platform::MacOS => format!("š {}", platform.to_string()),
1144 Platform::Windows => format!("šŖ {}", platform.to_string()),
1145 Platform::Linux => {
1146 let distro = detect_linux_distro();
1147 distro.display_name()
1148 },
1149 _ => format!("š„ļø {}", platform.to_string()),
1150 };
1151 println!(" {}", platform_display.green());
1152
1153 if env_info.is_docker {
1155 println!(" š³ Environment: {}", "Docker Container".cyan());
1156 }
1157 if env_info.is_wsl {
1158 println!(
1159 " šŖ Environment: {}",
1160 "Windows Subsystem for Linux".cyan()
1161 );
1162 }
1163 if env_info.is_ci {
1164 println!(" š¤ Environment: {}", "CI/CD Pipeline".cyan());
1165 }
1166 if env_info.is_ssh {
1167 println!(" š Connection: {}", "SSH Session".cyan());
1168 }
1169 if env_info.has_proxy {
1170 println!(" š Network: {}", "Behind Proxy".yellow());
1171 }
1172
1173 println!(" āļø Method: {}", install_method.green());
1174 println!(" š·ļø Version: {}", version_str.green());
1175
1176 if let Err(e) = run_preflight_checks(&platform, &install_method).await {
1178 println!("\nā ļø {}", "Pre-flight check warnings:".yellow());
1179 println!(" {}", e.to_string().yellow());
1180 }
1182
1183 println!("\nā³ {}", "Preparing installation...".cyan());
1184
1185 match install_method.as_str() {
1186 "homebrew" | "brew" => {
1187 println!("šŗ {}", "Installing via Homebrew...".green());
1188
1189 if !command_exists("brew") {
1190 println!("\nā {}", "Homebrew not found!".red());
1191 println!("š§ {}", "Install Homebrew first:".yellow());
1192 println!(" {}", "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"".bright_white());
1193 println!("\nš” {}", "Or try a different method:".yellow());
1194 println!(
1195 " {}",
1196 "kindly-tools install kindlyguard --method npm".bright_white()
1197 );
1198 return Err(anyhow::anyhow!("Homebrew not installed"));
1199 }
1200
1201 println!("\nš {}", "Installation steps:".cyan());
1202 println!(" 1ļøā£ Add our tap:");
1203 println!(
1204 " {} {}",
1205 "$".dimmed(),
1206 "brew tap kindly-software-inc/tap".bright_white()
1207 );
1208 println!(" 2ļøā£ Install KindlyGuard:");
1209 println!(
1210 " {} {}",
1211 "$".dimmed(),
1212 "brew install kindlyguard".bright_white()
1213 );
1214
1215 if version_str != "latest" {
1216 println!(
1217 "\nā ļø {}",
1218 "Note: Homebrew installs the latest version by default.".yellow()
1219 );
1220 println!(
1221 "š” {}",
1222 "For specific versions, use npm or cargo instead.".yellow()
1223 );
1224 }
1225
1226 let output = std::process::Command::new("brew").args(["tap"]).output()?;
1228
1229 let taps = String::from_utf8_lossy(&output.stdout);
1230 let tap_exists = taps.contains("kindly-software-inc/tap");
1231
1232 if !tap_exists {
1233 println!("\nā³ {}", "Adding tap...".yellow());
1234 let tap_status = std::process::Command::new("brew")
1235 .args(["tap", "kindly-software-inc/tap"])
1236 .status();
1237
1238 if tap_status.is_err() || !tap_status.unwrap().success() {
1239 loop {
1241 let recovery_method = show_recovery_menu("homebrew")?;
1242
1243 if recovery_method == RecoveryMethod::Cancel {
1244 return Err(anyhow::anyhow!("Installation cancelled"));
1245 }
1246
1247 execute_recovery(recovery_method, "homebrew", "kindlyguard", &platform)
1248 .await?;
1249
1250 let try_again = dialoguer::Confirm::new()
1251 .with_prompt("Try another recovery method?")
1252 .default(true)
1253 .interact()?;
1254
1255 if !try_again {
1256 break;
1257 }
1258 }
1259 return Ok(());
1260 }
1261 }
1262
1263 println!("\nā³ {}", "Installing package...".yellow());
1264 let install_status = std::process::Command::new("brew")
1265 .args(["install", "kindlyguard"])
1266 .status();
1267
1268 match install_status {
1269 Ok(s) if s.success() => {
1270 println!("\nā
{}", "Installation successful!".green().bold());
1271 },
1272 _ => {
1273 loop {
1275 let recovery_method = show_recovery_menu("homebrew")?;
1276
1277 if recovery_method == RecoveryMethod::Cancel {
1278 return Err(anyhow::anyhow!("Installation cancelled"));
1279 }
1280
1281 execute_recovery(recovery_method, "homebrew", "kindlyguard", &platform)
1282 .await?;
1283
1284 let try_again = dialoguer::Confirm::new()
1285 .with_prompt("Try another recovery method?")
1286 .default(true)
1287 .interact()?;
1288
1289 if !try_again {
1290 break;
1291 }
1292 }
1293 },
1294 }
1295 },
1296 "npm" => {
1297 println!("š¦ {}", "Installing via npm...".green());
1298
1299 if !command_exists("npm") {
1300 println!("\nā {}", "npm not found!".red());
1301
1302 let node_managers = detect_node_managers();
1304 if node_managers.has_any() {
1305 println!("š {}", "Detected Node.js version managers:".cyan());
1306 if node_managers.has_nvm {
1307 println!(
1308 " ā
nvm detected! Try: {}",
1309 "nvm install --lts && nvm use --lts".bright_white()
1310 );
1311 }
1312 if node_managers.has_fnm {
1313 println!(
1314 " ā
fnm detected! Try: {}",
1315 "fnm install --lts && fnm use lts-latest".bright_white()
1316 );
1317 }
1318 if node_managers.has_n {
1319 println!(" ā
n detected! Try: {}", "n lts".bright_white());
1320 }
1321 if node_managers.has_volta {
1322 println!(
1323 " ā
volta detected! Try: {}",
1324 "volta install node".bright_white()
1325 );
1326 }
1327 if node_managers.has_asdf {
1328 println!(
1329 " ā
asdf detected! Try: {}",
1330 "asdf plugin add nodejs && asdf install nodejs latest"
1331 .bright_white()
1332 );
1333 }
1334 println!(
1335 "\nš” {}",
1336 "After activating Node.js, run this command again!".yellow()
1337 );
1338 } else {
1339 println!("š¤ {}", "Let's fix this! Choose your platform:".yellow());
1340
1341 match platform {
1342 Platform::MacOS => {
1343 println!("\nš {}", "macOS Options:".cyan());
1344 println!(
1345 " šŗ Using Homebrew: {}",
1346 "brew install node".bright_white()
1347 );
1348 println!(
1349 " š„ Direct download: {}",
1350 "https://nodejs.org/".blue().underline()
1351 );
1352 println!("\nš {}", "Recommended: Use a version manager".green());
1353 println!(" ⢠fnm (fast): {}", "brew install fnm".bright_white());
1354 println!(
1355 " ⢠volta (reliable): {}",
1356 "brew install volta".bright_white()
1357 );
1358 },
1359 Platform::Linux => {
1360 let distro = detect_linux_distro();
1361 println!("\n{} {}", distro.display_name(), "Options:".cyan());
1362
1363 match distro {
1364 LinuxDistro::Ubuntu
1365 | LinuxDistro::Debian
1366 | LinuxDistro::Mint => {
1367 println!(
1368 " š¦ System package: {}",
1369 "sudo apt update && sudo apt install nodejs npm"
1370 .bright_white()
1371 );
1372 },
1373 LinuxDistro::Fedora
1374 | LinuxDistro::CentOS
1375 | LinuxDistro::RHEL => {
1376 println!(
1377 " š¦ System package: {}",
1378 "sudo dnf install nodejs npm".bright_white()
1379 );
1380 },
1381 LinuxDistro::Arch | LinuxDistro::Manjaro => {
1382 println!(
1383 " š¦ System package: {}",
1384 "sudo pacman -S nodejs npm".bright_white()
1385 );
1386 },
1387 LinuxDistro::Alpine => {
1388 println!(
1389 " š¦ System package: {}",
1390 "sudo apk add nodejs npm".bright_white()
1391 );
1392 },
1393 LinuxDistro::NixOS => {
1394 println!(
1395 " š¦ System package: {}",
1396 "nix-env -iA nixpkgs.nodejs".bright_white()
1397 );
1398 },
1399 _ => {
1400 println!(
1401 " š„ Direct download: {}",
1402 "https://nodejs.org/".blue().underline()
1403 );
1404 },
1405 }
1406
1407 println!("\nš {}", "Recommended: Use a version manager".green());
1408 println!(
1409 " ⢠fnm (fast): {}",
1410 "curl -fsSL https://fnm.vercel.app/install | bash"
1411 .bright_white()
1412 );
1413 println!(
1414 " ⢠volta (reliable): {}",
1415 "curl https://get.volta.sh | bash".bright_white()
1416 );
1417 },
1418 Platform::Windows => {
1419 println!("\nšŖ {}", "Windows Options:".cyan());
1420 println!(
1421 " š« Using Chocolatey: {}",
1422 "choco install nodejs".bright_white()
1423 );
1424 println!(
1425 " š Using Scoop: {}",
1426 "scoop install nodejs".bright_white()
1427 );
1428 println!(
1429 " š Using winget: {}",
1430 "winget install OpenJS.NodeJS".bright_white()
1431 );
1432 println!(
1433 " š„ Direct download: {}",
1434 "https://nodejs.org/".blue().underline()
1435 );
1436 println!("\nš {}", "Recommended: Use volta".green());
1437 println!(" ⢠Install: {}", "choco install volta".bright_white());
1438 },
1439 _ => {
1440 println!(
1441 " š„ Direct download: {}",
1442 "https://nodejs.org/".blue().underline()
1443 );
1444 },
1445 }
1446 }
1447
1448 println!(
1449 "\nš” {}",
1450 "After installing Node.js, run this command again!".yellow()
1451 );
1452 return Err(anyhow::anyhow!("npm not installed"));
1453 }
1454
1455 let package = if version_str != "latest" {
1456 format!("kindly-guard-server@{}", version_str)
1457 } else {
1458 "kindly-guard-server".to_string()
1459 };
1460
1461 println!("\nš {}", "Installation command:".cyan());
1462 println!(
1463 " {} npm install -g {}",
1464 "$".dimmed(),
1465 package.bright_white()
1466 );
1467
1468 println!("\nā³ {}", "Attempting installation...".yellow());
1469 let status = std::process::Command::new("npm")
1470 .args(["install", "-g", &package])
1471 .status();
1472
1473 match status {
1474 Ok(s) if s.success() => {
1475 println!("\nā
{}", "Installation successful!".green().bold());
1476 },
1477 _ => {
1478 loop {
1480 let recovery_method = show_recovery_menu("npm")?;
1481
1482 if recovery_method == RecoveryMethod::Cancel {
1483 return Err(anyhow::anyhow!("Installation cancelled"));
1484 }
1485
1486 execute_recovery(recovery_method, "npm", &package, &platform).await?;
1487
1488 let try_again = dialoguer::Confirm::new()
1490 .with_prompt("Try another recovery method?")
1491 .default(true)
1492 .interact()?;
1493
1494 if !try_again {
1495 break;
1496 }
1497 }
1498 },
1499 }
1500 },
1501 "cargo" => {
1502 println!("š¦ {}", "Installing KindlyGuard...".green());
1503 println!("š¦ {}", "Using GitHub releases for faster installation".cyan());
1504
1505 match install_kindlyguard_from_github(&version_str, &platform).await {
1507 Ok(_) => {
1508 },
1510 Err(e) => {
1511 println!("\nā ļø {}", format!("GitHub download failed: {}", e).yellow());
1512 println!("š {}", "Falling back to cargo install...".cyan());
1513
1514 if !command_exists("cargo") {
1516 println!("\nā {}", "Cargo not found!".red());
1517 println!("š¦ {}", "Let's install Rust and Cargo:".yellow());
1518 println!("\nš {}", "Quick install (all platforms):".cyan());
1519 println!(
1520 " {} {}",
1521 "$".dimmed(),
1522 "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
1523 .bright_white()
1524 );
1525
1526 match platform {
1527 Platform::Windows => {
1528 println!("\nšŖ {}", "Windows alternative:".cyan());
1529 println!(
1530 " š„ Download installer: {}",
1531 "https://rustup.rs/".blue().underline()
1532 );
1533 },
1534 _ => {},
1535 }
1536
1537 println!(
1538 "\nš” {}",
1539 "After installing Rust, restart your terminal and try again!".yellow()
1540 );
1541 return Err(anyhow::anyhow!("Installation failed - no fallback available"));
1542 }
1543
1544 let package = if version_str != "latest" {
1546 format!("kindlyguard@{}", version_str)
1547 } else {
1548 "kindlyguard".to_string()
1549 };
1550
1551 println!("\nš {}", "Fallback installation command:".cyan());
1552 println!(
1553 " {} cargo install {}",
1554 "$".dimmed(),
1555 package.bright_white()
1556 );
1557
1558 println!(
1559 "\nā³ {}",
1560 "Attempting installation (this may take a while)...".yellow()
1561 );
1562 let status = std::process::Command::new("cargo")
1563 .args(["install", &package])
1564 .status();
1565
1566 match status {
1567 Ok(s) if s.success() => {
1568 println!("\nā
{}", "Installation successful!".green().bold());
1569 println!("š {}", "Binary installed to ~/.cargo/bin/".cyan());
1570 },
1571 _ => {
1572 loop {
1574 let recovery_method = show_recovery_menu("cargo")?;
1575
1576 if recovery_method == RecoveryMethod::Cancel {
1577 return Err(anyhow::anyhow!("Installation cancelled"));
1578 }
1579
1580 execute_recovery(recovery_method, "cargo", &package, &platform).await?;
1581
1582 let try_again = dialoguer::Confirm::new()
1584 .with_prompt("Try another recovery method?")
1585 .default(true)
1586 .interact()?;
1587
1588 if !try_again {
1589 break;
1590 }
1591 }
1592 },
1593 }
1594 }
1595 }
1596 },
1597 "binary" => {
1598 println!("šæ {}", "Direct binary installation...".green());
1599
1600 let arch = crate::platform::Architecture::detect();
1601 println!(
1602 "\nšļø {}",
1603 format!("Detected architecture: {}", arch.name()).cyan()
1604 );
1605
1606 let auto_download = dialoguer::Confirm::new()
1608 .with_prompt("Download and install automatically?")
1609 .default(true)
1610 .interact()?;
1611
1612 if auto_download {
1613 match install_kindlyguard_from_github(&version_str, &platform).await {
1615 Ok(_) => {
1616 },
1618 Err(e) => {
1619 println!("\nā ļø {}", format!("Automatic download failed: {}", e).yellow());
1620 println!("š {}", "You can download manually:".cyan());
1621
1622 show_manual_download_instructions(&platform, &arch);
1624 }
1625 }
1626 } else {
1627 show_manual_download_instructions(&platform, &arch);
1628 }
1629 },
1630 _ => {
1631 println!(
1632 "\nā {}",
1633 format!("Unknown installation method: {}", install_method).red()
1634 );
1635 println!(
1636 "š¤ {}",
1637 "Valid methods: homebrew, npm, cargo, binary".yellow()
1638 );
1639
1640 let suggested = detect_best_install_method(&platform)?;
1642 println!(
1643 "\nš” {}",
1644 format!(
1645 "Try: kindly-tools install kindlyguard --method {}",
1646 suggested
1647 )
1648 .green()
1649 );
1650
1651 return Err(anyhow::anyhow!("Invalid installation method"));
1652 },
1653 }
1654
1655 println!("\nšÆ {}", "Next steps:".bold().green());
1656 println!(
1657 " š Start server: {}",
1658 "kindlyguard --stdio".bright_white()
1659 );
1660 println!(" š Get help: {}", "kindlyguard --help".bright_white());
1661 println!(" š§ Configure: {}", "kindlyguard config".bright_white());
1662
1663 println!("\n𩺠{}", "If something goes wrong:".cyan());
1664 show_troubleshooting_tips();
1665
1666 println!("\nš {}", "Verifying installation...".bold().cyan());
1668 verify_installation(&install_method)?;
1669
1670 Ok(())
1671 }
1672
1673 fn verify_installation(method: &str) -> Result<()> {
1675 use colored::*;
1676 use std::path::Path;
1677
1678 let mut checks_passed = true;
1679 let mut warnings = Vec::new();
1680
1681 println!("\nš {}", "Checking binary locations...".cyan());
1683
1684 let binary_locations: Vec<String> = match method {
1685 "homebrew" | "brew" => vec![
1686 "/usr/local/bin/kindlyguard".to_string(),
1687 "/opt/homebrew/bin/kindlyguard".to_string(),
1688 ],
1689 "npm" => {
1690 let npm_prefix = Command::new("npm")
1691 .args(["prefix", "-g"])
1692 .output()
1693 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
1694 .unwrap_or_default();
1695
1696 if npm_prefix.is_empty() {
1697 vec![
1698 "/usr/local/bin/kindlyguard".to_string(),
1699 "/usr/bin/kindlyguard".to_string(),
1700 ]
1701 } else {
1702 vec![
1703 format!("{}/bin/kindlyguard", npm_prefix),
1704 "/usr/local/bin/kindlyguard".to_string(),
1705 ]
1706 }
1707 },
1708 "cargo" => vec![
1709 format!(
1710 "{}/.cargo/bin/kindlyguard",
1711 env::var("HOME").unwrap_or_default()
1712 ),
1713 "/usr/local/bin/kindlyguard".to_string(),
1714 ],
1715 "binary" => vec![
1716 "/usr/local/bin/kindlyguard".to_string(),
1717 "/opt/kindlyguard/bin/kindlyguard".to_string(),
1718 format!("{}/bin/kindlyguard", env::var("HOME").unwrap_or_default()),
1719 ],
1720 _ => vec!["/usr/local/bin/kindlyguard".to_string()],
1721 };
1722
1723 let mut found_binary = None;
1724 for location in &binary_locations {
1725 if Path::new(location).exists() {
1726 println!(" ā
Found binary at: {}", location.green());
1727 found_binary = Some(location.clone());
1728 break;
1729 }
1730 }
1731
1732 if found_binary.is_none() {
1733 println!(" ā {}", "Binary not found in expected locations".red());
1734 checks_passed = false;
1735
1736 if let Ok(output) = Command::new("which").arg("kindlyguard").output() {
1738 if output.status.success() {
1739 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
1740 if !path.is_empty() {
1741 println!(" ā
Found binary via PATH at: {}", path.green());
1742 found_binary = Some(path);
1743 checks_passed = true;
1744 }
1745 }
1746 }
1747 }
1748
1749 if let Some(binary_path) = &found_binary {
1751 println!("\nš§ {}", "Checking binary execution...".cyan());
1752
1753 match Command::new(binary_path).arg("--version").output() {
1754 Ok(output) => {
1755 if output.status.success() {
1756 let version = String::from_utf8_lossy(&output.stdout);
1757 println!(" ā
Binary executes successfully");
1758 println!(" š Version: {}", version.trim().green());
1759 } else {
1760 println!(" ā {}", "Binary failed to execute".red());
1761 let stderr = String::from_utf8_lossy(&output.stderr);
1762 if !stderr.is_empty() {
1763 println!(" š” Error: {}", stderr.trim().yellow());
1764 }
1765 checks_passed = false;
1766 }
1767 },
1768 Err(e) => {
1769 println!(" ā {}", format!("Failed to run binary: {}", e).red());
1770 checks_passed = false;
1771 },
1772 }
1773 }
1774
1775 #[cfg(unix)]
1777 if let Some(binary_path) = &found_binary {
1778 println!("\nš {}", "Checking file permissions...".cyan());
1779
1780 use std::os::unix::fs::PermissionsExt;
1781 match std::fs::metadata(binary_path) {
1782 Ok(metadata) => {
1783 let mode = metadata.permissions().mode();
1784 let is_executable = mode & 0o111 != 0;
1785
1786 if is_executable {
1787 println!(" ā
Binary has executable permissions");
1788 } else {
1789 println!(" ā {}", "Binary is not executable".red());
1790 println!(
1791 " š” Fix with: {}",
1792 format!("chmod +x {}", binary_path).yellow()
1793 );
1794 checks_passed = false;
1795 }
1796 },
1797 Err(e) => {
1798 println!(
1799 " ā ļø {}",
1800 format!("Could not check permissions: {}", e).yellow()
1801 );
1802 warnings.push("Could not verify file permissions");
1803 },
1804 }
1805 }
1806
1807 println!("\nš {}", "Checking PATH configuration...".cyan());
1809
1810 if let Ok(path_var) = env::var("PATH") {
1811 let path_contains_binary = if let Some(binary_path) = &found_binary {
1812 if let Some(parent) = Path::new(binary_path).parent() {
1813 path_var.split(':').any(|p| Path::new(p) == parent)
1814 } else {
1815 false
1816 }
1817 } else {
1818 false
1819 };
1820
1821 if path_contains_binary || command_exists("kindlyguard") {
1822 println!(" ā
kindlyguard is accessible via PATH");
1823 } else {
1824 println!(" ā ļø {}", "kindlyguard directory not in PATH".yellow());
1825 warnings.push("PATH configuration needed");
1826
1827 detect_and_show_path_instructions(method);
1829 }
1830 }
1831
1832 println!("\nš {}", "Verification Summary".bold().blue());
1834
1835 if checks_passed && warnings.is_empty() {
1836 println!(
1837 "\nā
{}",
1838 "All checks passed! Installation verified.".bold().green()
1839 );
1840 println!("š {}", "You can now run: kindlyguard --help".green());
1841 } else if checks_passed && !warnings.is_empty() {
1842 println!(
1843 "\nā
{}",
1844 "Installation succeeded with warnings:".bold().yellow()
1845 );
1846 for warning in &warnings {
1847 println!(" ā ļø {}", warning.yellow());
1848 }
1849 } else {
1850 println!("\nā {}", "Installation verification failed!".bold().red());
1851 println!(
1852 "š§ {}",
1853 "Please check the errors above and try again.".yellow()
1854 );
1855 return Err(anyhow::anyhow!("Installation verification failed"));
1856 }
1857
1858 Ok(())
1859 }
1860
1861 fn show_manual_download_instructions(platform: &Platform, arch: &crate::platform::Architecture) {
1863 use colored::*;
1864
1865 println!("\nš„ {}", "Download options:".cyan());
1866 println!(
1867 " š Visit: {}",
1868 "https://github.com/kindly-software-inc/kindly-guard/releases"
1869 .blue()
1870 .underline()
1871 );
1872
1873 match platform {
1874 Platform::MacOS => {
1875 let arch_str = if *arch == crate::platform::Architecture::Arm64 {
1876 "aarch64"
1877 } else {
1878 "x86_64"
1879 };
1880 println!("\nš {}", "macOS binary:".cyan());
1881 println!(
1882 " š¦ File: {}",
1883 format!("kindlyguard-{}-apple-darwin", arch_str)
1884 .bright_white()
1885 );
1886
1887 println!("\nš {}", "Installation steps:".cyan());
1888 println!(" 1ļøā£ Download the binary file");
1889 println!(
1890 " 2ļøā£ Move to PATH: {}",
1891 "sudo mv kindlyguard /usr/local/bin/".bright_white()
1892 );
1893 println!(
1894 " 3ļøā£ Make executable: {}",
1895 "sudo chmod +x /usr/local/bin/kindlyguard".bright_white()
1896 );
1897 },
1898 Platform::Linux => {
1899 println!("\nš§ {}", "Linux binary:".cyan());
1900 println!(
1901 " š¦ File: {}",
1902 "kindlyguard-x86_64-unknown-linux-gnu".bright_white()
1903 );
1904
1905 println!("\nš {}", "Installation steps:".cyan());
1906 println!(" 1ļøā£ Download the binary file");
1907 println!(
1908 " 2ļøā£ Move to PATH: {}",
1909 "sudo mv kindlyguard /usr/local/bin/".bright_white()
1910 );
1911 println!(
1912 " 3ļøā£ Make executable: {}",
1913 "sudo chmod +x /usr/local/bin/kindlyguard".bright_white()
1914 );
1915 },
1916 Platform::Windows => {
1917 println!("\nšŖ {}", "Windows binary:".cyan());
1918 println!(
1919 " š¦ File: {}",
1920 "kindlyguard-x86_64-pc-windows-msvc.exe".bright_white()
1921 );
1922
1923 println!("\nš {}", "Installation steps:".cyan());
1924 println!(" 1ļøā£ Download the .exe file");
1925 println!(" š Move to C:\\Program Files\\KindlyGuard\\");
1926 println!(
1927 " š§ Add to PATH: {}",
1928 "%ProgramFiles%\\KindlyGuard".yellow()
1929 );
1930 },
1931 Platform::Unknown => {
1932 println!("\nā {}", "Unknown platform:".yellow());
1933 println!(" Please visit the releases page for manual download");
1934 },
1935 }
1936
1937 println!("\nā ļø {}", "Important:".yellow());
1938 println!(" š Verify checksums after download");
1939 println!(" š Check file permissions are correct");
1940 println!(" š Ensure binary is in your PATH");
1941 }
1942
1943 fn detect_and_show_path_instructions(method: &str) -> () {
1945 use colored::*;
1946
1947 let shell = env::var("SHELL").unwrap_or_default();
1949 let shell_name = if shell.contains("bash") {
1950 "bash"
1951 } else if shell.contains("zsh") {
1952 "zsh"
1953 } else if shell.contains("fish") {
1954 "fish"
1955 } else {
1956 "sh"
1957 };
1958
1959 println!("\nš” {}", "To add kindlyguard to your PATH:".cyan());
1960
1961 let path_to_add = match method {
1962 "npm" => {
1963 let npm_prefix = Command::new("npm")
1964 .args(["prefix", "-g"])
1965 .output()
1966 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
1967 .unwrap_or_else(|_| "/usr/local".to_string());
1968 format!("{}/bin", npm_prefix)
1969 },
1970 "cargo" => format!("$HOME/.cargo/bin"),
1971 "homebrew" | "brew" => {
1972 if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") {
1973 "/opt/homebrew/bin".to_string()
1974 } else {
1975 "/usr/local/bin".to_string()
1976 }
1977 },
1978 _ => "/usr/local/bin".to_string(),
1979 };
1980
1981 match shell_name {
1982 "bash" => {
1983 println!("\n š For Bash:");
1984 println!(" 1. Add to ~/.bashrc:");
1985 println!(
1986 " {} echo 'export PATH=\"{}:$PATH\"' >> ~/.bashrc",
1987 "$".dimmed(),
1988 path_to_add.bright_white()
1989 );
1990 println!(" 2. Reload:");
1991 println!(" {} source ~/.bashrc", "$".dimmed());
1992 },
1993 "zsh" => {
1994 println!("\n š For Zsh:");
1995 println!(" 1. Add to ~/.zshrc:");
1996 println!(
1997 " {} echo 'export PATH=\"{}:$PATH\"' >> ~/.zshrc",
1998 "$".dimmed(),
1999 path_to_add.bright_white()
2000 );
2001 println!(" 2. Reload:");
2002 println!(" {} source ~/.zshrc", "$".dimmed());
2003 },
2004 "fish" => {
2005 println!("\n š For Fish:");
2006 println!(" 1. Add to config:");
2007 println!(
2008 " {} fish_add_path {}",
2009 "$".dimmed(),
2010 path_to_add.bright_white()
2011 );
2012 println!(" 2. Or manually:");
2013 println!(
2014 " {} set -Ua fish_user_paths {}",
2015 "$".dimmed(),
2016 path_to_add.bright_white()
2017 );
2018 },
2019 _ => {
2020 println!("\n š For your shell:");
2021 println!(" 1. Add to your shell config file:");
2022 println!(
2023 " export PATH=\"{}:$PATH\"",
2024 path_to_add.bright_white()
2025 );
2026 println!(" 2. Reload your shell configuration");
2027 },
2028 }
2029
2030 println!("\n š” After updating PATH, restart your terminal or run the reload command");
2031 }
2032
2033 fn validate_and_normalize_version(version: &str) -> Result<String> {
2034 use colored::*;
2035
2036 let normalized = version
2038 .trim()
2039 .trim_start_matches('v')
2040 .trim_start_matches('V');
2041
2042 if normalized.is_empty() {
2044 println!("ā ļø {}", "Empty version specified, using 'latest'".yellow());
2045 return Ok("latest".to_string());
2046 }
2047
2048 let parts: Vec<&str> = normalized.split('.').collect();
2050 if parts.len() != 3 && normalized != "latest" {
2051 println!(
2052 "ā ļø {}",
2053 format!(
2054 "Version '{}' doesn't look like semantic versioning",
2055 version
2056 )
2057 .yellow()
2058 );
2059 println!("š” {}", "Expected format: X.Y.Z (e.g., 0.10.3)".yellow());
2060 println!(
2061 "š {}",
2062 "Available versions: latest, 0.10.3, 0.10.2, 0.10.1".cyan()
2063 );
2064
2065 }
2067
2068 Ok(normalized.to_string())
2069 }
2070
2071 fn detect_best_install_method(platform: &crate::platform::Platform) -> Result<String> {
2072 match platform {
2073 crate::platform::Platform::MacOS => {
2074 if command_exists("brew") {
2075 Ok("homebrew".to_string())
2076 } else if command_exists("npm") {
2077 Ok("npm".to_string())
2078 } else if command_exists("cargo") {
2079 Ok("cargo".to_string())
2080 } else {
2081 Ok("binary".to_string())
2082 }
2083 },
2084 crate::platform::Platform::Linux => {
2085 if command_exists("npm") {
2086 Ok("npm".to_string())
2087 } else if command_exists("cargo") {
2088 Ok("cargo".to_string())
2089 } else {
2090 Ok("binary".to_string())
2091 }
2092 },
2093 crate::platform::Platform::Windows => {
2094 if command_exists("npm") {
2095 Ok("npm".to_string())
2096 } else if command_exists("cargo") {
2097 Ok("cargo".to_string())
2098 } else {
2099 Ok("binary".to_string())
2100 }
2101 },
2102 _ => Ok("npm".to_string()),
2103 }
2104 }
2105
2106 async fn run_preflight_checks(
2107 _platform: &crate::platform::Platform,
2108 method: &str,
2109 ) -> Result<()> {
2110 use colored::*;
2111
2112 #[cfg(not(target_os = "windows"))]
2114 {
2115 if let Ok(output) = std::process::Command::new("df").args(["-h", "/"]).output() {
2116 let output_str = String::from_utf8_lossy(&output.stdout);
2117 if output_str.contains("100%")
2119 || output_str.contains("99%")
2120 || output_str.contains("98%")
2121 {
2122 println!("š¾ {}", "Warning: Disk space is running low!".yellow());
2123 println!(" {}", "Installation needs approximately 200MB".yellow());
2124 }
2125 }
2126 }
2127
2128 if method != "binary" {
2130 #[cfg(not(target_os = "windows"))]
2132 {
2133 if let Err(_) = std::process::Command::new("ping")
2134 .args(["-c", "1", "-W", "2", "8.8.8.8"])
2135 .output()
2136 {
2137 println!("š {}", "Network connectivity might be limited".yellow());
2138 println!(
2139 " {}",
2140 "If installation fails, check your internet connection".yellow()
2141 );
2142 }
2143 }
2144 }
2145
2146 Ok(())
2147 }
2148
2149 fn show_troubleshooting_tips() {
2150 use colored::*;
2151
2152 println!(" š Check internet connection");
2153 println!(" š Verify disk space (200MB needed)");
2154 println!(" š Ensure admin/sudo permissions");
2155 println!(" š Try a different installation method");
2156 println!("\nš {}", "Need help?".cyan());
2157 println!(
2158 " š Docs: {}",
2159 "https://github.com/kindly-software-inc/kindly-guard/wiki"
2160 .blue()
2161 .underline()
2162 );
2163 println!(
2164 " š Issues: {}",
2165 "https://github.com/kindly-software-inc/kindly-guard/issues"
2166 .blue()
2167 .underline()
2168 );
2169 }
2170
2171 async fn install_all() -> Result<()> {
2172 use colored::*;
2173
2174 println!(
2175 "\nš {}",
2176 "Installing all recommended tools...".bold().blue()
2177 );
2178 println!("š¦ {}", "This will install:".cyan());
2179 println!(" 1ļøā£ KindlyGuard MCP Server");
2180 println!(" 2ļøā£ Recommended MCP servers");
2181 println!(" 3ļøā£ Development dependencies");
2182
2183 println!("\nā³ {}", "Step 1/3: Installing KindlyGuard...".cyan());
2184 install_kindlyguard(None, None).await?;
2185
2186 println!("\nā³ {}", "Step 2/3: Installing MCP servers...".cyan());
2187 install_mcp_servers(None).await?;
2188
2189 println!("\nā³ {}", "Step 3/3: Installing dev dependencies...".cyan());
2190 install_dev_deps().await?;
2191
2192 println!(
2193 "\nš {}",
2194 "All tools installed successfully!".bold().green()
2195 );
2196 println!("⨠{}", "Your development environment is ready!".green());
2197
2198 Ok(())
2199 }
2200
2201 async fn install_mcp_servers(server: Option<&str>) -> Result<()> {
2202 use colored::*;
2203 use dialoguer::Confirm;
2204
2205 let servers = if let Some(s) = server {
2206 vec![(s, get_server_description(s))]
2207 } else {
2208 vec![
2209 ("tree-sitter", "š³ Parse and analyze code structure"),
2210 ("ast-grep", "š Search code with AST patterns"),
2211 ("filesystem", "š Enhanced file system access"),
2212 ]
2213 };
2214
2215 println!("\nš {}", "MCP Server Installation".bold().blue());
2216
2217 for (server_name, description) in servers {
2218 println!("\nš¦ {}: {}", server_name.cyan(), description);
2219
2220 if Confirm::new()
2221 .with_prompt(format!(" Install '{}'?", server_name))
2222 .default(true)
2223 .interact()?
2224 {
2225 println!(
2226 " ā³ {}",
2227 format!("Installing {}...", server_name).yellow()
2228 );
2229
2230 println!(
2232 " ā
{}",
2233 format!("{} installed successfully!", server_name).green()
2234 );
2235
2236 match server_name {
2238 "tree-sitter" => {
2239 println!(
2240 " š” {}",
2241 "Tip: Use for code navigation and refactoring".yellow()
2242 );
2243 },
2244 "ast-grep" => {
2245 println!(" š” {}", "Tip: Great for finding code patterns".yellow());
2246 },
2247 "filesystem" => {
2248 println!(
2249 " š” {}",
2250 "Tip: Provides secure file access to Claude".yellow()
2251 );
2252 },
2253 _ => {},
2254 }
2255 } else {
2256 println!(" āļø {}", format!("Skipping {}", server_name).dimmed());
2257 }
2258 }
2259
2260 println!(
2261 "\nš {}",
2262 "Note: Restart Claude Desktop after installing MCP servers".cyan()
2263 );
2264
2265 Ok(())
2266 }
2267
2268 fn get_server_description(server: &str) -> &'static str {
2269 match server {
2270 "tree-sitter" => "š³ Parse and analyze code structure",
2271 "ast-grep" => "š Search code with AST patterns",
2272 "filesystem" => "š Enhanced file system access",
2273 "semgrep" => "š”ļø Security vulnerability scanning",
2274 "github" => "š GitHub repository integration",
2275 _ => "š¦ MCP server extension",
2276 }
2277 }
2278
2279 async fn install_dev_deps() -> Result<()> {
2280 use colored::*;
2281
2282 println!("\nš ļø {}", "Development Dependencies Check".bold().blue());
2283
2284 let platform = crate::platform::Platform::detect();
2285 let packages = match platform {
2286 crate::platform::Platform::Linux => vec![
2287 (
2288 "build-essential",
2289 "šØ C/C++ compiler toolchain",
2290 "sudo apt install build-essential",
2291 ),
2292 (
2293 "pkg-config",
2294 "š¦ Library configuration tool",
2295 "sudo apt install pkg-config",
2296 ),
2297 (
2298 "libssl-dev",
2299 "š SSL development headers",
2300 "sudo apt install libssl-dev",
2301 ),
2302 ],
2303 crate::platform::Platform::MacOS => vec![
2304 (
2305 "xcode-select",
2306 "š Xcode command line tools",
2307 "xcode-select --install",
2308 ),
2309 (
2310 "pkg-config",
2311 "š¦ Library configuration tool",
2312 "brew install pkg-config",
2313 ),
2314 ],
2315 crate::platform::Platform::Windows => vec![(
2316 "visual-studio",
2317 "šŖ Visual Studio Build Tools",
2318 "Download from https://visualstudio.microsoft.com/downloads/",
2319 )],
2320 _ => vec![],
2321 };
2322
2323 let mut missing = Vec::new();
2324
2325 println!("\nš {}", "Checking system dependencies...".cyan());
2326
2327 for (pkg, description, install_cmd) in &packages {
2328 print!(" {} {}: ", "ā¢".dimmed(), description);
2329
2330 let is_installed = match *pkg {
2332 "xcode-select" => command_exists("xcodebuild"),
2333 "visual-studio" => {
2334 std::path::Path::new("C:\\Program Files\\Microsoft Visual Studio").exists()
2336 || std::path::Path::new("C:\\Program Files (x86)\\Microsoft Visual Studio")
2337 .exists()
2338 },
2339 _ => command_exists(pkg),
2340 };
2341
2342 if is_installed {
2343 println!("{}", "ā
Installed".green());
2344 } else {
2345 println!("{}", "ā Not found".red());
2346 missing.push((pkg, description, install_cmd));
2347 }
2348 }
2349
2350 if !missing.is_empty() {
2351 println!("\nā ļø {}", "Missing dependencies detected!".yellow());
2352 println!("š {}", "Installation commands:".cyan());
2353
2354 for (pkg, _desc, cmd) in missing {
2355 println!("\n {} {}:", "ā¢".dimmed(), pkg.bright_white());
2356 println!(" {}", cmd.bright_white());
2357 }
2358
2359 println!(
2360 "\nš” {}",
2361 "Install these dependencies for optimal development experience".yellow()
2362 );
2363 } else {
2364 println!(
2365 "\nā
{}",
2366 "All development dependencies are installed!".green()
2367 );
2368 }
2369
2370 println!("\nš {}", "Recommended Rust tools:".cyan());
2372 println!(
2373 " {} cargo-watch - {}",
2374 "ā¢".dimmed(),
2375 "Auto-rebuild on file changes".yellow()
2376 );
2377 println!(
2378 " {} cargo-nextest - {}",
2379 "ā¢".dimmed(),
2380 "3x faster test runner".yellow()
2381 );
2382 println!(
2383 " {} sccache - {}",
2384 "ā¢".dimmed(),
2385 "Compilation cache for faster builds".yellow()
2386 );
2387
2388 println!(
2389 "\nš” {}",
2390 "Install with: cargo install cargo-watch cargo-nextest sccache".cyan()
2391 );
2392
2393 Ok(())
2394 }
2395}
2396
2397pub mod mcp {
2398 use super::*;
2399 use clap::Subcommand;
2400 use serde::{Deserialize, Serialize};
2401 use std::collections::HashMap;
2402 use std::io::Write;
2403 use std::process::{Command, Stdio};
2404
2405 #[derive(clap::Args)]
2406 pub struct McpCommand {
2407 #[command(subcommand)]
2408 command: McpSubcommands,
2409 }
2410
2411 #[derive(Subcommand)]
2412 enum McpSubcommands {
2413 Setup {
2415 #[arg(short, long)]
2417 non_interactive: bool,
2418
2419 #[arg(short, long)]
2421 force: bool,
2422 },
2423
2424 Verify {
2426 #[arg(short, long)]
2428 verbose: bool,
2429 },
2430
2431 Status {
2433 #[arg(short, long)]
2435 processes: bool,
2436 },
2437
2438 Start {
2440 #[arg(short, long)]
2442 daemon: bool,
2443 },
2444
2445 Stop {
2447 #[arg(short, long)]
2449 force: bool,
2450 },
2451
2452 List,
2454
2455 Config {
2457 #[arg(short, long)]
2459 file: Option<PathBuf>,
2460
2461 #[arg(short, long)]
2463 show: bool,
2464 },
2465
2466 Test {
2468 server: String,
2470 },
2471 }
2472
2473 #[derive(Serialize, Deserialize)]
2474 struct McpConfig {
2475 #[serde(rename = "mcpServers")]
2476 servers: std::collections::HashMap<String, ServerConfig>,
2477 }
2478
2479 #[derive(Serialize, Deserialize)]
2480 struct ServerConfig {
2481 #[serde(rename = "type", default)]
2482 server_type: Option<String>,
2483 command: String,
2484 args: Vec<String>,
2485 #[serde(default)]
2486 env: std::collections::HashMap<String, String>,
2487 }
2488
2489 impl Execute for McpCommand {
2490 async fn execute(&self) -> Result<()> {
2491 match &self.command {
2492 McpSubcommands::Setup {
2493 non_interactive,
2494 force,
2495 } => setup_mcp_server(*non_interactive, *force).await,
2496 McpSubcommands::Verify { verbose } => verify_mcp_setup(*verbose).await,
2497 McpSubcommands::Status { processes } => show_mcp_status(*processes).await,
2498 McpSubcommands::Start { daemon } => start_mcp_server(*daemon).await,
2499 McpSubcommands::Stop { force } => stop_mcp_server(*force).await,
2500 McpSubcommands::List => list_mcp_servers().await,
2501 McpSubcommands::Config { file, show } => {
2502 configure_mcp(file.as_deref(), *show).await
2503 },
2504 McpSubcommands::Test { server } => test_mcp_server(server).await,
2505 }
2506 }
2507 }
2508
2509 async fn setup_mcp_server(non_interactive: bool, force: bool) -> Result<()> {
2510 tracing::info!("Setting up MCP server for KindlyGuard");
2511
2512 let config_path = get_mcp_config_path()?;
2514 if config_path.exists() && !force {
2515 tracing::warn!("MCP configuration already exists at {:?}", config_path);
2516 if !non_interactive {
2517 let proceed = dialoguer::Confirm::new()
2518 .with_prompt("Configuration exists. Overwrite?")
2519 .default(false)
2520 .interact()?;
2521 if !proceed {
2522 tracing::info!("Setup cancelled");
2523 return Ok(());
2524 }
2525 } else {
2526 tracing::info!("Use --force to overwrite existing configuration");
2527 return Ok(());
2528 }
2529 }
2530
2531 if !non_interactive {
2533 let build = dialoguer::Confirm::new()
2534 .with_prompt("Build kindly-guard-server first?")
2535 .default(true)
2536 .interact()?;
2537 if build {
2538 tracing::info!("Building kindly-guard-server in release mode...");
2539 let status = Command::new("cargo")
2540 .args(["build", "--release", "--package", "kindly-guard-server"])
2541 .current_dir(find_project_root()?)
2542 .status()?;
2543 if !status.success() {
2544 return Err(anyhow::anyhow!("Build failed"));
2545 }
2546 }
2547 }
2548
2549 let kg_server = find_kindlyguard_server()?;
2551 tracing::info!("Found KindlyGuard server at: {:?}", kg_server);
2552
2553 let mcp_server_dir = home_dir()?.join(".claude/mcp-servers/kindly-guard");
2555 std::fs::create_dir_all(&mcp_server_dir)?;
2556
2557 let target_binary = mcp_server_dir.join("kindly-guard");
2559 std::fs::copy(&kg_server, &target_binary)?;
2560 #[cfg(unix)]
2561 {
2562 use std::os::unix::fs::PermissionsExt;
2563 let mut perms = std::fs::metadata(&target_binary)?.permissions();
2564 perms.set_mode(0o755);
2565 std::fs::set_permissions(&target_binary, perms)?;
2566 }
2567
2568 let server_config_file = mcp_server_dir.join("config.toml");
2570 if !server_config_file.exists() || force {
2571 let config_content = r#"# Kindly Guard Configuration
2572mode = "standard"
2573log_level = "info"
2574
2575[rate_limit]
2576window_secs = 60
2577max_requests = 100
2578
2579[scanner]
2580max_input_size = 1048576 # 1MB
2581patterns_file = ""
2582
2583[metrics]
2584enabled = true
2585export_interval_secs = 60
2586
2587[auth]
2588require_auth = false
2589"#;
2590 std::fs::write(&server_config_file, config_content)?;
2591 }
2592
2593 let mut config = if config_path.exists() {
2595 let content = std::fs::read_to_string(&config_path)?;
2596 serde_json::from_str(&content)?
2597 } else {
2598 McpConfig {
2599 servers: HashMap::new(),
2600 }
2601 };
2602
2603 let mut env = HashMap::new();
2605 env.insert("RUST_LOG".to_string(), "info".to_string());
2606
2607 config.servers.insert(
2608 "kindly-guard".to_string(),
2609 ServerConfig {
2610 server_type: Some("stdio".to_string()),
2611 command: target_binary.to_string_lossy().to_string(),
2612 args: vec![
2613 "--config".to_string(),
2614 server_config_file.to_string_lossy().to_string(),
2615 ],
2616 env,
2617 },
2618 );
2619
2620 let content = serde_json::to_string_pretty(&config)?;
2622 std::fs::write(&config_path, content)?;
2623
2624 tracing::info!("MCP configuration saved to: {:?}", config_path);
2625 tracing::info!("Setup complete! Restart Claude Desktop to use the MCP server.");
2626
2627 Ok(())
2628 }
2629
2630 async fn verify_mcp_setup(verbose: bool) -> Result<()> {
2631 tracing::info!("Verifying MCP configuration");
2632
2633 let config_path = get_mcp_config_path()?;
2635 if !config_path.exists() {
2636 tracing::error!("MCP configuration not found at {:?}", config_path);
2637 return Err(anyhow::anyhow!("MCP not configured"));
2638 }
2639
2640 let content = std::fs::read_to_string(&config_path)?;
2642 let config: McpConfig = serde_json::from_str(&content)?;
2643
2644 if let Some(server) = config.servers.get("kindly-guard") {
2646 let command_path = Path::new(&server.command);
2647 if !command_path.exists() {
2648 tracing::error!("Server binary not found at: {:?}", command_path);
2649 return Err(anyhow::anyhow!("Server binary not found"));
2650 }
2651
2652 if verbose {
2654 tracing::info!("Testing server execution...");
2655 let output = Command::new(&server.command).arg("--version").output()?;
2656
2657 if output.status.success() {
2658 let version = String::from_utf8_lossy(&output.stdout);
2659 tracing::info!("Server version: {}", version.trim());
2660 }
2661 }
2662
2663 tracing::info!("Testing MCP protocol communication...");
2665 test_mcp_protocol(&server.command, &server.args)?;
2666
2667 tracing::info!("MCP configuration verified successfully");
2668 } else {
2669 tracing::error!("KindlyGuard server not configured");
2670 return Err(anyhow::anyhow!("Server not in configuration"));
2671 }
2672
2673 Ok(())
2674 }
2675
2676 async fn show_mcp_status(show_processes: bool) -> Result<()> {
2677 tracing::info!("Checking MCP server status");
2678
2679 let config_path = get_mcp_config_path()?;
2681 if !config_path.exists() {
2682 tracing::error!("MCP not configured");
2683 return Ok(());
2684 }
2685
2686 let content = std::fs::read_to_string(&config_path)?;
2687 let config: McpConfig = serde_json::from_str(&content)?;
2688
2689 tracing::info!("Configuration loaded from: {:?}", config_path);
2690
2691 if let Some(server) = config.servers.get("kindly-guard") {
2693 tracing::info!("KindlyGuard server configured:");
2694 tracing::info!(" Command: {}", server.command);
2695 tracing::info!(" Args: {:?}", server.args);
2696 } else {
2697 tracing::warn!("KindlyGuard server not configured");
2698 }
2699
2700 if show_processes {
2702 check_running_processes()?;
2703 }
2704
2705 Ok(())
2706 }
2707
2708 async fn start_mcp_server(daemon: bool) -> Result<()> {
2709 tracing::info!("Starting MCP server");
2710
2711 let config_path = get_mcp_config_path()?;
2713 let content = std::fs::read_to_string(&config_path)?;
2714 let config: McpConfig = serde_json::from_str(&content)?;
2715
2716 let server = config
2717 .servers
2718 .get("kindly-guard")
2719 .ok_or_else(|| anyhow::anyhow!("KindlyGuard server not configured"))?;
2720
2721 if daemon {
2722 tracing::info!("Starting server in daemon mode");
2724
2725 let mut cmd = Command::new(&server.command);
2726 cmd.args(&server.args);
2727 cmd.stdin(Stdio::null());
2728 cmd.stdout(Stdio::null());
2729 cmd.stderr(Stdio::null());
2730
2731 for (key, value) in &server.env {
2732 cmd.env(key, value);
2733 }
2734
2735 cmd.spawn()?;
2736 tracing::info!("Server started in background");
2737 } else {
2738 tracing::info!("Starting server in foreground mode");
2740 tracing::info!("Press Ctrl+C to stop");
2741
2742 let mut cmd = Command::new(&server.command);
2743 cmd.args(&server.args);
2744
2745 for (key, value) in &server.env {
2746 cmd.env(key, value);
2747 }
2748
2749 let status = cmd.status()?;
2750 if !status.success() {
2751 tracing::error!("Server exited with status: {:?}", status);
2752 }
2753 }
2754
2755 Ok(())
2756 }
2757
2758 async fn stop_mcp_server(force: bool) -> Result<()> {
2759 tracing::info!("Stopping MCP server");
2760
2761 let pids = find_kindlyguard_processes()?;
2763
2764 if pids.is_empty() {
2765 tracing::info!("No running KindlyGuard processes found");
2766 return Ok(());
2767 }
2768
2769 tracing::info!("Found {} running process(es)", pids.len());
2770
2771 for pid in pids {
2772 if force {
2773 Command::new("kill")
2774 .arg("-9")
2775 .arg(pid.to_string())
2776 .status()?;
2777 tracing::info!("Force killed process {}", pid);
2778 } else {
2779 Command::new("kill")
2780 .arg("-TERM")
2781 .arg(pid.to_string())
2782 .status()?;
2783 tracing::info!("Sent TERM signal to process {}", pid);
2784 }
2785 }
2786
2787 Ok(())
2788 }
2789
2790 async fn list_mcp_servers() -> Result<()> {
2791 let config_path = get_mcp_config_path()?;
2792
2793 if !config_path.exists() {
2794 tracing::warn!("No MCP configuration found at {:?}", config_path);
2795 return Ok(());
2796 }
2797
2798 let content = tokio::fs::read_to_string(&config_path).await?;
2799 let config: McpConfig = serde_json::from_str(&content)?;
2800
2801 tracing::info!("Installed MCP servers:");
2802 for (name, server) in config.servers.iter() {
2803 tracing::info!(" {} - {}", name, server.command);
2804 }
2805
2806 Ok(())
2807 }
2808
2809 async fn configure_mcp(file: Option<&Path>, show: bool) -> Result<()> {
2810 let config_path = file
2811 .map(PathBuf::from)
2812 .unwrap_or_else(|| get_mcp_config_path().unwrap());
2813
2814 if show {
2815 if !config_path.exists() {
2816 tracing::error!("Configuration file not found: {:?}", config_path);
2817 return Ok(());
2818 }
2819
2820 let content = std::fs::read_to_string(&config_path)?;
2821 println!("{}", content);
2822 } else if let Some(custom_file) = file {
2823 tracing::info!("Loading configuration from: {:?}", custom_file);
2824
2825 if !custom_file.exists() {
2826 return Err(anyhow::anyhow!("Configuration file not found"));
2827 }
2828
2829 let content = std::fs::read_to_string(custom_file)?;
2831 let _: McpConfig = serde_json::from_str(&content)?;
2832
2833 let default_path = get_mcp_config_path()?;
2835 std::fs::copy(custom_file, &default_path)?;
2836 tracing::info!("Configuration updated at: {:?}", default_path);
2837 } else {
2838 tracing::info!("Opening configuration editor...");
2840 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
2841
2842 Command::new(&editor).arg(&config_path).status()?;
2843 }
2844
2845 Ok(())
2846 }
2847
2848 async fn test_mcp_server(server: &str) -> Result<()> {
2849 tracing::info!("Testing MCP server '{}'...", server);
2850
2851 let config_path = get_mcp_config_path()?;
2852 let content = std::fs::read_to_string(&config_path)?;
2853 let config: McpConfig = serde_json::from_str(&content)?;
2854
2855 if let Some(server_config) = config.servers.get(server) {
2856 test_mcp_protocol(&server_config.command, &server_config.args)?;
2857 tracing::info!("Test completed successfully");
2858 } else {
2859 tracing::error!("Server '{}' not found in configuration", server);
2860 }
2861
2862 Ok(())
2863 }
2864
2865 fn get_mcp_config_path() -> Result<PathBuf> {
2868 let home = home_dir()?;
2869
2870 let candidates = vec![home.join(".mcp.json"), home.join(".config/claude/mcp.json")];
2872
2873 for path in &candidates {
2874 if path.exists() {
2875 return Ok(path.clone());
2876 }
2877 }
2878
2879 Ok(home.join(".mcp.json"))
2881 }
2882
2883 fn find_project_root() -> Result<PathBuf> {
2884 let mut current = std::env::current_dir()?;
2885
2886 loop {
2887 if current.join("Cargo.toml").exists() && current.join("kindly-guard-server").exists() {
2888 return Ok(current);
2889 }
2890
2891 if let Some(parent) = current.parent() {
2892 current = parent.to_path_buf();
2893 } else {
2894 break;
2895 }
2896 }
2897
2898 let home = home_dir()?;
2900 let candidates = vec![
2901 home.join("kindly-guard"),
2902 PathBuf::from("/home/samuel/kindly-guard"),
2903 ];
2904
2905 for path in candidates {
2906 if path.join("Cargo.toml").exists() && path.join("kindly-guard-server").exists() {
2907 return Ok(path);
2908 }
2909 }
2910
2911 Err(anyhow::anyhow!("Could not find kindly-guard project root"))
2912 }
2913
2914 fn find_kindlyguard_server() -> Result<PathBuf> {
2915 let candidates = vec![
2916 PathBuf::from("target/release/kindly-guard-server"),
2917 PathBuf::from("target/debug/kindly-guard-server"),
2918 PathBuf::from("../kindly-guard-server/target/release/kindly-guard-server"),
2919 PathBuf::from("../kindly-guard-server/target/debug/kindly-guard-server"),
2920 PathBuf::from("/usr/local/bin/kindly-guard-server"),
2921 PathBuf::from("/usr/bin/kindly-guard-server"),
2922 home_dir()?.join(".cargo/bin/kindly-guard-server"),
2923 ];
2924
2925 for path in candidates {
2926 if path.exists() {
2927 return Ok(path.canonicalize()?);
2928 }
2929 }
2930
2931 if let Ok(output) = Command::new("which").arg("kindly-guard-server").output() {
2933 if output.status.success() {
2934 let path = String::from_utf8_lossy(&output.stdout);
2935 return Ok(PathBuf::from(path.trim()));
2936 }
2937 }
2938
2939 Err(anyhow::anyhow!(
2940 "KindlyGuard server not found. Build it with 'cargo build --release' in the kindly-guard directory"
2941 ))
2942 }
2943
2944 fn test_mcp_protocol(command: &str, args: &[String]) -> Result<()> {
2945 let init_request = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"kindly-tools","version":"0.1.0"}}}"#;
2946
2947 let mut child = Command::new(command)
2948 .args(args)
2949 .stdin(Stdio::piped())
2950 .stdout(Stdio::piped())
2951 .stderr(Stdio::null())
2952 .spawn()?;
2953
2954 if let Some(stdin) = child.stdin.as_mut() {
2955 stdin.write_all(init_request.as_bytes())?;
2956 stdin.write_all(b"\n")?;
2957 stdin.flush()?;
2958 }
2959
2960 std::thread::sleep(std::time::Duration::from_millis(500));
2962
2963 let output = child.wait_with_output()?;
2964 let response = String::from_utf8_lossy(&output.stdout);
2965
2966 if response.contains("jsonrpc") && response.contains("result") {
2967 tracing::info!("MCP protocol test successful");
2968 } else if response.contains("error") {
2969 tracing::error!("MCP protocol test failed");
2970 return Err(anyhow::anyhow!("MCP protocol error"));
2971 } else {
2972 tracing::warn!("Unexpected MCP protocol response");
2973 }
2974
2975 Ok(())
2976 }
2977
2978 fn check_running_processes() -> Result<()> {
2979 tracing::info!("Checking for running processes...");
2980
2981 let output = Command::new("ps").args(["aux"]).output()?;
2982
2983 let output_str = String::from_utf8_lossy(&output.stdout);
2984 let mut found_any = false;
2985
2986 for line in output_str.lines() {
2987 if line.contains("kindly-guard-server") && !line.contains("grep") {
2988 println!("{}", line);
2989 found_any = true;
2990 }
2991 }
2992
2993 if !found_any {
2994 tracing::info!("No running KindlyGuard processes found");
2995 }
2996
2997 Ok(())
2998 }
2999
3000 fn find_kindlyguard_processes() -> Result<Vec<u32>> {
3001 let output = Command::new("pgrep")
3002 .arg("-f")
3003 .arg("kindly-guard-server")
3004 .output()?;
3005
3006 if !output.status.success() {
3007 return Ok(vec![]);
3008 }
3009
3010 let output_str = String::from_utf8_lossy(&output.stdout);
3011 let pids: Vec<u32> = output_str
3012 .lines()
3013 .filter_map(|line| line.trim().parse().ok())
3014 .collect();
3015
3016 Ok(pids)
3017 }
3018} pub mod wrap {
3022 use super::*;
3023 use clap::Args;
3024
3025 #[derive(Debug, Args)]
3027 pub struct WrapCommand {
3028 #[arg(trailing_var_arg = true, required = true)]
3030 pub command: Vec<String>,
3031
3032 #[arg(short, long, default_value = "http://localhost:8080")]
3034 pub server: String,
3035
3036 #[arg(short, long)]
3038 pub block: bool,
3039 }
3040
3041 impl Execute for WrapCommand {
3042 async fn execute(&self) -> Result<()> {
3043 crate::commands::wrap::wrap_command(
3044 self.command.clone(),
3045 self.server.clone(),
3046 self.block,
3047 )
3048 .await
3049 }
3050 }
3051}
3052
3053pub mod monitor {
3054 use super::*;
3055 use clap::Args;
3056
3057 #[derive(Debug, Args)]
3059 pub struct MonitorCommand {
3060 #[arg(short, long, default_value = "http://localhost:8080")]
3062 pub url: String,
3063
3064 #[arg(short, long, default_value = "5")]
3066 pub interval: u64,
3067 }
3068
3069 impl Execute for MonitorCommand {
3070 async fn execute(&self) -> Result<()> {
3071 crate::commands::monitor::run(self.url.clone(), self.interval).await
3072 }
3073 }
3074}
3075
3076pub mod shield {
3077 use super::*;
3078
3079 pub use crate::commands::shield::ShieldCommand;
3080
3081 impl Execute for ShieldCommand {
3082 async fn execute(&self) -> Result<()> {
3083 let cmd = self.clone();
3085 cmd.run().await
3086 }
3087 }
3088}
3089
3090pub mod utils {
3091 use super::*;
3092 use std::process::Command;
3093
3094 pub fn run_command(cmd: &str, args: &[&str]) -> Result<String> {
3096 let output = Command::new(cmd).args(args).output()?;
3097
3098 if !output.status.success() {
3099 anyhow::bail!("Command failed: {} {}", cmd, args.join(" "));
3100 }
3101
3102 Ok(String::from_utf8(output.stdout)?)
3103 }
3104
3105 pub fn is_ci() -> bool {
3107 std::env::var("CI").is_ok()
3108 }
3109
3110 pub fn current_git_branch() -> Result<String> {
3112 run_command("git", &["rev-parse", "--abbrev-ref", "HEAD"]).map(|s| s.trim().to_string())
3113 }
3114}