Skip to content

Commit c4e84f7

Browse files
committed
feat(skills): auto-update bundled agent skills after CLI upgrade
When ~/.hotdata/skills/hotdata exists but the bundle is older than the CLI or incomplete, refresh from the matching release tarball before other commands. Skip if skills were never installed, if ~/.agents/.skill-lock.json manages installs, or when HOTDATA_SKILLS_AUTO_UPDATE is disabled. Route skill download progress to stderr so stdout stays clean for JSON/piped output.
1 parent 133616e commit c4e84f7

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎src/main.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ fn main() {
9191
util::set_debug(true);
9292
}
9393

94+
let skip_skill_auto_update =
95+
cli.command.is_none() || matches!(&cli.command, Some(Commands::Skills { .. }));
96+
if !skip_skill_auto_update {
97+
skill::maybe_auto_update_after_cli_upgrade();
98+
}
99+
94100
match cli.command {
95101
None => {
96102
use clap::CommandFactory;

‎src/skill.rs‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,56 @@ fn all_skill_stores_present() -> bool {
8181
.all(|name| skill_store_path(name).exists())
8282
}
8383

84+
/// When unset or non-zero: allow automatic skill refresh after CLI upgrade (`maybe_auto_update_after_cli_upgrade`).
85+
/// Set to `0`, `false`, or `no` to disable.
86+
fn skills_auto_update_env_enabled() -> bool {
87+
match std::env::var("HOTDATA_SKILLS_AUTO_UPDATE") {
88+
Ok(s) if s.trim().is_empty() => true,
89+
Ok(s) => !matches!(
90+
s.trim().to_ascii_lowercase().as_str(),
91+
"0" | "false" | "no"
92+
),
93+
Err(_) => true,
94+
}
95+
}
96+
97+
/// If the user has previously installed agent skills (`~/.hotdata/skills/hotdata` exists) but the on-disk
98+
/// bundle is older than this CLI or incomplete, download the matching release tarball and refresh symlinks.
99+
/// Does nothing when skills were never installed, when [`is_managed_by_skills_agent`] is true, or when
100+
/// [`skills_auto_update_env_enabled`] is false. Download failures print a warning and do not exit.
101+
pub fn maybe_auto_update_after_cli_upgrade() {
102+
if !skills_auto_update_env_enabled() || is_managed_by_skills_agent() {
103+
return;
104+
}
105+
if !skill_store_path(PRIMARY_SKILL_NAME).exists() {
106+
return;
107+
}
108+
109+
let current = Version::parse(CURRENT_VERSION).expect("invalid package version");
110+
let needs_refresh = match read_installed_version() {
111+
Some(v) if v >= current && all_skill_stores_present() => false,
112+
_ => true,
113+
};
114+
if !needs_refresh {
115+
return;
116+
}
117+
118+
if let Err(e) = download_and_extract() {
119+
eprintln!(
120+
"{}",
121+
format!("warning: could not auto-update agent skills: {e}").yellow()
122+
);
123+
return;
124+
}
125+
126+
let _symlinks = ensure_symlinks();
127+
128+
eprintln!(
129+
"{}",
130+
format!("Agent skills updated to v{current}.").green()
131+
);
132+
}
133+
84134
fn is_managed_by_skills_agent() -> bool {
85135
let content = match fs::read_to_string(agents_lock_path()) {
86136
Ok(c) => c,
@@ -95,7 +145,7 @@ fn is_managed_by_skills_agent() -> bool {
95145

96146
fn download_and_extract() -> Result<(), String> {
97147
let url = download_url();
98-
println!("Downloading skill...");
148+
eprintln!("Downloading skill...");
99149

100150
// Binary download — can't route through `send_debug` (which calls
101151
// `resp.text()` and would corrupt the gzip stream). Log the

0 commit comments

Comments
 (0)