fix(cli): remove the marked PATH block and Codewhale completions on uninstall

A receipted release copy of `codewhale uninstall --yes` removed its program
files but left the user to hand-delete the PATH lines and completion files the
installer had told them to create. #7091 asks for a complete, verifiable
uninstall; this completes the shell half of it.

Uninstall now plans and applies two extra removals, both scoped to this
install and gated on the same receipted copy that owns the program files:

- This install's exact three-line `# >>> codewhale PATH >>>` block, in the
  same profile list the old report walked. The block is only matched when its
  middle line names this install directory, so another copy's block and a bare
  `export PATH=…` line the user wrote are never touched. The profile is edited
  by staging replacement bytes and renaming the original aside, so a failure
  installing the replacement restores the user's own file by name; a symlinked,
  unreadable or non-UTF-8 profile is reported and skipped. The profile's mode
  is adopted before the guard captures it, since `GuardedFile` pins ctime.
- The Codewhale-generated completion files at their documented paths, honouring
  `XDG_DATA_HOME` and `XDG_CONFIG_HOME`. They are kept while another Codewhale
  copy remains on PATH, because a second install still wants them.

Cleanup failures after the program is already gone are reported individually
and exit non-zero rather than being silently swallowed. `--help`/dry-run
prints `Would remove` and changes nothing.

The installers still never edit shell profiles themselves; they now print the
PATH line as that marked block, so there is something provably ours to remove.
A bare `export PATH=…` line from an older install remains the user's.

Verified: `cargo test -p codewhale-cli --lib update::` 109 passed / 0 failed
(269 filtered); `vitest run lib/public-installer-contract.test.ts` 25 passed,
covering the printed marker block for zsh/bash/dash and the
unrecognised-shell case. Release build of `codewhale-cli` clean, no warnings.

Not verified: no real Windows host, no installed release artifact, and no run
against a profile this repository does not own. Refs #7091
This commit is contained in:
CodeWhale Bot committed 2026-10-31 00:11:53 -07:00
1 parent 6d9089434b
commit 20f091d6e3
7 files changed
+335 -38

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`exec` and `doctor` follow the TUI onto local Ollama:** with no cloud key and no explicit route, `codewhale exec` uses a running local Ollama chat model and says so, and `codewhale doctor --probe-local` checks that route. Nothing is saved. (#7115)
- **Thinking starts at auto:** new configurations and the built-in default use `reasoning_effort = "auto"` instead of `max`; an effort you set is never changed.
- **DSH Git review scopes:** the extension host brokers bounded, read-only Git metadata to reviewed DSH plugins through a new `git_review_scope` action (extension-host policy v8).
- **Safer uninstall:** program removal never edits shell profiles; matching Codewhale PATH blocks are reported for manual cleanup. Data purge requires a receipted removal in the same confirmed run. Both Unix installers reject unsafe or managed destinations before creating them and keep shell profiles unchanged. Uninstall instructions now match the explicit-home purge scope in English and Chinese.
- **Safer, more complete uninstall:** for a receipted release copy, `codewhale uninstall --yes` now also removes this install's exact three-line `# >>> codewhale PATH >>>` block from your shell profiles and the Codewhale-generated completion files at their documented paths (bash, zsh and fish, honouring `XDG_DATA_HOME` and `XDG_CONFIG_HOME`). It touches nothing else in a profile, leaves a symlinked or unreadable profile for you to edit, keeps the completion files while another Codewhale copy remains on PATH, and exits non-zero if that cleanup fails after the program is already gone. The `codewhale.net/install.sh` hint and the install guide now print the PATH line as that marked block, so there is something provably yours to remove; a bare `export PATH=…` line you added earlier is never touched. Data purge requires a receipted removal in the same confirmed run. Both Unix installers reject unsafe or managed destinations before creating them and still never edit shell profiles themselves. Uninstall instructions match the explicit-home purge scope in English and Chinese. Not yet observed on a real Windows host or an installed release artifact. (Refs #7091)
- **Nested and deletable threads (Runtime API):** a fork (including a backtrack fork) records `parent_thread_id`, `POST /v1/threads` accepts one naming an existing thread, and `GET /v1/threads/summary` returns it so clients can nest child conversations. `DELETE /v1/threads/{id}` removes an idle thread with its turns, items, events and goal, and its saved session unless another thread shares it or a terminal holds it open; a running thread answers 409.
- **Hunk actions in Review (Runtime API):** `POST /v1/git/stage`, `/v1/git/unstage` and `/v1/git/discard` take a `patch` holding hunks of the one path in `paths` and apply only those hunks through `git apply`, under the same `expect` preconditions as whole-path writes. A patch that no longer applies fails with git's message and writes nothing; renames, mode changes and binary patches are refused. The tasks tool can also `suggest` a task, shown as a Start/Hide card, without starting anything.
- **Live `/models` ceilings reach the route:** a provider that states per-model context and output limits in its own `/models` body now keeps them instead of falling back to the legacy 128K window. OpenAI `limit` blocks, flat `context_length` / `context_window` / `max_model_len` / `max_context_length` fields, `max_output_tokens` / `max_completion_tokens`, and a relay `metadata` block (`context_limit`, `context_budget`, `output_budget`) are read; `metadata.context_limit` outranks `context_budget` because the first is the model capability and the second the share one deployment accepts. Each field is parsed on its own, so an unreadable one costs only its own candidate instead of hiding the readable fields below it.
+294 -13
View File
@@ -163,7 +163,7 @@ fn remove(yes: bool) -> Result<bool> {
let method = InstallMethod::detect(&identity.path);
println!("Selected executable: {}", identity.path.display());
println!(
"Program removal preserves user state, credentials, sessions, workspaces and services. Uninstall never edits shell profiles; for a receipted release copy it reports recognizable Codewhale PATH blocks in accessible profiles for you to remove."
"Program removal preserves user state, credentials, sessions, workspaces and services. For a receipted release copy it also removes this install's exact Codewhale PATH block from your shell profiles and Codewhale-generated completion files at the documented paths; nothing else in a profile is touched."
);
if !method.supports_self_update() {
println!(
@@ -275,7 +275,7 @@ fn remove(yes: bool) -> Result<bool> {
directory.join(file.name()).display()
);
}
report_path_blocks(&directory);
let shell = ShellIntegration::plan(&directory, &identity.path, yes);
validate_primary_update_identity(&identity)?;
if !yes {
println!(
@@ -359,6 +359,7 @@ fn remove(yes: bool) -> Result<bool> {
);
}
}
let shell_failures = shell.apply();
println!(
"Removed this receipted release installation. Other installations were not removed. {}",
if cfg!(windows) {
@@ -367,17 +368,76 @@ fn remove(yes: bool) -> Result<bool> {
"Run hash -r in your shell."
}
);
if !shell_failures.is_empty() {
bail!(
"The program files were removed, but shell integration cleanup is incomplete: {}",
shell_failures.join("; ")
);
}
Ok(true)
}
const PATH_BLOCK_START: &str = "# >>> codewhale PATH >>>";
const PATH_BLOCK_END: &str = "# <<< codewhale PATH <<<";
const MAX_PROFILE: usize = 1024 * 1024;
const MAX_COMPLETION: usize = 1024 * 1024;
fn report_path_blocks(directory: &Path) {
let Some(home) = dirs::home_dir() else {
return;
};
struct ProfileEdit {
profile: PathBuf,
file: GuardedFile,
updated: Vec<u8>,
#[cfg(unix)]
mode: u32,
}
#[derive(Default)]
struct ShellIntegration {
edits: Vec<ProfileEdit>,
completions: Vec<(PathBuf, GuardedFile)>,
}
impl ShellIntegration {
fn plan(directory: &Path, selected: &Path, yes: bool) -> Self {
let Some(home) = dirs::home_dir() else {
return Self::default();
};
let verb = if yes { "Remove" } else { "Would remove" };
Self {
edits: plan_path_blocks(&home, directory, verb),
completions: plan_completions(&home, directory, selected, verb),
}
}
fn apply(self) -> Vec<String> {
let mut failures = Vec::new();
for edit in self.edits {
let profile = edit.profile.clone();
match apply_profile_edit(edit) {
Ok(()) => println!(
"Removed the Codewhale PATH block from {}",
profile.display()
),
Err(error) => failures.push(format!(
"{} was not changed ({error:#}); delete its Codewhale PATH block by hand",
profile.display()
)),
}
}
for (path, mut file) in self.completions {
let mut unlinked = false;
match file.remove_checked(&mut unlinked) {
Ok(()) => println!("Removed completion file {}", path.display()),
Err(error) => failures.push(format!(
"{} was not removed ({error}); delete it by hand",
path.display()
)),
}
}
failures
}
}
fn plan_path_blocks(home: &Path, directory: &Path, verb: &str) -> Vec<ProfileEdit> {
let zdotdir = std::env::var_os("ZDOTDIR").map(PathBuf::from);
let mut profiles: Vec<PathBuf> = [
".zshrc",
@@ -419,6 +479,7 @@ fn report_path_blocks(directory: &Path) {
rest == format!("{value}\" $PATH") && names_directory(value)
})
};
let mut edits = Vec::new();
for profile in profiles {
let (Some(parent), Some(name)) = (
profile.parent(),
@@ -438,27 +499,37 @@ fn report_path_blocks(directory: &Path) {
continue;
}
};
let text = match file.read_bounded(MAX_PROFILE).map(String::from_utf8) {
Ok(Ok(text)) => text,
_ => {
let bytes = match file.read_bounded(MAX_PROFILE) {
Ok(bytes) => bytes,
Err(error) => {
println!(
"Not read: {} is not a UTF-8 file within the size limit.",
"Not read: {} ({error}). If it holds a Codewhale PATH block, remove that block by hand.",
profile.display()
);
continue;
}
};
let lines: Vec<&str> = text.lines().map(str::trim_end).collect();
let Ok(text) = String::from_utf8(bytes) else {
println!(
"Not read: {} is not a UTF-8 file within the size limit.",
profile.display()
);
continue;
};
let segments: Vec<&str> = text.split_inclusive('\n').collect();
let lines: Vec<&str> = segments.iter().map(|line| line.trim_end()).collect();
let mut removed = vec![false; lines.len()];
let mut index = 0;
while index < lines.len() {
if lines[index] == PATH_BLOCK_START {
if lines.get(index + 2) == Some(&PATH_BLOCK_END) && ours(lines[index + 1]) {
println!(
"Codewhale PATH block for this install: {} lines {}-{}. Uninstall does not edit shell profiles; delete those lines yourself to drop the PATH entry.",
"{verb} the Codewhale PATH block for this install: {} lines {}-{}",
profile.display(),
index + 1,
index + 3
);
removed[index..index + 3].fill(true);
index += 3;
continue;
}
@@ -476,6 +547,216 @@ fn report_path_blocks(directory: &Path) {
}
index += 1;
}
if !removed.contains(&true) {
continue;
}
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(&profile) {
Ok(metadata) => metadata.permissions().mode() & 0o7777,
Err(error) => {
println!(
"Not changed: {} ({error}); delete its Codewhale PATH block by hand.",
profile.display()
);
continue;
}
}
};
let updated = segments
.iter()
.zip(&removed)
.filter(|(_, removed)| !**removed)
.flat_map(|(segment, _)| segment.bytes())
.collect();
edits.push(ProfileEdit {
profile,
file,
updated,
#[cfg(unix)]
mode,
});
}
edits
}
fn apply_profile_edit(edit: ProfileEdit) -> Result<()> {
let ProfileEdit {
profile,
mut file,
updated,
#[cfg(unix)]
mode,
} = edit;
let parent = profile
.parent()
.context("profile has no parent directory")?;
let name = profile
.file_name()
.and_then(|name| name.to_str())
.context("profile name is not UTF-8")?
.to_string();
#[cfg(unix)]
let mut staged = stage_profile(parent, &updated, mode)?;
#[cfg(not(unix))]
let mut staged = stage_profile(parent, &updated)?;
// Rename the profile aside instead of unlinking it: a failure while the
// replacement is installed can then put the user's own bytes back by name.
let retired = GuardedFile::recovery_name();
file.move_to_vacant(&retired).with_context(|| {
format!(
"{} could not be set aside for its replacement",
profile.display()
)
})?;
if let Err(error) = staged.move_to_vacant(&name) {
let restore = file.move_to_vacant(&name).map_err(|restore| {
format!(
"the unchanged profile is retained at {} and could not be restored ({restore})",
parent.join(&retired).display()
)
});
let mut discarded = false;
let _ = staged.remove_checked(&mut discarded);
return match restore {
Ok(()) => Err(error).with_context(|| {
format!(
"{} was left unchanged because its replacement could not be installed",
profile.display()
)
}),
Err(restore) => Err(anyhow!("{error}; {restore}")),
};
}
let mut unlinked = false;
if file.remove_checked(&mut unlinked).is_err() || !unlinked {
println!(
"Retired copy of {} is retained at {}; delete it by hand",
profile.display(),
parent.join(&retired).display()
);
}
Ok(())
}
/// Stage the replacement bytes, then adopt the profile's mode before the guard
/// captures them. `GuardedFile` pins ctime, so a chmod after `stage` invalidates
/// the very file the caller is about to move into place.
fn stage_profile(parent: &Path, updated: &[u8], #[cfg(unix)] mode: u32) -> Result<GuardedFile> {
let staged = GuardedFile::stage(parent, updated, false)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let path = parent.join(staged.name());
let name = staged.name().to_string();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))?;
drop(staged);
return GuardedFile::open_bounded(parent, &name, MAX_PROFILE)?
.context("the staged profile replacement disappeared before it could be verified");
}
#[cfg(not(unix))]
Ok(staged)
}
fn plan_completions(
home: &Path,
directory: &Path,
selected: &Path,
verb: &str,
) -> Vec<(PathBuf, GuardedFile)> {
let absolute = |name: &str| {
std::env::var_os(name)
.map(PathBuf::from)
.filter(|path| path.is_absolute())
};
let data = absolute("XDG_DATA_HOME").unwrap_or_else(|| home.join(".local").join("share"));
let config = absolute("XDG_CONFIG_HOME").unwrap_or_else(|| home.join(".config"));
let candidates = [
(
clap_complete::Shell::Bash,
data.join("bash-completion").join("completions"),
"codewhale",
),
(clap_complete::Shell::Zsh, home.join(".zfunc"), "_codewhale"),
(
clap_complete::Shell::Fish,
config.join("fish").join("completions"),
"codewhale.fish",
),
];
let present: Vec<_> = candidates
.into_iter()
.filter(|(_, directory, name)| directory.join(name).symlink_metadata().is_ok())
.collect();
if present.is_empty() {
return Vec::new();
}
// The receipt's other commands (`codew`) and the selected executable are
// this installation, not a second one. Only a copy outside the directory
// being removed keeps the completions.
let install_directory = std::fs::canonicalize(directory).ok();
let others_remain = codewhale_release::installs::inventory(Some(selected))
.entries
.iter()
.any(|entry| {
std::fs::canonicalize(&entry.path).ok().as_deref() != Some(selected)
&& entry
.path
.parent()
.and_then(|parent| std::fs::canonicalize(parent).ok())
.is_none_or(|parent| Some(&parent) != install_directory.as_ref())
});
let mut files = Vec::new();
for (shell, directory, name) in present {
let path = directory.join(name);
if others_remain {
println!(
"Kept completion file {}: another Codewhale copy remains on PATH.",
path.display()
);
continue;
}
let mut file = match GuardedFile::open_bounded(&directory, name, MAX_COMPLETION) {
Ok(Some(file)) => file,
Ok(None) => continue,
Err(error) => {
println!(
"Not read: {} ({error}). If you generated it with codewhale completion, delete it by hand.",
path.display()
);
continue;
}
};
let generated = file
.read_bounded(MAX_COMPLETION)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.is_some_and(|text| is_generated_completion(shell, &text));
if generated {
println!("{verb} completion file {}", path.display());
files.push((path, file));
} else {
println!(
"Left {} unchanged: it is not a Codewhale-generated completion script.",
path.display()
);
}
}
files
}
fn is_generated_completion(shell: clap_complete::Shell, text: &str) -> bool {
let bin = crate::COMPLETION_BIN_NAME;
match shell {
clap_complete::Shell::Bash => {
text.contains(&format!("_{bin}()")) && text.contains(&format!("complete -F _{bin}"))
}
clap_complete::Shell::Zsh => {
text.starts_with(&format!("#compdef {bin}")) && text.contains(&format!("_{bin}()"))
}
clap_complete::Shell::Fish => text.contains(&format!("complete -c {bin}")),
_ => false,
}
}
@@ -649,7 +930,7 @@ fn purge(yes: bool, selected: Option<&Path>) -> Result<()> {
);
}
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."
"Not covered: OS keychain/keyring entries (run codewhale auth clear --provider <id> before removing the program) and per-workspace .codewhale directories."
);
if !yes {
println!(
+1 -1
View File
@@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`exec` and `doctor` follow the TUI onto local Ollama:** with no cloud key and no explicit route, `codewhale exec` uses a running local Ollama chat model and says so, and `codewhale doctor --probe-local` checks that route. Nothing is saved. (#7115)
- **Thinking starts at auto:** new configurations and the built-in default use `reasoning_effort = "auto"` instead of `max`; an effort you set is never changed.
- **DSH Git review scopes:** the extension host brokers bounded, read-only Git metadata to reviewed DSH plugins through a new `git_review_scope` action (extension-host policy v8).
- **Safer uninstall:** program removal never edits shell profiles; matching Codewhale PATH blocks are reported for manual cleanup. Data purge requires a receipted removal in the same confirmed run. Both Unix installers reject unsafe or managed destinations before creating them and keep shell profiles unchanged. Uninstall instructions now match the explicit-home purge scope in English and Chinese.
- **Safer, more complete uninstall:** for a receipted release copy, `codewhale uninstall --yes` now also removes this install's exact three-line `# >>> codewhale PATH >>>` block from your shell profiles and the Codewhale-generated completion files at their documented paths (bash, zsh and fish, honouring `XDG_DATA_HOME` and `XDG_CONFIG_HOME`). It touches nothing else in a profile, leaves a symlinked or unreadable profile for you to edit, keeps the completion files while another Codewhale copy remains on PATH, and exits non-zero if that cleanup fails after the program is already gone. The `codewhale.net/install.sh` hint and the install guide now print the PATH line as that marked block, so there is something provably yours to remove; a bare `export PATH=…` line you added earlier is never touched. Data purge requires a receipted removal in the same confirmed run. Both Unix installers reject unsafe or managed destinations before creating them and still never edit shell profiles themselves. Uninstall instructions match the explicit-home purge scope in English and Chinese. Not yet observed on a real Windows host or an installed release artifact. (Refs #7091)
- **Nested and deletable threads (Runtime API):** a fork (including a backtrack fork) records `parent_thread_id`, `POST /v1/threads` accepts one naming an existing thread, and `GET /v1/threads/summary` returns it so clients can nest child conversations. `DELETE /v1/threads/{id}` removes an idle thread with its turns, items, events and goal, and its saved session unless another thread shares it or a terminal holds it open; a running thread answers 409.
- **Hunk actions in Review (Runtime API):** `POST /v1/git/stage`, `/v1/git/unstage` and `/v1/git/discard` take a `patch` holding hunks of the one path in `paths` and apply only those hunks through `git apply`, under the same `expect` preconditions as whole-path writes. A patch that no longer applies fails with git's message and writes nothing; renames, mode changes and binary patches are refused. The tasks tool can also `suggest` a task, shown as a Start/Hide card, without starting anything.
- **Live `/models` ceilings reach the route:** a provider that states per-model context and output limits in its own `/models` body now keeps them instead of falling back to the legacy 128K window. OpenAI `limit` blocks, flat `context_length` / `context_window` / `max_model_len` / `max_context_length` fields, `max_output_tokens` / `max_completion_tokens`, and a relay `metadata` block (`context_limit`, `context_budget`, `output_budget`) are read; `metadata.context_limit` outranks `context_budget` because the first is the model capability and the second the share one deployment accepts. Each field is parsed on its own, so an unreadable one costs only its own candidate instead of hiding the readable fields below it.
+10 -6
View File
@@ -64,7 +64,7 @@ still need an available release asset source.
curl -fsSL https://codewhale.net/install.sh | sh
# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # zsh: use ~/.zshrc
printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc # zsh: use ~/.zshrc
export PATH="$HOME/.local/bin:$PATH"
codewhale --version # -> codewhale 0.10.0 (1be1a703b975)
@@ -194,7 +194,8 @@ Installed checksummed release commands:
PATH selects no codewhale command; this install is /home/you/.local/bin/codewhale
…
Put /home/you/.local/bin first on PATH in future shells (run once; this installer does not edit shell profiles):
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc
The marker lines let codewhale uninstall remove exactly this block; a bare export line is yours and is never touched.
Then run: . ~/.bashrc (or open a new terminal)
…
```
@@ -234,13 +235,16 @@ prints only the current-shell `export` line. On Ubuntu and Debian, `~/.profile`
Fix it once:
```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # bash (macOS login bash: ~/.bash_profile, or ~/.profile if only that exists)
# echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc # zsh
printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc # bash (macOS login bash: ~/.bash_profile, or ~/.profile if only that exists)
# printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.zshrc # zsh
# fish_add_path ~/.local/bin # fish (untested on this VM)
export PATH="$HOME/.local/bin:$PATH"; hash -r
command -v codewhale codew
```
The three-line block is marked so `codewhale uninstall` can remove exactly it later. A bare
`export PATH=…` line is yours: uninstall never touches it, because other tools share `~/.local/bin`.
### Options
```bash
@@ -1038,7 +1042,7 @@ Not sure which one you have? Run `codewhale doctor --installs`: it lists every c
The Unix installers and the Windows archive installer write `.codewhale-install.sha256` beside the commands. Uninstall accepts only existing regular, single-link files whose bytes match that receipt. On Unix, receipts, selected files and their retained parent directory must belong to the current user and must not be writable by group or others. Optional compiled-host files use their existing release receipt. All selected files pass preflight before any removal; the executing command is removed last. Modified files, symlinks, ambiguous receipts, shared writable directories, legacy unreceipted installs and unrelated files are preserved. No recursive directory removal is performed. A failed removal reports which entries were unlinked, any remaining recovery paths, and any durability failure. If the command receipt was unlinked while the executable remains, recovery republishes it only to a vacant receipt name.
The command runs before configuration, credential migration and telemetry, so malformed config does not block it. It preserves data, credentials, workspaces, shell profiles, completions and services. It never edits a shell profile; for a receipted release copy, it reports recognizable Codewhale PATH blocks in accessible profiles so you can delete those lines yourself. Package-managed copies get removal guidance; custom Cargo roots with local Cargo metadata are recognized. Other command copies still on PATH are listed without running them. Refresh the shell command cache with `hash -r` and inspect the remaining entries.
The command runs before configuration, credential migration and telemetry, so malformed config does not block it. It preserves data, credentials, workspaces and services. For a receipted release copy it also removes this install's exact three-line `# >>> codewhale PATH >>>` block from your shell profiles, and the Codewhale-generated completion files at the documented paths (`~/.local/share/bash-completion/completions/codewhale`, `~/.zfunc/_codewhale`, `~/.config/fish/completions/codewhale.fish`, honouring `XDG_DATA_HOME` and `XDG_CONFIG_HOME`). Nothing else in a profile is touched, a symlinked or unreadable profile is left for you to edit by hand, and the completion files are kept while another Codewhale copy remains on PATH. If any of that cleanup fails, the command says so and exits non-zero even though the program is already gone. Package-managed copies get removal guidance; custom Cargo roots with local Cargo metadata are recognized. Other command copies still on PATH are listed without running them. Refresh the shell command cache with `hash -r` and inspect the remaining entries.
Successful self-update renews an existing command receipt, and writes one for a pre-receipt directory whose commands it just installed; it never claims unknown siblings. This command does not remove a remote-setup service or a desktop application.
@@ -1065,7 +1069,7 @@ by the purge; review the preview and preserve anything needed before confirming.
|---|---|
| OS keyring entries | Run `codewhale auth clear --provider <id>` before removing the program. |
| Per-workspace `.codewhale` directories | Inspect the known workspace individually; uninstall does not search or remove them. |
| Shell PATH lines and completion files | Review the exact paths reported by uninstall; shell profiles are left unchanged. |
| A bare `export PATH=…` line you added yourself, and completion files outside the documented paths | Yours to review: uninstall only removes the marked block and the documented Codewhale-generated completion files. |
| Other installed command copies | Use `codewhale doctor --installs`; each copy follows its own owner and receipt. |
| Legacy snapshots with explicit `CODEWHALE_HOME` | Review them separately; explicit-home purge does not include them. |
+18 -9
View File
@@ -46,7 +46,7 @@ target/release/codewhale --version
curl -fsSL https://codewhale.net/install.sh | sh
# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # zsh: use ~/.zshrc
printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc # zsh: use ~/.zshrc
export PATH="$HOME/.local/bin:$PATH"
codewhale --version # -> codewhale 0.10.0 (1be1a703b975)
@@ -150,7 +150,8 @@ Installed checksummed release commands:
PATH selects no codewhale command; this install is /home/you/.local/bin/codewhale
…
Put /home/you/.local/bin first on PATH in future shells (run once; this installer does not edit shell profiles):
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc
The marker lines let codewhale uninstall remove exactly this block; a bare export line is yours and is never touched.
Then run: . ~/.bashrc (or open a new terminal)
…
```
@@ -173,13 +174,16 @@ Then run: . ~/.bashrc (or open a new terminal)
一次性修复:
```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # bash (macOS login bash: ~/.bash_profile, or ~/.profile if only that exists)
# echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc # zsh
printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc # bash (macOS login bash: ~/.bash_profile, or ~/.profile if only that exists)
# printf '%s\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.zshrc # zsh
# fish_add_path ~/.local/bin # fish (untested on this VM)
export PATH="$HOME/.local/bin:$PATH"; hash -r
command -v codewhale codew
```
这个三行块带有标记,`codewhale uninstall` 之后可以精确地移除它。你自己加的裸 `export PATH=…`
行归你自己管理:卸载从不改动它,因为 `~/.local/bin` 也被其他工具共用。
### 选项
```bash
@@ -690,9 +694,14 @@ codewhale auth clear --provider <id>
| Homebrew | `brew uninstall codewhale`;不再使用该 tap 时才执行 untap。 |
| 旧 curl 安装或其他手动副本 | 已知旧安装目录内、与当前程序字节完全一致且确属当前用户的文件可建立收据后移除;其他来源不明的文件会保留,需先核实具体路径和来源。 |
卸载只移除所选安装收据拥有的命令、宿主文件和收据。它不会修改 shell 配置;
匹配的 PATH 行会列出供手动核对。Windows 正在运行的程序会先改名,退出后由
辅助进程删除。Windows 分支已通过编译检查,仍需在真实 Windows 主机上验证。
卸载移除所选安装收据拥有的命令、宿主文件和收据;对带收据的发布版安装,还会移除
该安装在 shell 配置文件中的精确三行 `# >>> codewhale PATH >>>` 块,以及文档中
约定路径上由 Codewhale 生成的补全文件(`~/.local/share/bash-completion/completions/codewhale`、
`~/.zfunc/_codewhale`、`~/.config/fish/completions/codewhale.fish`,并遵循
`XDG_DATA_HOME` 和 `XDG_CONFIG_HOME`)。配置文件中的其他内容一概不动;符号链接或
无法读取的配置文件留给你手动修改;只要 PATH 上还有其他 Codewhale 副本,补全文件
就会保留。这部分清理若有失败,命令会如实报告并以非零状态退出,即使程序本身已经删除。
Windows 正在运行的程序会先改名,退出后由辅助进程删除。Windows 分支已通过编译检查,仍需在真实 Windows 主机上验证。
重复卸载不会取得其他副本的所有权;重新安装会创建新的收据。
### 第 3 步:可选的数据清理
@@ -708,8 +717,8 @@ codewhale auth clear --provider <id>
清理会删除这些目标内的会话、工作区快照和恢复副本;确认前应先核对预览并保留
仍需使用的内容。重新安装程序不能恢复已清理的数据。
每个工作区的 `.codewhale`、系统钥匙串、shell PATH 行和补全文件不在自动清理
范围内。请逐项检查已知路径;卸载不会扫描整个主目录。远程服务和桌面应用也有
每个工作区的 `.codewhale` 和系统钥匙串不在自动清理范围内;你自己加的裸 `export PATH=…`
行,以及约定路径之外的补全文件,也由你自己检查。请逐项检查已知路径;卸载不会扫描整个主目录。远程服务和桌面应用也有
各自的安装所有权,不由所选 CLI 安装的收据代管。
---
+9 -7
View File
@@ -182,11 +182,11 @@ describe.skipIf(process.platform === "win32")("public installer compatibility co
});
it.each([
["/bin/zsh", "Linux", `echo 'export PATH="INSTALL:$PATH"' >> ~/.zshrc`, ". ~/.zshrc"],
["/bin/bash", "Linux", `echo 'export PATH="INSTALL:$PATH"' >> ~/.bashrc`, ". ~/.bashrc"],
["/bin/bash", "Darwin", `echo 'export PATH="INSTALL:$PATH"' >> ~/.bash_profile`, ". ~/.bash_profile"],
["/bin/zsh", "Linux", `printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH="INSTALL:$PATH"' '# <<< codewhale PATH <<<' >> ~/.zshrc`, ". ~/.zshrc"],
["/bin/bash", "Linux", `printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH="INSTALL:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bashrc`, ". ~/.bashrc"],
["/bin/bash", "Darwin", `printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH="INSTALL:$PATH"' '# <<< codewhale PATH <<<' >> ~/.bash_profile`, ". ~/.bash_profile"],
["/usr/bin/fish", "Linux", `fish_add_path "INSTALL"`, "this fish shell"],
["/bin/dash", "Linux", `echo 'export PATH="INSTALL:$PATH"' >> ~/.profile`, ". ~/.profile"],
["/bin/dash", "Linux", `printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH="INSTALL:$PATH"' '# <<< codewhale PATH <<<' >> ~/.profile`, ". ~/.profile"],
] as const)(
"prints the persistent PATH line for %s on %s without editing a profile",
(shell, os, persist, reload) => {
@@ -204,7 +204,7 @@ describe.skipIf(process.platform === "win32")("public installer compatibility co
it.skipIf(!existsSync("/bin/dash"))("the printed profile and reload commands work in dash", () => {
const { installDir, result } = installFixture(false, { shell: "/bin/dash", os: "Linux" });
const persist = result.stdout.split("\n").find((line) => line.trim().startsWith("echo 'export PATH="));
const persist = result.stdout.split("\n").find((line) => line.trim().startsWith("printf '%s\\n' '# >>> codewhale PATH >>>'"));
const reload = result.stdout.match(/Then run: (.+?) \(or open a new terminal\)/)?.[1];
expect(persist).toBeDefined();
expect(reload).toBeDefined();
@@ -221,7 +221,9 @@ describe.skipIf(process.platform === "win32")("public installer compatibility co
it("writes the default directory as $HOME/.local/bin in the persistent line", () => {
const { result } = installFixture(false, { installUnderHome: true });
expect(result.stdout).toContain(`echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc`);
expect(result.stdout).toContain(
`printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH="$HOME/.local/bin:$PATH"' '# <<< codewhale PATH <<<' >> ~/.zshrc`,
);
});
it.each([
@@ -231,7 +233,7 @@ describe.skipIf(process.platform === "win32")("public installer compatibility co
] as const)("keeps macOS login bash reading %j by naming %s", (homeFiles, profile) => {
const { result } = installFixture(false, { shell: "/bin/bash", os: "Darwin", homeFiles: [...homeFiles] });
expect(result.stdout).toContain(`:$PATH"' >> ${profile}\n`);
expect(result.stdout).toContain(`'# <<< codewhale PATH <<<' >> ${profile}\n`);
});
it.each(["/bin/tcsh", "/usr/bin/nu"])("prints no POSIX line for the unrecognised shell %s", (shell) => {
+2 -1
View File
@@ -567,7 +567,8 @@ if [ "$path_selected" -eq 0 ]; then
*) profile=".profile" ;;
esac
say "Put $install_dir first on PATH in future shells (run once; this installer does not edit shell profiles):"
say " echo 'export PATH=\"$path_dir:\$PATH\"' >> ~/$profile"
say " printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH=\"$path_dir:\$PATH\"' '# <<< codewhale PATH <<<' >> ~/$profile"
say "The marker lines let codewhale uninstall remove exactly this block; a bare export line is yours and is never touched."
say "Then run: . ~/$profile (or open a new terminal)"
say "Or for this shell only:"
say " export PATH=\"$path_dir:\$PATH\"; hash -r"