diff --git a/Cargo.lock b/Cargo.lock index 0248f5267..5a2ebd3c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,9 +445,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", @@ -2314,6 +2314,7 @@ dependencies = [ name = "proto_cli" version = "0.17.1" dependencies = [ + "chrono", "clap 4.4.3", "clap_complete", "convert_case", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1f51b51ee..1e3860a33 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -31,6 +31,7 @@ proto_core = { version = "0.17.4", path = "../core" } proto_pdk_api = { version = "0.7.1", path = "../pdk-api" } proto_schema_plugin = { version = "0.11.4", path = "../schema-plugin" } proto_wasm_plugin = { version = "0.6.5", path = "../wasm-plugin" } +chrono = "0.4.31" clap = { workspace = true, features = ["derive", "env"] } clap_complete = { workspace = true } convert_case = { workspace = true } diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 658cf8bf8..b138c83c9 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -1,7 +1,8 @@ use crate::commands::{ AddPluginArgs, AliasArgs, BinArgs, CleanArgs, CompletionsArgs, GlobalArgs, InstallArgs, InstallGlobalArgs, ListArgs, ListGlobalArgs, ListRemoteArgs, LocalArgs, PluginsArgs, - RemovePluginArgs, RunArgs, SetupArgs, UnaliasArgs, UninstallArgs, UninstallGlobalArgs, + RemovePluginArgs, RunArgs, SetupArgs, ToolsArgs, UnaliasArgs, UninstallArgs, + UninstallGlobalArgs, }; use clap::builder::styling::{Color, Style, Styles}; use clap::{Parser, Subcommand, ValueEnum}; @@ -191,6 +192,9 @@ pub enum Commands { #[command(name = "setup", about = "Setup proto for your current shell.")] Setup(SetupArgs), + #[command(name = "tools", about = "List all installed tools and their versions.")] + Tools(ToolsArgs), + #[command(alias = "ua", name = "unalias", about = "Remove an alias from a tool.")] Unalias(UnaliasArgs), diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index cf101e21e..857bf6061 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -15,6 +15,7 @@ mod plugins; mod remove_plugin; mod run; mod setup; +mod tools; mod unalias; mod uninstall; mod uninstall_global; @@ -37,6 +38,7 @@ pub use plugins::*; pub use remove_plugin::*; pub use run::*; pub use setup::*; +pub use tools::*; pub use unalias::*; pub use uninstall::*; pub use uninstall_global::*; diff --git a/crates/cli/src/commands/plugins.rs b/crates/cli/src/commands/plugins.rs index 3d7f37755..72dcdaca7 100644 --- a/crates/cli/src/commands/plugins.rs +++ b/crates/cli/src/commands/plugins.rs @@ -1,14 +1,13 @@ +use crate::helpers::load_configured_tools; use clap::Args; use miette::IntoDiagnostic; -use proto_core::{ - load_tool_from_locator, Id, PluginLocator, ProtoEnvironment, ToolsConfig, UserConfig, -}; +use proto_core::{Id, PluginLocator}; use serde::Serialize; use starbase::system; -use starbase_styles::color; +use starbase_styles::color::{self, OwoStyle}; use starbase_utils::json; -use std::collections::HashMap; -use tracing::debug; +use std::collections::HashSet; +use tracing::info; fn render_entry>(label: &str, value: V) { println!( @@ -34,30 +33,21 @@ pub struct PluginsArgs { #[system] pub async fn plugins(args: ArgsRef) { - let proto = ProtoEnvironment::new()?; - let user_config = UserConfig::load()?; - - let mut tools_config = ToolsConfig::load_upwards()?; - tools_config.inherit_builtin_plugins(); - - let mut plugins = HashMap::new(); - plugins.extend(&user_config.plugins); - plugins.extend(&tools_config.plugins); - - debug!("Loading plugins"); + if !args.json { + info!("Loading plugins..."); + } let mut items = vec![]; - for (id, locator) in plugins { - let tool = load_tool_from_locator(&id, &proto, &locator, &user_config).await?; - + load_configured_tools(HashSet::new(), |tool, locator| { items.push(PluginItem { - id: id.to_owned(), - locator: locator.to_owned(), + id: tool.id.to_owned(), + locator, name: tool.metadata.name, version: tool.metadata.plugin_version, }); - } + }) + .await?; items.sort_by(|a, d| a.id.cmp(&d.id)); @@ -70,7 +60,7 @@ pub async fn plugins(args: ArgsRef) { for item in items { println!( "{} {} {} {}", - color::id(item.id), + OwoStyle::new().bold().style(color::id(item.id)), color::muted("-"), item.name, color::muted_light(if let Some(version) = item.version { diff --git a/crates/cli/src/commands/tools.rs b/crates/cli/src/commands/tools.rs new file mode 100644 index 000000000..a855ead82 --- /dev/null +++ b/crates/cli/src/commands/tools.rs @@ -0,0 +1,128 @@ +use crate::helpers::load_configured_tools; +use chrono::{DateTime, NaiveDateTime}; +use clap::Args; +use miette::IntoDiagnostic; +use proto_core::Id; +use starbase::system; +use starbase_styles::color::{self, OwoStyle}; +use starbase_utils::json; +use std::collections::{HashMap, HashSet}; +use tracing::info; + +#[derive(Args, Clone, Debug)] +pub struct ToolsArgs { + #[arg(help = "IDs of tool to list")] + id: Vec, + + #[arg(long, help = "Print the list in JSON format")] + json: bool, +} + +#[system] +pub async fn tools(args: ArgsRef) { + if !args.json { + info!("Loading tools..."); + } + + let mut tools = vec![]; + + load_configured_tools(HashSet::from_iter(&args.id), |tool, _| { + if !tool.manifest.installed_versions.is_empty() { + tools.push(tool); + } + }) + .await?; + + tools.sort_by(|a, d| a.id.cmp(&d.id)); + + if args.json { + let items = tools + .into_iter() + .map(|t| (t.id, t.manifest)) + .collect::>(); + + println!("{}", json::to_string_pretty(&items).into_diagnostic()?); + + return Ok(()); + } + + for tool in tools { + println!( + "{} {} {}", + OwoStyle::new().bold().style(color::id(&tool.id)), + color::muted("-"), + tool.metadata.name, + ); + + println!(" Store: {}", color::path(tool.get_inventory_dir())); + + if !tool.manifest.aliases.is_empty() { + println!(" Aliases:"); + + for (alias, version) in &tool.manifest.aliases { + println!( + " {} {} {}", + color::hash(version.to_string()), + color::muted("="), + color::label(alias), + ); + } + } + + if !tool.manifest.installed_versions.is_empty() { + println!(" Versions:"); + + let mut versions = tool.manifest.installed_versions.iter().collect::>(); + versions.sort(); + + for version in versions { + let mut comments = vec![]; + let mut is_default = false; + + if let Some(meta) = &tool.manifest.versions.get(version) { + if let Some(at) = create_datetime(meta.installed_at) { + comments.push(format!("installed {}", at.format("%x"))); + } + + if let Some(last_used) = &meta.last_used_at { + if let Some(at) = create_datetime(*last_used) { + comments.push(format!("last used {}", at.format("%x"))); + } + } + } + + if tool + .manifest + .default_version + .as_ref() + .is_some_and(|dv| dv == &version.to_unresolved_spec()) + { + comments.push("default version".into()); + is_default = true; + } + + if comments.is_empty() { + println!(" {}", color::hash(version.to_string())); + } else { + println!( + " {} {} {}", + if is_default { + color::symbol(version.to_string()) + } else { + color::hash(version.to_string()) + }, + color::muted("-"), + color::muted_light(comments.join(", ")) + ); + } + } + } + + println!(); + } +} + +fn create_datetime(millis: u128) -> Option { + DateTime::from_timestamp((millis / 1000) as i64, ((millis % 1000) * 1_000_000) as u32) + .map(|dt| dt.naive_local()) +} diff --git a/crates/cli/src/helpers.rs b/crates/cli/src/helpers.rs index 2a544d5ee..5a01ddb82 100644 --- a/crates/cli/src/helpers.rs +++ b/crates/cli/src/helpers.rs @@ -1,8 +1,12 @@ use futures::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; -use proto_core::{get_temp_dir, ProtoError}; +use proto_core::{ + get_temp_dir, load_tool_from_locator, Id, PluginLocator, ProtoEnvironment, ProtoError, Tool, + ToolsConfig, UserConfig, +}; use starbase_utils::fs; use std::cmp; +use std::collections::{HashMap, HashSet}; use std::env; use std::io::Write; use std::path::PathBuf; @@ -83,3 +87,31 @@ pub async fn download_to_temp_with_progress_bar( Ok(temp_file) } + +pub async fn load_configured_tools( + filter: HashSet<&Id>, + mut op: impl FnMut(Tool, PluginLocator), +) -> miette::Result<()> { + let proto = ProtoEnvironment::new()?; + let mut user_config = UserConfig::load()?; + + let mut tools_config = ToolsConfig::load_upwards()?; + tools_config.inherit_builtin_plugins(); + + let mut plugins = HashMap::new(); + plugins.extend(std::mem::take(&mut user_config.plugins)); + plugins.extend(tools_config.plugins); + + for (id, locator) in plugins { + if !filter.is_empty() && !filter.contains(&id) { + continue; + } + + op( + load_tool_from_locator(&id, &proto, &locator, &user_config).await?, + locator, + ); + } + + Ok(()) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index ec08df6c3..5ec97ab4f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -57,6 +57,7 @@ async fn main() -> MainResult { Commands::RemovePlugin(args) => app.execute_with_args(commands::remove_plugin, args), Commands::Run(args) => app.execute_with_args(commands::run, args), Commands::Setup(args) => app.execute_with_args(commands::setup, args), + Commands::Tools(args) => app.execute_with_args(commands::tools, args), Commands::Unalias(args) => app.execute_with_args(commands::unalias, args), Commands::Uninstall(args) => app.execute_with_args(commands::uninstall, args), Commands::UninstallGlobal(args) => app.execute_with_args(commands::uninstall_global, args),