huskies: merge 1138 story In-container huskies self-update — huskies upgrade pulls a fresh binary without docker rebuild

This commit is contained in:
dave
2026-05-18 13:33:50 +00:00
parent d10634c7d6
commit 0ec5c05de8
6 changed files with 590 additions and 2 deletions
+85 -2
View File
@@ -27,6 +27,14 @@ pub(crate) struct CliArgs {
/// forwards all `prompt_permission` tool calls to the gateway over a WebSocket.
/// Also readable from the `HUSKIES_UPSTREAM_GATEWAY` env var.
pub(crate) upstream_gateway: Option<String>,
/// Whether the `upgrade` subcommand was given.
pub(crate) upgrade: bool,
/// Source URL for the `upgrade` subcommand (`--source <URL>`).
///
/// If omitted, the upgrade subcommand falls back to
/// `HUSKIES_BINARY_SOURCE` env var, then derives the URL from
/// `HUSKIES_UPSTREAM_GATEWAY`.
pub(crate) upgrade_source: Option<String>,
}
/// Parse CLI arguments into `CliArgs`, or exit early for `--help` / `--version`.
@@ -41,6 +49,8 @@ pub(crate) fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
let mut join_token: Option<String> = None;
let mut gateway_url: Option<String> = None;
let mut upstream_gateway: Option<String> = None;
let mut upgrade = false;
let mut upgrade_source: Option<String> = None;
let mut i = 0;
while i < args.len() {
@@ -120,6 +130,19 @@ pub(crate) fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
"agent" => {
agent = true;
}
"upgrade" => {
upgrade = true;
}
"--source" => {
i += 1;
if i >= args.len() {
return Err("--source requires a value".to_string());
}
upgrade_source = Some(args[i].clone());
}
a if a.starts_with("--source=") => {
upgrade_source = Some(a["--source=".len()..].to_string());
}
a if a.starts_with('-') => {
return Err(format!("unknown option: {a}"));
}
@@ -147,6 +170,8 @@ pub(crate) fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
join_token,
gateway_url,
upstream_gateway,
upgrade,
upgrade_source,
})
}
@@ -155,12 +180,16 @@ pub(crate) fn print_help() {
println!("huskies init [OPTIONS] [PATH]");
println!("huskies agent --rendezvous <URL> [OPTIONS] [PATH]");
println!("huskies --gateway [OPTIONS] [PATH]");
println!("huskies upgrade [--source <URL>]");
println!();
println!("Serve a huskies project.");
println!();
println!("COMMANDS:");
println!(" init Scaffold a new .huskies/ project and start the interactive setup wizard.");
println!(" agent Run as a headless build agent — syncs CRDT state, claims and runs work.");
println!(" init Scaffold a new .huskies/ project and start the interactive setup wizard.");
println!(" agent Run as a headless build agent — syncs CRDT state, claims and runs work.");
println!(
" upgrade Fetch a new huskies binary from SOURCE and atomically replace the current"
);
println!();
println!("ARGS:");
println!(
@@ -190,6 +219,8 @@ pub(crate) fn print_help() {
println!(" sled connects to WS URL and forwards all");
println!(" prompt_permission calls via the uplink protocol.");
println!(" Also readable from HUSKIES_UPSTREAM_GATEWAY env var.");
println!(" --source <URL> Binary source URL for the `upgrade` subcommand.");
println!(" Falls back to HUSKIES_BINARY_SOURCE env var.");
}
/// Resolve the optional positional path argument into an absolute `PathBuf`.
@@ -399,6 +430,58 @@ mod tests {
assert!(parse_cli_args(&args).is_err());
}
// ── upgrade subcommand ──────────────────────────────────────────
#[test]
fn parse_upgrade_subcommand() {
let args = vec!["upgrade".to_string()];
let result = parse_cli_args(&args).unwrap();
assert!(result.upgrade);
assert_eq!(result.upgrade_source, None);
}
#[test]
fn parse_upgrade_with_source_flag() {
let args = vec![
"upgrade".to_string(),
"--source".to_string(),
"http://gateway:3000/api/huskies-binary".to_string(),
];
let result = parse_cli_args(&args).unwrap();
assert!(result.upgrade);
assert_eq!(
result.upgrade_source,
Some("http://gateway:3000/api/huskies-binary".to_string())
);
}
#[test]
fn parse_upgrade_with_source_equals_syntax() {
let args = vec![
"upgrade".to_string(),
"--source=http://gw:3000/api/b".to_string(),
];
let result = parse_cli_args(&args).unwrap();
assert!(result.upgrade);
assert_eq!(
result.upgrade_source,
Some("http://gw:3000/api/b".to_string())
);
}
#[test]
fn parse_upgrade_source_missing_value_is_error() {
let args = vec!["upgrade".to_string(), "--source".to_string()];
assert!(parse_cli_args(&args).is_err());
}
#[test]
fn parse_no_args_upgrade_is_false() {
let result = parse_cli_args(&[]).unwrap();
assert!(!result.upgrade);
assert_eq!(result.upgrade_source, None);
}
// ── resolve_path_arg ────────────────────────────────────────────
#[test]