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_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}