Remove a previously installed integration, guided by its receipt. Files the user modified since install are kept (and reported). Settings entries are removed by `(target, hooks_key, command_prefix)` without needing the original package.
(agent: &str)
| 437 | /// entries are removed by `(target, hooks_key, command_prefix)` without |
| 438 | /// needing the original package. |
| 439 | pub fn uninstall(agent: &str) -> AgentResult<UninstallOutcome> { |
| 440 | let receipt = Receipt::load(agent)?.ok_or_else(|| AgentError::Integration { |
| 441 | agent: agent.to_string(), |
| 442 | reason: "not installed (no receipt found)".to_string(), |
| 443 | })?; |
| 444 | |
| 445 | let mut removed = Vec::new(); |
| 446 | let mut kept_modified = Vec::new(); |
| 447 | |
| 448 | for file in &receipt.files { |
| 449 | if !file.dst.exists() { |
| 450 | continue; |
| 451 | } |
| 452 | let current = hash_file(&file.dst)?; |
| 453 | if current == file.blake3 { |
| 454 | std::fs::remove_file(&file.dst)?; |
| 455 | // Best-effort: drop the parent dir if we just emptied it. |
| 456 | if let Some(parent) = file.dst.parent() { |
| 457 | let _ = std::fs::remove_dir(parent); |
| 458 | } |
| 459 | removed.push(file.dst.clone()); |
| 460 | } else { |
| 461 | kept_modified.push(file.dst.clone()); |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | let mut settings_hooks_removed = 0; |
| 466 | for settings in &receipt.settings { |
| 467 | let outcome = hooks_manifest::uninstall_prefixed( |
| 468 | &settings.target, |
| 469 | &settings.hooks_key, |
| 470 | &settings.command_prefix, |
| 471 | )?; |
| 472 | settings_hooks_removed += outcome.removed; |
| 473 | } |
| 474 | |
| 475 | Receipt::remove(agent)?; |
| 476 | |
| 477 | Ok(UninstallOutcome { |
| 478 | agent: agent.to_string(), |
| 479 | removed, |
| 480 | kept_modified, |
| 481 | settings_hooks_removed, |
| 482 | }) |
| 483 | } |
| 484 | |
| 485 | enum Decision { |
| 486 | Install, |
no test coverage detected