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