From f7de0a4f50397c32c9a5253a5b4d94153ea38287 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 15 Nov 2023 12:46:59 -0800 Subject: [PATCH] new: Add `proto tool` subcommand. (#288) --- CHANGELOG.md | 11 +- crates/cli/src/app.rs | 73 ++++- crates/cli/src/commands/add_plugin.rs | 51 +-- crates/cli/src/commands/mod.rs | 1 + crates/cli/src/commands/plugins.rs | 111 +------ crates/cli/src/commands/remove_plugin.rs | 56 +--- crates/cli/src/commands/tool/add.rs | 49 +++ crates/cli/src/commands/tool/info.rs | 130 ++++++++ crates/cli/src/commands/tool/list.rs | 128 ++++++++ crates/cli/src/commands/tool/list_plugins.rs | 71 ++++ crates/cli/src/commands/tool/mod.rs | 11 + crates/cli/src/commands/tool/remove.rs | 54 +++ crates/cli/src/commands/tools.rs | 147 +-------- crates/cli/src/main.rs | 16 +- crates/cli/src/printer.rs | 180 ++++++++++ crates/cli/tests/plugins_test.rs | 307 +++++++++--------- .../{add_plugin_test.rs => tool_add_test.rs} | 9 +- ...ove_plugin_test.rs => tool_remove_test.rs} | 9 +- crates/core/src/tool.rs | 2 +- 19 files changed, 909 insertions(+), 507 deletions(-) create mode 100644 crates/cli/src/commands/tool/add.rs create mode 100644 crates/cli/src/commands/tool/info.rs create mode 100644 crates/cli/src/commands/tool/list.rs create mode 100644 crates/cli/src/commands/tool/list_plugins.rs create mode 100644 crates/cli/src/commands/tool/mod.rs create mode 100644 crates/cli/src/commands/tool/remove.rs create mode 100644 crates/cli/src/printer.rs rename crates/cli/tests/{add_plugin_test.rs => tool_add_test.rs} (94%) rename crates/cli/tests/{remove_plugin_test.rs => tool_remove_test.rs} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 067eb24b1..85e1dcf18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,17 @@ ## Unreleased -#### Updates +#### 💥 Breaking + +- Deprecated and moved tool/plugin commands to `proto tool` subcommand. + - Moved `proto add-plugin` to `proto tool add`. + - Moved `proto remove-plugin` to `proto tool remove`. + - Moved `proto plugins` to `proto tool list-plugins`. + - Moved `proto tools` to `proto tool list`. + +#### 🚀 Updates +- Added a `proto tool info` command for viewing information about a tool and its plugin. - Added a `detect-strategy` setting to `~/.proto/config.toml` to configure which strategy to use when detecting a version. Accepts: - `first-available` (default) - Will use the first available version that is found. Either from `.prototools` or a tool specific file (`.nvmrc`, etc). - `prefer-prototools` - Prefer a `.prototools` version, even if found in a parent directory. If none found, falls back to tool specific file. diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index c313a52f7..8cf9b68c1 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -1,8 +1,8 @@ use crate::commands::{ - AddPluginArgs, AliasArgs, BinArgs, CleanArgs, CompletionsArgs, InstallArgs, InstallGlobalArgs, - ListArgs, ListGlobalArgs, ListRemoteArgs, MigrateArgs, OutdatedArgs, PinArgs, PluginsArgs, - RemovePluginArgs, RunArgs, SetupArgs, ToolsArgs, UnaliasArgs, UninstallArgs, - UninstallGlobalArgs, + tool::{AddToolArgs, ListToolPluginsArgs, ListToolsArgs, RemoveToolArgs, ToolInfoArgs}, + AliasArgs, BinArgs, CleanArgs, CompletionsArgs, InstallArgs, InstallGlobalArgs, ListArgs, + ListGlobalArgs, ListRemoteArgs, MigrateArgs, OutdatedArgs, PinArgs, RunArgs, SetupArgs, + UnaliasArgs, UninstallArgs, UninstallGlobalArgs, }; use clap::builder::styling::{Color, Style, Styles}; use clap::{Parser, Subcommand, ValueEnum}; @@ -85,9 +85,10 @@ pub enum Commands { alias = "ap", name = "add-plugin", about = "Add a plugin.", - long_about = "Add a plugin to the local .prototools config, or global ~/.proto/config.toml config." + long_about = "Add a plugin to the local .prototools config, or global ~/.proto/config.toml config.", + hide = true )] - AddPlugin(AddPluginArgs), + AddPlugin(AddToolArgs), #[command( alias = "a", @@ -176,16 +177,21 @@ pub enum Commands { )] Pin(PinArgs), - #[command(name = "plugins", about = "List all active and configured plugins.")] - Plugins(PluginsArgs), + #[command( + name = "plugins", + about = "List all active and configured plugins.", + hide = true + )] + Plugins(ListToolPluginsArgs), #[command( alias = "rp", name = "remove-plugin", about = "Remove a plugin.", - long_about = "Remove a plugin from the local .prototools config, or global ~/.proto/config.toml config." + long_about = "Remove a plugin from the local .prototools config, or global ~/.proto/config.toml config.", + hide = true )] - RemovePlugin(RemovePluginArgs), + RemovePlugin(RemoveToolArgs), #[command( alias = "r", @@ -198,8 +204,18 @@ 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(name = "tool", about = "Operations for managing tools and plugins.")] + Tool { + #[command(subcommand)] + command: ToolCommands, + }, + + #[command( + name = "tools", + about = "List all installed tools and their versions.", + hide = true + )] + Tools(ListToolsArgs), #[command(alias = "ua", name = "unalias", about = "Remove an alias from a tool.")] Unalias(UnaliasArgs), @@ -233,3 +249,36 @@ pub enum Commands { )] Use, } + +#[derive(Clone, Debug, Subcommand)] +pub enum ToolCommands { + #[command( + name = "add", + about = "Add a tool plugin.", + long_about = "Add a plugin to the local .prototools config, or global ~/.proto/config.toml config." + )] + Add(AddToolArgs), + + #[command( + name = "info", + about = "Display information about a tool and its plugin." + )] + Info(ToolInfoArgs), + + #[command(name = "list", about = "List all installed tools and their versions.")] + List(ListToolsArgs), + + #[command( + alias = "plugins", + name = "list-plugins", + about = "List all active and configured plugins." + )] + ListPlugins(ListToolPluginsArgs), + + #[command( + name = "remove", + about = "Remove a tool plugin.", + long_about = "Remove a plugin from the local .prototools config, or global ~/.proto/config.toml config." + )] + Remove(RemoveToolArgs), +} diff --git a/crates/cli/src/commands/add_plugin.rs b/crates/cli/src/commands/add_plugin.rs index e90567d9b..56b9eb673 100644 --- a/crates/cli/src/commands/add_plugin.rs +++ b/crates/cli/src/commands/add_plugin.rs @@ -1,49 +1,14 @@ -use clap::Args; -use proto_core::{Id, PluginLocator, ToolsConfig, UserConfig}; +use crate::commands::tool; use starbase::system; use starbase_styles::color; -use tracing::info; - -#[derive(Args, Clone, Debug)] -pub struct AddPluginArgs { - #[arg(required = true, help = "ID of plugin")] - id: Id, - - #[arg(required = true, help = "Locator string to find and load the plugin")] - plugin: PluginLocator, - - #[arg( - long, - help = "Add to the global user config instead of local .prototools" - )] - global: bool, -} +use tracing::warn; #[system] -pub async fn add_plugin(args: ArgsRef) { - if args.global { - let mut user_config = UserConfig::load()?; - user_config - .plugins - .insert(args.id.clone(), args.plugin.clone()); - user_config.save()?; - - info!( - "Added plugin {} to global {}", - color::id(&args.id), - color::path(&user_config.path), - ); - - return Ok(()); - } - - let mut config = ToolsConfig::load()?; - config.plugins.insert(args.id.clone(), args.plugin.clone()); - config.save()?; - - info!( - "Added plugin {} to local {}", - color::id(&args.id), - color::path(&config.path) +pub async fn add_plugin_old() { + warn!( + "This command is deprecated, use {} instead", + color::shell("proto tool add") ); + + tool::add(states, resources, emitters).await?; } diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 8edb5e360..a24179182 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -16,6 +16,7 @@ mod plugins; mod remove_plugin; mod run; mod setup; +pub mod tool; mod tools; mod unalias; mod uninstall; diff --git a/crates/cli/src/commands/plugins.rs b/crates/cli/src/commands/plugins.rs index d511a4d9f..53192d866 100644 --- a/crates/cli/src/commands/plugins.rs +++ b/crates/cli/src/commands/plugins.rs @@ -1,107 +1,14 @@ -use crate::helpers::load_configured_tools; -use clap::Args; -use miette::IntoDiagnostic; -use proto_core::{Id, PluginLocator}; -use serde::Serialize; +use crate::commands::tool; use starbase::system; -use starbase_styles::color::{self, OwoStyle}; -use starbase_utils::json; -use std::io::{BufWriter, Write}; -use tracing::info; - -#[derive(Serialize)] -pub struct PluginItem { - id: Id, - locator: PluginLocator, - name: String, - version: Option, -} - -#[derive(Args, Clone, Debug)] -pub struct PluginsArgs { - #[arg(long, help = "Print the list in JSON format")] - json: bool, -} +use starbase_styles::color; +use tracing::warn; #[system] -pub async fn plugins(args: ArgsRef) { - if !args.json { - info!("Loading plugins..."); - } - - let tools = load_configured_tools().await?; - - let mut items = tools - .into_iter() - .map(|tool| PluginItem { - id: tool.id.to_owned(), - locator: tool.locator.unwrap(), - name: tool.metadata.name, - version: tool.metadata.plugin_version, - }) - .collect::>(); - - items.sort_by(|a, d| a.id.cmp(&d.id)); - - if args.json { - println!("{}", json::to_string_pretty(&items).into_diagnostic()?); - - return Ok(()); - } - - let stdout = std::io::stdout(); - let mut buffer = BufWriter::new(stdout.lock()); - - for item in items { - writeln!( - buffer, - "{} {} {}", - OwoStyle::new().bold().style(color::id(item.id)), - color::muted("-"), - color::muted_light(if let Some(version) = item.version { - format!("{} v{version}", item.name) - } else { - item.name - }) - ) - .unwrap(); - - match item.locator { - PluginLocator::SourceFile { path, .. } => { - writeln!( - buffer, - " Source: {}", - color::path(path.canonicalize().unwrap()) - ) - .unwrap(); - } - PluginLocator::SourceUrl { url } => { - writeln!(buffer, " Source: {}", color::url(url)).unwrap(); - } - PluginLocator::GitHub(github) => { - writeln!(buffer, " GitHub: {}", color::label(&github.repo_slug)).unwrap(); - - writeln!( - buffer, - " Tag: {}", - color::hash(github.tag.as_deref().unwrap_or("latest")), - ) - .unwrap(); - } - PluginLocator::Wapm(wapm) => { - writeln!(buffer, " Package: {}", color::label(&wapm.package_name)).unwrap(); - - writeln!( - buffer, - " Version: {}", - color::hash(wapm.version.as_deref().unwrap_or("latest")), - ) - .unwrap(); - } - }; - - writeln!(buffer).unwrap(); - } +pub async fn plugins() { + warn!( + "This command is deprecated, use {} instead", + color::shell("proto tool list-plugins") + ); - buffer.flush().unwrap(); + tool::list_plugins(states, resources, emitters).await?; } diff --git a/crates/cli/src/commands/remove_plugin.rs b/crates/cli/src/commands/remove_plugin.rs index 512145497..4423e18d4 100644 --- a/crates/cli/src/commands/remove_plugin.rs +++ b/crates/cli/src/commands/remove_plugin.rs @@ -1,54 +1,14 @@ -use crate::error::ProtoCliError; -use clap::Args; -use proto_core::{Id, ToolsConfig, UserConfig, TOOLS_CONFIG_NAME}; +use crate::commands::tool; use starbase::system; use starbase_styles::color; -use std::env; -use std::path::PathBuf; -use tracing::info; - -#[derive(Args, Clone, Debug)] -pub struct RemovePluginArgs { - #[arg(required = true, help = "ID of plugin")] - id: Id, - - #[arg( - long, - help = "Remove from the global user config instead of local .prototools" - )] - global: bool, -} +use tracing::warn; #[system] -pub async fn remove_plugin(args: ArgsRef) { - if args.global { - let mut user_config = UserConfig::load()?; - user_config.plugins.remove(&args.id); - user_config.save()?; - - info!( - "Removed plugin {} from global {}", - color::id(&args.id), - color::path(&user_config.path), - ); - - return Ok(()); - } - - let local_path = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - let config_path = local_path.join(TOOLS_CONFIG_NAME); - - if !config_path.exists() { - return Err(ProtoCliError::MissingToolsConfigInCwd { path: config_path }.into()); - } - - let mut config = ToolsConfig::load_from(local_path)?; - config.plugins.remove(&args.id); - config.save()?; - - info!( - "Removed plugin {} from local {}", - color::id(&args.id), - color::path(&config.path) +pub async fn remove_plugin_old() { + warn!( + "This command is deprecated, use {} instead", + color::shell("proto tool remove") ); + + tool::remove(states, resources, emitters).await?; } diff --git a/crates/cli/src/commands/tool/add.rs b/crates/cli/src/commands/tool/add.rs new file mode 100644 index 000000000..7fc6540da --- /dev/null +++ b/crates/cli/src/commands/tool/add.rs @@ -0,0 +1,49 @@ +use clap::Args; +use proto_core::{Id, PluginLocator, ToolsConfig, UserConfig}; +use starbase::system; +use starbase_styles::color; +use tracing::info; + +#[derive(Args, Clone, Debug)] +pub struct AddToolArgs { + #[arg(required = true, help = "ID of tool")] + id: Id, + + #[arg(required = true, help = "Locator string to find and load the plugin")] + plugin: PluginLocator, + + #[arg( + long, + help = "Add to the global user config instead of local .prototools" + )] + global: bool, +} + +#[system] +pub async fn add(args: ArgsRef) { + if args.global { + let mut user_config = UserConfig::load()?; + user_config + .plugins + .insert(args.id.clone(), args.plugin.clone()); + user_config.save()?; + + info!( + "Added plugin {} to global {}", + color::id(&args.id), + color::path(&user_config.path), + ); + + return Ok(()); + } + + let mut config = ToolsConfig::load()?; + config.plugins.insert(args.id.clone(), args.plugin.clone()); + config.save()?; + + info!( + "Added plugin {} to local {}", + color::id(&args.id), + color::path(&config.path) + ); +} diff --git a/crates/cli/src/commands/tool/info.rs b/crates/cli/src/commands/tool/info.rs new file mode 100644 index 000000000..cfd5dfa26 --- /dev/null +++ b/crates/cli/src/commands/tool/info.rs @@ -0,0 +1,130 @@ +use crate::printer::Printer; +use clap::Args; +use miette::IntoDiagnostic; +use proto_core::{detect_version, load_tool, ExecutableLocation, Id, PluginLocator}; +use proto_pdk_api::ToolMetadataOutput; +use serde::Serialize; +use starbase::system; +use starbase_styles::color; +use starbase_utils::json; +use std::path::PathBuf; + +#[derive(Serialize)] +pub struct ToolInfo { + bins: Vec, + exe_path: PathBuf, + globals_dir: Option, + globals_prefix: Option, + id: Id, + inventory_dir: PathBuf, + metadata: ToolMetadataOutput, + name: String, + plugin: PluginLocator, + shims: Vec, +} + +#[derive(Args, Clone, Debug)] +pub struct ToolInfoArgs { + #[arg(required = true, help = "ID of tool")] + id: Id, + + #[arg(long, help = "Print the info in JSON format")] + json: bool, +} + +#[system] +pub async fn info(args: ArgsRef) { + let mut tool = load_tool(&args.id).await?; + let version = detect_version(&tool, None).await?; + + tool.resolve_version(&version, false).await?; + tool.create_executables(false, false).await?; + tool.locate_globals_dir().await?; + + if args.json { + let info = ToolInfo { + bins: tool.get_bin_locations()?, + exe_path: tool.get_exe_path()?.to_path_buf(), + globals_dir: tool.get_globals_bin_dir().map(|p| p.to_path_buf()), + globals_prefix: tool.get_globals_prefix().map(|p| p.to_owned()), + inventory_dir: tool.get_inventory_dir(), + shims: tool.get_shim_locations()?, + id: tool.id, + name: tool.metadata.name.clone(), + metadata: tool.metadata, + plugin: tool.locator.unwrap(), + }; + + println!("{}", json::to_string_pretty(&info).into_diagnostic()?); + + return Ok(()); + } + + let mut printer = Printer::new(); + + printer.header(&tool.id, &tool.metadata.name); + + // INVENTORY + + printer.named_section("Inventory", |p| { + p.entry("Store", color::path(tool.get_inventory_dir())); + + p.entry("Executable", color::path(tool.get_exe_path()?)); + + if let Some(dir) = tool.get_globals_bin_dir() { + p.entry("Globals directory", color::path(dir)); + } + + if let Some(prefix) = tool.get_globals_prefix() { + p.entry("Globals prefix", color::property(prefix)); + } + + p.entry_list( + "Binaries", + tool.get_bin_locations()?.into_iter().map(|bin| { + format!( + "{} {}", + color::path(bin.path), + if bin.primary { + color::muted_light("(primary)") + } else { + "".into() + } + ) + }), + Some(color::failure("None")), + ); + + p.entry_list( + "Shims", + tool.get_shim_locations()?.into_iter().map(|shim| { + format!( + "{} {}", + color::path(shim.path), + if shim.primary { + color::muted_light("(primary)") + } else { + "".into() + } + ) + }), + Some(color::failure("None")), + ); + + Ok(()) + })?; + + // PLUGIN + + printer.named_section("Plugin", |p| { + if let Some(version) = &tool.metadata.plugin_version { + p.entry("Version", color::hash(version)); + } + + p.locator(tool.locator.as_ref().unwrap()); + + Ok(()) + })?; + + printer.flush(); +} diff --git a/crates/cli/src/commands/tool/list.rs b/crates/cli/src/commands/tool/list.rs new file mode 100644 index 000000000..c6dfbaa95 --- /dev/null +++ b/crates/cli/src/commands/tool/list.rs @@ -0,0 +1,128 @@ +use crate::error::ProtoCliError; +use crate::helpers::load_configured_tools_with_filters; +use crate::printer::Printer; +use chrono::{DateTime, NaiveDateTime}; +use clap::Args; +use miette::IntoDiagnostic; +use proto_core::Id; +use starbase::system; +use starbase_styles::color; +use starbase_utils::json; +use std::collections::{HashMap, HashSet}; +use tracing::info; + +#[derive(Args, Clone, Debug)] +pub struct ListToolsArgs { + #[arg(help = "ID of tools to list")] + ids: Vec, + + #[arg(long, help = "Print the list in JSON format")] + json: bool, +} + +#[system] +pub async fn list(args: ArgsRef) { + if !args.json { + info!("Loading tools..."); + } + + let tools = load_configured_tools_with_filters(HashSet::from_iter(&args.ids)).await?; + + let mut tools = tools + .into_iter() + .filter(|tool| !tool.manifest.installed_versions.is_empty()) + .collect::>(); + + tools.sort_by(|a, d| a.id.cmp(&d.id)); + + if tools.is_empty() { + return Err(ProtoCliError::NoInstalledTools.into()); + } + + 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(()); + } + + let mut printer = Printer::new(); + + for tool in tools { + printer.line(); + printer.header(&tool.id, &tool.metadata.name); + + printer.section(|p| { + p.entry("Store", color::path(tool.get_inventory_dir())); + + p.entry_map( + "Aliases", + tool.manifest + .aliases + .iter() + .map(|(k, v)| (color::hash(v.to_string()), color::label(k))) + .collect::>(), + None, + ); + + let mut versions = tool.manifest.installed_versions.iter().collect::>(); + versions.sort(); + + p.entry_map( + "Versions", + versions + .iter() + .map(|version| { + 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 is_default { + color::invalid(version.to_string()) + } else { + color::hash(version.to_string()) + }, + color::muted_light(comments.join(", ")), + ) + }) + .collect::>(), + None, + ); + + Ok(()) + })?; + } + + printer.flush(); +} + +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/commands/tool/list_plugins.rs b/crates/cli/src/commands/tool/list_plugins.rs new file mode 100644 index 000000000..635a62912 --- /dev/null +++ b/crates/cli/src/commands/tool/list_plugins.rs @@ -0,0 +1,71 @@ +use crate::helpers::load_configured_tools; +use crate::printer::Printer; +use clap::Args; +use miette::IntoDiagnostic; +use proto_core::{Id, PluginLocator}; +use serde::Serialize; +use starbase::system; +use starbase_utils::json; +use tracing::info; + +#[derive(Serialize)] +pub struct PluginItem { + id: Id, + locator: PluginLocator, + name: String, + version: Option, +} + +#[derive(Args, Clone, Debug)] +pub struct ListToolPluginsArgs { + #[arg(long, help = "Print the list in JSON format")] + json: bool, +} + +#[system] +pub async fn list_plugins(args: ArgsRef) { + if !args.json { + info!("Loading plugins..."); + } + + let tools = load_configured_tools().await?; + + let mut items = tools + .into_iter() + .map(|tool| PluginItem { + id: tool.id.to_owned(), + locator: tool.locator.unwrap(), + name: tool.metadata.name, + version: tool.metadata.plugin_version, + }) + .collect::>(); + + items.sort_by(|a, d| a.id.cmp(&d.id)); + + if args.json { + println!("{}", json::to_string_pretty(&items).into_diagnostic()?); + + return Ok(()); + } + + let mut printer = Printer::new(); + + for item in items { + printer.line(); + printer.header( + item.id, + if let Some(version) = item.version { + format!("{} v{version}", item.name) + } else { + item.name + }, + ); + + printer.section(|p| { + p.locator(item.locator); + Ok(()) + })?; + } + + printer.flush(); +} diff --git a/crates/cli/src/commands/tool/mod.rs b/crates/cli/src/commands/tool/mod.rs new file mode 100644 index 000000000..17a1441b1 --- /dev/null +++ b/crates/cli/src/commands/tool/mod.rs @@ -0,0 +1,11 @@ +mod add; +mod info; +mod list; +mod list_plugins; +mod remove; + +pub use add::*; +pub use info::*; +pub use list::*; +pub use list_plugins::*; +pub use remove::*; diff --git a/crates/cli/src/commands/tool/remove.rs b/crates/cli/src/commands/tool/remove.rs new file mode 100644 index 000000000..72fd376c4 --- /dev/null +++ b/crates/cli/src/commands/tool/remove.rs @@ -0,0 +1,54 @@ +use crate::error::ProtoCliError; +use clap::Args; +use proto_core::{Id, ToolsConfig, UserConfig, TOOLS_CONFIG_NAME}; +use starbase::system; +use starbase_styles::color; +use std::env; +use std::path::PathBuf; +use tracing::info; + +#[derive(Args, Clone, Debug)] +pub struct RemoveToolArgs { + #[arg(required = true, help = "ID of tool")] + id: Id, + + #[arg( + long, + help = "Remove from the global user config instead of local .prototools" + )] + global: bool, +} + +#[system] +pub async fn remove(args: ArgsRef) { + if args.global { + let mut user_config = UserConfig::load()?; + user_config.plugins.remove(&args.id); + user_config.save()?; + + info!( + "Removed plugin {} from global {}", + color::id(&args.id), + color::path(&user_config.path), + ); + + return Ok(()); + } + + let local_path = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let config_path = local_path.join(TOOLS_CONFIG_NAME); + + if !config_path.exists() { + return Err(ProtoCliError::MissingToolsConfigInCwd { path: config_path }.into()); + } + + let mut config = ToolsConfig::load_from(local_path)?; + config.plugins.remove(&args.id); + config.save()?; + + info!( + "Removed plugin {} from local {}", + color::id(&args.id), + color::path(&config.path) + ); +} diff --git a/crates/cli/src/commands/tools.rs b/crates/cli/src/commands/tools.rs index 307fbb743..a4943aaf3 100644 --- a/crates/cli/src/commands/tools.rs +++ b/crates/cli/src/commands/tools.rs @@ -1,143 +1,14 @@ -use crate::error::ProtoCliError; -use crate::helpers::load_configured_tools_with_filters; -use chrono::{DateTime, NaiveDateTime}; -use clap::Args; -use miette::IntoDiagnostic; -use proto_core::Id; +use crate::commands::tool; use starbase::system; -use starbase_styles::color::{self, OwoStyle}; -use starbase_utils::json; -use std::collections::{HashMap, HashSet}; -use std::io::{BufWriter, Write}; -use tracing::info; - -#[derive(Args, Clone, Debug)] -pub struct ToolsArgs { - #[arg(help = "IDs of tool to list")] - ids: Vec, - - #[arg(long, help = "Print the list in JSON format")] - json: bool, -} +use starbase_styles::color; +use tracing::warn; #[system] -pub async fn tools(args: ArgsRef) { - if !args.json { - info!("Loading tools..."); - } - - let tools = load_configured_tools_with_filters(HashSet::from_iter(&args.ids)).await?; - - let mut tools = tools - .into_iter() - .filter(|tool| !tool.manifest.installed_versions.is_empty()) - .collect::>(); - - tools.sort_by(|a, d| a.id.cmp(&d.id)); - - if tools.is_empty() { - return Err(ProtoCliError::NoInstalledTools.into()); - } - - 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(()); - } - - let stdout = std::io::stdout(); - let mut buffer = BufWriter::new(stdout.lock()); - - for tool in tools { - writeln!( - buffer, - "{} {} {}", - OwoStyle::new().bold().style(color::id(&tool.id)), - color::muted("-"), - color::muted_light(&tool.metadata.name), - ) - .unwrap(); - - writeln!(buffer, " Store: {}", color::path(tool.get_inventory_dir())).unwrap(); - - if !tool.manifest.aliases.is_empty() { - writeln!(buffer, " Aliases:").unwrap(); - - for (alias, version) in &tool.manifest.aliases { - writeln!( - buffer, - " {} {} {}", - color::hash(version.to_string()), - color::muted("="), - color::label(alias), - ) - .unwrap(); - } - } - - if !tool.manifest.installed_versions.is_empty() { - writeln!(buffer, " Versions:").unwrap(); - - 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() { - writeln!(buffer, " {}", color::hash(version.to_string())).unwrap(); - } else { - writeln!( - buffer, - " {} {} {}", - if is_default { - color::symbol(version.to_string()) - } else { - color::hash(version.to_string()) - }, - color::muted("-"), - color::muted_light(comments.join(", ")) - ) - .unwrap(); - } - } - } - - writeln!(buffer).unwrap(); - } - - buffer.flush().unwrap(); -} +pub async fn tools() { + warn!( + "This command is deprecated, use {} instead", + color::shell("proto tool list") + ); -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()) + tool::list(states, resources, emitters).await?; } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 7ab092ef8..e12f867ca 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,9 +2,10 @@ mod app; mod commands; mod error; mod helpers; +mod printer; mod shell; -use app::{App as CLI, Commands}; +use app::{App as CLI, Commands, ToolCommands}; use clap::Parser; use starbase::{tracing::TracingOptions, App, MainResult}; use starbase_utils::string_vec; @@ -48,7 +49,7 @@ async fn main() -> MainResult { let mut app = App::new(); match cli.command { - Commands::AddPlugin(args) => app.execute_with_args(commands::add_plugin, args), + Commands::AddPlugin(args) => app.execute_with_args(commands::add_plugin_old, args), Commands::Alias(args) => app.execute_with_args(commands::alias, args), Commands::Bin(args) => app.execute_with_args(commands::bin, args), Commands::Clean(args) => app.execute_with_args(commands::clean, args), @@ -62,9 +63,18 @@ async fn main() -> MainResult { 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), + Commands::RemovePlugin(args) => app.execute_with_args(commands::remove_plugin_old, args), Commands::Run(args) => app.execute_with_args(commands::run, args), Commands::Setup(args) => app.execute_with_args(commands::setup, args), + Commands::Tool { command } => match command { + ToolCommands::Add(args) => app.execute_with_args(commands::tool::add, args), + ToolCommands::Info(args) => app.execute_with_args(commands::tool::info, args), + ToolCommands::List(args) => app.execute_with_args(commands::tool::list, args), + ToolCommands::ListPlugins(args) => { + app.execute_with_args(commands::tool::list_plugins, args) + } + ToolCommands::Remove(args) => app.execute_with_args(commands::tool::remove, 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), diff --git a/crates/cli/src/printer.rs b/crates/cli/src/printer.rs new file mode 100644 index 000000000..add78fe8a --- /dev/null +++ b/crates/cli/src/printer.rs @@ -0,0 +1,180 @@ +use proto_core::PluginLocator; +use starbase_styles::color::{self, OwoStyle}; +use std::io::{BufWriter, StdoutLock, Write}; + +pub struct Printer<'std> { + buffer: BufWriter>, + depth: u8, +} + +impl<'std> Printer<'std> { + pub fn new() -> Self { + let stdout = std::io::stdout(); + let buffer = BufWriter::new(stdout.lock()); + + Printer { buffer, depth: 0 } + } + + pub fn flush(&mut self) { + self.line(); + self.buffer.flush().unwrap(); + } + + pub fn line(&mut self) { + writeln!(&mut self.buffer).unwrap(); + } + + pub fn header, V: AsRef>(&mut self, id: K, name: V) { + self.indent(); + + writeln!( + &mut self.buffer, + "{} {} {}", + OwoStyle::new().bold().style(color::id(id.as_ref())), + color::muted("-"), + color::muted_light(name.as_ref()), + ) + .unwrap(); + } + + pub fn section( + &mut self, + func: impl FnOnce(&mut Printer) -> miette::Result<()>, + ) -> miette::Result<()> { + self.depth += 1; + func(self)?; + self.depth -= 1; + + Ok(()) + } + + pub fn named_section>( + &mut self, + name: T, + func: impl FnOnce(&mut Printer) -> miette::Result<()>, + ) -> miette::Result<()> { + writeln!(&mut self.buffer).unwrap(); + + self.indent(); + + writeln!( + &mut self.buffer, + "{}", + OwoStyle::new() + .bold() + .style(color::muted_light(name.as_ref())) + ) + .unwrap(); + + self.section(func)?; + + Ok(()) + } + + pub fn indent(&mut self) { + if self.depth > 0 { + write!(&mut self.buffer, "{}", " ".repeat(self.depth as usize)).unwrap(); + } + } + + pub fn entry, V: AsRef>(&mut self, key: K, value: V) { + self.indent(); + + writeln!(&mut self.buffer, "{}: {}", key.as_ref(), value.as_ref()).unwrap(); + } + + pub fn entry_list, I: IntoIterator, V: AsRef>( + &mut self, + key: K, + list: I, + empty: Option, + ) { + let items = list.into_iter().collect::>(); + + if items.is_empty() { + if let Some(fallback) = empty { + self.entry(key, fallback); + } + } else { + self.indent(); + + writeln!(&mut self.buffer, "{}:", key.as_ref()).unwrap(); + + self.depth += 1; + + for item in items { + self.indent(); + + writeln!(&mut self.buffer, "{} {}", color::muted("-"), item.as_ref()).unwrap(); + } + + self.depth -= 1; + } + } + + pub fn entry_map< + K: AsRef, + I: IntoIterator, + V1: AsRef, + V2: AsRef, + >( + &mut self, + key: K, + map: I, + empty: Option, + ) { + let items = map.into_iter().collect::>(); + + if items.is_empty() { + if let Some(fallback) = empty { + self.entry(key, fallback); + } + } else { + self.indent(); + + writeln!(&mut self.buffer, "{}:", key.as_ref()).unwrap(); + + self.depth += 1; + + for item in items { + self.indent(); + + writeln!( + &mut self.buffer, + "{} {} {}", + item.0.as_ref(), + color::muted("-"), + item.1.as_ref() + ) + .unwrap(); + } + + self.depth -= 1; + } + } + + pub fn locator>(&mut self, locator: L) { + match locator.as_ref() { + PluginLocator::SourceFile { path, .. } => { + self.entry("Source", color::path(path.canonicalize().unwrap())); + } + PluginLocator::SourceUrl { url } => { + self.entry("Source", color::url(url)); + } + PluginLocator::GitHub(github) => { + self.entry("GitHub", color::label(&github.repo_slug)); + self.entry( + "Tag", + color::hash(github.tag.as_deref().unwrap_or("latest")), + ); + } + PluginLocator::Wapm(wapm) => { + self.entry("Package", color::label(&wapm.package_name)); + self.entry( + "Release", + color::hash(wapm.version.as_deref().unwrap_or("latest")), + ); + } + }; + } +} diff --git a/crates/cli/tests/plugins_test.rs b/crates/cli/tests/plugins_test.rs index 6f0659dce..849a97d4f 100644 --- a/crates/cli/tests/plugins_test.rs +++ b/crates/cli/tests/plugins_test.rs @@ -40,190 +40,195 @@ where } } -#[tokio::test] -async fn downloads_and_installs_plugin_from_file() { - let user_config = UserConfig::default(); - - run_tests(|root| { - let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - - load_tool_from_locator( - Id::raw("moon"), - ProtoEnvironment::from(root).unwrap(), - PluginLocator::SourceFile { - file: "./tests/fixtures/moon-schema.toml".into(), - path: root_dir.join("./tests/fixtures/moon-schema.toml"), - }, - &user_config, - ) - }) - .await; -} +mod plugins { + use super::*; -#[tokio::test] -#[should_panic(expected = "does not exist")] -async fn errors_for_missing_file() { - let user_config = UserConfig::default(); - - run_tests(|root| { - let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - - load_tool_from_locator( - Id::raw("moon"), - ProtoEnvironment::from(root).unwrap(), - PluginLocator::SourceFile { - file: "./some/fake/path.toml".into(), - path: root_dir.join("./some/fake/path.toml"), - }, - &user_config, - ) - }) - .await; -} + #[tokio::test] + async fn downloads_and_installs_plugin_from_file() { + let user_config = UserConfig::default(); + + run_tests(|root| { + let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + load_tool_from_locator( + Id::raw("moon"), + ProtoEnvironment::from(root).unwrap(), + PluginLocator::SourceFile { + file: "./tests/fixtures/moon-schema.toml".into(), + path: root_dir.join("./tests/fixtures/moon-schema.toml"), + }, + &user_config, + ) + }) + .await; + } -#[tokio::test] -async fn downloads_and_installs_plugin_from_url() { - let user_config = UserConfig::default(); - - run_tests(|root| { - load_tool_from_locator( - Id::raw("moon"), - ProtoEnvironment::from(root).unwrap(), - PluginLocator::SourceUrl { - url: "https://raw.githubusercontent.com/moonrepo/moon/master/proto-plugin.toml" - .into(), - }, - &user_config, - ) - }) - .await; -} + #[tokio::test] + #[should_panic(expected = "does not exist")] + async fn errors_for_missing_file() { + let user_config = UserConfig::default(); + + run_tests(|root| { + let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + load_tool_from_locator( + Id::raw("moon"), + ProtoEnvironment::from(root).unwrap(), + PluginLocator::SourceFile { + file: "./some/fake/path.toml".into(), + path: root_dir.join("./some/fake/path.toml"), + }, + &user_config, + ) + }) + .await; + } -#[tokio::test] -#[should_panic(expected = "does not exist")] -async fn errors_for_broken_url() { - let user_config = UserConfig::default(); - - run_tests(|root| { - load_tool_from_locator( - Id::raw("moon"), - ProtoEnvironment::from(root).unwrap(), - PluginLocator::SourceUrl { - url: "https://raw.githubusercontent.com/moonrepo/moon/some/fake/path.toml".into(), - }, - &user_config, - ) - }) - .await; -} + #[tokio::test] + async fn downloads_and_installs_plugin_from_url() { + let user_config = UserConfig::default(); + + run_tests(|root| { + load_tool_from_locator( + Id::raw("moon"), + ProtoEnvironment::from(root).unwrap(), + PluginLocator::SourceUrl { + url: "https://raw.githubusercontent.com/moonrepo/moon/master/proto-plugin.toml" + .into(), + }, + &user_config, + ) + }) + .await; + } -mod builtins { - use super::*; + #[tokio::test] + #[should_panic(expected = "does not exist")] + async fn errors_for_broken_url() { + let user_config = UserConfig::default(); + + run_tests(|root| { + load_tool_from_locator( + Id::raw("moon"), + ProtoEnvironment::from(root).unwrap(), + PluginLocator::SourceUrl { + url: "https://raw.githubusercontent.com/moonrepo/moon/some/fake/path.toml" + .into(), + }, + &user_config, + ) + }) + .await; + } - // Bun doesn't support Windows - #[cfg(not(windows))] - #[test] - fn supports_bun() { - let temp = create_empty_sandbox(); + mod builtins { + use super::*; - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("bun").assert(); + // Bun doesn't support Windows + #[cfg(not(windows))] + #[test] + fn supports_bun() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("bun").assert(); - #[test] - fn supports_deno() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("deno").assert(); + #[test] + fn supports_deno() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("deno").assert(); - #[test] - fn supports_go() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("go").assert(); + #[test] + fn supports_go() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("go").assert(); - #[test] - fn supports_node() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd - .arg("install") - .arg("node") - .arg("--") - .arg("--no-bundled-npm") - .assert(); + #[test] + fn supports_node() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd + .arg("install") + .arg("node") + .arg("--") + .arg("--no-bundled-npm") + .assert(); - #[test] - fn supports_npm() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("npm").assert(); + #[test] + fn supports_npm() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("npm").assert(); - #[test] - fn supports_pnpm() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("pnpm").assert(); + #[test] + fn supports_pnpm() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("pnpm").assert(); - #[test] - fn supports_yarn() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("yarn").assert(); + #[test] + fn supports_yarn() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("yarn").assert(); - #[test] - fn supports_python() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("python").assert(); + #[test] + fn supports_python() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("python").assert(); - #[test] - fn supports_rust() { - let temp = create_empty_sandbox(); + assert.success(); + } - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("rust").assert(); + #[test] + fn supports_rust() { + let temp = create_empty_sandbox(); - assert.success(); - } + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("rust").assert(); + + assert.success(); + } - #[test] - fn supports_toml_schema() { - let temp = create_empty_sandbox_with_tools(); + #[test] + fn supports_toml_schema() { + let temp = create_empty_sandbox_with_tools(); - let mut cmd = create_proto_command(temp.path()); - let assert = cmd.arg("install").arg("moon-test").assert(); + let mut cmd = create_proto_command(temp.path()); + let assert = cmd.arg("install").arg("moon-test").assert(); - assert.success(); + assert.success(); + } } } diff --git a/crates/cli/tests/add_plugin_test.rs b/crates/cli/tests/tool_add_test.rs similarity index 94% rename from crates/cli/tests/add_plugin_test.rs rename to crates/cli/tests/tool_add_test.rs index c6a3f5224..f3807fc5c 100644 --- a/crates/cli/tests/add_plugin_test.rs +++ b/crates/cli/tests/tool_add_test.rs @@ -5,7 +5,7 @@ use starbase_sandbox::predicates::prelude::*; use std::collections::BTreeMap; use utils::*; -mod add_plugin { +mod tool_add { use super::*; #[test] @@ -14,7 +14,8 @@ mod add_plugin { let mut cmd = create_proto_command(sandbox.path()); let assert = cmd - .arg("add-plugin") + .arg("tool") + .arg("add") .arg("id") .arg("some-fake-value") .assert(); @@ -32,7 +33,7 @@ mod add_plugin { assert!(!config_file.exists()); let mut cmd = create_proto_command(sandbox.path()); - cmd.arg("add-plugin") + cmd.arg("tool").arg("add") .arg("id") .arg("source:https://github.com/moonrepo/schema-plugin/releases/latest/download/schema_plugin.wasm") .assert() @@ -61,7 +62,7 @@ mod add_plugin { assert!(!config_file.exists()); let mut cmd = create_proto_command(sandbox.path()); - cmd.arg("add-plugin") + cmd.arg("tool").arg("add") .arg("id") .arg("source:https://github.com/moonrepo/schema-plugin/releases/latest/download/schema_plugin.wasm") .arg("--global") diff --git a/crates/cli/tests/remove_plugin_test.rs b/crates/cli/tests/tool_remove_test.rs similarity index 89% rename from crates/cli/tests/remove_plugin_test.rs rename to crates/cli/tests/tool_remove_test.rs index 699568795..f1569750e 100644 --- a/crates/cli/tests/remove_plugin_test.rs +++ b/crates/cli/tests/tool_remove_test.rs @@ -5,7 +5,7 @@ use starbase_sandbox::predicates::prelude::*; use std::collections::BTreeMap; use utils::*; -mod remove_plugin { +mod plugin_remove { use super::*; #[test] @@ -13,7 +13,7 @@ mod remove_plugin { let sandbox = create_empty_sandbox(); let mut cmd = create_proto_command(sandbox.path()); - let assert = cmd.arg("remove-plugin").arg("id").assert(); + let assert = cmd.arg("tool").arg("remove").arg("id").assert(); assert.stderr(predicate::str::contains( "No .prototools has been found in current directory.", @@ -31,7 +31,7 @@ mod remove_plugin { config.save().unwrap(); let mut cmd = create_proto_command(sandbox.path()); - cmd.arg("remove-plugin").arg("id").assert().success(); + cmd.arg("tool").arg("remove").arg("id").assert().success(); let config = ToolsConfig::load_from(sandbox.path()).unwrap(); @@ -49,7 +49,8 @@ mod remove_plugin { config.save().unwrap(); let mut cmd = create_proto_command(sandbox.path()); - cmd.arg("remove-plugin") + cmd.arg("tool") + .arg("remove") .arg("id") .arg("--global") .assert() diff --git a/crates/core/src/tool.rs b/crates/core/src/tool.rs index 706f64bba..6e0a7ace5 100644 --- a/crates/core/src/tool.rs +++ b/crates/core/src/tool.rs @@ -28,7 +28,7 @@ use std::time::{Duration, SystemTime}; use tracing::{debug, trace, warn}; use warpgate::{download_from_url_to_file, Id, PluginContainer, PluginLocator, VirtualPath}; -#[derive(Debug, Default)] +#[derive(Debug, Default, Serialize)] pub struct ExecutableLocation { pub config: ExecutableConfig, pub name: String,