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