fix(uninstall): purge data only after a receipted removal, refuse unsafe install directories

`codewhale uninstall --yes --purge-data` deleted ~/.codewhale even when the run only printed a package manager's removal command and left the program installed. Purge now runs only in a confirmed run that removed a receipted installation. Adopting an older curl-installed copy opens every candidate without following links and refuses symlinks, hard links and files owned by another user before any receipt is written, and uninstall resolves the selected executable path on Unix. Recognizable Codewhale PATH blocks in shell profiles are reported for manual cleanup; uninstall never edits a profile. The website installer now creates new directories privately and refuses system, Homebrew and MacPorts bin directories, the home directory, `/`, and directories other users or groups can write, with the `chmod go-w` remedy in its message. INSTALL.md (English and Chinese), both changelogs and the generated agent docs follow.

Validation: a macOS arm64 release build of this exact uninstall.rs (4,160 build inputs unchanged) and a sandboxed probe of the resulting binary in synthetic directories passed 4/4 cases (plain removal, purge preview and purge, package-managed copy keeps data, hard-link refusal; network and the real home denied). An x86_64-pc-windows-gnu zig build of the same source compiled and linked (21m06s, inputs unchanged). scripts/release/install-safety.test.js passed 33/33 (website and archive installers) on a scratch export of this tree; rustfmt --all, sh -n install.sh and sync-changelog --check pass, and the generated install entry was re-derived with the repository's own generator. Not covered: no run on a Windows host, no MSVC build, no real npm, Cargo, Homebrew, WinGet or Scoop removal, no hosted CI, and no installer-written PATH block exists yet, so the report path only sees blocks written by hand. The PowerShell and NSIS routes and product-owned PATH integration (F2) remain open; #7091 stays open.

Refs #7091

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-10-28 23:35:57 -07:00
1 parent 4d600eaaee
commit cce6d3a84d
8 files changed
+205 -65

No files matched your search

+2 -1
View File
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **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. The website installer rejects shared-writable and system directories and keeps shell profiles unchanged.
- **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.
@@ -20,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
- **Updates install themselves:** release-binary installs check for a new release every 6 hours (was 1), download and checksum-verify it in the background, and apply it the next time `codewhale` starts, never during a session. One line says `Updated to v… · What's new: … · Undo: codewhale update --rollback`. Until releases are signed, the download waits for your OK (one `[y/N]` question at the next start, or `/update install`); a release with a valid Ed25519 manifest signature applies on its own. `codewhale update --rollback` puts back the version the last update replaced and keeps the rolled-back version from returning automatically. Turn it down with `codewhale config set update.mode notify` (or `off`, or `CODEWHALE_UPDATE_MODE`); an organization's root-owned `policy.toml` overrides both. Offline and rate-limited checks retry about an hour later instead of on every launch. Package-manager and desktop-app copies only get their manager's command. Builds with the release key refuse a missing or invalid signature, and re-check the staged binary against the signed manifest at start. The checksum manifest now always comes from GitHub Releases; on Linux x64 the CNB mirror can still serve the binary, but only bytes that GitHub's manifest lists.
- **One view of every Codewhale on the machine:** `codewhale doctor` has an Installations section and `codewhale doctor --installs` (`--json`, `--brief`) lists every `codewhale`, `codew`, `codewhale-tui`, `deepseek` and `deepseek-tui` with its owner: install receipt, package manager (with its exact removal command), the desktop app, a backup, or not ours. Nothing found is ever run. `codewhale doctor --fix` adds one plan to tidy them: old backups move to `~/.codewhale/recovery/<stamp>/` (kept 30 days, then deleted by a later `--fix`), an unreceipted `codew` is brought in line with `codewhale`, and the directory gets an install receipt; each move prints its undo and `codewhale doctor --restore <stamp>` reverses the lot. Package-managed, desktop-app, symlinked and foreign files are never touched, and neither is user data. `codewhale uninstall` now lists other copies with the same classification.
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. A copy from an older `curl | sh` installer that wrote no receipt is adopted on `--yes` when it sits in an install.sh default directory: the commands byte-identical to the running program get a receipt and are removed, and anything else is preserved. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
- **Uninstall guidance on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`. With `--yes` it runs only after a receipted installation was removed in that same run, and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. A copy from an older `curl | sh` installer that wrote no receipt is adopted on `--yes` when it sits in a known older curl-installer directory: the commands byte-identical to the running program get a receipt and are removed, anything else is preserved, and a symlink or hard link among them stops adoption before any receipt is written. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
- **GitHub Action reviews:** analysis can export a structured Engine review for a separate trusted publisher job. The publisher checks the current PR revision and line anchors, deduplicates its findings, and keeps uncertain or preexisting claims out of inline comments.
- **DingTalk bridge:** an optional internal-app Stream adapter reuses the same Engine, shared admission and approval helpers. Tenant/staff allowlists, durable acceptance before acknowledgement and retained uncertain deliveries protect recovery. Managed account linking is separate from this advanced self-hosted bridge.
- **Answerless output-limit recovery:** opt into `[reasoning_only] on_output_limit = true` to request an answer again after a length stop with no answer or tool call, including hidden reasoning and ordinary Fleet workers. Retries share `max_reprompts`, retain existing output and control limits, and record request-scoped nudge receipts without adding them to resumed conversation context. RLM keeps its existing stop protocol. Thanks [@M-Maciej](https://github.com/M-Maciej) for the long-proof Fleet report. (#6990)
+168 -43
View File
@@ -127,9 +127,18 @@ pub(crate) fn run(yes: bool, purge_data: bool) -> Result<()> {
let selected = std::env::current_exe().ok();
let result = remove(yes);
report_path_copies(selected.as_deref());
result?;
let removed = result?;
if purge_data {
purge(yes)
if !yes {
return purge(false);
}
if removed {
return purge(true);
}
println!(
"Data was not purged: this run did not remove a receipted installation. Data is shared by every install and is kept."
);
Ok(())
} else {
println!(
"User data was not touched. To also delete Codewhale's own data directory, add --purge-data."
@@ -138,12 +147,20 @@ pub(crate) fn run(yes: bool, purge_data: bool) -> Result<()> {
}
}
fn remove(yes: bool) -> Result<()> {
fn remove(yes: bool) -> Result<bool> {
let identity = update_executable_identity()?;
#[cfg(not(windows))]
let identity = UpdateExecutableIdentity {
path: identity
.path
.canonicalize()
.context("failed to resolve the selected executable path")?,
..identity
};
let method = InstallMethod::detect(&identity.path);
println!("Selected executable: {}", identity.path.display());
println!(
"Program removal preserves user state, credentials, sessions, workspaces, shell profiles and services."
"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."
);
if !method.supports_self_update() {
println!(
@@ -151,14 +168,14 @@ fn remove(yes: bool) -> Result<()> {
method.label(),
codewhale_release::installs::manager_command(method, &identity.path)
);
return Ok(());
return Ok(false);
}
if let Some(uninstaller) = windows_installer_uninstaller(&identity.path) {
println!(
"This copy belongs to the Codewhale Windows installer. Remove it from Settings > Apps > CodeWhale, or run: \"{}\"",
uninstaller.display()
);
return Ok(());
return Ok(false);
}
if protected_update_path(&identity.path) {
bail!(
@@ -180,7 +197,7 @@ fn remove(yes: bool) -> Result<()> {
let ownership = match CommandReceipt::capture(&identity.path)? {
Some(ownership) => ownership,
None => {
let (directory, entries) = curl_install_entries(&identity).with_context(|| format!(
let (directory, entries) = curl_install_entries(&identity)?.with_context(|| format!(
"No command ownership receipt beside {}. This legacy/manual copy is preserved. Inspect its exact path and provenance before removing it manually; reinstalling into a fresh directory creates a receipted install.", identity.path.display()))?;
println!(
"No ownership receipt yet: {} is a curl-installer location and these commands are byte-identical to this Codewhale {}.",
@@ -194,7 +211,7 @@ fn remove(yes: bool) -> Result<()> {
println!(
"No files were changed. Run this executable again with uninstall --yes to record a receipt for exactly these files and remove them."
);
return Ok(());
return Ok(false);
}
let text: String = entries
.iter()
@@ -255,12 +272,13 @@ fn remove(yes: bool) -> Result<()> {
directory.join(file.name()).display()
);
}
report_path_blocks(&directory);
validate_primary_update_identity(&identity)?;
if !yes {
println!(
"No files were changed. Run this executable again with uninstall --yes to remove only this plan."
);
return Ok(());
return Ok(false);
}
#[cfg(unix)]
{
@@ -346,51 +364,156 @@ fn remove(yes: bool) -> Result<()> {
"Run hash -r in your shell."
}
);
Ok(())
Ok(true)
}
const PATH_BLOCK_START: &str = "# >>> codewhale PATH >>>";
const PATH_BLOCK_END: &str = "# <<< codewhale PATH <<<";
const MAX_PROFILE: usize = 1024 * 1024;
fn report_path_blocks(directory: &Path) {
let Some(home) = dirs::home_dir() else {
return;
};
let zdotdir = std::env::var_os("ZDOTDIR").map(PathBuf::from);
let mut profiles: Vec<PathBuf> = [
".zshrc",
".bashrc",
".bash_profile",
".bash_login",
".profile",
]
.iter()
.map(|name| home.join(name))
.chain(zdotdir.map(|dir| dir.join(".zshrc")))
.chain(std::iter::once(
home.join(".config")
.join("fish")
.join("conf.d")
.join("codewhale.fish"),
))
.collect();
profiles.sort();
profiles.dedup();
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)
};
let ours = |line: &str| {
line.strip_prefix("export PATH=\"")
.and_then(|rest| rest.strip_suffix(":$PATH\""))
.is_some_and(|value| names_directory(value))
|| line
.strip_prefix("contains -- \"")
.and_then(|rest| rest.split_once("\" $PATH; or set -gx PATH \""))
.is_some_and(|(value, rest)| {
rest == format!("{value}\" $PATH") && names_directory(value)
})
};
for profile in profiles {
let (Some(parent), Some(name)) = (
profile.parent(),
profile.file_name().and_then(|name| name.to_str()),
) else {
continue;
};
let mut file = match GuardedFile::open_bounded(parent, name, MAX_PROFILE) {
Ok(Some(file)) => file,
Ok(None) => continue,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
println!(
"Not read: {} ({error}). If it holds a Codewhale PATH block, remove that block by hand.",
profile.display()
);
continue;
}
};
let text = match file.read_bounded(MAX_PROFILE).map(String::from_utf8) {
Ok(Ok(text)) => text,
_ => {
println!(
"Not read: {} is not a UTF-8 file within the size limit.",
profile.display()
);
continue;
}
};
let lines: Vec<&str> = text.lines().map(str::trim_end).collect();
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.",
profile.display(),
index + 1,
index + 3
);
index += 3;
continue;
}
println!(
"Codewhale PATH marker at {} line {} does not form a block for this install; left unchanged.",
profile.display(),
index + 1
);
} else if lines[index] == PATH_BLOCK_END {
println!(
"Codewhale PATH end marker at {} line {} has no start marker; left unchanged.",
profile.display(),
index + 1
);
}
index += 1;
}
}
}
fn curl_install_entries(
identity: &UpdateExecutableIdentity,
) -> Option<(PathBuf, Vec<(String, String)>)> {
let directory = identity.path.parent()?.canonicalize().ok()?;
let home = dirs::home_dir()?.canonicalize().ok()?;
) -> Result<Option<(PathBuf, Vec<(String, String)>)>> {
let Some(directory) = identity
.path
.parent()
.and_then(|parent| parent.canonicalize().ok())
else {
return Ok(None);
};
let Some(home) = dirs::home_dir().and_then(|home| home.canonicalize().ok()) else {
return Ok(None);
};
let local = home.join(".local");
let known = directory == local.join("bin")
|| directory == local.join("codewhale").join("bin")
|| (cfg!(windows) && directory == home.join("bin"))
|| (directory.parent() == Some(local.as_path())
&& directory
.file_name()?
.to_str()?
.starts_with("codewhale-release."));
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("codewhale-release.")));
if !known {
return None;
return Ok(None);
}
let entries: Vec<(String, String)> = COMMANDS
.iter()
.filter(|name| {
let path = directory.join(name);
std::fs::symlink_metadata(&path)
.is_ok_and(|meta| meta.is_file() && owned_by_user(&meta))
&& std::fs::read(&path).is_ok_and(|bytes| sha256_hex(&bytes) == identity.file_hash)
})
.map(|name| ((*name).to_string(), identity.file_hash.clone()))
.collect();
entries
.iter()
.any(|(name, _)| identity.path.file_name() == Some(std::ffi::OsStr::new(name)))
.then_some((directory, entries))
}
#[cfg(unix)]
fn owned_by_user(meta: &std::fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
meta.uid() == codewhale_config::private_directory::PrivateDirectory::current_user_id()
}
#[cfg(not(unix))]
fn owned_by_user(_meta: &std::fs::Metadata) -> bool {
true
let mut entries = Vec::new();
for name in COMMANDS.iter().copied() {
let Some(mut file) = GuardedFile::open(&directory, name)? else {
continue;
};
file.verify_owned()?;
if file.sha256() == identity.file_hash.as_str() {
entries.push((name.to_string(), identity.file_hash.clone()));
}
}
let selected_listed = identity.path.file_name().is_some_and(|file_name| {
entries
.iter()
.any(|(name, _)| file_name == std::ffi::OsStr::new(name))
});
Ok(selected_listed.then_some((directory, entries)))
}
fn windows_installer_uninstaller(executable: &Path) -> Option<PathBuf> {
@@ -491,7 +614,9 @@ fn purge(yes: bool) -> Result<()> {
"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."
);
if !yes {
println!("No data was changed. Add --yes to delete exactly these directories.");
println!(
"No data was changed. Adding --yes deletes these directories only after a receipted installation is removed in the same run."
);
return Ok(());
}
for target in present {
+2 -1
View File
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **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. The website installer rejects shared-writable and system directories and keeps shell profiles unchanged.
- **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.
@@ -20,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
- **Updates install themselves:** release-binary installs check for a new release every 6 hours (was 1), download and checksum-verify it in the background, and apply it the next time `codewhale` starts, never during a session. One line says `Updated to v… · What's new: … · Undo: codewhale update --rollback`. Until releases are signed, the download waits for your OK (one `[y/N]` question at the next start, or `/update install`); a release with a valid Ed25519 manifest signature applies on its own. `codewhale update --rollback` puts back the version the last update replaced and keeps the rolled-back version from returning automatically. Turn it down with `codewhale config set update.mode notify` (or `off`, or `CODEWHALE_UPDATE_MODE`); an organization's root-owned `policy.toml` overrides both. Offline and rate-limited checks retry about an hour later instead of on every launch. Package-manager and desktop-app copies only get their manager's command. Builds with the release key refuse a missing or invalid signature, and re-check the staged binary against the signed manifest at start. The checksum manifest now always comes from GitHub Releases; on Linux x64 the CNB mirror can still serve the binary, but only bytes that GitHub's manifest lists.
- **One view of every Codewhale on the machine:** `codewhale doctor` has an Installations section and `codewhale doctor --installs` (`--json`, `--brief`) lists every `codewhale`, `codew`, `codewhale-tui`, `deepseek` and `deepseek-tui` with its owner: install receipt, package manager (with its exact removal command), the desktop app, a backup, or not ours. Nothing found is ever run. `codewhale doctor --fix` adds one plan to tidy them: old backups move to `~/.codewhale/recovery/<stamp>/` (kept 30 days, then deleted by a later `--fix`), an unreceipted `codew` is brought in line with `codewhale`, and the directory gets an install receipt; each move prints its undo and `codewhale doctor --restore <stamp>` reverses the lot. Package-managed, desktop-app, symlinked and foreign files are never touched, and neither is user data. `codewhale uninstall` now lists other copies with the same classification.
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. A copy from an older `curl | sh` installer that wrote no receipt is adopted on `--yes` when it sits in an install.sh default directory: the commands byte-identical to the running program get a receipt and are removed, and anything else is preserved. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
- **Uninstall guidance on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`. With `--yes` it runs only after a receipted installation was removed in that same run, and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. A copy from an older `curl | sh` installer that wrote no receipt is adopted on `--yes` when it sits in a known older curl-installer directory: the commands byte-identical to the running program get a receipt and are removed, anything else is preserved, and a symlink or hard link among them stops adoption before any receipt is written. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
- **GitHub Action reviews:** analysis can export a structured Engine review for a separate trusted publisher job. The publisher checks the current PR revision and line anchors, deduplicates its findings, and keeps uncertain or preexisting claims out of inline comments.
- **DingTalk bridge:** an optional internal-app Stream adapter reuses the same Engine, shared admission and approval helpers. Tenant/staff allowlists, durable acceptance before acknowledgement and retained uncertain deliveries protect recovery. Managed account linking is separate from this advanced self-hosted bridge.
- **Answerless output-limit recovery:** opt into `[reasoning_only] on_output_limit = true` to request an answer again after a length stop with no answer or tool call, including hidden reasoning and ordinary Fleet workers. Retries share `max_reprompts`, retain existing output and control limits, and record request-scoped nudge receipts without adding them to resumed conversation context. RLM keeps its existing stop protocol. Thanks [@M-Maciej](https://github.com/M-Maciej) for the long-proof Fleet report. (#6990)
+12 -9
View File
@@ -173,9 +173,11 @@ What it does (verified):
a temporary copy otherwise.
* It **never uses sudo and never edits your shell profile.** It refuses to
install into system or package-manager directories (`/usr/bin`,
`~/.cargo/bin`, Homebrew, `node_modules`, `/nix/store`…) and refuses to
overwrite a *different* existing `codewhale` unless you ask for it with
`CODEWHALE_FORCE_REINSTALL=1` (see [re-running the
`~/.cargo/bin`, Homebrew and MacPorts bin directories, `node_modules`,
`/nix/store`…), into your home directory or `/`, or into a directory that
other users or groups can write (`chmod go-w <dir>` fixes one you own). It
also refuses to overwrite a *different* existing `codewhale` unless you ask
for it with `CODEWHALE_FORCE_REINSTALL=1` (see [re-running the
installer](#re-running-the-installer)).
Expected output:
@@ -342,8 +344,9 @@ tar -xzf codewhale-linux-x64.tar.gz
cd codewhale-linux-x64 && ./install.sh # -> ~/.local/bin; PREFIX=/some/dir ./install.sh -> /some/dir/bin
```
The archive's `install.sh` behaves like the website installer: no sudo, it
leaves differing existing files alone, and it prints the same PATH hint.
The archive's `install.sh` never uses sudo, leaves differing existing files
alone, never edits a shell profile, and asks nothing; it prints only the
current-shell `export` line.
**Upgrade:** `codewhale update` works for both 3a and 3b, because they're
"direct binary" installs. **Uninstall:** delete the two files (see §12).
@@ -1020,7 +1023,7 @@ codewhale auth clear --provider deepseek
| Windows setup (NSIS) | Settings > Apps > CodeWhale > Uninstall, or `%LOCALAPPDATA%\Programs\CodeWhale\Uninstall.exe`. `codewhale uninstall` prints this path. |
| WinGet | `winget uninstall --id HunterBown.CodeWhale` |
| Scoop | `scoop uninstall codewhale` |
| older `curl \| sh` install without a receipt | `~/.local/bin/codewhale uninstall --yes`: in an install.sh default directory (`~/.local/bin`, `~/.local/codewhale/bin`, `~/.local/codewhale-release.*`, or `%USERPROFILE%\bin` on Windows) it records a receipt for the commands that are byte-identical to the running program, then removes only those. |
| older `curl \| sh` install without a receipt | `~/.local/bin/codewhale uninstall --yes`: in a known older curl-installer directory (`~/.local/bin`, `~/.local/codewhale/bin`, `~/.local/codewhale-release.*`, or `%USERPROFILE%\bin` on Windows) it records a receipt for the commands whose bytes match the running program, then removes only those. A symlink, a hard link, or a file you do not own among them stops it before any receipt is written. |
| manual copy elsewhere (§3) | Inspect the exact command paths and provenance before removing them manually; automatic uninstall preserves them. |
| npm | `npm uninstall -g codewhale` (`codewhale uninstall` prints the `--prefix` for this copy) |
| Cargo | `cargo uninstall codewhale-cli` (`codewhale uninstall` prints the owning `--root`) |
@@ -1030,7 +1033,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. 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, 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.
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.
@@ -1040,7 +1043,7 @@ Repeating `uninstall` after removal is safe: the removed copy is gone, and any o
### Step 3: remove data. Program removal never does this.
`codewhale uninstall --purge-data` previews deleting `~/.codewhale` (or `$CODEWHALE_HOME`) and `~/.deepseek/snapshots`. Add `--yes` to delete exactly those directories. It refuses a path that is a symlink, your home directory or one of its parents, or (on Unix) a directory owned by another user. It also refuses an explicit `CODEWHALE_HOME` not named `.codewhale`; delete that one by hand after you review it. It does not search for per-workspace `.codewhale` directories, keyring entries, shell profile lines or completion files. Run `codewhale auth clear --provider <id>` first to remove keyring entries. To remove data by hand instead:
`codewhale uninstall --purge-data` previews deleting `~/.codewhale` (or `$CODEWHALE_HOME`) and `~/.deepseek/snapshots`. Add `--yes` to delete exactly those directories. The purge runs only in the same `--yes` run that removed a receipted installation; a package-managed copy, a plan-only run, or a failed removal keeps all data. It refuses a path that is a symlink, your home directory or one of its parents, or (on Unix) a directory owned by another user. It also refuses an explicit `CODEWHALE_HOME` not named `.codewhale`; delete that one by hand after you review it. It does not search for per-workspace `.codewhale` directories, keyring entries, shell profile lines or completion files. Run `codewhale auth clear --provider <id>` first to remove keyring entries. To remove data by hand instead:
| Path | What it is | Size seen |
|---|---|---|
@@ -1048,7 +1051,7 @@ Repeating `uninstall` after removal is safe: the removed copy is gone, and any o
| `~/.deepseek/snapshots/` | v0.10.0 stores its per-turn **copies of your workspaces** here (a legacy path). Contains the contents of every repo you ran it in. | 0.2–0.6 MB here; grows with repo size |
| `<every repo you used>/.codewhale/` | per-workspace state/lock dir | tiny |
| completion files | `~/.local/share/bash-completion/completions/codewhale`, `~/.zfunc/_codewhale`, `~/.config/fish/completions/codewhale.fish` | – |
| PATH lines you added | `~/.bashrc`, `~/.zshrc`, `~/.profile` | – |
| PATH lines or Codewhale PATH blocks you added | `~/.bashrc`, `~/.zshrc`, `~/.profile`, `~/.config/fish/conf.d/codewhale.fish` | – |
```bash
rm -rf ~/.codewhale ~/.deepseek/snapshots
+6 -4
View File
@@ -134,7 +134,7 @@ curl -fsSL https://codewhale.net/install.sh | sh
* 使用校验过的发布内容分别安装 `~/.local/bin/codewhale` 和
`~/.local/bin/codew`。当两个清单摘要相同时,下载暂存阶段尽量使用硬链接,
失败时使用临时副本。
* **从不使用 sudo,也从不修改你的 shell 配置文件。** 它拒绝安装到系统或包管理器目录(`/usr/bin`、`~/.cargo/bin`、Homebrew、`node_modules`、`/nix/store` 等),也拒绝覆盖一个*不同的*已有 `codewhale`。
* **从不使用 sudo,也从不修改你的 shell 配置文件。** 它拒绝安装到系统或包管理器目录(`/usr/bin`、`~/.cargo/bin`、Homebrew 与 MacPorts 的 bin 目录、`node_modules`、`/nix/store` 等)、你的主目录或 `/`,以及其他用户或用户组可写的目录(你自己的目录可用 `chmod go-w <目录>` 修复);也拒绝覆盖一个*不同的*已有 `codewhale`。
预期输出:
@@ -262,7 +262,7 @@ cd codewhale-linux-x64 && ./install.sh # -> ~/.local/bin; PREFIX=/
(注意:校验清单是 *bundles*,不是 *artifacts*。)
压缩包里的 `install.sh` 与网站上的安装器行为一致:不用 sudo,不动内容不同的已有文件,并打印同样的 PATH 提示。
压缩包里的 `install.sh` 不用 sudo,不动内容不同的已有文件,从不修改 shell 配置文件,也不提问;它只打印针对当前 shell 的 `export` 行。
**升级:** `codewhale update` 对 3a 和 3b 都适用,因为它们都属于"直接二进制"安装。**卸载:** 删除那两个文件(见 §12)。
@@ -679,7 +679,7 @@ codewhale auth clear --provider deepseek
| Cargo | `cargo uninstall codewhale-cli` |
| Homebrew | `brew uninstall codewhale && brew untap Hmbown/deepseek-tui`(同时会移除它的 node 依赖) |
### 第 3 步:删除数据。没有任何卸载器会替你做这件事。
### 第 3 步:删除数据。程序删除从不做这件事。
| 路径 | 内容 | 所见大小 |
|---|---|---|
@@ -687,7 +687,7 @@ codewhale auth clear --provider deepseek
| `~/.deepseek/snapshots/` | v0.10.0 把每个回合的**工作区副本**存在这里(一个旧路径)。里面包含你运行过它的每个仓库的内容。 | 这里为 0.2–0.6 MB;随仓库大小增长 |
| `<你用过的每个仓库>/.codewhale/` | 每个工作区的状态/锁目录 | 很小 |
| 补全文件 | `~/.local/share/bash-completion/completions/codewhale`、`~/.zfunc/_codewhale`、`~/.config/fish/completions/codewhale.fish` | – |
| 你添加的 PATH 行 | `~/.bashrc`、`~/.zshrc`、`~/.profile` | – |
| 你添加的 PATH 行或 Codewhale PATH 代码块 | `~/.bashrc`、`~/.zshrc`、`~/.profile`、`~/.config/fish/conf.d/codewhale.fish` | – |
```bash
rm -rf ~/.codewhale ~/.deepseek/snapshots
@@ -700,6 +700,8 @@ find ~ -type d -name .codewhale -prune -print # review, then delete the ones
Codewhale 没有在 `$HOME` 和它被使用过的仓库之外写入任何东西:没有系统文件、服务或 cron 任务。(我检查了测试用户在其主目录之外拥有的每一个文件。)上面的命令只删除 `~/.deepseek/snapshots`。如果你仍在使用较早的 DeepSeek-TUI,`~/.deepseek` 里的其余部分(它的配置和会话)不会被动。
`codewhale uninstall --purge-data --yes` 只会在同一次运行中成功删除了一个带收据的安装之后,才删除数据目录(`~/.codewhale` 或 `$CODEWHALE_HOME`,以及 `~/.deepseek/snapshots`)。包管理器安装、仅预览的运行或删除失败时,数据都会保留。它也不会删除钥匙串条目、每个工作区的 `.codewhale` 目录、shell 配置行或补全文件。
---
## 13. 故障排查
+4 -2
View File
@@ -50,12 +50,14 @@ asset="$(basename "$url")"
cp "$INSTALL_TEST_ASSETS/$asset" "$output"
`);
const destination = path.join(home, ".local", "bin");
const env = { ...process.env, HOME: home, PATH: `${bin}:${process.env.PATH}`,
fs.mkdirSync(destination, { recursive: true });
fs.chmodSync(destination, 0o755);
const env = { ...process.env, HOME: home, PATH: `${bin}:${process.env.PATH}`, CI: "1",
CODEWHALE_VERSION: "v0.9.11", CODEWHALE_INSTALL_DIR: destination,
PREFIX: path.dirname(destination), INSTALL_TEST_ASSETS: assets,
INSTALL_TEST_EXECUTED: path.join(root, "executed"),
INSTALL_TEST_DOWNLOADS: path.join(root, "downloads") };
for (const key of ["CODEWHALE_RELEASE_BASE_URL", "DEEPSEEK_TUI_RELEASE_BASE_URL", "CODEWHALE_SKIP_GLIBC_CHECK", "DEEPSEEK_TUI_SKIP_GLIBC_CHECK", "DEEPSEEK_SKIP_GLIBC_CHECK", "TERMUX_VERSION"]) delete env[key];
for (const key of ["CODEWHALE_RELEASE_BASE_URL", "DEEPSEEK_TUI_RELEASE_BASE_URL", "CODEWHALE_SKIP_GLIBC_CHECK", "DEEPSEEK_TUI_SKIP_GLIBC_CHECK", "DEEPSEEK_SKIP_GLIBC_CHECK", "TERMUX_VERSION", "CODEWHALE_HOME", "CODEWHALE_FORCE_REINSTALL", "CODEWHALE_INSTALL_COMPILED_HOST", "SHELL", "ZDOTDIR"]) delete env[key];
function run() {
const script = kind === "website" ? path.join(repo, "web/public/install.sh") : path.join(archive, "install.sh");
return spawnSync(kind === "website" ? "sh" : "bash", [script], { env, encoding: "utf8", timeout: 10000 });
+2 -2
View File
@@ -5,8 +5,8 @@
"sources": [
{
"path": "docs/INSTALL.md",
"sha256": "f7476f4342d7392896b6485741fdeac5c2a3694f712ac47e74b4100394a28e72",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#Line truncated
"sha256": "479b9fc8fcd03beea743c2609f7b9cf5a2225b9a9d28b23f7a03644f1f935c8b",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#Line truncated
}
]
},
+9 -3
View File
@@ -238,14 +238,20 @@ case "$install_dir" in
*) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
esac
[ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
(umask 077; mkdir -p "$install_dir") || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
install_dir="$(cd -P "$install_dir" && pwd)"
case "$install_dir/" in
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
/bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*|/opt/homebrew/bin/*|/usr/local/bin/*|/opt/local/bin/*)
fail "refusing managed/system directory $install_dir; use a fresh user directory"
;;
esac
if [ "$install_dir" = / ] || { [ -n "${HOME:-}" ] && [ "$install_dir" = "$(cd -P "$HOME" 2>/dev/null && pwd)" ]; }; then
fail "refusing $install_dir; use a fresh directory such as \$HOME/.local/bin"
fi
[ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
if [ -n "$(find "$install_dir" -prune \( -perm -020 -o -perm -002 \) -print)" ]; then
fail "$install_dir is writable by its group or others; run chmod go-w '$install_dir' or choose a directory only you can write"
fi
for cargo_receipt in "$install_dir/../.crates.toml" "$install_dir/../.crates2.json"; do
if [ -e "$cargo_receipt" ] || [ -L "$cargo_receipt" ]; then
fail "Cargo install metadata beside this directory; use a fresh directory instead"
@@ -483,7 +489,7 @@ say " $install_dir/codew"
if [ -n "$recovery_dir" ]; then
say "Previous files are kept in $recovery_dir for 30 days. Undo: \"$install_dir/codewhale\" doctor --restore $(basename "$recovery_dir")"
fi
"$install_dir/codewhale" doctor --installs --brief 2>/dev/null || true
"$install_dir/codewhale" doctor --installs --brief </dev/null 2>/dev/null || true
say ""
say "Use this installation: \"$install_dir/codewhale\""