diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad9b8909b..f90a7f6d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,5 +101,5 @@ jobs: - run: just build - run: just build-wasm - run: cargo run -- --help - - run: cargo run -- list-remote node - - run: cargo run -- list-remote wasm-test + - run: cargo run -- versions node + - run: cargo run -- versions wasm-test diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea285606..6c5cffa23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,14 @@ #### 💥 Breaking +- Removed the `proto list` and `proto list-remote` commands, use `proto versions` instead. - Removed the `--global` option from `proto alias`, `unalias`, `pin`, and `unpin`, use `--to` or `--from` instead. - Removed the `--purge` option from `proto clean`, use `proto uninstall` instead. -## Unreleased +#### 🚀 Updates - Added a `--yes` option to `proto outdated`, that skips confirmation prompts. +- Added a new command, `proto versions `, that lists all available remote and installed versions/aliases. ## 0.43.3 diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 86d70762c..3b8fbb526 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -2,8 +2,8 @@ use crate::commands::{ debug::{DebugConfigArgs, DebugEnvArgs}, plugin::{AddPluginArgs, InfoPluginArgs, ListPluginsArgs, RemovePluginArgs, SearchPluginArgs}, ActivateArgs, AliasArgs, BinArgs, CleanArgs, CompletionsArgs, DiagnoseArgs, InstallArgs, - ListArgs, ListRemoteArgs, MigrateArgs, OutdatedArgs, PinArgs, RegenArgs, RunArgs, SetupArgs, - StatusArgs, UnaliasArgs, UninstallArgs, UnpinArgs, UpgradeArgs, + MigrateArgs, OutdatedArgs, PinArgs, RegenArgs, RunArgs, SetupArgs, StatusArgs, UnaliasArgs, + UninstallArgs, UnpinArgs, UpgradeArgs, VersionsArgs, }; use clap::builder::styling::{Color, Style, Styles}; use clap::{Parser, Subcommand, ValueEnum}; @@ -184,21 +184,6 @@ pub enum Commands { )] Install(InstallArgs), - #[command( - alias = "ls", - name = "list", - about = "List installed versions for a tool." - )] - List(ListArgs), - - #[command( - alias = "lsr", - name = "list-remote", - about = "List available versions for a tool.", - long_about = "List available versions by resolving versions from the tool's remote release manifest." - )] - ListRemote(ListRemoteArgs), - #[command( name = "migrate", about = "Migrate breaking changes for the proto installation." @@ -277,6 +262,14 @@ pub enum Commands { about = "Upgrade proto to the latest version." )] Upgrade(UpgradeArgs), + + #[command( + alias = "vs", + name = "versions", + about = "List available versions for a tool.", + long_about = "List available versions for a tool by resolving versions from the tool's remote release manifest." + )] + Versions(VersionsArgs), } #[derive(Clone, Debug, Subcommand)] @@ -296,7 +289,7 @@ pub enum PluginCommands { #[command( name = "add", about = "Add a plugin to manage a tool.", - long_about = "Add a plugin to the local ./.prototools config, or global ~/.proto/.prototools config." + long_about = "Add a plugin to a .prototools config file to enable and manage that tool." )] Add(AddPluginArgs), @@ -315,7 +308,7 @@ pub enum PluginCommands { #[command( name = "remove", about = "Remove a plugin and unmanage a tool.", - long_about = "Remove a plugin from the local ./.prototools config, or global ~/.proto/.prototools config." + long_about = "Remove a plugin from a .prototools config file and unmanage that tool." )] Remove(RemovePluginArgs), diff --git a/crates/cli/src/commands/list.rs b/crates/cli/src/commands/list.rs deleted file mode 100644 index 2e0696b6d..000000000 --- a/crates/cli/src/commands/list.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::session::{LoadToolOptions, ProtoSession}; -use clap::Args; -use iocraft::prelude::element; -use proto_core::Id; -use starbase::AppResult; -use starbase_console::ui::*; -use tracing::debug; - -#[derive(Args, Clone, Debug)] -pub struct ListArgs { - #[arg(required = true, help = "ID of tool")] - id: Id, - - #[arg(long, help = "Include local aliases in the output")] - aliases: bool, -} - -#[tracing::instrument(skip_all)] -pub async fn list(session: ProtoSession, args: ListArgs) -> AppResult { - let tool = session - .load_tool_with_options( - &args.id, - LoadToolOptions { - inherit_local: true, - ..Default::default() - }, - ) - .await?; - - debug!(manifest = ?tool.inventory.manifest.path, "Using versions from manifest"); - - if tool.installed_versions.is_empty() { - session.console.render(element! { - Notice(variant: Variant::Failure) { - StyledText( - content: format!( - "No versions installed locally, try installing the latest version with proto install {}", - args.id - ), - ) - } - })?; - - return Ok(Some(1)); - } - - session.console.out.write_line( - tool.installed_versions - .iter() - .map(|v| v.to_string()) - .collect::>() - .join("\n"), - )?; - - if args.aliases && !tool.local_aliases.is_empty() { - session.console.out.write_line( - tool.local_aliases - .iter() - .map(|(k, v)| format!("{k} -> {v}")) - .collect::>() - .join("\n"), - )?; - } - - Ok(None) -} diff --git a/crates/cli/src/commands/list_remote.rs b/crates/cli/src/commands/list_remote.rs deleted file mode 100644 index b7ecc7613..000000000 --- a/crates/cli/src/commands/list_remote.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::session::{LoadToolOptions, ProtoSession}; -use clap::Args; -use iocraft::prelude::element; -use proto_core::Id; -use starbase::AppResult; -use starbase_console::ui::*; -use tracing::debug; - -#[derive(Args, Clone, Debug)] -pub struct ListRemoteArgs { - #[arg(required = true, help = "ID of tool")] - id: Id, - - #[arg(long, help = "Include remote aliases in the output")] - aliases: bool, -} - -#[tracing::instrument(skip_all)] -pub async fn list_remote(session: ProtoSession, args: ListRemoteArgs) -> AppResult { - let tool = session - .load_tool_with_options( - &args.id, - LoadToolOptions { - inherit_remote: true, - ..Default::default() - }, - ) - .await?; - - debug!("Loading versions from remote"); - - if tool.remote_versions.is_empty() { - session.console.render(element! { - Notice(variant: Variant::Failure) { - StyledText( - content: "No versions available from remote registry" - ) - } - })?; - - return Ok(Some(1)); - } - - session.console.out.write_line( - tool.remote_versions - .iter() - .map(|v| v.to_string()) - .collect::>() - .join("\n"), - )?; - - if args.aliases && !tool.remote_aliases.is_empty() { - session.console.out.write_line( - tool.remote_aliases - .iter() - .map(|(k, v)| format!("{k} -> {v}")) - .collect::>() - .join("\n"), - )?; - } - - Ok(None) -} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 3d081b8ca..4e7b40578 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -6,8 +6,6 @@ mod completions; pub mod debug; mod diagnose; mod install; -mod list; -mod list_remote; mod migrate; mod outdated; mod pin; @@ -20,6 +18,7 @@ mod unalias; mod uninstall; mod unpin; mod upgrade; +mod versions; pub use activate::*; pub use alias::*; @@ -28,8 +27,6 @@ pub use clean::*; pub use completions::*; pub use diagnose::*; pub use install::*; -pub use list::*; -pub use list_remote::*; pub use migrate::*; pub use outdated::*; pub use pin::*; @@ -41,3 +38,4 @@ pub use unalias::*; pub use uninstall::*; pub use unpin::*; pub use upgrade::*; +pub use versions::*; diff --git a/crates/cli/src/commands/plugin/add.rs b/crates/cli/src/commands/plugin/add.rs index 872b3cf7a..6ec708719 100644 --- a/crates/cli/src/commands/plugin/add.rs +++ b/crates/cli/src/commands/plugin/add.rs @@ -31,14 +31,11 @@ pub async fn add(session: ProtoSession, args: AddPluginArgs) -> AppResult { // the recent addition! #[cfg(not(debug_assertions))] { - use proto_core::load_tool_from_locator; - use starbase_styles::color::apply_style_tags; - - let tool = load_tool_from_locator(&args.id, &session.env, &args.plugin).await?; + let tool = proto_core::load_tool_from_locator(&args.id, &session.env, &args.plugin).await?; if !tool.metadata.deprecations.is_empty() { session.console.render(element! { - Notice(title: "Deprecations", variant: Variant::Info) { + Notice(title: "Deprecations".to_owned(), variant: Variant::Info) { List { #(tool.metadata.deprecations.iter().map(|message| { element! { diff --git a/crates/cli/src/commands/versions.rs b/crates/cli/src/commands/versions.rs new file mode 100644 index 000000000..10e84adbf --- /dev/null +++ b/crates/cli/src/commands/versions.rs @@ -0,0 +1,146 @@ +use crate::components::create_datetime; +use crate::session::{LoadToolOptions, ProtoSession}; +use clap::Args; +use indexmap::IndexMap; +use iocraft::prelude::{element, Box}; +use proto_core::{Id, UnresolvedVersionSpec, VersionSpec}; +use serde::Serialize; +use starbase::AppResult; +use starbase_console::ui::*; +use starbase_utils::json; +use std::collections::BTreeMap; +use tracing::debug; + +#[derive(Args, Clone, Debug)] +pub struct VersionsArgs { + #[arg(required = true, help = "ID of tool")] + id: Id, + + #[arg(long, help = "Include aliases in the output")] + aliases: bool, + + #[arg(long, help = "Only display installed versions")] + installed: bool, + + #[arg(long, help = "Print the versions in JSON format")] + json: bool, +} + +#[derive(Serialize)] +pub struct VersionItem { + #[serde(skip_serializing_if = "Option::is_none")] + installed_at: Option, + version: VersionSpec, +} + +#[derive(Serialize)] +pub struct VersionsInfo { + versions: Vec, + local_aliases: BTreeMap, + remote_aliases: BTreeMap, +} + +#[tracing::instrument(skip_all)] +pub async fn versions(session: ProtoSession, args: VersionsArgs) -> AppResult { + let tool = session + .load_tool_with_options( + &args.id, + LoadToolOptions { + inherit_local: true, + inherit_remote: true, + ..Default::default() + }, + ) + .await?; + + debug!("Loading versions from remote"); + + if tool.remote_versions.is_empty() { + session.console.render(element! { + Notice(variant: Variant::Failure) { + StyledText( + content: "No versions available from remote registry" + ) + } + })?; + + return Ok(Some(1)); + } + + let versions = tool + .remote_versions + .iter() + .filter_map(|version| { + let installed_at = tool + .inventory + .manifest + .versions + .get(version) + .map(|meta| meta.installed_at); + + if args.installed && installed_at.is_none() { + None + } else { + Some(VersionItem { + installed_at, + version: version.to_owned(), + }) + } + }) + .collect::>(); + + if args.json { + let info = VersionsInfo { + versions, + local_aliases: tool.local_aliases, + remote_aliases: tool.remote_aliases, + }; + + session.console.out.write_line(json::format(&info, true)?)?; + + return Ok(None); + } + + let mut aliases = IndexMap::<&String, &UnresolvedVersionSpec>::default(); + + if args.aliases && !args.installed { + aliases.extend(&tool.remote_aliases); + aliases.extend(&tool.local_aliases); + } + + session.console.render(element! { + Container { + #(versions.into_iter().map(|item| { + element! { + Box { + #(if let Some(timestamp) = item.installed_at { + element! { + StyledText( + content: format!( + "{} - installed {}", + item.version, + create_datetime(timestamp).unwrap_or_default().format("%x") + ), + ) + } + } else { + element! { + StyledText(content: item.version.to_string()) + } + }) + } + } + })) + + #(aliases.into_iter().map(|(alias, version)| { + element! { + Box { + StyledText(content: format!("{alias} → {version}")) + } + } + })) + } + })?; + + Ok(None) +} diff --git a/crates/cli/src/components/mod.rs b/crates/cli/src/components/mod.rs index 3847c9b05..b0560f63c 100644 --- a/crates/cli/src/components/mod.rs +++ b/crates/cli/src/components/mod.rs @@ -10,7 +10,14 @@ pub use issues_list::*; pub use locator::*; pub use versions_map::*; +use chrono::{DateTime, NaiveDateTime}; + pub fn is_path_like(value: impl AsRef) -> bool { let value = value.as_ref(); value.contains('/') || value.contains("\\") } + +pub 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/components/versions_map.rs b/crates/cli/src/components/versions_map.rs index e14943632..e14b21a64 100644 --- a/crates/cli/src/components/versions_map.rs +++ b/crates/cli/src/components/versions_map.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, NaiveDateTime}; +use super::create_datetime; use iocraft::prelude::*; use proto_core::layout::Inventory; use proto_core::{UnresolvedVersionSpec, VersionSpec}; @@ -63,8 +63,3 @@ pub fn VersionsMap<'a>(props: &VersionsMapProps<'a>) -> impl Into } } } - -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/main.rs b/crates/cli/src/main.rs index ba947a90c..2bc2a3aef 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -85,8 +85,6 @@ async fn main() -> MainResult { }, Commands::Diagnose(args) => commands::diagnose(session, args).await, Commands::Install(args) => commands::install(session, args).await, - Commands::List(args) => commands::list(session, args).await, - Commands::ListRemote(args) => commands::list_remote(session, args).await, Commands::Migrate(args) => commands::migrate(session, args).await, Commands::Outdated(args) => commands::outdated(session, args).await, Commands::Pin(args) => commands::pin(session, args).await, @@ -105,6 +103,7 @@ async fn main() -> MainResult { Commands::Uninstall(args) => commands::uninstall(session, args).await, Commands::Unpin(args) => commands::unpin(session, args).await, Commands::Upgrade(args) => commands::upgrade(session, args).await, + Commands::Versions(args) => commands::versions(session, args).await, } }) .await?; diff --git a/crates/cli/tests/list_remote_test.rs b/crates/cli/tests/list_remote_test.rs deleted file mode 100644 index 6ac48c18a..000000000 --- a/crates/cli/tests/list_remote_test.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod utils; - -use starbase_sandbox::output_to_string; -use utils::*; - -mod list_remote { - use super::*; - - #[test] - fn lists_remote_versions() { - let sandbox = create_empty_proto_sandbox(); - - let assert = sandbox.run_bin(|cmd| { - cmd.arg("list-remote").arg("npm"); - }); - - // Without stderr - let output = output_to_string(&assert.inner.get_output().stdout); - - assert!(output.split('\n').collect::>().len() > 1); - } -} diff --git a/crates/cli/tests/list_test.rs b/crates/cli/tests/list_test.rs deleted file mode 100644 index 752cd3104..000000000 --- a/crates/cli/tests/list_test.rs +++ /dev/null @@ -1,36 +0,0 @@ -mod utils; - -use proto_core::{ToolManifest, VersionSpec}; -use starbase_sandbox::output_to_string; -use utils::*; - -mod list { - use super::*; - - #[test] - fn lists_local_versions() { - let sandbox = create_empty_proto_sandbox(); - - let mut manifest = - ToolManifest::load(sandbox.path().join(".proto/tools/node/manifest.json")).unwrap(); - manifest - .installed_versions - .insert(VersionSpec::parse("19.0.0").unwrap()); - manifest - .installed_versions - .insert(VersionSpec::parse("18.0.0").unwrap()); - manifest - .installed_versions - .insert(VersionSpec::parse("17.0.0").unwrap()); - manifest.save().unwrap(); - - let assert = sandbox.run_bin(|cmd| { - cmd.arg("list").arg("node"); - }); - - // Without stderr - let output = output_to_string(&assert.inner.get_output().stdout); - - assert_eq!(output.split('\n').collect::>().len(), 4); // includes header - } -} diff --git a/crates/cli/tests/versions_test.rs b/crates/cli/tests/versions_test.rs new file mode 100644 index 000000000..ea66fddad --- /dev/null +++ b/crates/cli/tests/versions_test.rs @@ -0,0 +1,89 @@ +mod utils; + +use proto_core::{ToolManifest, ToolManifestVersion, VersionSpec}; +use starbase_sandbox::output_to_string; +use utils::*; + +mod versions { + use super::*; + + #[test] + fn lists_remote_versions() { + let sandbox = create_empty_proto_sandbox(); + + let assert = sandbox.run_bin(|cmd| { + cmd.arg("versions").arg("npm"); + }); + + // Without stderr + let output = output_to_string(&assert.inner.get_output().stdout); + + assert!(output.split('\n').collect::>().len() > 1); + } + + #[test] + fn lists_local_versions() { + let sandbox = create_empty_proto_sandbox(); + let versions = vec!["19.0.0", "18.0.0", "17.0.0"]; + + let mut manifest = + ToolManifest::load(sandbox.path().join(".proto/tools/node/manifest.json")).unwrap(); + + for version in &versions { + manifest.versions.insert( + VersionSpec::parse(version).unwrap(), + ToolManifestVersion::default(), + ); + } + + manifest.save().unwrap(); + + let assert = sandbox.run_bin(|cmd| { + cmd.arg("versions").arg("node"); + }); + + // Without stderr + let output = output_to_string(&assert.inner.get_output().stdout); + let mut count = 0; + + for line in output.lines() { + for version in &versions { + if line.starts_with(version) { + count += 1; + assert!(line.contains("installed")); + } + } + } + + assert_eq!(count, 3); + } + + #[test] + fn only_displays_local_versions() { + let sandbox = create_empty_proto_sandbox(); + let versions = vec!["19.0.0", "18.0.0", "17.0.0"]; + + let mut manifest = + ToolManifest::load(sandbox.path().join(".proto/tools/node/manifest.json")).unwrap(); + + for version in &versions { + manifest.versions.insert( + VersionSpec::parse(version).unwrap(), + ToolManifestVersion::default(), + ); + } + + manifest.save().unwrap(); + + let assert = sandbox.run_bin(|cmd| { + cmd.arg("versions").arg("node").arg("--installed"); + }); + + assert.debug(); + + // Without stderr + let output = output_to_string(&assert.inner.get_output().stdout); + + assert_eq!(output.lines().collect::>().len(), 3); + } +}