Skip to main content

st/
service_manager.rs

1// Service Manager for Smart Tree Daemon
2// Cross-platform system-level service management:
3//   Linux  → systemd (/etc/systemd/system/)
4//   macOS  → launchd (/Library/LaunchDaemons/)
5//   Windows → sc.exe (Windows Service)
6//
7// `st service install` auto-escalates privileges on all platforms.
8
9use 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// =============================================================================
17// PLATFORM DETECTION
18// =============================================================================
19
20#[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
53// =============================================================================
54// CONSTANTS
55// =============================================================================
56
57const DAEMON_PORT: u16 = 28428;
58
59// Linux
60const SYSTEMD_DAEMON_SERVICE: &str = "smart-tree-daemon.service";
61const SYSTEMD_SYSTEM_PATH: &str = "/etc/systemd/system";
62
63// macOS
64const LAUNCHD_DAEMON_LABEL: &str = "is.8b.smart-tree-daemon";
65const LAUNCHD_DAEMON_PLIST: &str = "/Library/LaunchDaemons/is.8b.smart-tree-daemon.plist";
66
67// Windows
68#[cfg(target_os = "windows")]
69const WINDOWS_SERVICE_NAME: &str = "SmartTreeDaemon";
70
71// =============================================================================
72// PRIVILEGE ESCALATION
73// =============================================================================
74
75/// Check if running as root/admin
76fn is_elevated() -> bool {
77    #[cfg(unix)]
78    {
79        unsafe { libc::geteuid() == 0 }
80    }
81    #[cfg(windows)]
82    {
83        // Check for admin by trying to open a privileged registry key
84        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
95/// Re-execute the current command with elevated privileges.
96/// Returns Ok(true) if we re-launched (caller should exit), Ok(false) if already elevated.
97fn 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        // Try pkexec first (graphical prompt), fall back to sudo
110        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        // osascript for graphical prompt, or sudo for terminal
133        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            // Use osascript for GUI prompt
144            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        // Use PowerShell Start-Process -Verb RunAs for UAC prompt
165        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
193// =============================================================================
194// CROSS-PLATFORM PUBLIC API
195// =============================================================================
196
197/// Install the Smart Tree daemon as a system-level service.
198/// Auto-escalates privileges if needed.
199pub 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
215/// Uninstall the system-level service.
216pub 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
225/// Start the service.
226pub 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
235/// Stop the service.
236pub 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
245/// Show service status.
246pub 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
255/// Show service logs.
256pub 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
265/// Deprecated: use install() instead. Kept for --daemon-install backward compat.
266pub fn daemon_install_system() -> Result<()> {
267    eprintln!("Note: --daemon-install is deprecated, use `st service install` instead.");
268    install()
269}
270
271// =============================================================================
272// LINUX (systemd) — /etc/systemd/system/
273// =============================================================================
274
275fn linux_install() -> Result<()> {
276    // Escalate if not root
277    if escalate_privileges(&["service", "install"])? {
278        return Ok(()); // child process did the work
279    }
280
281    // We're root now
282    info!("Installing systemd system service...");
283
284    // 1. Find/copy binary to /usr/local/bin
285    let install_path = install_binary()?;
286
287    // 2. Create state directory
288    fs::create_dir_all("/var/lib/smart-tree")
289        .context("Failed to create /var/lib/smart-tree")?;
290
291    // 3. Store integrity hash
292    let hash = compute_file_hash(&install_path)?;
293    fs::write("/var/lib/smart-tree/daemon.sha256", &hash)?;
294
295    // 4. Write the systemd service file
296    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    // 5. Reload and enable
302    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    // status doesn't need root — systemctl status works for any user
347    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
404// =============================================================================
405// macOS (launchd) — /Library/LaunchDaemons/
406// =============================================================================
407
408fn macos_install() -> Result<()> {
409    if escalate_privileges(&["service", "install"])? {
410        return Ok(());
411    }
412
413    info!("Installing LaunchDaemon...");
414
415    // 1. Install binary
416    let install_path = install_binary()?;
417
418    // 2. Create state directory
419    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    // 3. Store integrity hash
423    let hash = compute_file_hash(&install_path)?;
424    fs::write(state_dir.join("daemon.sha256"), &hash)?;
425
426    // 4. Create log directory
427    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    // 5. Write the LaunchDaemon plist
431    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    // 6. Set ownership (must be owned by root:wheel for launchd)
436    run_command("chown", &["root:wheel", LAUNCHD_DAEMON_PLIST])?;
437    run_command("chmod", &["644", LAUNCHD_DAEMON_PLIST])?;
438
439    // 7. Load the daemon
440    // On macOS 10.10+, use launchctl bootstrap. Older: launchctl load.
441    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            // Fallback to legacy load
451            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    // Try modern bootout, fall back to legacy unload
469    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    // kickstart forces immediate start
493    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            // Fallback: bootstrap loads + starts
503            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            // Show PID if available
548            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            // Fallback: check launchctl list
557            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        // Try system log
582        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
636// =============================================================================
637// WINDOWS (sc.exe Windows Service)
638// =============================================================================
639
640fn 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        // 1. Find/copy binary
655        let install_path = install_binary()?;
656
657        // 2. Create state directory
658        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        // 3. Store integrity hash
664        let hash = compute_file_hash(&install_path)?;
665        fs::write(state_dir.join("daemon.sha256"), &hash)?;
666
667        // 4. Create the Windows service using sc.exe
668        let bin_arg = format!(
669            "\"{}\" --http-daemon --daemon-port {}",
670            install_path.display(),
671            DAEMON_PORT
672        );
673
674        // Delete existing service first (ignore error if it doesn't exist)
675        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        // Set description
693        run_command(
694            "sc.exe",
695            &[
696                "description",
697                WINDOWS_SERVICE_NAME,
698                "Smart Tree - AI-friendly directory visualization daemon",
699            ],
700        )?;
701
702        // Set recovery: restart on failure
703        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        // Start the service
714        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
821// =============================================================================
822// SHARED UTILITIES
823// =============================================================================
824
825/// Copy the current binary to the system install path.
826/// Returns the path where the binary was installed.
827fn 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(&current_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
858/// Compute SHA256 hash of a file for integrity verification.
859fn 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
878/// Run a command, inheriting stdio.
879fn 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
895/// Find a binary on PATH.
896fn which_binary(name: &str) -> Option<PathBuf> {
897    which::which(name).ok()
898}
899
900/// Print the post-install success banner.
901fn 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
914// =============================================================================
915// GPG SIGNATURE VERIFICATION
916// =============================================================================
917
918/// 8bit-wraith's official GPG key fingerprint for signed releases
919pub const OFFICIAL_GPG_FINGERPRINT: &str = "wraith@8b.is";
920
921/// Check if this is an officially signed build
922pub 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
975/// Print signature verification banner on first run
976pub 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
1024// =============================================================================
1025// GUARDIAN — kept for backward compatibility
1026// =============================================================================
1027
1028/// Verify the installed binary hasn't been tampered with
1029pub 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
1056/// Install Smart Tree Guardian as a root daemon
1057pub 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    // 1. Install binary
1068    let target_bin = install_binary()?;
1069
1070    // 2. Create state directory & store hash
1071    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    // 3. Write service file (platform-specific)
1076    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
1133/// Uninstall Smart Tree Guardian
1134pub 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
1165/// Show Guardian daemon status
1166pub 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}