A safe, simple, extensible, and fast agent harness
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename ein-server to eind #1

open opened by mstallmo.com targeting main from codebase-reorg
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:vzio3dyompxwp7sguhzroqc4/sh.tangled.repo.pull/3mkzxeodaiv22
+368
Interdiff #2 #3
.github/workflows/release.yml

This file has not been changed.

CLAUDE.md

This file has not been changed.

Cargo.lock

This file has not been changed.

Cargo.toml

This file has not been changed.

README.md

This file has not been changed.

crates/ein-tui/src/bootstrap.rs

This file has not been changed.

crates/ein-tui/src/input.rs

This file has not been changed.

crates/ein-tui/src/lib.rs

This file has not been changed.

crates/ein-tui/src/render.rs

This file has not been changed.

eind/Cargo.toml

This file has not been changed.

eind/migrations/20260408224258_create_sessions.sql

This file has not been changed.

eind/src/grpc.rs

This file has not been changed.

eind/src/lib.rs

This file has not been changed.

eind/src/main.rs

This file has not been changed.

eind/src/model_client.rs

This file has not been changed.

eind/src/model_client/bindings.rs

This file has not been changed.

eind/src/model_client/syscalls.rs

This file has not been changed.

eind/src/persistence.rs

This file has not been changed.

eind/src/plugins.rs

This file has not been changed.

eind/src/tools.rs

This file has not been changed.

eind/src/tools/bindings.rs

This file has not been changed.

eind/src/tools/syscalls.rs

This file has not been changed.

specs/EIN-SERVER-CODE-REVIEW-v1.md

This file has not been changed.

specs/EIN-SERVER-CODE-REVIEW-v2.md

This file has not been changed.

+368
ein/src/bootstrap.rs
··· 1 + // SPDX-License-Identifier: Apache-2.0 2 + // Copyright 2026 Mason Stallmo 3 + 4 + //! Bootstrap logic: downloads `eind` on first run and registers it as a 5 + //! system service (macOS LaunchAgent or Linux systemd user service). 6 + 7 + // These items are only called from the #[cfg(not(debug_assertions))] block in 8 + // lib.rs, so they appear unused in debug builds. That's intentional. 9 + #![cfg_attr(debug_assertions, allow(dead_code))] 10 + 11 + use anyhow::{Context, Result}; 12 + use std::{ 13 + io, 14 + os::unix::fs::PermissionsExt, 15 + path::{Path, PathBuf}, 16 + }; 17 + use tar::Archive; 18 + use tokio::{fs, process::Command, task}; 19 + use xz2::read::XzDecoder; 20 + 21 + const GITHUB_REPO: &str = "mstallmo/ein"; 22 + 23 + /// Path where `ein` installs the server binary: `~/.ein/bin/eind`. 24 + pub fn server_bin_path() -> PathBuf { 25 + dirs::home_dir() 26 + .expect("home directory not found") 27 + .join(".ein") 28 + .join("bin") 29 + .join("eind") 30 + } 31 + 32 + /// Compile-time target triple used to select the right GitHub release asset. 33 + pub fn target_triple() -> &'static str { 34 + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] 35 + return "aarch64-apple-darwin"; 36 + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] 37 + return "x86_64-apple-darwin"; 38 + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] 39 + return "aarch64-unknown-linux-gnu"; 40 + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] 41 + return "x86_64-unknown-linux-gnu"; 42 + #[allow(unreachable_code)] 43 + "" 44 + } 45 + 46 + /// Downloads the `eind` binary for the current platform from GitHub 47 + /// releases and writes it to `~/.ein/bin/eind` with executable permissions. 48 + pub async fn download_server(version: &str) -> Result<()> { 49 + let ver = version.trim_start_matches('v'); 50 + let tag = format!("v{ver}"); 51 + let triple = target_triple(); 52 + // cargo-dist names archives as "{package}-{triple}.tar.xz" (no version in filename). 53 + let archive_name = format!("eind-{triple}.tar.xz"); 54 + let url = format!("https://github.com/{GITHUB_REPO}/releases/download/{tag}/{archive_name}"); 55 + 56 + let dest = server_bin_path(); 57 + fs::create_dir_all(dest.parent().unwrap()) 58 + .await 59 + .context("failed to create ~/.ein/bin")?; 60 + 61 + println!("Downloading {url}..."); 62 + 63 + let response = reqwest::get(&url) 64 + .await 65 + .with_context(|| format!("failed to fetch {url}"))?; 66 + 67 + if !response.status().is_success() { 68 + anyhow::bail!("download failed: HTTP {}", response.status()); 69 + } 70 + 71 + let bytes = response 72 + .bytes() 73 + .await 74 + .context("failed to read response body")?; 75 + 76 + let dest_clone = dest.clone(); 77 + task::spawn_blocking(move || extract_server(&bytes, &dest_clone)) 78 + .await 79 + .context("extraction task panicked")??; 80 + 81 + // Make the binary executable. 82 + let mut perms = fs::metadata(&dest).await?.permissions(); 83 + perms.set_mode(0o755); 84 + fs::set_permissions(&dest, perms).await?; 85 + 86 + println!("eind installed to {}", dest.display()); 87 + Ok(()) 88 + } 89 + 90 + /// Extracts the `eind` binary from a tar.xz archive into `dest`. 91 + fn extract_server(bytes: &[u8], dest: &Path) -> Result<()> { 92 + let xz = XzDecoder::new(io::Cursor::new(bytes)); 93 + let mut archive = Archive::new(xz); 94 + 95 + for entry in archive 96 + .entries() 97 + .context("failed to read archive entries")? 98 + { 99 + let mut entry = entry.context("corrupt archive entry")?; 100 + let entry_path = entry.path().context("entry has no path")?; 101 + 102 + // The archive contains exactly one file: the `eind` binary. 103 + // Accept it regardless of any leading directory component. 104 + let file_name = entry_path 105 + .file_name() 106 + .and_then(|n| n.to_str()) 107 + .unwrap_or(""); 108 + 109 + if file_name == "eind" { 110 + let mut file = std::fs::File::create(dest) 111 + .with_context(|| format!("failed to create {}", dest.display()))?; 112 + io::copy(&mut entry, &mut file).context("failed to write eind")?; 113 + return Ok(()); 114 + } 115 + } 116 + 117 + anyhow::bail!("eind binary not found in archive") 118 + } 119 + 120 + // --------------------------------------------------------------------------- 121 + // Service registration 122 + // --------------------------------------------------------------------------- 123 + 124 + /// Ensures `eind` is registered as a system service. 125 + /// 126 + /// On macOS, installs a LaunchAgent plist and loads it. 127 + /// On Linux, writes a systemd user unit and enables it. 128 + /// On other platforms, does nothing (the TUI's retry loop handles reconnects). 129 + pub async fn ensure_service_installed() -> Result<()> { 130 + #[cfg(target_os = "macos")] 131 + return ensure_launchagent_installed().await; 132 + 133 + #[cfg(target_os = "linux")] 134 + return ensure_systemd_installed().await; 135 + 136 + #[cfg(not(any(target_os = "macos", target_os = "linux")))] 137 + Ok(()) 138 + } 139 + 140 + // --------------------------------------------------------------------------- 141 + // Uninstall 142 + // --------------------------------------------------------------------------- 143 + 144 + /// Stops and removes the `eind` service and binary installed by 145 + /// [`ensure_service_installed`] and [`download_server`]. 146 + /// 147 + /// Returns a list of completed step descriptions for display in the TUI. 148 + /// User config and session data in `~/.ein/` are left intact. 149 + pub async fn uninstall() -> Result<Vec<String>> { 150 + let mut steps: Vec<String> = Vec::new(); 151 + #[cfg(target_os = "macos")] 152 + uninstall_launchagent(&mut steps).await?; 153 + #[cfg(target_os = "linux")] 154 + uninstall_systemd(&mut steps).await?; 155 + remove_server_binary(&mut steps).await?; 156 + Ok(steps) 157 + } 158 + 159 + async fn remove_server_binary(steps: &mut Vec<String>) -> Result<()> { 160 + let path = server_bin_path(); 161 + if path.exists() { 162 + fs::remove_file(&path) 163 + .await 164 + .with_context(|| format!("failed to remove {}", path.display()))?; 165 + steps.push(format!("Removed {}", path.display())); 166 + } 167 + Ok(()) 168 + } 169 + 170 + #[cfg(target_os = "macos")] 171 + async fn uninstall_launchagent(steps: &mut Vec<String>) -> Result<()> { 172 + let plist = launchagent_plist_path(); 173 + // Ignore errors — the service may already be stopped/unloaded. 174 + let _ = Command::new("launchctl") 175 + .args(["unload", plist.to_str().unwrap_or("")]) 176 + .output() 177 + .await; 178 + if plist.exists() { 179 + fs::remove_file(&plist) 180 + .await 181 + .with_context(|| format!("failed to remove {}", plist.display()))?; 182 + steps.push(format!("Removed LaunchAgent ({})", LAUNCH_AGENT_LABEL)); 183 + } 184 + Ok(()) 185 + } 186 + 187 + #[cfg(target_os = "linux")] 188 + async fn uninstall_systemd(steps: &mut Vec<String>) -> Result<()> { 189 + let unit = systemd_unit_path(); 190 + let _ = Command::new("systemctl") 191 + .args(["--user", "stop", SYSTEMD_SERVICE_NAME]) 192 + .output() 193 + .await; 194 + let _ = Command::new("systemctl") 195 + .args(["--user", "disable", SYSTEMD_SERVICE_NAME]) 196 + .output() 197 + .await; 198 + if unit.exists() { 199 + fs::remove_file(&unit) 200 + .await 201 + .with_context(|| format!("failed to remove {}", unit.display()))?; 202 + steps.push(format!( 203 + "Removed systemd user service ({})", 204 + SYSTEMD_SERVICE_NAME 205 + )); 206 + } 207 + let _ = Command::new("systemctl") 208 + .args(["--user", "daemon-reload"]) 209 + .output() 210 + .await; 211 + Ok(()) 212 + } 213 + 214 + // --------------------------------------------------------------------------- 215 + // macOS LaunchAgent 216 + // --------------------------------------------------------------------------- 217 + 218 + #[cfg(target_os = "macos")] 219 + const LAUNCH_AGENT_LABEL: &str = "com.ein.eind"; 220 + 221 + #[cfg(target_os = "macos")] 222 + fn launchagent_plist_path() -> PathBuf { 223 + dirs::home_dir() 224 + .expect("home directory not found") 225 + .join("Library") 226 + .join("LaunchAgents") 227 + .join(format!("{LAUNCH_AGENT_LABEL}.plist")) 228 + } 229 + 230 + #[cfg(target_os = "macos")] 231 + async fn ensure_launchagent_installed() -> Result<()> { 232 + // Check if already loaded. 233 + let status = Command::new("launchctl") 234 + .args(["list", LAUNCH_AGENT_LABEL]) 235 + .output() 236 + .await 237 + .context("launchctl not found")?; 238 + 239 + if status.status.success() { 240 + return Ok(()); // Already running. 241 + } 242 + 243 + let plist_path = launchagent_plist_path(); 244 + let bin = server_bin_path(); 245 + let log = dirs::home_dir() 246 + .expect("home directory not found") 247 + .join(".ein") 248 + .join("server.log"); 249 + 250 + let plist = format!( 251 + r#"<?xml version="1.0" encoding="UTF-8"?> 252 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 253 + <plist version="1.0"> 254 + <dict> 255 + <key>Label</key> 256 + <string>{LAUNCH_AGENT_LABEL}</string> 257 + <key>ProgramArguments</key> 258 + <array> 259 + <string>{bin}</string> 260 + </array> 261 + <key>RunAtLoad</key> 262 + <true/> 263 + <key>KeepAlive</key> 264 + <true/> 265 + <key>StandardOutPath</key> 266 + <string>{log}</string> 267 + <key>StandardErrorPath</key> 268 + <string>{log}</string> 269 + </dict> 270 + </plist> 271 + "#, 272 + LAUNCH_AGENT_LABEL = LAUNCH_AGENT_LABEL, 273 + bin = bin.display(), 274 + log = log.display(), 275 + ); 276 + 277 + fs::create_dir_all(plist_path.parent().unwrap()) 278 + .await 279 + .context("failed to create LaunchAgents directory")?; 280 + fs::write(&plist_path, plist) 281 + .await 282 + .context("failed to write plist")?; 283 + 284 + let output = Command::new("launchctl") 285 + .args(["load", plist_path.to_str().unwrap()]) 286 + .output() 287 + .await 288 + .context("failed to run launchctl load")?; 289 + 290 + if !output.status.success() { 291 + let stderr = String::from_utf8_lossy(&output.stderr); 292 + anyhow::bail!("launchctl load failed: {stderr}"); 293 + } 294 + 295 + println!("eind registered as LaunchAgent ({LAUNCH_AGENT_LABEL})"); 296 + Ok(()) 297 + } 298 + 299 + // --------------------------------------------------------------------------- 300 + // Linux systemd user service 301 + // --------------------------------------------------------------------------- 302 + 303 + #[cfg(target_os = "linux")] 304 + const SYSTEMD_SERVICE_NAME: &str = "eind"; 305 + 306 + #[cfg(target_os = "linux")] 307 + fn systemd_unit_path() -> PathBuf { 308 + dirs::home_dir() 309 + .expect("home directory not found") 310 + .join(".config") 311 + .join("systemd") 312 + .join("user") 313 + .join(format!("{SYSTEMD_SERVICE_NAME}.service")) 314 + } 315 + 316 + #[cfg(target_os = "linux")] 317 + async fn ensure_systemd_installed() -> Result<()> { 318 + // Check if already enabled. 319 + let status = Command::new("systemctl") 320 + .args(["--user", "is-enabled", SYSTEMD_SERVICE_NAME]) 321 + .output() 322 + .await 323 + .context("systemctl not found")?; 324 + 325 + if status.status.success() { 326 + return Ok(()); // Already enabled. 327 + } 328 + 329 + let unit_path = systemd_unit_path(); 330 + let bin = server_bin_path(); 331 + 332 + let unit = format!( 333 + "[Unit]\nDescription=Ein server\n\n[Service]\nExecStart={bin}\nRestart=always\n\n[Install]\nWantedBy=default.target\n", 334 + bin = bin.display(), 335 + ); 336 + 337 + fs::create_dir_all(unit_path.parent().unwrap()) 338 + .await 339 + .context("failed to create systemd user directory")?; 340 + fs::write(&unit_path, unit) 341 + .await 342 + .context("failed to write systemd unit")?; 343 + 344 + let reload = Command::new("systemctl") 345 + .args(["--user", "daemon-reload"]) 346 + .output() 347 + .await 348 + .context("failed to run systemctl daemon-reload")?; 349 + 350 + if !reload.status.success() { 351 + let stderr = String::from_utf8_lossy(&reload.stderr); 352 + anyhow::bail!("systemctl daemon-reload failed: {stderr}"); 353 + } 354 + 355 + let enable = Command::new("systemctl") 356 + .args(["--user", "enable", "--now", SYSTEMD_SERVICE_NAME]) 357 + .output() 358 + .await 359 + .context("failed to run systemctl enable")?; 360 + 361 + if !enable.status.success() { 362 + let stderr = String::from_utf8_lossy(&enable.stderr); 363 + anyhow::bail!("systemctl enable failed: {stderr}"); 364 + } 365 + 366 + println!("eind registered as systemd user service ({SYSTEMD_SERVICE_NAME})"); 367 + Ok(()) 368 + }

History

6 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
Rename ein-server to eind
merge conflicts detected
expand
  • .github/workflows/release.yml:42
  • CLAUDE.md:12
  • Cargo.lock:1002
  • Cargo.toml:1
  • README.md:4
  • specs/EIN-SERVER-CODE-REVIEW-v1.md:1
  • specs/EIN-SERVER-CODE-REVIEW-v2.md:1
expand 0 comments
1 commit
expand
Rename ein-server to eind
expand 0 comments
1 commit
expand
Rename ein-server to eind
expand 0 comments
1 commit
expand
Rename ein-server to eind
expand 0 comments
1 commit
expand
Rename ein-server to eind
expand 0 comments
1 commit
expand
Rename ein-server to eind
expand 0 comments