feat(update):Add proxy option to update command
Update docs to introduce update command proxy options
This commit is contained in:
@@ -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
|
||||
|
||||
[](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
|
||||
@@ -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
|
||||
|
||||
[](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
|
||||
@@ -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
|
||||
|
||||
[](https://github.com/Hmbown/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
|
||||
+36
-3
@@ -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
@@ -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(¤t_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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user