From 7ad3fa67d16e93a141dafc91d6abe6a71ab90be3 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Fri, 29 Sep 2023 14:35:44 -0700 Subject: [PATCH] new: Add `proto outdated` command. (#222) --- CHANGELOG.md | 1 + crates/cli/src/app.rs | 10 +- crates/cli/src/commands/add_plugin.rs | 6 +- crates/cli/src/commands/mod.rs | 2 + crates/cli/src/commands/outdated.rs | 126 +++++++++++++++++++++++++ crates/cli/src/commands/pin.rs | 7 +- crates/cli/src/main.rs | 1 + crates/cli/tests/add_plugin_test.rs | 2 +- crates/core/src/tools_config.rs | 38 +++++--- crates/core/src/version_resolver.rs | 7 ++ crates/core/tests/tools_config_test.rs | 3 +- 11 files changed, 174 insertions(+), 29 deletions(-) create mode 100644 crates/cli/src/commands/outdated.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cdc10419..ec4efbdc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ #### 🚀 Updates +- Added a `proto outdated` command that'll check for new versions of configured tools. - Added a `proto pin` command, which is a merge of the old `proto global` and `proto local` commands. - Added a `pin-latest` setting to `~/.proto/config.toml` that'll automatically pin tools when they're being installed with the "latest" version. - Updated `proto install` to auto-clean stale plugins after a successful installation. diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 940f41849..488d0bdd5 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -1,7 +1,7 @@ use crate::commands::{ AddPluginArgs, AliasArgs, BinArgs, CleanArgs, CompletionsArgs, InstallArgs, InstallGlobalArgs, - ListArgs, ListGlobalArgs, ListRemoteArgs, PinArgs, PluginsArgs, RemovePluginArgs, RunArgs, - SetupArgs, ToolsArgs, UnaliasArgs, UninstallArgs, UninstallGlobalArgs, + ListArgs, ListGlobalArgs, ListRemoteArgs, OutdatedArgs, PinArgs, PluginsArgs, RemovePluginArgs, + RunArgs, SetupArgs, ToolsArgs, UnaliasArgs, UninstallArgs, UninstallGlobalArgs, }; use clap::builder::styling::{Color, Style, Styles}; use clap::{Parser, Subcommand, ValueEnum}; @@ -155,6 +155,12 @@ pub enum Commands { )] ListRemote(ListRemoteArgs), + #[command( + name = "outdated", + about = "Check if configured tool versions are out of date." + )] + Outdated(OutdatedArgs), + #[command( alias = "p", name = "pin", diff --git a/crates/cli/src/commands/add_plugin.rs b/crates/cli/src/commands/add_plugin.rs index 8bf7d5350..e90567d9b 100644 --- a/crates/cli/src/commands/add_plugin.rs +++ b/crates/cli/src/commands/add_plugin.rs @@ -2,8 +2,6 @@ use clap::Args; use proto_core::{Id, PluginLocator, ToolsConfig, UserConfig}; use starbase::system; use starbase_styles::color; -use std::env; -use std::path::PathBuf; use tracing::info; #[derive(Args, Clone, Debug)] @@ -39,9 +37,7 @@ pub async fn add_plugin(args: ArgsRef) { return Ok(()); } - let local_path = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - - let mut config = ToolsConfig::load_from(local_path)?; + let mut config = ToolsConfig::load()?; config.plugins.insert(args.id.clone(), args.plugin.clone()); config.save()?; diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 541642ff4..463fb086c 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -9,6 +9,7 @@ mod install_global; mod list; mod list_global; mod list_remote; +mod outdated; mod pin; mod plugins; mod remove_plugin; @@ -31,6 +32,7 @@ pub use install_global::*; pub use list::*; pub use list_global::*; pub use list_remote::*; +pub use outdated::*; pub use pin::*; pub use plugins::*; pub use remove_plugin::*; diff --git a/crates/cli/src/commands/outdated.rs b/crates/cli/src/commands/outdated.rs new file mode 100644 index 000000000..3e3ff7d53 --- /dev/null +++ b/crates/cli/src/commands/outdated.rs @@ -0,0 +1,126 @@ +use clap::Args; +use miette::IntoDiagnostic; +use proto_core::{load_tool, ToolsConfig, UnresolvedVersionSpec, VersionSpec}; +use serde::Serialize; +use starbase::system; +use starbase_styles::color::{self, OwoStyle}; +use starbase_utils::json; +use std::collections::HashMap; +use std::process; +use tracing::{debug, info}; + +#[derive(Args, Clone, Debug)] +pub struct OutdatedArgs { + #[arg(long, help = "Print the list in JSON format")] + json: bool, + + #[arg( + long, + help = "Check for latest available version ignoring requirements and ranges" + )] + latest: bool, + + #[arg(long, help = "Update and write the versions to the local .prototools")] + update: bool, +} + +#[derive(Serialize)] +pub struct OutdatedItem { + is_latest: bool, + version_config: UnresolvedVersionSpec, + current_version: VersionSpec, + newer_version: VersionSpec, +} + +#[system] +pub async fn outdated(args: ArgsRef) { + let mut tools_config = ToolsConfig::load_closest()?; + let initial_version = UnresolvedVersionSpec::default(); // latest + + if tools_config.tools.is_empty() { + eprintln!("No configured tools in .prototools"); + process::exit(1); + } + + if !args.json { + info!("Checking for newer versions..."); + } + + let mut items = HashMap::new(); + let mut tool_versions = HashMap::new(); + + for (tool_id, config_version) in &tools_config.tools { + let mut tool = load_tool(tool_id).await?; + tool.disable_caching(); + + debug!("Checking {}", tool.get_name()); + + let mut comments = vec![]; + let versions = tool.load_version_resolver(&initial_version).await?; + let current_version = versions.resolve(config_version)?; + let is_latest = args.latest || matches!(config_version, UnresolvedVersionSpec::Version(_)); + + comments.push(format!( + "current version {} {}", + color::symbol(current_version.to_string()), + color::muted_light(format!("(via {})", config_version)) + )); + + let newer_version = versions.resolve_without_manifest(if is_latest { + &initial_version // latest alias + } else { + config_version // req, range, etc + })?; + + comments.push(format!( + "{} {}", + if is_latest { + "latest version" + } else { + "newer version" + }, + color::symbol(newer_version.to_string()) + )); + + let is_outdated = match (¤t_version, &newer_version) { + (VersionSpec::Version(a), VersionSpec::Version(b)) => b > a, + _ => false, + }; + + if is_outdated { + comments.push(color::success("update available!")); + } + + if args.update { + tool_versions.insert(tool.id.clone(), newer_version.to_unresolved_spec()); + } + + if args.json { + items.insert( + tool.id, + OutdatedItem { + is_latest, + version_config: config_version.to_owned(), + current_version, + newer_version, + }, + ); + } else { + println!( + "{} {} {}", + OwoStyle::new().bold().style(color::id(&tool.id)), + color::muted("-"), + comments.join(&color::muted_light(", ")) + ); + } + } + + if args.update { + tools_config.tools.extend(tool_versions); + tools_config.save()?; + } + + if args.json { + println!("{}", json::to_string_pretty(&items).into_diagnostic()?); + } +} diff --git a/crates/cli/src/commands/pin.rs b/crates/cli/src/commands/pin.rs index 58d60dbe7..8742d9e48 100644 --- a/crates/cli/src/commands/pin.rs +++ b/crates/cli/src/commands/pin.rs @@ -1,6 +1,3 @@ -use std::env; -use std::path::PathBuf; - use clap::Args; use proto_core::{load_tool, Id, Tool, ToolsConfig, UnresolvedVersionSpec}; use starbase::{system, SystemResult}; @@ -33,9 +30,7 @@ pub fn internal_pin(tool: &mut Tool, args: &PinArgs) -> SystemResult { "Wrote the global version", ); } else { - let local_path = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - - let mut config = ToolsConfig::load_from(local_path)?; + let mut config = ToolsConfig::load()?; config.tools.insert(args.id.clone(), args.spec.clone()); config.save()?; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 026469457..ad9386c60 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -53,6 +53,7 @@ async fn main() -> MainResult { Commands::List(args) => app.execute_with_args(commands::list, args), Commands::ListGlobal(args) => app.execute_with_args(commands::list_global, args), Commands::ListRemote(args) => app.execute_with_args(commands::list_remote, args), + Commands::Outdated(args) => app.execute_with_args(commands::outdated, args), Commands::Pin(args) => app.execute_with_args(commands::pin, args), Commands::Plugins(args) => app.execute_with_args(commands::plugins, args), Commands::RemovePlugin(args) => app.execute_with_args(commands::remove_plugin, args), diff --git a/crates/cli/tests/add_plugin_test.rs b/crates/cli/tests/add_plugin_test.rs index dd62ddd96..c6a3f5224 100644 --- a/crates/cli/tests/add_plugin_test.rs +++ b/crates/cli/tests/add_plugin_test.rs @@ -40,7 +40,7 @@ mod add_plugin { assert!(config_file.exists()); - let manifest = ToolsConfig::load(config_file).unwrap(); + let manifest = ToolsConfig::load_from(sandbox.path()).unwrap(); assert_eq!( manifest.plugins, diff --git a/crates/core/src/tools_config.rs b/crates/core/src/tools_config.rs index db3777487..c7d106221 100644 --- a/crates/core/src/tools_config.rs +++ b/crates/core/src/tools_config.rs @@ -40,23 +40,25 @@ impl ToolsConfig { } } - pub fn load_from>(dir: P) -> miette::Result { - Self::load(dir.as_ref().join(TOOLS_CONFIG_NAME)) + #[tracing::instrument(skip_all)] + pub fn load() -> miette::Result { + let working_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + + Self::load_from(working_dir) } - #[tracing::instrument(skip_all)] - pub fn load>(path: P) -> miette::Result { - let path = path.as_ref(); + pub fn load_from>(dir: P) -> miette::Result { + let path = dir.as_ref().join(TOOLS_CONFIG_NAME); let mut config: ToolsConfig = if path.exists() { debug!(file = ?path, "Loading {}", TOOLS_CONFIG_NAME); - toml::from_str(&fs::read_file_with_lock(path)?).into_diagnostic()? + toml::from_str(&fs::read_file_with_lock(&path)?).into_diagnostic()? } else { ToolsConfig::default() }; - config.path = path.to_owned(); + config.path = path.clone(); // Update plugin file paths to be absolute for locator in config.plugins.values_mut() { @@ -72,29 +74,37 @@ impl ToolsConfig { Ok(config) } + pub fn load_closest() -> miette::Result { + let working_dir = env::current_dir().expect("Unknown current working directory!"); + + Self::load_upwards_from(working_dir, true) + } + pub fn load_upwards() -> miette::Result { let working_dir = env::current_dir().expect("Unknown current working directory!"); - Self::load_upwards_from(working_dir) + Self::load_upwards_from(working_dir, false) } - pub fn load_upwards_from

(starting_dir: P) -> miette::Result + pub fn load_upwards_from

(starting_dir: P, stop_at_first: bool) -> miette::Result where P: AsRef, { - trace!("Traversing upwards and loading all .prototools files"); + trace!("Traversing upwards and loading .prototools files"); let mut current_dir = Some(starting_dir.as_ref()); let mut config = ToolsConfig::default(); while let Some(dir) = current_dir { - let path = dir.join(TOOLS_CONFIG_NAME); - - if path.exists() { - let mut parent_config = Self::load(&path)?; + if dir.join(TOOLS_CONFIG_NAME).exists() { + let mut parent_config = Self::load_from(dir)?; parent_config.merge(config); config = parent_config; + + if stop_at_first { + break; + } } match dir.parent() { diff --git a/crates/core/src/version_resolver.rs b/crates/core/src/version_resolver.rs index 1d6a367dc..7f700bd4d 100644 --- a/crates/core/src/version_resolver.rs +++ b/crates/core/src/version_resolver.rs @@ -52,6 +52,13 @@ impl<'tool> VersionResolver<'tool> { pub fn resolve(&self, candidate: &UnresolvedVersionSpec) -> miette::Result { resolve_version(candidate, &self.versions, &self.aliases, self.manifest) } + + pub fn resolve_without_manifest( + &self, + candidate: &UnresolvedVersionSpec, + ) -> miette::Result { + resolve_version(candidate, &self.versions, &self.aliases, None) + } } pub fn match_highest_version<'l, I>(req: &'l VersionReq, versions: I) -> Option diff --git a/crates/core/tests/tools_config_test.rs b/crates/core/tests/tools_config_test.rs index d16702c23..3ea89cc50 100644 --- a/crates/core/tests/tools_config_test.rs +++ b/crates/core/tests/tools_config_test.rs @@ -162,7 +162,8 @@ foo = "source:./test.toml" "#, ); - let config = ToolsConfig::load_upwards_from(sandbox.path().join("one/two/three")).unwrap(); + let config = + ToolsConfig::load_upwards_from(sandbox.path().join("one/two/three"), false).unwrap(); assert_eq!( config.tools,