lean_ctx/
proxy_autostart.rs1#[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
78pub 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}