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