Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,20 @@ Supports global or project scope installation. Project scope installs Skills int
- `--all` install Skills for all supported agents
- `--detected-only` install Skills for supported agents that were detected on the system

## Self-update

`clickhousectl` can update itself to the latest release:

```bash
# Update to the latest version
clickhousectl update

# Check for updates without installing
clickhousectl update --check
```

The CLI also checks for updates in the background (at most once per 24 hours) and displays a notice when a newer version is available.

## Cloud integration testing

Cloud commands are tested against a real ClickHouse Cloud workspace. All changes to Cloud commands must pass CI testing before merge. CI tests are under [`tests/cloud_cli.rs`](tests/cloud_cli.rs).
Expand Down
14 changes: 14 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ CONTEXT FOR AGENTS:
Using any flags skips interactive mode. Project scope is the default. Universal `.agents/skills`
is always included.")]
Skills(SkillsArgs),

/// Update clickhousectl to the latest version
#[command(after_help = "\
CONTEXT FOR AGENTS:
Self-update command. Downloads the latest clickhousectl release from GitHub and replaces the
current binary. Use --check to see if an update is available without installing.")]
Update(UpdateArgs),
}

#[derive(Args, Debug)]
Expand All @@ -88,6 +95,13 @@ pub struct SkillsArgs {
pub global: bool,
}

#[derive(Args, Debug)]
pub struct UpdateArgs {
/// Check for updates without installing
#[arg(long)]
pub check: bool,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
41 changes: 40 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ mod init;
mod local;
mod paths;
mod skills;
mod update;
mod user_agent;
mod version_manager;

use clap::Parser;
use cli::{
ActivityCommands, AuthCommands, BackupCommands, BackupConfigCommands, Cli, CloudArgs,
CloudCommands, Commands, InvitationCommands, KeyCommands, MemberCommands, OrgCommands,
PrivateEndpointCommands, QueryEndpointCommands, ServiceCommands, SkillsArgs,
PrivateEndpointCommands, QueryEndpointCommands, ServiceCommands, SkillsArgs, UpdateArgs,
};

use cloud::CloudClient;
Expand All @@ -22,8 +23,25 @@ use error::{Error, Result};
async fn main() {
let cli = Cli::parse();

// For non-update commands: print a cached update notice (sync, no network)
// and spawn a background task to refresh the cache.
let is_update_cmd = matches!(cli.command, Commands::Update(_));
let cache_refresh = if !is_update_cmd {
update::print_cached_update_notice();
Some(tokio::spawn(update::refresh_update_cache()))
} else {
None
};

let result = run(cli.command).await;

// Give the cache refresh a brief window to finish so short-lived commands
// don't always drop it before the write completes. The background HTTP
// request itself has a 400ms timeout, so 500ms here is enough headroom.
if let Some(handle) = cache_refresh {
let _ = tokio::time::timeout(std::time::Duration::from_millis(500), handle).await;
}

if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
Expand All @@ -35,6 +53,27 @@ async fn run(cmd: Commands) -> Result<()> {
Commands::Local(args) => local::run(args.command, args.json).await,
Commands::Skills(args) => run_skills(args).await,
Commands::Cloud(args) => run_cloud(*args).await,
Commands::Update(args) => run_update(args).await,
}
}

async fn run_update(args: UpdateArgs) -> Result<()> {
if args.check {
match update::check_for_update().await? {
Some((current, latest)) => {
println!(
"Update available: v{} → v{}",
current, latest
);
println!("Run `clickhousectl update` to upgrade.");
}
None => {
println!("Already up to date (v{}).", env!("CARGO_PKG_VERSION"));
}
}
Ok(())
} else {
update::perform_update().await
}
}

Expand Down
Loading
Loading