From 2bc40cd97367c3bcbb6f3272e63eb34ca3d41271 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Sat, 23 May 2026 12:00:30 -0700 Subject: [PATCH] feat(update): auto-install and update skills during hotdata update After the binary is atomically swapped, download and install the matching skills tarball for the new version. Uses the target version URL rather than CURRENT_VERSION (old binary is still running at call time). Skills are installed even if never previously set up. Failures print a warning without rolling back the binary update. Refactors download_and_extract() into a download_and_extract_from_url() helper so the existing and new versioned paths share one implementation. Co-Authored-By: Claude Sonnet 4.6 --- src/skill.rs | 32 +++++++++++++++++++++++++++++--- src/update.rs | 6 ++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/skill.rs b/src/skill.rs index 700d00e..09d9174 100644 --- a/src/skill.rs +++ b/src/skill.rs @@ -195,16 +195,19 @@ fn is_managed_by_skills_agent() -> bool { } fn download_and_extract() -> Result<(), String> { - let url = download_url(); + download_and_extract_from_url(&download_url()) +} + +fn download_and_extract_from_url(url: &str) -> Result<(), String> { eprintln!("Downloading skill..."); // Binary download — can't route through `send_debug` (which calls // `resp.text()` and would corrupt the gzip stream). Log the // request line manually so `--debug` still shows the URL. - crate::util::debug_request("GET", &url, &[], None); + crate::util::debug_request("GET", url, &[], None); let client = reqwest::blocking::Client::new(); let resp = client - .get(&url) + .get(url) .send() .map_err(|e| format!("error downloading skill: {e}"))?; @@ -250,6 +253,29 @@ fn download_and_extract() -> Result<(), String> { Ok(()) } +/// Download and install skills for `version`. Called from `run_update()` after +/// the binary has been atomically swapped so that skills match the new CLI on +/// first use. Uses the release tarball URL for `version` (not `CURRENT_VERSION`, +/// which is still the old binary at call time). Skips silently when managed by +/// a skills agent. Prints a warning on failure so the binary update is not +/// rolled back. +pub fn install_for_version(version: &Version) { + if is_managed_by_skills_agent() { + return; + } + let url = format!("https://github.com/{REPO}/releases/download/v{version}/skills.tar.gz"); + if let Err(e) = download_and_extract_from_url(&url) { + eprintln!( + "{}", + format!("warning: could not update agent skills: {e}").yellow() + ); + return; + } + let _symlinks = ensure_symlinks(); + clear_skill_auto_update_suppression(); + println!("{}", format!("Agent skills updated to v{version}.").green()); +} + fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), String> { fs::create_dir_all(dst).map_err(|e| format!("error creating directory: {e}"))?; for entry in fs::read_dir(src).map_err(|e| format!("error reading directory: {e}"))? { diff --git a/src/update.rs b/src/update.rs index 3aa8ba2..92bec7d 100644 --- a/src/update.rs +++ b/src/update.rs @@ -185,6 +185,12 @@ pub fn run_update() { } println!("{}", format!("Updated to v{latest}.").green()); + // Install/update skills to match the new binary. The tarball URL is built + // from `latest` (not CURRENT_VERSION) because the old binary is still + // running at this point — we want the skills for the version we just + // downloaded, not the one we replaced. + crate::skill::install_for_version(&latest); + // Bust the cache so the notice clears on the next run. write_cache(&UpdateCheckCache { checked_at: now_secs(),