1use anyhow::{Context, Result};
10use std::env;
11use std::fs;
12use std::path::PathBuf;
13use std::process::{Command, Stdio};
14use tracing::{error, info, warn};
15
16#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum Platform {
22 Linux,
23 MacOS,
24 Windows,
25 Unknown,
26}
27
28impl Platform {
29 pub fn current() -> Self {
30 #[cfg(target_os = "linux")]
31 return Platform::Linux;
32
33 #[cfg(target_os = "macos")]
34 return Platform::MacOS;
35
36 #[cfg(target_os = "windows")]
37 return Platform::Windows;
38
39 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
40 return Platform::Unknown;
41 }
42
43 pub fn service_manager_name(&self) -> &'static str {
44 match self {
45 Platform::Linux => "systemd",
46 Platform::MacOS => "launchctl",
47 Platform::Windows => "sc.exe",
48 Platform::Unknown => "unknown",
49 }
50 }
51}
52
53const DAEMON_PORT: u16 = 28428;
58
59const SYSTEMD_DAEMON_SERVICE: &str = "smart-tree-daemon.service";
61const SYSTEMD_SYSTEM_PATH: &str = "/etc/systemd/system";
62
63const LAUNCHD_DAEMON_LABEL: &str = "is.8b.smart-tree-daemon";
65const LAUNCHD_DAEMON_PLIST: &str = "/Library/LaunchDaemons/is.8b.smart-tree-daemon.plist";
66
67#[cfg(target_os = "windows")]
69const WINDOWS_SERVICE_NAME: &str = "SmartTreeDaemon";
70
71fn is_elevated() -> bool {
77 #[cfg(unix)]
78 {
79 unsafe { libc::geteuid() == 0 }
80 }
81 #[cfg(windows)]
82 {
83 use std::process::Command;
85 Command::new("net")
86 .args(["session"])
87 .stdout(Stdio::null())
88 .stderr(Stdio::null())
89 .status()
90 .map(|s| s.success())
91 .unwrap_or(false)
92 }
93}
94
95fn escalate_privileges(args: &[&str]) -> Result<bool> {
98 if is_elevated() {
99 return Ok(false);
100 }
101
102 let exe = env::current_exe().context("Could not determine executable path")?;
103
104 println!("This operation requires elevated privileges.");
105 println!();
106
107 #[cfg(target_os = "linux")]
108 {
109 let cmd = if which_binary("pkexec").is_some() {
111 println!("Requesting permission via pkexec...");
112 let mut c = Command::new("pkexec");
113 c.arg(&exe);
114 c.args(args);
115 c
116 } else {
117 println!("Requesting permission via sudo...");
118 let mut c = Command::new("sudo");
119 c.arg(&exe);
120 c.args(args);
121 c
122 };
123 let status = run_elevated(cmd)?;
124 if !status.success() {
125 anyhow::bail!("Elevated command failed (exit code: {:?})", status.code());
126 }
127 return Ok(true);
128 }
129
130 #[cfg(target_os = "macos")]
131 {
132 if env::var("TERM_PROGRAM").is_ok() || env::var("SSH_CONNECTION").is_ok() {
134 println!("Requesting permission via sudo...");
135 let mut cmd = Command::new("sudo");
136 cmd.arg(&exe);
137 cmd.args(args);
138 let status = run_elevated(cmd)?;
139 if !status.success() {
140 anyhow::bail!("Elevated command failed");
141 }
142 } else {
143 let args_str = std::iter::once(exe.to_string_lossy().to_string())
145 .chain(args.iter().map(|a| a.to_string()))
146 .collect::<Vec<_>>()
147 .join("' '");
148 let script = format!(
149 "do shell script \"'{}' \" with administrator privileges",
150 args_str
151 );
152 let mut cmd = Command::new("osascript");
153 cmd.args(["-e", &script]);
154 let status = run_elevated(cmd)?;
155 if !status.success() {
156 anyhow::bail!("Elevated command failed");
157 }
158 }
159 return Ok(true);
160 }
161
162 #[cfg(target_os = "windows")]
163 {
164 let args_joined = args.join(" ");
166 let ps_cmd = format!(
167 "Start-Process -FilePath '{}' -ArgumentList '{}' -Verb RunAs -Wait",
168 exe.display(),
169 args_joined
170 );
171 println!("Requesting administrator permission...");
172 let mut cmd = Command::new("powershell");
173 cmd.args(["-Command", &ps_cmd]);
174 let status = run_elevated(cmd)?;
175 if !status.success() {
176 anyhow::bail!("Elevated command failed");
177 }
178 return Ok(true);
179 }
180
181 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
182 anyhow::bail!("Privilege escalation not supported on this platform")
183}
184
185fn run_elevated(mut cmd: Command) -> Result<std::process::ExitStatus> {
186 cmd.stdout(Stdio::inherit())
187 .stderr(Stdio::inherit())
188 .stdin(Stdio::inherit())
189 .status()
190 .context("Failed to run elevated command")
191}
192
193pub fn install() -> Result<()> {
200 let platform = Platform::current();
201 println!(
202 "Installing Smart Tree daemon as system service ({})...",
203 platform.service_manager_name()
204 );
205 println!();
206
207 match platform {
208 Platform::Linux => linux_install(),
209 Platform::MacOS => macos_install(),
210 Platform::Windows => windows_install(),
211 Platform::Unknown => anyhow::bail!("Unsupported platform for service management"),
212 }
213}
214
215pub fn uninstall() -> Result<()> {
217 match Platform::current() {
218 Platform::Linux => linux_uninstall(),
219 Platform::MacOS => macos_uninstall(),
220 Platform::Windows => windows_uninstall(),
221 Platform::Unknown => anyhow::bail!("Unsupported platform"),
222 }
223}
224
225pub fn start() -> Result<()> {
227 match Platform::current() {
228 Platform::Linux => linux_start(),
229 Platform::MacOS => macos_start(),
230 Platform::Windows => windows_start(),
231 Platform::Unknown => anyhow::bail!("Unsupported platform"),
232 }
233}
234
235pub fn stop() -> Result<()> {
237 match Platform::current() {
238 Platform::Linux => linux_stop(),
239 Platform::MacOS => macos_stop(),
240 Platform::Windows => windows_stop(),
241 Platform::Unknown => anyhow::bail!("Unsupported platform"),
242 }
243}
244
245pub fn status() -> Result<()> {
247 match Platform::current() {
248 Platform::Linux => linux_status(),
249 Platform::MacOS => macos_status(),
250 Platform::Windows => windows_status(),
251 Platform::Unknown => anyhow::bail!("Unsupported platform"),
252 }
253}
254
255pub fn logs() -> Result<()> {
257 match Platform::current() {
258 Platform::Linux => linux_logs(),
259 Platform::MacOS => macos_logs(),
260 Platform::Windows => windows_logs(),
261 Platform::Unknown => anyhow::bail!("Unsupported platform"),
262 }
263}
264
265pub fn daemon_install_system() -> Result<()> {
267 eprintln!("Note: --daemon-install is deprecated, use `st service install` instead.");
268 install()
269}
270
271fn linux_install() -> Result<()> {
276 if escalate_privileges(&["service", "install"])? {
278 return Ok(()); }
280
281 info!("Installing systemd system service...");
283
284 let install_path = install_binary()?;
286
287 fs::create_dir_all("/var/lib/smart-tree")
289 .context("Failed to create /var/lib/smart-tree")?;
290
291 let hash = compute_file_hash(&install_path)?;
293 fs::write("/var/lib/smart-tree/daemon.sha256", &hash)?;
294
295 let service_dest = PathBuf::from(SYSTEMD_SYSTEM_PATH).join(SYSTEMD_DAEMON_SERVICE);
297 let service_content = generate_systemd_unit(&install_path);
298 fs::write(&service_dest, &service_content)
299 .with_context(|| format!("Failed to write {}", service_dest.display()))?;
300
301 run_command("systemctl", &["daemon-reload"])?;
303 run_command("systemctl", &["enable", "--now", SYSTEMD_DAEMON_SERVICE])?;
304
305 print_install_success("systemctl status smart-tree-daemon", "journalctl -u smart-tree-daemon -f");
306 Ok(())
307}
308
309fn linux_uninstall() -> Result<()> {
310 if escalate_privileges(&["service", "uninstall"])? {
311 return Ok(());
312 }
313
314 let _ = run_command("systemctl", &["stop", SYSTEMD_DAEMON_SERVICE]);
315 let _ = run_command("systemctl", &["disable", SYSTEMD_DAEMON_SERVICE]);
316
317 let service_path = PathBuf::from(SYSTEMD_SYSTEM_PATH).join(SYSTEMD_DAEMON_SERVICE);
318 if service_path.exists() {
319 fs::remove_file(&service_path)?;
320 }
321 run_command("systemctl", &["daemon-reload"])?;
322
323 println!("Service uninstalled.");
324 Ok(())
325}
326
327fn linux_start() -> Result<()> {
328 if escalate_privileges(&["service", "start"])? {
329 return Ok(());
330 }
331 run_command("systemctl", &["start", SYSTEMD_DAEMON_SERVICE])?;
332 println!("Service started. Dashboard: http://localhost:{}", DAEMON_PORT);
333 Ok(())
334}
335
336fn linux_stop() -> Result<()> {
337 if escalate_privileges(&["service", "stop"])? {
338 return Ok(());
339 }
340 run_command("systemctl", &["stop", SYSTEMD_DAEMON_SERVICE])?;
341 println!("Service stopped.");
342 Ok(())
343}
344
345fn linux_status() -> Result<()> {
346 println!("Smart Tree Daemon Status (Linux/systemd)");
348 println!("─────────────────────────────────────────");
349 let _ = run_command("systemctl", &["status", SYSTEMD_DAEMON_SERVICE, "--no-pager"]);
350 Ok(())
351}
352
353fn linux_logs() -> Result<()> {
354 let _ = run_command("journalctl", &["-u", SYSTEMD_DAEMON_SERVICE, "-n", "50", "--no-pager", "-f"]);
355 Ok(())
356}
357
358fn generate_systemd_unit(binary_path: &PathBuf) -> String {
359 format!(
360 r#"[Unit]
361Description=Smart Tree Daemon - AI Context Service
362Documentation=https://github.com/8b-is/smart-tree
363After=network-online.target
364Wants=network-online.target
365
366[Service]
367Type=simple
368ExecStart={binary} --http-daemon
369
370StateDirectory=smart-tree
371StateDirectoryMode=0755
372RuntimeDirectory=smart-tree
373RuntimeDirectoryMode=0755
374
375Environment=RUST_LOG=info
376Environment=ST_TOKEN_PATH=/var/lib/smart-tree/daemon.token
377
378Restart=always
379RestartSec=10
380TimeoutStopSec=30
381
382StandardOutput=journal
383StandardError=journal
384SyslogIdentifier=smart-tree-daemon
385
386WorkingDirectory=/var/lib/smart-tree
387
388NoNewPrivileges=yes
389ProtectSystem=strict
390ProtectHome=read-only
391PrivateTmp=yes
392ProtectKernelTunables=yes
393ProtectKernelModules=yes
394ProtectControlGroups=yes
395ReadOnlyPaths=/home
396
397[Install]
398WantedBy=multi-user.target
399"#,
400 binary = binary_path.display(),
401 )
402}
403
404fn macos_install() -> Result<()> {
409 if escalate_privileges(&["service", "install"])? {
410 return Ok(());
411 }
412
413 info!("Installing LaunchDaemon...");
414
415 let install_path = install_binary()?;
417
418 let state_dir = PathBuf::from("/var/lib/smart-tree");
420 fs::create_dir_all(&state_dir).context("Failed to create /var/lib/smart-tree")?;
421
422 let hash = compute_file_hash(&install_path)?;
424 fs::write(state_dir.join("daemon.sha256"), &hash)?;
425
426 let log_dir = PathBuf::from("/var/log/smart-tree");
428 fs::create_dir_all(&log_dir).context("Failed to create /var/log/smart-tree")?;
429
430 let plist = generate_launchd_daemon_plist(&install_path);
432 fs::write(LAUNCHD_DAEMON_PLIST, &plist)
433 .with_context(|| format!("Failed to write {}", LAUNCHD_DAEMON_PLIST))?;
434
435 run_command("chown", &["root:wheel", LAUNCHD_DAEMON_PLIST])?;
437 run_command("chmod", &["644", LAUNCHD_DAEMON_PLIST])?;
438
439 let load_result = Command::new("launchctl")
442 .args(["bootstrap", "system", LAUNCHD_DAEMON_PLIST])
443 .stdout(Stdio::inherit())
444 .stderr(Stdio::inherit())
445 .status();
446
447 match load_result {
448 Ok(s) if s.success() => {}
449 _ => {
450 info!("bootstrap failed, trying legacy load...");
452 run_command("launchctl", &["load", "-w", LAUNCHD_DAEMON_PLIST])?;
453 }
454 }
455
456 print_install_success(
457 "sudo launchctl list | grep smart-tree",
458 "tail -f /var/log/smart-tree/daemon.log",
459 );
460 Ok(())
461}
462
463fn macos_uninstall() -> Result<()> {
464 if escalate_privileges(&["service", "uninstall"])? {
465 return Ok(());
466 }
467
468 let _ = Command::new("launchctl")
470 .args(["bootout", "system", LAUNCHD_DAEMON_PLIST])
471 .stdout(Stdio::null())
472 .stderr(Stdio::null())
473 .status();
474 let _ = Command::new("launchctl")
475 .args(["unload", LAUNCHD_DAEMON_PLIST])
476 .stdout(Stdio::null())
477 .stderr(Stdio::null())
478 .status();
479
480 if PathBuf::from(LAUNCHD_DAEMON_PLIST).exists() {
481 fs::remove_file(LAUNCHD_DAEMON_PLIST)?;
482 }
483
484 println!("Service uninstalled.");
485 Ok(())
486}
487
488fn macos_start() -> Result<()> {
489 if escalate_privileges(&["service", "start"])? {
490 return Ok(());
491 }
492 let result = Command::new("launchctl")
494 .args(["kickstart", &format!("system/{}", LAUNCHD_DAEMON_LABEL)])
495 .stdout(Stdio::inherit())
496 .stderr(Stdio::inherit())
497 .status();
498
499 match result {
500 Ok(s) if s.success() => {}
501 _ => {
502 run_command("launchctl", &["load", "-w", LAUNCHD_DAEMON_PLIST])?;
504 }
505 }
506
507 println!("Service started. Dashboard: http://localhost:{}", DAEMON_PORT);
508 Ok(())
509}
510
511fn macos_stop() -> Result<()> {
512 if escalate_privileges(&["service", "stop"])? {
513 return Ok(());
514 }
515 let _ = Command::new("launchctl")
516 .args(["kill", "SIGTERM", &format!("system/{}", LAUNCHD_DAEMON_LABEL)])
517 .stdout(Stdio::inherit())
518 .stderr(Stdio::inherit())
519 .status();
520
521 println!("Service stopped.");
522 Ok(())
523}
524
525fn macos_status() -> Result<()> {
526 println!("Smart Tree Daemon Status (macOS/launchd)");
527 println!("─────────────────────────────────────────");
528
529 if !PathBuf::from(LAUNCHD_DAEMON_PLIST).exists() {
530 println!("Status: NOT INSTALLED");
531 println!("Install: st service install");
532 return Ok(());
533 }
534
535 let output = Command::new("launchctl")
536 .args(["print", &format!("system/{}", LAUNCHD_DAEMON_LABEL)])
537 .output();
538
539 match output {
540 Ok(out) if out.status.success() => {
541 let text = String::from_utf8_lossy(&out.stdout);
542 if text.contains("state = running") {
543 println!("Status: RUNNING");
544 } else {
545 println!("Status: LOADED (not running)");
546 }
547 for line in text.lines() {
549 let trimmed = line.trim();
550 if trimmed.starts_with("pid =") || trimmed.starts_with("state =") {
551 println!(" {}", trimmed);
552 }
553 }
554 }
555 _ => {
556 let list = Command::new("launchctl")
558 .args(["list"])
559 .output();
560 if let Ok(out) = list {
561 let text = String::from_utf8_lossy(&out.stdout);
562 if text.contains(LAUNCHD_DAEMON_LABEL) {
563 println!("Status: LOADED");
564 } else {
565 println!("Status: NOT LOADED");
566 }
567 }
568 }
569 }
570
571 Ok(())
572}
573
574fn macos_logs() -> Result<()> {
575 let log_path = "/var/log/smart-tree/daemon.log";
576 if PathBuf::from(log_path).exists() {
577 println!("Showing logs from {}", log_path);
578 println!("─────────────────────────────────");
579 run_command("tail", &["-f", log_path])?;
580 } else {
581 println!("Showing logs from system log...");
583 run_command("log", &["show", "--predicate", "process == \"st\"", "--last", "1h", "--style", "compact"])?;
584 }
585 Ok(())
586}
587
588fn generate_launchd_daemon_plist(binary_path: &PathBuf) -> String {
589 format!(
590 r#"<?xml version="1.0" encoding="UTF-8"?>
591<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
592<plist version="1.0">
593<dict>
594 <key>Label</key>
595 <string>{label}</string>
596
597 <key>ProgramArguments</key>
598 <array>
599 <string>{binary}</string>
600 <string>--http-daemon</string>
601 </array>
602
603 <key>RunAtLoad</key>
604 <true/>
605
606 <key>KeepAlive</key>
607 <true/>
608
609 <key>WorkingDirectory</key>
610 <string>/var/lib/smart-tree</string>
611
612 <key>StandardOutPath</key>
613 <string>/var/log/smart-tree/daemon.log</string>
614
615 <key>StandardErrorPath</key>
616 <string>/var/log/smart-tree/daemon.log</string>
617
618 <key>EnvironmentVariables</key>
619 <dict>
620 <key>RUST_LOG</key>
621 <string>info</string>
622 <key>ST_TOKEN_PATH</key>
623 <string>/var/lib/smart-tree/daemon.token</string>
624 </dict>
625
626 <key>ThrottleInterval</key>
627 <integer>10</integer>
628</dict>
629</plist>
630"#,
631 label = LAUNCHD_DAEMON_LABEL,
632 binary = binary_path.display(),
633 )
634}
635
636fn windows_install() -> Result<()> {
641 #[cfg(not(target_os = "windows"))]
642 {
643 anyhow::bail!("Windows service management is only available on Windows");
644 }
645
646 #[cfg(target_os = "windows")]
647 {
648 if escalate_privileges(&["service", "install"])? {
649 return Ok(());
650 }
651
652 info!("Installing Windows service...");
653
654 let install_path = install_binary()?;
656
657 let state_dir = dirs::data_dir()
659 .unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
660 .join("SmartTree");
661 fs::create_dir_all(&state_dir)?;
662
663 let hash = compute_file_hash(&install_path)?;
665 fs::write(state_dir.join("daemon.sha256"), &hash)?;
666
667 let bin_arg = format!(
669 "\"{}\" --http-daemon --daemon-port {}",
670 install_path.display(),
671 DAEMON_PORT
672 );
673
674 let _ = Command::new("sc.exe")
676 .args(["delete", WINDOWS_SERVICE_NAME])
677 .stdout(Stdio::null())
678 .stderr(Stdio::null())
679 .status();
680
681 run_command(
682 "sc.exe",
683 &[
684 "create",
685 WINDOWS_SERVICE_NAME,
686 &format!("binPath= {}", bin_arg),
687 "start= auto",
688 "DisplayName= Smart Tree Daemon",
689 ],
690 )?;
691
692 run_command(
694 "sc.exe",
695 &[
696 "description",
697 WINDOWS_SERVICE_NAME,
698 "Smart Tree - AI-friendly directory visualization daemon",
699 ],
700 )?;
701
702 run_command(
704 "sc.exe",
705 &[
706 "failure",
707 WINDOWS_SERVICE_NAME,
708 "reset= 86400",
709 "actions= restart/10000/restart/30000/restart/60000",
710 ],
711 )?;
712
713 run_command("sc.exe", &["start", WINDOWS_SERVICE_NAME])?;
715
716 print_install_success(
717 "sc.exe query SmartTreeDaemon",
718 "Get-EventLog -LogName Application -Source SmartTreeDaemon",
719 );
720 Ok(())
721 }
722}
723
724fn windows_uninstall() -> Result<()> {
725 #[cfg(not(target_os = "windows"))]
726 {
727 anyhow::bail!("Windows service management is only available on Windows");
728 }
729
730 #[cfg(target_os = "windows")]
731 {
732 if escalate_privileges(&["service", "uninstall"])? {
733 return Ok(());
734 }
735
736 let _ = Command::new("sc.exe")
737 .args(["stop", WINDOWS_SERVICE_NAME])
738 .stdout(Stdio::null())
739 .stderr(Stdio::null())
740 .status();
741
742 run_command("sc.exe", &["delete", WINDOWS_SERVICE_NAME])?;
743 println!("Service uninstalled.");
744 Ok(())
745 }
746}
747
748fn windows_start() -> Result<()> {
749 #[cfg(not(target_os = "windows"))]
750 {
751 anyhow::bail!("Windows service management is only available on Windows");
752 }
753
754 #[cfg(target_os = "windows")]
755 {
756 if escalate_privileges(&["service", "start"])? {
757 return Ok(());
758 }
759 run_command("sc.exe", &["start", WINDOWS_SERVICE_NAME])?;
760 println!("Service started. Dashboard: http://localhost:{}", DAEMON_PORT);
761 Ok(())
762 }
763}
764
765fn windows_stop() -> Result<()> {
766 #[cfg(not(target_os = "windows"))]
767 {
768 anyhow::bail!("Windows service management is only available on Windows");
769 }
770
771 #[cfg(target_os = "windows")]
772 {
773 if escalate_privileges(&["service", "stop"])? {
774 return Ok(());
775 }
776 run_command("sc.exe", &["stop", WINDOWS_SERVICE_NAME])?;
777 println!("Service stopped.");
778 Ok(())
779 }
780}
781
782fn windows_status() -> Result<()> {
783 #[cfg(not(target_os = "windows"))]
784 {
785 anyhow::bail!("Windows service management is only available on Windows");
786 }
787
788 #[cfg(target_os = "windows")]
789 {
790 println!("Smart Tree Daemon Status (Windows)");
791 println!("──────────────────────────────────");
792 let _ = run_command("sc.exe", &["query", WINDOWS_SERVICE_NAME]);
793 Ok(())
794 }
795}
796
797fn windows_logs() -> Result<()> {
798 #[cfg(not(target_os = "windows"))]
799 {
800 anyhow::bail!("Windows service management is only available on Windows");
801 }
802
803 #[cfg(target_os = "windows")]
804 {
805 let log_path = dirs::data_dir()
806 .unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
807 .join("SmartTree")
808 .join("daemon.log");
809
810 if log_path.exists() {
811 println!("Showing logs from {}", log_path.display());
812 run_command("powershell", &["-Command", &format!("Get-Content -Path '{}' -Tail 50 -Wait", log_path.display())])?;
813 } else {
814 println!("Log file not found at {}", log_path.display());
815 println!("Try: Get-EventLog -LogName Application -Source SmartTreeDaemon -Newest 50");
816 }
817 Ok(())
818 }
819}
820
821fn install_binary() -> Result<PathBuf> {
828 let current_exe = env::current_exe().context("Failed to get current executable path")?;
829
830 #[cfg(unix)]
831 let install_path = PathBuf::from("/usr/local/bin/st");
832
833 #[cfg(windows)]
834 let install_path = {
835 let prog = env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
836 let dir = PathBuf::from(prog).join("SmartTree");
837 fs::create_dir_all(&dir)?;
838 dir.join("st.exe")
839 };
840
841 if current_exe != install_path {
842 println!(" Copying binary to {}...", install_path.display());
843 fs::copy(¤t_exe, &install_path)
844 .with_context(|| format!("Failed to copy binary to {}", install_path.display()))?;
845
846 #[cfg(unix)]
847 {
848 use std::os::unix::fs::PermissionsExt;
849 fs::set_permissions(&install_path, fs::Permissions::from_mode(0o755))?;
850 }
851 } else {
852 println!(" Binary already at {}.", install_path.display());
853 }
854
855 Ok(install_path)
856}
857
858fn compute_file_hash(path: &std::path::Path) -> Result<String> {
860 use sha2::{Digest, Sha256};
861 use std::io::Read;
862
863 let mut file =
864 fs::File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
865
866 let mut hasher = Sha256::new();
867 let mut buffer = [0u8; 8192];
868 loop {
869 let n = file.read(&mut buffer)?;
870 if n == 0 {
871 break;
872 }
873 hasher.update(&buffer[..n]);
874 }
875 Ok(format!("{:x}", hasher.finalize()))
876}
877
878fn run_command(command: &str, args: &[&str]) -> Result<()> {
880 info!("Running: {} {}", command, args.join(" "));
881 let status = Command::new(command)
882 .args(args)
883 .stdout(Stdio::inherit())
884 .stderr(Stdio::inherit())
885 .status()
886 .with_context(|| format!("Failed to execute: {}", command))?;
887
888 if !status.success() {
889 error!("Command failed: {} (exit: {})", command, status);
890 anyhow::bail!("Command failed: {} {}", command, args.join(" "));
891 }
892 Ok(())
893}
894
895fn which_binary(name: &str) -> Option<PathBuf> {
897 which::which(name).ok()
898}
899
900fn print_install_success(status_cmd: &str, logs_cmd: &str) {
902 println!();
903 println!("Smart Tree daemon installed and running!");
904 println!();
905 println!(" Dashboard: http://localhost:{}", DAEMON_PORT);
906 println!(" Token: /var/lib/smart-tree/daemon.token");
907 println!();
908 println!(" Status: {}", status_cmd);
909 println!(" Logs: {}", logs_cmd);
910 println!(" Stop: st service stop");
911 println!(" Uninstall: st service uninstall");
912}
913
914pub const OFFICIAL_GPG_FINGERPRINT: &str = "wraith@8b.is";
920
921pub fn verify_gpg_signature() -> SignatureStatus {
923 let sig_path = PathBuf::from("/usr/local/bin/st.sig");
924 let binary_path = PathBuf::from("/usr/local/bin/st");
925
926 if !sig_path.exists() {
927 return SignatureStatus::Unsigned;
928 }
929
930 let output = Command::new("gpg")
931 .args([
932 "--verify",
933 sig_path.to_string_lossy().as_ref(),
934 binary_path.to_string_lossy().as_ref(),
935 ])
936 .output();
937
938 match output {
939 Ok(result) => {
940 let stderr = String::from_utf8_lossy(&result.stderr);
941 if result.status.success() {
942 if stderr.contains(OFFICIAL_GPG_FINGERPRINT) {
943 SignatureStatus::OfficialBuild
944 } else {
945 SignatureStatus::CommunityBuild(extract_signer(&stderr))
946 }
947 } else if stderr.contains("BAD signature") {
948 SignatureStatus::TamperedOrInvalid
949 } else {
950 SignatureStatus::Unsigned
951 }
952 }
953 Err(_) => SignatureStatus::GpgNotAvailable,
954 }
955}
956
957fn extract_signer(gpg_output: &str) -> String {
958 for line in gpg_output.lines() {
959 if line.contains("Good signature from") {
960 return line.to_string();
961 }
962 }
963 "Unknown signer".to_string()
964}
965
966#[derive(Debug, Clone, PartialEq)]
967pub enum SignatureStatus {
968 OfficialBuild,
969 CommunityBuild(String),
970 Unsigned,
971 TamperedOrInvalid,
972 GpgNotAvailable,
973}
974
975pub fn print_signature_banner() {
977 let first_run_marker = dirs::data_dir()
978 .map(|d| d.join("smart-tree").join(".first_run_complete"))
979 .unwrap_or_else(|| PathBuf::from("/tmp/.st_first_run"));
980
981 if first_run_marker.exists() {
982 return;
983 }
984
985 let status = verify_gpg_signature();
986
987 println!();
988 match status {
989 SignatureStatus::OfficialBuild => {
990 println!(" OFFICIAL BUILD - Signed by 8bit-wraith (wraith@8b.is)");
991 println!(" This binary is cryptographically verified as an authentic release.");
992 }
993 SignatureStatus::CommunityBuild(ref signer) => {
994 println!(" COMMUNITY BUILD - Signed but NOT by the official 8b.is key.");
995 println!(
996 " Signer: {}",
997 &signer[..signer.len().min(70)]
998 );
999 println!(" Verify you trust this signer before proceeding.");
1000 }
1001 SignatureStatus::Unsigned => {
1002 println!(" UNSIGNED BUILD - No GPG signature found.");
1003 println!(" This is normal for dev builds or self-compiled versions.");
1004 println!(" Official releases: https://i1.is/smart-tree");
1005 }
1006 SignatureStatus::TamperedOrInvalid => {
1007 println!(" WARNING: SIGNATURE VERIFICATION FAILED");
1008 println!(" The binary signature does NOT match the file contents!");
1009 println!(" Re-download from https://i1.is/smart-tree");
1010 }
1011 SignatureStatus::GpgNotAvailable => {
1012 println!(" GPG not available - signature verification skipped.");
1013 println!(" Install gnupg to enable verification of official builds.");
1014 }
1015 }
1016 println!();
1017
1018 if let Some(parent) = first_run_marker.parent() {
1019 let _ = fs::create_dir_all(parent);
1020 }
1021 let _ = fs::write(&first_run_marker, "shown");
1022}
1023
1024pub fn guardian_verify_integrity() -> Result<bool> {
1030 let installed_path = PathBuf::from("/usr/local/bin/st");
1031
1032 if !installed_path.exists() {
1033 warn!("Binary not found at /usr/local/bin/st");
1034 return Ok(false);
1035 }
1036
1037 let installed_hash = compute_file_hash(&installed_path)?;
1038 let hash_file = PathBuf::from("/var/lib/smart-tree/guardian.sha256");
1039
1040 if hash_file.exists() {
1041 let stored_hash = fs::read_to_string(&hash_file)?.trim().to_string();
1042 if installed_hash != stored_hash {
1043 error!("INTEGRITY VIOLATION: Binary has been modified!");
1044 error!(" Expected: {}", stored_hash);
1045 error!(" Found: {}", installed_hash);
1046 return Ok(false);
1047 }
1048 info!("Binary integrity verified");
1049 Ok(true)
1050 } else {
1051 warn!("No stored hash found - cannot verify integrity");
1052 Ok(true)
1053 }
1054}
1055
1056pub fn guardian_install() -> Result<()> {
1058 println!("Smart Tree Guardian - System-wide AI Protection Daemon");
1059 println!();
1060
1061 if escalate_privileges(&["--guardian-install"])? {
1062 return Ok(());
1063 }
1064
1065 info!("Installing Guardian daemon...");
1066
1067 let target_bin = install_binary()?;
1069
1070 fs::create_dir_all("/var/lib/smart-tree")?;
1072 let hash = compute_file_hash(&target_bin)?;
1073 fs::write("/var/lib/smart-tree/guardian.sha256", &hash)?;
1074
1075 match Platform::current() {
1077 Platform::Linux => {
1078 let service_content = include_str!("../systemd/smart-tree-guardian.service");
1079 let service_path = format!("{}/{}", SYSTEMD_SYSTEM_PATH, "smart-tree-guardian.service");
1080 fs::write(&service_path, service_content)?;
1081 run_command("systemctl", &["daemon-reload"])?;
1082 run_command("systemctl", &["enable", "smart-tree-guardian.service"])?;
1083 run_command("systemctl", &["start", "smart-tree-guardian.service"])?;
1084 }
1085 Platform::MacOS => {
1086 let plist = format!(
1087 r#"<?xml version="1.0" encoding="UTF-8"?>
1088<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1089<plist version="1.0">
1090<dict>
1091 <key>Label</key>
1092 <string>is.8b.smart-tree-guardian</string>
1093 <key>ProgramArguments</key>
1094 <array>
1095 <string>{}</string>
1096 <string>--guardian-daemon</string>
1097 </array>
1098 <key>RunAtLoad</key>
1099 <true/>
1100 <key>KeepAlive</key>
1101 <true/>
1102 <key>WorkingDirectory</key>
1103 <string>/var/lib/smart-tree</string>
1104 <key>StandardOutPath</key>
1105 <string>/var/log/smart-tree/guardian.log</string>
1106 <key>StandardErrorPath</key>
1107 <string>/var/log/smart-tree/guardian.log</string>
1108</dict>
1109</plist>
1110"#,
1111 target_bin.display()
1112 );
1113 let plist_path = "/Library/LaunchDaemons/is.8b.smart-tree-guardian.plist";
1114 fs::create_dir_all("/var/log/smart-tree")?;
1115 fs::write(plist_path, &plist)?;
1116 run_command("chown", &["root:wheel", plist_path])?;
1117 run_command("chmod", &["644", plist_path])?;
1118 let _ = Command::new("launchctl")
1119 .args(["bootstrap", "system", plist_path])
1120 .status();
1121 }
1122 _ => {
1123 anyhow::bail!("Guardian is currently supported on Linux and macOS only");
1124 }
1125 }
1126
1127 println!();
1128 println!("Guardian installed and running!");
1129 println!(" Status: st --guardian-status");
1130 Ok(())
1131}
1132
1133pub fn guardian_uninstall() -> Result<()> {
1135 if escalate_privileges(&["--guardian-uninstall"])? {
1136 return Ok(());
1137 }
1138
1139 match Platform::current() {
1140 Platform::Linux => {
1141 let _ = run_command("systemctl", &["stop", "smart-tree-guardian.service"]);
1142 let _ = run_command("systemctl", &["disable", "smart-tree-guardian.service"]);
1143 let path = format!("{}/smart-tree-guardian.service", SYSTEMD_SYSTEM_PATH);
1144 if PathBuf::from(&path).exists() {
1145 fs::remove_file(&path)?;
1146 }
1147 run_command("systemctl", &["daemon-reload"])?;
1148 }
1149 Platform::MacOS => {
1150 let plist = "/Library/LaunchDaemons/is.8b.smart-tree-guardian.plist";
1151 let _ = Command::new("launchctl")
1152 .args(["bootout", "system", plist])
1153 .status();
1154 if PathBuf::from(plist).exists() {
1155 fs::remove_file(plist)?;
1156 }
1157 }
1158 _ => {}
1159 }
1160
1161 println!("Guardian uninstalled.");
1162 Ok(())
1163}
1164
1165pub fn guardian_status() -> Result<()> {
1167 println!("Smart Tree Guardian Status");
1168 println!("─────────────────────────");
1169
1170 match Platform::current() {
1171 Platform::Linux => {
1172 let service_path = format!("{}/smart-tree-guardian.service", SYSTEMD_SYSTEM_PATH);
1173 if !PathBuf::from(&service_path).exists() {
1174 println!("Status: NOT INSTALLED");
1175 println!("Install: st --guardian-install");
1176 return Ok(());
1177 }
1178 let _ = run_command("systemctl", &["status", "smart-tree-guardian.service", "--no-pager"]);
1179 }
1180 Platform::MacOS => {
1181 let plist = "/Library/LaunchDaemons/is.8b.smart-tree-guardian.plist";
1182 if !PathBuf::from(plist).exists() {
1183 println!("Status: NOT INSTALLED");
1184 println!("Install: st --guardian-install");
1185 return Ok(());
1186 }
1187 let output = Command::new("launchctl")
1188 .args(["print", "system/is.8b.smart-tree-guardian"])
1189 .output();
1190 match output {
1191 Ok(out) if out.status.success() => {
1192 println!("Status: RUNNING");
1193 let text = String::from_utf8_lossy(&out.stdout);
1194 for line in text.lines() {
1195 let trimmed = line.trim();
1196 if trimmed.starts_with("pid =") || trimmed.starts_with("state =") {
1197 println!(" {}", trimmed);
1198 }
1199 }
1200 }
1201 _ => println!("Status: LOADED (check sudo launchctl list | grep guardian)"),
1202 }
1203 }
1204 _ => {
1205 println!("Guardian status not available on this platform.");
1206 }
1207 }
1208
1209 Ok(())
1210}