feat(update):Add proxy option to update command

Update docs to introduce update command proxy options
This commit is contained in:
AccMoment
2026-05-27 20:58:48 +08:00
parent 54151a4bc9
commit 6dd8394dfe
5 changed files with 128 additions and 26 deletions
+3
View File
@@ -51,6 +51,9 @@ cargo install codewhale-cli --locked --force
cargo install codewhale-tui --locked --force
```
> codewhale update は --proxy をサポートしており、プロキシ経由で更新できます
> 例: codewhale update --proxy https://localhost:7897
[![CI](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml/badge.svg)](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/codewhale)](https://www.npmjs.com/package/codewhale)
[![crates.io](https://img.shields.io/crates/v/codewhale-cli?label=crates.io)](https://crates.io/crates/codewhale-cli)
+3
View File
@@ -64,6 +64,9 @@ cargo install codewhale-cli --locked --force
cargo install codewhale-tui --locked --force
```
> codewhale update now supports --proxy, update through a proxy
> eg: codewhale update --proxy https://localhost:7897
[![CI](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml/badge.svg)](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/codewhale)](https://www.npmjs.com/package/codewhale)
[![crates.io](https://img.shields.io/crates/v/codewhale-cli?label=crates.io)](https://crates.io/crates/codewhale-cli)
+2
View File
@@ -59,6 +59,8 @@ brew update && brew upgrade deepseek-tui
cargo install codewhale-cli --locked --force
cargo install codewhale-tui --locked --force
```
> codewhale update 现在可添加 --proxy ,通过代理下载更新
> eg: codewhale update --proxy https://localhost:7897
[![CI](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml/badge.svg)](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/codewhale)](https://www.npmjs.com/package/codewhale)
+36 -3
View File
@@ -240,6 +240,8 @@ struct UpdateArgs {
/// Update to the latest beta release instead of the latest stable release.
#[arg(long)]
beta: bool,
#[arg(long)]
proxy: Option<String>,
}
#[derive(Debug, Args)]
@@ -569,7 +571,7 @@ fn run() -> Result<()> {
Ok(())
}
Some(Commands::Metrics(args)) => run_metrics_command(args),
Some(Commands::Update(args)) => update::run_update(args.beta),
Some(Commands::Update(args)) => update::run_update(args),
None => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
let forwarded = root_tui_passthrough(&cli)?;
@@ -1817,13 +1819,19 @@ mod tests {
let cli = parse_ok(&["codewhale", "update"]);
assert!(matches!(
cli.command,
Some(Commands::Update(UpdateArgs { beta: false }))
Some(Commands::Update(UpdateArgs {
beta: false,
proxy: None
}))
));
let cli = parse_ok(&["codewhale", "update", "--beta"]);
assert!(matches!(
cli.command,
Some(Commands::Update(UpdateArgs { beta: true }))
Some(Commands::Update(UpdateArgs {
beta: true,
proxy: None
}))
));
}
@@ -2427,6 +2435,31 @@ mod tests {
let _ = std::fs::remove_file(path);
}
#[test]
fn udpate_parse_with_proxy() {
let cli = parse_ok(&["deepseek", "update", "--proxy", "http:localhost:7897"]);
let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update with proxy, got {other:?}"),
};
assert_eq!(
args.proxy.expect("should have proxy"),
"http:localhost:7897"
);
}
#[test]
fn udpate_parse_without_proxy() {
let cli = parse_ok(&["deepseek", "update"]);
let args = match cli.command {
Some(Commands::Update(args)) => args,
other => panic!("expected Update, got {other:?}"),
};
assert!(args.proxy.is_none());
}
#[test]
fn dispatch_keyring_recovery_self_heals_into_config_file() {
use codewhale_secrets::{InMemoryKeyringStore, KeyringStore};
+84 -23
View File
@@ -4,11 +4,13 @@
//! `github.com/Hmbown/CodeWhale/releases/latest`, downloads the
//! platform-correct binary, verifies its SHA256 checksum, and atomically
//! replaces the currently running binary.
use crate::UpdateArgs;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use reqwest::Proxy;
use std::io::Write;
const CHECKSUM_MANIFEST_ASSET: &str = "codewhale-artifacts-sha256.txt";
@@ -26,19 +28,27 @@ const LEGACY_UPDATE_VERSION_ENV: &str = "DEEPSEEK_VERSION";
const UPDATE_USER_AGENT: &str = "codewhale-updater";
/// Run the self-update workflow.
pub fn run_update(beta: bool) -> Result<()> {
pub fn run_update(args: UpdateArgs) -> Result<()> {
let beta = args.beta;
let current_exe =
std::env::current_exe().context("failed to determine current executable path")?;
let targets = update_targets_for_exe(&current_exe);
let channel = ReleaseChannel::from_beta_flag(beta);
let current_version = env!("CARGO_PKG_VERSION");
let proxy = if let Some(proxy_str) = &args.proxy {
validate_and_build_proxy(proxy_str)?
} else {
None
};
println!("Checking for {} updates...", channel.label());
println!("Current binary: {}", current_exe.display());
println!("Current version: v{current_version}");
// Step 1: Fetch latest release metadata
let fetched = fetch_latest_release(channel).with_context(update_network_fallback_hint)?;
let fetched =
fetch_latest_release(channel, &proxy).with_context(update_network_fallback_hint)?;
let release = &fetched.release;
let latest_tag = &release.tag_name;
println!("Latest {} release: {latest_tag}", channel.label());
@@ -59,8 +69,8 @@ pub fn run_update(beta: bool) -> Result<()> {
let checksum_manifest = match select_checksum_manifest_asset(release) {
Some(checksum_asset) => {
println!("Downloading {}...", checksum_asset.name);
let checksum_bytes =
download_url(&checksum_asset.browser_download_url).with_context(|| {
let checksum_bytes = download_url(&checksum_asset.browser_download_url, &proxy)
.with_context(|| {
format!(
"failed to download {}\n{}",
checksum_asset.name,
@@ -95,7 +105,7 @@ pub fn run_update(beta: bool) -> Result<()> {
})?;
println!("Downloading {}...", asset.name);
let bytes = download_url(&asset.browser_download_url).with_context(|| {
let bytes = download_url(&asset.browser_download_url, &proxy).with_context(|| {
format!(
"failed to download {}\n{}",
asset.name,
@@ -174,6 +184,49 @@ enum ReleaseSource {
Mirror { base_url: String },
}
// Validate the proxy URL and optionally test connectivity before proceeding.
fn validate_and_build_proxy(proxy_str: &str) -> Result<Option<Proxy>> {
let valid_url = reqwest::Url::parse(proxy_str).with_context(|| {
format!(
"invalid proxy URL: {proxy_str}\n\
Expected format: http://host:port, https://host:port, or socks5://host:port"
)
})?;
let proxy = reqwest::Proxy::all(valid_url)?;
// Quick connectivity test through the proxy
let client = reqwest::blocking::Client::builder()
.proxy(proxy.clone())
.user_agent(UPDATE_USER_AGENT)
.timeout(Duration::from_secs(10))
.build()
.context("Could not build proxy HTTP client")?;
match client.head(LATEST_RELEASE_URL).send() {
Ok(_) => Ok(Some(proxy)),
Err(e) => {
// Give a clear actionable error rather than a raw reqwest error
let hint = if e.is_timeout() || e.is_connect() {
"could not connect to the proxy server"
} else if e.is_request() {
"the request was sent but no response was received"
} else {
"an unexpected network error occurred"
};
bail!(
"proxy connectivity failed: {hint}\n\
Proxy URL: {proxy_str}\n\
Details: {e}\n\
Please verify:\n\
- The proxy URL is correct\n\
- The proxy server is running and reachable\n\
- The proxy allows outbound connections to api.github.com"
)
}
}
}
pub(crate) fn release_arch_for_rust_arch(arch: &str) -> &str {
match arch {
"aarch64" => "arm64",
@@ -342,15 +395,21 @@ struct Asset {
browser_download_url: String,
}
fn update_http_client() -> Result<reqwest::blocking::Client> {
reqwest::blocking::Client::builder()
fn update_http_client(proxy: &Option<Proxy>) -> Result<reqwest::blocking::Client> {
let mut builder = reqwest::blocking::Client::builder();
if let Some(p) = proxy {
builder = builder.proxy(p.clone());
}
builder
.user_agent(UPDATE_USER_AGENT)
.build()
.context("failed to build update HTTP client")
}
/// Fetch the latest release metadata from GitHub.
fn fetch_latest_release(channel: ReleaseChannel) -> Result<FetchedRelease> {
fn fetch_latest_release(channel: ReleaseChannel, proxy: &Option<Proxy>) -> Result<FetchedRelease> {
let version = update_version_from_env().unwrap_or_else(|| env!("CARGO_PKG_VERSION").into());
if let Some(base_url) = release_base_url_from_env(&version) {
return Ok(FetchedRelease {
@@ -364,8 +423,8 @@ fn fetch_latest_release(channel: ReleaseChannel) -> Result<FetchedRelease> {
});
}
let release = match channel {
ReleaseChannel::Stable => fetch_latest_release_from_url(LATEST_RELEASE_URL),
ReleaseChannel::Beta => fetch_latest_beta_release_from_url(RELEASES_URL),
ReleaseChannel::Stable => fetch_latest_release_from_url(LATEST_RELEASE_URL, proxy),
ReleaseChannel::Beta => fetch_latest_beta_release_from_url(RELEASES_URL, proxy),
}?;
Ok(FetchedRelease {
release,
@@ -454,8 +513,8 @@ fn update_network_fallback_hint() -> String {
)
}
fn fetch_latest_release_from_url(url: &str) -> Result<Release> {
let client = update_http_client()?;
fn fetch_latest_release_from_url(url: &str, proxy: &Option<Proxy>) -> Result<Release> {
let client = update_http_client(proxy)?;
let response = client
.get(url)
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
@@ -477,8 +536,8 @@ fn fetch_latest_release_from_url(url: &str) -> Result<Release> {
Ok(release)
}
fn fetch_latest_beta_release_from_url(url: &str) -> Result<Release> {
let client = update_http_client()?;
fn fetch_latest_beta_release_from_url(url: &str, proxy: &Option<Proxy>) -> Result<Release> {
let client = update_http_client(proxy)?;
let response = client
.get(url)
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
@@ -553,8 +612,8 @@ fn version_is_beta(version: &semver::Version) -> bool {
}
/// Download a URL to bytes.
fn download_url(url: &str) -> Result<Vec<u8>> {
let client = update_http_client()?;
fn download_url(url: &str, proxy: &Option<Proxy>) -> Result<Vec<u8>> {
let client = update_http_client(proxy)?;
let response = client
.get(url)
.send()
@@ -1119,7 +1178,8 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
]
}"#;
let (url, request_rx, handle) = serve_http_once("200 OK", "application/json", body);
let release = fetch_latest_release_from_url(&url).expect("release JSON should parse");
let release =
fetch_latest_release_from_url(&url, &None).expect("release JSON should parse");
assert_eq!(release.tag_name, "v9.9.9");
assert_eq!(release.assets.len(), 2);
@@ -1142,7 +1202,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
fn fetch_latest_release_from_url_reports_http_errors() {
let (url, _request_rx, handle) =
serve_http_once("500 Internal Server Error", "text/plain", b"server broke");
let err = fetch_latest_release_from_url(&url).expect_err("HTTP 500 should fail");
let err = fetch_latest_release_from_url(&url, &None).expect_err("HTTP 500 should fail");
assert!(
err.to_string().contains("HTTP 500"),
@@ -1162,8 +1222,8 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
{ "tag_name": "v0.9.0-beta.1", "prerelease": true, "assets": [] }
]"#;
let (url, request_rx, handle) = serve_http_once("200 OK", "application/json", body);
let release =
fetch_latest_beta_release_from_url(&url).expect("beta release JSON should parse");
let release = fetch_latest_beta_release_from_url(&url, &None)
.expect("beta release JSON should parse");
assert_eq!(release.tag_name, "v0.9.0-beta.2");
assert!(release.prerelease);
@@ -1184,7 +1244,8 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
{ "tag_name": "v0.9.0", "prerelease": false, "assets": [] }
]"#;
let (url, _request_rx, handle) = serve_http_once("200 OK", "application/json", body);
let err = fetch_latest_beta_release_from_url(&url).expect_err("missing beta should fail");
let err =
fetch_latest_beta_release_from_url(&url, &None).expect_err("missing beta should fail");
assert!(
err.to_string().contains("no beta release found"),
@@ -1197,7 +1258,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
fn download_url_reads_binary_body_with_updater_user_agent() {
let (url, request_rx, handle) =
serve_http_once("200 OK", "application/octet-stream", b"\0binary bytes");
let bytes = download_url(&url).expect("binary download should succeed");
let bytes = download_url(&url, &None).expect("binary download should succeed");
assert_eq!(bytes, b"\0binary bytes");