Merge pull request #2281 from AccMoment/add_proxy_option_to_update_command
feat(update):Add proxy option to update command
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ chrono = { version = "0.4.43", features = ["serde"] }
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
clap_complete = "4.5"
|
||||
dirs = "6.0.0"
|
||||
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls"] }
|
||||
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls", "socks"] }
|
||||
rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
|
||||
@@ -53,6 +53,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)
|
||||
|
||||
@@ -66,6 +66,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)
|
||||
|
||||
@@ -61,6 +61,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)
|
||||
|
||||
+18
-4
@@ -247,6 +247,9 @@ struct UpdateArgs {
|
||||
/// Only check the latest release; do not download or replace binaries.
|
||||
#[arg(long)]
|
||||
check: bool,
|
||||
/// Proxy URL to use for update HTTP requests.
|
||||
#[arg(long, value_name = "URL")]
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
@@ -576,7 +579,7 @@ fn run() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Some(Commands::Metrics(args)) => run_metrics_command(args),
|
||||
Some(Commands::Update(args)) => update::run_update(args.beta, args.check),
|
||||
Some(Commands::Update(args)) => update::run_update(args.beta, args.check, args.proxy),
|
||||
None => {
|
||||
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
|
||||
let forwarded = root_tui_passthrough(&cli)?;
|
||||
@@ -1840,7 +1843,8 @@ mod tests {
|
||||
cli.command,
|
||||
Some(Commands::Update(UpdateArgs {
|
||||
beta: false,
|
||||
check: false
|
||||
check: false,
|
||||
proxy: None
|
||||
}))
|
||||
));
|
||||
|
||||
@@ -1849,7 +1853,8 @@ mod tests {
|
||||
cli.command,
|
||||
Some(Commands::Update(UpdateArgs {
|
||||
beta: true,
|
||||
check: false
|
||||
check: false,
|
||||
proxy: None
|
||||
}))
|
||||
));
|
||||
|
||||
@@ -1858,9 +1863,18 @@ mod tests {
|
||||
cli.command,
|
||||
Some(Commands::Update(UpdateArgs {
|
||||
beta: false,
|
||||
check: true
|
||||
check: true,
|
||||
proxy: None
|
||||
}))
|
||||
));
|
||||
|
||||
let cli = parse_ok(&["codewhale", "update", "--proxy", "socks5://127.0.0.1:1080"]);
|
||||
let Some(Commands::Update(args)) = cli.command else {
|
||||
panic!("expected update command");
|
||||
};
|
||||
assert!(!args.beta);
|
||||
assert!(!args.check);
|
||||
assert_eq!(args.proxy.as_deref(), Some("socks5://127.0.0.1:1080"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+90
-32
@@ -12,27 +12,31 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use codewhale_release::{
|
||||
CHECKSUM_MANIFEST_ASSET, ReleaseChannel, ReleaseQuery, UPDATE_USER_AGENT,
|
||||
compare_release_versions, fetch_release_json_blocking, is_beta_tag,
|
||||
latest_release_tag_blocking, mirror_asset_url, resolve_release_query, update_is_needed,
|
||||
update_network_fallback_hint,
|
||||
compare_release_versions, is_beta_tag, mirror_asset_url, resolve_release_query,
|
||||
update_is_needed, update_network_fallback_hint,
|
||||
};
|
||||
use reqwest::Proxy;
|
||||
use std::io::Write;
|
||||
|
||||
/// Run the self-update workflow.
|
||||
pub fn run_update(beta: bool, check_only: bool) -> Result<()> {
|
||||
pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Result<()> {
|
||||
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 = proxy_arg
|
||||
.as_deref()
|
||||
.map(validate_and_build_proxy)
|
||||
.transpose()?;
|
||||
|
||||
println!("Checking for {} updates...", channel.label());
|
||||
println!("Current binary: {}", current_exe.display());
|
||||
println!("Current version: v{current_version}");
|
||||
|
||||
if check_only {
|
||||
let latest_tag =
|
||||
latest_release_tag_blocking(channel).with_context(update_network_fallback_hint)?;
|
||||
let latest_tag = latest_release_tag(channel, proxy.as_ref())
|
||||
.with_context(update_network_fallback_hint)?;
|
||||
println!("Latest {} release: {latest_tag}", channel.label());
|
||||
if update_is_needed(channel, current_version, &latest_tag)? {
|
||||
println!("Update available. Run `codewhale update` to install {latest_tag}.");
|
||||
@@ -50,7 +54,8 @@ pub fn run_update(beta: bool, check_only: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
// 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.as_ref()).with_context(update_network_fallback_hint)?;
|
||||
let release = &fetched.release;
|
||||
let latest_tag = &release.tag_name;
|
||||
println!("Latest {} release: {latest_tag}", channel.label());
|
||||
@@ -71,8 +76,8 @@ pub fn run_update(beta: bool, check_only: 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.as_ref())
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to download {}\n{}",
|
||||
checksum_asset.name,
|
||||
@@ -107,13 +112,14 @@ pub fn run_update(beta: bool, check_only: bool) -> Result<()> {
|
||||
})?;
|
||||
|
||||
println!("Downloading {}...", asset.name);
|
||||
let bytes = download_url(&asset.browser_download_url).with_context(|| {
|
||||
format!(
|
||||
"failed to download {}\n{}",
|
||||
asset.name,
|
||||
update_network_fallback_hint()
|
||||
)
|
||||
})?;
|
||||
let bytes =
|
||||
download_url(&asset.browser_download_url, proxy.as_ref()).with_context(|| {
|
||||
format!(
|
||||
"failed to download {}\n{}",
|
||||
asset.name,
|
||||
update_network_fallback_hint()
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(checksums) = &checksum_manifest {
|
||||
let expected = checksums
|
||||
@@ -335,15 +341,36 @@ struct Asset {
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
fn update_http_client() -> Result<reqwest::blocking::Client> {
|
||||
reqwest::blocking::Client::builder()
|
||||
/// Validate the proxy URL format and build a proxy for update HTTP requests.
|
||||
pub(crate) fn validate_and_build_proxy(proxy_str: &str) -> Result<Proxy> {
|
||||
let proxy_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"
|
||||
)
|
||||
})?;
|
||||
Proxy::all(proxy_url).context("failed to configure update proxy")
|
||||
}
|
||||
|
||||
fn update_http_client(proxy: Option<&Proxy>) -> Result<reqwest::blocking::Client> {
|
||||
let mut builder = reqwest::blocking::Client::builder();
|
||||
if let Some(proxy) = proxy {
|
||||
builder = builder.proxy(proxy.clone());
|
||||
}
|
||||
builder
|
||||
.user_agent(UPDATE_USER_AGENT)
|
||||
.build()
|
||||
.context("failed to build update HTTP client")
|
||||
}
|
||||
|
||||
fn latest_release_tag(channel: ReleaseChannel, proxy: Option<&Proxy>) -> Result<String> {
|
||||
match fetch_latest_release(channel, proxy)? {
|
||||
FetchedRelease { release, .. } => Ok(release.tag_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
match resolve_release_query(channel) {
|
||||
ReleaseQuery::Mirror { base_url, version } => Ok(FetchedRelease {
|
||||
release: release_from_mirror_base_url(
|
||||
@@ -355,11 +382,11 @@ fn fetch_latest_release(channel: ReleaseChannel) -> Result<FetchedRelease> {
|
||||
source: UpdateReleaseSource::Mirror { base_url },
|
||||
}),
|
||||
ReleaseQuery::GitHubLatest { url } => Ok(FetchedRelease {
|
||||
release: fetch_latest_release_from_url(url)?,
|
||||
release: fetch_latest_release_from_url(url, proxy)?,
|
||||
source: UpdateReleaseSource::GitHub,
|
||||
}),
|
||||
ReleaseQuery::GitHubReleaseList { url } => Ok(FetchedRelease {
|
||||
release: fetch_latest_beta_release_from_url(url)?,
|
||||
release: fetch_latest_beta_release_from_url(url, proxy)?,
|
||||
source: UpdateReleaseSource::GitHub,
|
||||
}),
|
||||
}
|
||||
@@ -392,8 +419,25 @@ fn release_from_mirror_base_url(
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_latest_release_from_url(url: &str) -> Result<Release> {
|
||||
let body = fetch_release_json_blocking(url, "release info")?;
|
||||
fn fetch_release_json(url: &str, description: &str, proxy: Option<&Proxy>) -> Result<String> {
|
||||
let client = update_http_client(proxy)?;
|
||||
let response = client
|
||||
.get(url)
|
||||
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
|
||||
.send()
|
||||
.with_context(|| format!("failed to fetch {description} from {url}"))?;
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.text()
|
||||
.with_context(|| format!("failed to read {description} response body from {url}"))?;
|
||||
if !status.is_success() {
|
||||
bail!("failed to fetch {description} from {url}: HTTP {status}\n{body}");
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
fn fetch_latest_release_from_url(url: &str, proxy: Option<&Proxy>) -> Result<Release> {
|
||||
let body = fetch_release_json(url, "release info", proxy)?;
|
||||
let release: Release = serde_json::from_str(&body).with_context(|| {
|
||||
format!("failed to parse release JSON from GitHub API. Response: {body}")
|
||||
})?;
|
||||
@@ -401,8 +445,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 body = fetch_release_json_blocking(url, "release list")?;
|
||||
fn fetch_latest_beta_release_from_url(url: &str, proxy: Option<&Proxy>) -> Result<Release> {
|
||||
let body = fetch_release_json(url, "release list", proxy)?;
|
||||
// GitHub caps this endpoint at 100 releases per page. CodeWhale uses the
|
||||
// first page as the latest-beta search window, matching GitHub's ordering.
|
||||
let releases: Vec<Release> = serde_json::from_str(&body).with_context(|| {
|
||||
@@ -416,8 +460,8 @@ fn fetch_latest_beta_release_from_url(url: &str) -> Result<Release> {
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@@ -978,6 +1022,19 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
|
||||
(format!("http://{addr}/release"), request_rx, handle)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_and_build_proxy_accepts_supported_proxy_urls() {
|
||||
validate_and_build_proxy("http://localhost:7897").expect("http proxy");
|
||||
validate_and_build_proxy("https://proxy.example.com:8080").expect("https proxy");
|
||||
validate_and_build_proxy("socks5://127.0.0.1:1080").expect("socks proxy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_and_build_proxy_rejects_malformed_urls() {
|
||||
let err = validate_and_build_proxy("not a valid url").expect_err("malformed URL");
|
||||
assert!(err.to_string().contains("invalid proxy URL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_latest_release_from_url_reads_mocked_release_json() {
|
||||
let body = br#"{
|
||||
@@ -988,7 +1045,7 @@ 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);
|
||||
@@ -1011,7 +1068,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"),
|
||||
@@ -1032,7 +1089,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
|
||||
]"#;
|
||||
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");
|
||||
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);
|
||||
@@ -1053,7 +1110,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"),
|
||||
@@ -1066,7 +1124,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