- commit
- 8b2c091ca03d59b6d6684e8bf0786b6d4fc46f9b
- parent
- 94693f867da01b06306942884b656c86e83e0a3f
- author
- Dami <dami.etoile@gmail.com>
| M | Cargo.lock | 1 | + |
| M | crates/ | 1 | + |
| M | crates/ | 50 | +++++++++++++++++++++++++++++- |
@@ -831,6 +831,7 @@ dependencies = [
831831 "devsite-proto",832832 "iroh",833833 "reqwest 0.12.28",834+ "semver",834835 "serde",835836 "serde_json",836837 "tokio",@@ -10,6 +10,7 @@ use devsite_daemon::Daemon;
1010 use devsite_proto::{AccountId, ResourceId, SignedCapability};1111 use serde::{Deserialize, Serialize};1212 use std::collections::HashSet;13+use std::io::IsTerminal;1314 use std::net::{IpAddr, Ipv4Addr, SocketAddr};1415 use std::sync::Arc;1516 use std::time::{Duration, Instant};@@ -501,6 +502,25 @@ fn enrolled_server(config: &DaemonConfig) -> Result<&str> {
501502 .context("this credential is not bound to a control-plane origin — log in again")502503 }503504 505+fn validate_cli_version(minimum: &str, current: &str) -> Result<()> {506+ let minimum = semver::Version::parse(minimum)507+ .context("server reported an invalid minimum CLI version")?;508+ let current =509+ semver::Version::parse(current).context("this CLI has invalid version metadata")?;510+ if current < minimum {511+ bail!("server requires CLI {minimum} or newer, but this is CLI {current}");512+ }513+ Ok(())514+}515+516+fn green_check() -> &'static str {517+ if std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none() {518+ "\x1b[32m✓\x1b[0m"519+ } else {520+ "✓"521+ }522+}523+504524 async fn login(paths: &Paths, server: &str, token: Option<String>, output: Output) -> Result<()> {505525 let ticket = match token {506526 Some(ticket) => ticket,@@ -527,6 +547,8 @@ async fn login(paths: &Paths, server: &str, token: Option<String>, output: Outpu
527547 server_config.api_version528548 );529549 }550+ let cli_version = env!("CARGO_PKG_VERSION");551+ validate_cli_version(&server_config.minimum_cli_version, cli_version)?;530552 let pubkey: PubKeyResponse = bootstrap.get("/api/pubkey").await?;531553 let identity = paths.load_or_create_identity()?;532554 let endpoint_id = identity.public().to_string();@@ -570,15 +592,19 @@ async fn login(paths: &Paths, server: &str, token: Option<String>, output: Outpu
570592 serde_json::json!({571593 "handle": handle,572594 "server": server.trim_end_matches('/'),595+ "cli_version": cli_version,573596 "minimum_cli_version": server_config.minimum_cli_version,597+ "cli_compatible": true,574598 "control_plane_key": control_plane_key,575599 }),576600 );577601 } else {578602 println!("signed in as {handle}");579603 println!(580- "server requires CLI {} or newer",581- server_config.minimum_cli_version604+ "{} CLI {} meets server requirement ({} or newer)",605+ green_check(),606+ cli_version,607+ server_config.minimum_cli_version,582608 );583609 println!("pinned control plane key {control_plane_key}");584610 }@@ -1423,3 +1449,23 @@ async fn reload_config(paths: Paths, daemon: Arc<Daemon>, output: Output) -> Res
14231449 }14241450 }14251451 }1452+1453+1454+mod tests {1455+ use super::validate_cli_version;1456+1457+ 1458+ fn cli_version_validation_accepts_equal_or_newer_versions() {1459+ assert!(validate_cli_version("0.3.0", "0.3.0").is_ok());1460+ assert!(validate_cli_version("0.3.0", "0.4.0").is_ok());1461+ }1462+1463+ 1464+ fn cli_version_validation_rejects_older_or_invalid_versions() {1465+ assert!(validate_cli_version("0.3.1", "0.3.0")1466+ .unwrap_err()1467+ .to_string()1468+ .contains("server requires CLI 0.3.1 or newer"));1469+ assert!(validate_cli_version("not-a-version", "0.3.0").is_err());1470+ }1471+}