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