Skip to main content

lean_ctx/
proxy_autostart.rs

1#[cfg(any(target_os = "macos", target_os = "linux"))]
2use std::path::PathBuf;
3
4#[cfg(target_os = "macos")]
5const PLIST_LABEL: &str = "com.leanctx.proxy";
6#[cfg(target_os = "linux")]
7const SYSTEMD_SERVICE: &str = "lean-ctx-proxy";
8
9pub fn install(port: u16, quiet: bool) {
10    let binary = find_binary();
11    if binary.is_empty() {
12        if !quiet {
13            eprintln!("  Cannot find lean-ctx binary for autostart");
14        }
15        return;
16    }
17
18    #[cfg(target_os = "macos")]
19    install_launchagent(&binary, port, quiet);
20
21    #[cfg(target_os = "linux")]
22    install_systemd(&binary, port, quiet);
23
24    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
25    {
26        let _ = (&binary, quiet);
27        println!("  Autostart not supported on this platform");
28        println!("  Run manually: lean-ctx proxy start --port={port}");
29    }
30}
31
32pub fn uninstall(_quiet: bool) {
33    #[cfg(target_os = "macos")]
34    uninstall_launchagent(_quiet);
35
36    #[cfg(target_os = "linux")]
37    uninstall_systemd(_quiet);
38}
39
40pub fn status() {
41    #[cfg(target_os = "macos")]
42    {
43        let plist_path = launchagent_path();
44        if plist_path.exists() {
45            println!("  LaunchAgent: installed at {}", plist_path.display());
46            let output = std::process::Command::new("launchctl")
47                .args(["list", PLIST_LABEL])
48                .output();
49            match output {
50                Ok(o) if o.status.success() => println!("  Status: loaded"),
51                _ => println!(
52                    "  Status: not loaded (run: launchctl load {})",
53                    plist_path.display()
54                ),
55            }
56        } else {
57            println!("  LaunchAgent: not installed");
58        }
59    }
60
61    #[cfg(target_os = "linux")]
62    {
63        let service_path = systemd_path();
64        if service_path.exists() {
65            println!("  systemd user service: installed");
66            let output = std::process::Command::new("systemctl")
67                .args(["--user", "is-active", SYSTEMD_SERVICE])
68                .output();
69            match output {
70                Ok(o) => {
71                    let state = String::from_utf8_lossy(&o.stdout).trim().to_string();
72                    println!("  Status: {state}");
73                }
74                Err(_) => println!("  Status: unknown"),
75            }
76        } else {
77            println!("  systemd service: not installed");
78        }
79    }
80
81    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
82    {
83        println!("  Autostart not available on this platform");
84    }
85}
86
87#[cfg(target_os = "macos")]
88fn launchagent_path() -> PathBuf {
89    dirs::home_dir()
90        .unwrap_or_else(|| PathBuf::from("/tmp"))
91        .join("Library/LaunchAgents")
92        .join(format!("{PLIST_LABEL}.plist"))
93}
94
95#[cfg(target_os = "macos")]
96fn install_launchagent(binary: &str, port: u16, quiet: bool) {
97    let plist_dir = dirs::home_dir()
98        .unwrap_or_else(|| PathBuf::from("/tmp"))
99        .join("Library/LaunchAgents");
100    let _ = std::fs::create_dir_all(&plist_dir);
101
102    let plist_path = plist_dir.join(format!("{PLIST_LABEL}.plist"));
103    let log_dir = dirs::home_dir()
104        .unwrap_or_else(|| PathBuf::from("/tmp"))
105        .join(".lean-ctx/logs");
106    let _ = std::fs::create_dir_all(&log_dir);
107
108    let plist = format!(
109        r#"<?xml version="1.0" encoding="UTF-8"?>
110<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
111<plist version="1.0">
112<dict>
113    <key>Label</key>
114    <string>{PLIST_LABEL}</string>
115    <key>ProgramArguments</key>
116    <array>
117        <string>{binary}</string>
118        <string>proxy</string>
119        <string>start</string>
120        <string>--port={port}</string>
121    </array>
122    <key>RunAtLoad</key>
123    <true/>
124    <key>KeepAlive</key>
125    <true/>
126    <key>StandardOutPath</key>
127    <string>{stdout}</string>
128    <key>StandardErrorPath</key>
129    <string>{stderr}</string>
130</dict>
131</plist>"#,
132        stdout = log_dir.join("proxy.stdout.log").display(),
133        stderr = log_dir.join("proxy.stderr.log").display(),
134    );
135
136    let _ = std::fs::write(&plist_path, &plist);
137
138    let _ = std::process::Command::new("launchctl")
139        .args(["unload", &plist_path.to_string_lossy()])
140        .output();
141
142    let result = std::process::Command::new("launchctl")
143        .args(["load", &plist_path.to_string_lossy()])
144        .output();
145
146    if !quiet {
147        match result {
148            Ok(o) if o.status.success() => {
149                println!("  Installed LaunchAgent: {}", plist_path.display());
150                println!("  Proxy will start on login and restart if stopped");
151            }
152            Ok(o) => {
153                let err = String::from_utf8_lossy(&o.stderr);
154                println!("  Created LaunchAgent but load failed: {err}");
155                println!("  Try: launchctl load {}", plist_path.display());
156            }
157            Err(e) => {
158                println!("  Created LaunchAgent at {}", plist_path.display());
159                println!("  Could not load: {e}");
160            }
161        }
162    }
163}
164
165#[cfg(target_os = "macos")]
166fn uninstall_launchagent(quiet: bool) {
167    let plist_path = launchagent_path();
168    if !plist_path.exists() {
169        if !quiet {
170            println!("  LaunchAgent not installed, nothing to remove");
171        }
172        return;
173    }
174
175    let _ = std::process::Command::new("launchctl")
176        .args(["unload", &plist_path.to_string_lossy()])
177        .output();
178
179    let _ = std::fs::remove_file(&plist_path);
180    if !quiet {
181        println!("  Removed LaunchAgent: {}", plist_path.display());
182    }
183}
184
185#[cfg(target_os = "linux")]
186fn systemd_path() -> PathBuf {
187    dirs::home_dir()
188        .unwrap_or_else(|| PathBuf::from("/tmp"))
189        .join(".config/systemd/user")
190        .join(format!("{SYSTEMD_SERVICE}.service"))
191}
192
193#[cfg(target_os = "linux")]
194fn install_systemd(binary: &str, port: u16, quiet: bool) {
195    let service_dir = dirs::home_dir()
196        .unwrap_or_else(|| PathBuf::from("/tmp"))
197        .join(".config/systemd/user");
198    let _ = std::fs::create_dir_all(&service_dir);
199
200    let service_path = service_dir.join(format!("{SYSTEMD_SERVICE}.service"));
201
202    let unit = format!(
203        r#"[Unit]
204Description=lean-ctx API Proxy
205After=network.target
206
207[Service]
208Type=simple
209ExecStart={binary} proxy start --port={port}
210Restart=on-failure
211RestartSec=5
212Environment=RUST_LOG=info
213
214[Install]
215WantedBy=default.target
216"#
217    );
218
219    let _ = std::fs::write(&service_path, &unit);
220
221    let _ = std::process::Command::new("systemctl")
222        .args(["--user", "daemon-reload"])
223        .output();
224
225    let result = std::process::Command::new("systemctl")
226        .args(["--user", "enable", "--now", SYSTEMD_SERVICE])
227        .output();
228
229    if !quiet {
230        match result {
231            Ok(o) if o.status.success() => {
232                println!("  Installed systemd user service: {SYSTEMD_SERVICE}");
233                println!("  Proxy will start on login and restart if stopped");
234            }
235            Ok(o) => {
236                let err = String::from_utf8_lossy(&o.stderr);
237                println!("  Created service file but enable failed: {err}");
238            }
239            Err(e) => {
240                println!("  Created service file at {}", service_path.display());
241                println!("  Could not enable: {e}");
242            }
243        }
244    }
245}
246
247#[cfg(target_os = "linux")]
248fn uninstall_systemd(quiet: bool) {
249    let service_path = systemd_path();
250    if !service_path.exists() {
251        if !quiet {
252            println!("  systemd service not installed, nothing to remove");
253        }
254        return;
255    }
256
257    let _ = std::process::Command::new("systemctl")
258        .args(["--user", "stop", SYSTEMD_SERVICE])
259        .output();
260    let _ = std::process::Command::new("systemctl")
261        .args(["--user", "disable", SYSTEMD_SERVICE])
262        .output();
263    let _ = std::fs::remove_file(&service_path);
264    let _ = std::process::Command::new("systemctl")
265        .args(["--user", "daemon-reload"])
266        .output();
267
268    if !quiet {
269        println!("  Removed systemd service: {SYSTEMD_SERVICE}");
270    }
271}
272
273fn find_binary() -> String {
274    std::env::current_exe()
275        .map(|p| p.to_string_lossy().to_string())
276        .unwrap_or_else(|_| which_lean_ctx().unwrap_or_default())
277}
278
279fn which_lean_ctx() -> Option<String> {
280    std::process::Command::new("which")
281        .arg("lean-ctx")
282        .output()
283        .ok()
284        .filter(|o| o.status.success())
285        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
286}