mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 01:49:49 -10:00
uninstall: preflight purge targets before program removal; non-destructive rollback recipe (#7091)
Canonicalizes home/install paths and dot spellings, rejects a purge target that contains the home or install directory, keeps ownership and symlink guards, names recovery/snapshot/shared-data consequences, uses a literal environment-bound PowerShell path for deferred Windows deletion and canonicalizes both sides of the Windows PATH-block match. INSTALL.md's go-back-further recipe now moves the working binaries aside and restores them if the install fails. Verified: macOS sandboxed probe on a stripped copy of the frozen-tree CLI (sha256 d28274cb...), 4 cases pass (plain, purge, managed, hardlink); receipt in artifacts/claude-uninstall-probe-0415/probes/receipt.json. NOT verified: Linux and Windows execution, package-manager receipts, installer refusal rules in scripts/release/install.sh, zh-Hans section 12 still suggesting rm. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
1f0c4d0daa
commit
78580d3d4d
2 files changed
+57
-17
No files matched your search
@@ -125,15 +125,18 @@ impl CommandReceipt {
|
||||
|
||||
pub(crate) fn run(yes: bool, purge_data: bool) -> Result<()> {
|
||||
let selected = std::env::current_exe().ok();
|
||||
if purge_data {
|
||||
purge_targets(selected.as_deref())?;
|
||||
}
|
||||
let result = remove(yes);
|
||||
report_path_copies(selected.as_deref());
|
||||
let removed = result?;
|
||||
if purge_data {
|
||||
if !yes {
|
||||
return purge(false);
|
||||
return purge(false, selected.as_deref());
|
||||
}
|
||||
if removed {
|
||||
return purge(true);
|
||||
return purge(true, selected.as_deref());
|
||||
}
|
||||
println!(
|
||||
"Data was not purged: this run did not remove a receipted installation. Data is shared by every install and is kept."
|
||||
@@ -357,7 +360,7 @@ fn remove(yes: bool) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Removed this receipted release installation. Data and other installations are unchanged. {}",
|
||||
"Removed this receipted release installation. Other installations were not removed. {}",
|
||||
if cfg!(windows) {
|
||||
"Remove its directory from your user PATH if you added it, then open a new terminal."
|
||||
} else {
|
||||
@@ -395,12 +398,15 @@ fn report_path_blocks(directory: &Path) {
|
||||
.collect();
|
||||
profiles.sort();
|
||||
profiles.dedup();
|
||||
let install_directory = directory.canonicalize().ok();
|
||||
let names_directory = |value: &str| {
|
||||
let expanded = match value.strip_prefix("$HOME/") {
|
||||
Some(rest) => home.join(rest),
|
||||
None => PathBuf::from(value),
|
||||
};
|
||||
expanded.canonicalize().is_ok_and(|path| path == directory)
|
||||
expanded
|
||||
.canonicalize()
|
||||
.is_ok_and(|path| install_directory.as_ref() == Some(&path))
|
||||
};
|
||||
let ours = |line: &str| {
|
||||
line.strip_prefix("export PATH=\"")
|
||||
@@ -538,13 +544,13 @@ fn delete_after_exit(path: &Path) -> Result<()> {
|
||||
const DETACHED_PROCESS: u32 = 0x0000_0008;
|
||||
let system = std::env::var_os("SystemRoot").context("SystemRoot is not set")?;
|
||||
let powershell = Path::new(&system).join(r"System32\WindowsPowerShell\v1.0\powershell.exe");
|
||||
let literal = path.display().to_string().replace('\'', "''");
|
||||
std::process::Command::new(powershell)
|
||||
.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"])
|
||||
.arg(format!(
|
||||
"Wait-Process -Id {} -ErrorAction SilentlyContinue; Remove-Item -LiteralPath '{literal}' -Force",
|
||||
"Wait-Process -Id {} -ErrorAction SilentlyContinue; Remove-Item -LiteralPath $env:CODEWHALE_UNINSTALL_RETIRED_PATH -Force",
|
||||
std::process::id()
|
||||
))
|
||||
.env("CODEWHALE_UNINSTALL_RETIRED_PATH", path)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
@@ -554,23 +560,28 @@ fn delete_after_exit(path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn purge(yes: bool) -> Result<()> {
|
||||
fn purge_targets(selected: Option<&Path>) -> Result<Vec<PathBuf>> {
|
||||
let home = codewhale_config::codewhale_home()?;
|
||||
let explicit = codewhale_config::codewhale_home_is_explicit();
|
||||
let mut targets = vec![home.clone()];
|
||||
if !explicit {
|
||||
targets.push(codewhale_config::legacy_deepseek_home()?.join("snapshots"));
|
||||
}
|
||||
let user_home = dirs::home_dir().context("failed to resolve home directory")?;
|
||||
println!(
|
||||
"Data purge scope: Codewhale's config, plaintext secrets file, sessions, logs, skills, plugins, tasks and caches in the directories below. Nothing else is searched for."
|
||||
);
|
||||
let user_home = dirs::home_dir()
|
||||
.context("failed to resolve home directory")?
|
||||
.canonicalize()
|
||||
.context("failed to resolve the real home directory")?;
|
||||
let install_directory = selected
|
||||
.and_then(Path::parent)
|
||||
.map(std::fs::canonicalize)
|
||||
.transpose()
|
||||
.context("failed to resolve the install directory before data purge")?;
|
||||
let mut present = Vec::new();
|
||||
for target in targets {
|
||||
let target: PathBuf = target.components().collect();
|
||||
let metadata = match std::fs::symlink_metadata(&target) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
println!("Already absent: {}", target.display());
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -578,9 +589,9 @@ fn purge(yes: bool) -> Result<()> {
|
||||
.with_context(|| format!("failed to inspect {}", target.display()));
|
||||
}
|
||||
};
|
||||
if !metadata.is_dir() || user_home.starts_with(&target) || target.parent().is_none() {
|
||||
if !metadata.is_dir() {
|
||||
bail!(
|
||||
"Preserved {}: it is not a real directory, or it is your home or one of its parents; no data was deleted",
|
||||
"Preserved {}: it is not a real directory; no data was deleted",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
@@ -603,12 +614,39 @@ fn purge(yes: bool) -> Result<()> {
|
||||
);
|
||||
}
|
||||
}
|
||||
let target = target
|
||||
.canonicalize()
|
||||
.with_context(|| format!("failed to resolve data directory {}", target.display()))?;
|
||||
if user_home.starts_with(&target)
|
||||
|| target.parent().is_none()
|
||||
|| install_directory
|
||||
.as_ref()
|
||||
.is_some_and(|install| install.starts_with(&target))
|
||||
{
|
||||
bail!(
|
||||
"Preserved {}: it contains the home or install directory; no data was deleted",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
present.push(target);
|
||||
}
|
||||
Ok(present)
|
||||
}
|
||||
|
||||
fn purge(yes: bool, selected: Option<&Path>) -> Result<()> {
|
||||
let present = purge_targets(selected)?;
|
||||
println!(
|
||||
"Data purge scope: Codewhale's config, plaintext secrets file, sessions, logs, skills, plugins, tasks, caches, recovery records and snapshots in the directories below. Nothing else is searched for. Data can be shared by other installations; purging it affects those copies too."
|
||||
);
|
||||
if present.is_empty() {
|
||||
println!("Codewhale data directories are already absent.");
|
||||
}
|
||||
for target in &present {
|
||||
println!(
|
||||
"{} {}",
|
||||
if yes { "Delete" } else { "Would delete" },
|
||||
target.display()
|
||||
);
|
||||
present.push(target);
|
||||
}
|
||||
println!(
|
||||
"Not covered: OS keychain/keyring entries (run codewhale auth clear --provider <id> before removing the program), per-workspace .codewhale directories, shell profile lines and completion files."
|
||||
|
||||
+4
-2
@@ -696,8 +696,10 @@ go back further than one version, replace the files:
|
||||
|
||||
```bash
|
||||
dir="$(dirname "$(command -v codewhale)")" # the install PATH actually selects
|
||||
rm "$dir/codewhale" "$dir/codew"
|
||||
curl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=v0.9.13 CODEWHALE_INSTALL_DIR="$dir" sh
|
||||
mkdir -p "$dir/.keep" && mv "$dir/codewhale" "$dir/codew" "$dir/.keep/"
|
||||
curl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=v0.9.13 CODEWHALE_INSTALL_DIR="$dir" sh \
|
||||
&& rm -r "$dir/.keep" \
|
||||
|| mv "$dir/.keep/codewhale" "$dir/.keep/codew" "$dir/" # restore on failure
|
||||
hash -r; codewhale --version # codewhale 0.9.13 (a0b81f619b66)
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user