use serde::Serialize; #[derive(Debug, thiserror::Error)] pub enum AcpError { #[error("agent process failed to spawn: {0}")] SpawnFailed(String), #[error("connection not found: {0}")] ConnectionNotFound(String), #[error("ACP protocol error: {0}")] Protocol(String), #[error("agent process exited unexpectedly")] ProcessExited, #[error("binary download failed: {0}")] DownloadFailed(String), #[error("platform not supported: {0}")] PlatformNotSupported(String), } impl AcpError { pub fn protocol(raw: impl Into) -> Self { let raw = raw.into(); let sanitized = sanitize_protocol_message(&raw); if is_executable_format_error(&sanitized) { return Self::Protocol( "Agent executable appears incompatible or corrupted. Please retry to re-download it." .into(), ); } Self::Protocol(sanitized) } } impl Serialize for AcpError { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } fn sanitize_protocol_message(raw: &str) -> String { let without_spawned_at = regex::Regex::new(r#"\s*,?\s*"spawned_at"\s*:\s*"[^"]*"\s*,?"#) .ok() .map(|re| re.replace_all(raw, "").into_owned()) .unwrap_or_else(|| raw.to_string()); let without_dangling_comma = regex::Regex::new(r#",\s*([}\]])"#) .ok() .map(|re| re.replace_all(&without_spawned_at, "$1").into_owned()) .unwrap_or(without_spawned_at); regex::Regex::new(r#"/(?:Users|home)/[^"\s]+"#) .ok() .map(|re| { re.replace_all(&without_dangling_comma, "") .into_owned() }) .unwrap_or(without_dangling_comma) } fn is_executable_format_error(message: &str) -> bool { let lowered = message.to_lowercase(); lowered.contains("malformed mach-o file") || lowered.contains("exec format error") || lowered.contains("bad cpu type in executable") || lowered.contains("not a valid win32 application") || lowered.contains("is not a valid application for this os platform") }