diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d677eb74..d8e50f6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ #### 🚀 Updates - 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. ## 0.18.5 diff --git a/Cargo.lock b/Cargo.lock index 1983e9a9c..018637f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,15 +409,6 @@ dependencies = [ "winx", ] -[[package]] -name = "castaway" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" -dependencies = [ - "rustversion", -] - [[package]] name = "cbindgen" version = "0.24.3" @@ -561,19 +552,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - [[package]] name = "console" version = "0.15.5" @@ -1428,31 +1406,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "garde" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ae9eba929c46323014c5ec1adf6d2fa94d68ff1947c489c165fcd7f216b34f2" -dependencies = [ - "compact_str", - "garde_derive", - "once_cell", - "regex", - "smallvec", -] - -[[package]] -name = "garde_derive" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da663fa0f4a2bb40b93ee1b2f77656276b999df3b88574ddafe51d72fc1dfdd9" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.37", -] - [[package]] name = "generic-array" version = "0.14.6" @@ -2831,12 +2784,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - [[package]] name = "ryu" version = "1.0.13" @@ -2861,43 +2808,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "schematic" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe561cb5c8380d36ef80bf81a7cc16acfde8a0d130dd3394cc0cb48809d065ab" -dependencies = [ - "garde", - "indexmap 2.0.0", - "miette", - "schematic_macros", - "schematic_types", - "serde", - "serde_path_to_error", - "starbase_styles", - "thiserror", - "tracing", -] - -[[package]] -name = "schematic_macros" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc05cf6b1a3cb5c631b2955e63581b6ab1386eee26f71d5f0472dda99c61f84" -dependencies = [ - "convert_case", - "darling 0.20.3", - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "schematic_types" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f0fb227e27447defdd9e26f121ac10836120b122c35d05b02c6e588a91353d" - [[package]] name = "scopeguard" version = "1.1.0" @@ -2993,16 +2903,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_spanned" version = "0.6.3" @@ -3267,12 +3167,6 @@ dependencies = [ "wax", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.10.0" @@ -3862,7 +3756,6 @@ dependencies = [ "human-sort", "once_cell", "regex", - "schematic", "semver", "serde", ] diff --git a/crates/cli/src/commands/install.rs b/crates/cli/src/commands/install.rs index d989167c4..0a939a2fc 100644 --- a/crates/cli/src/commands/install.rs +++ b/crates/cli/src/commands/install.rs @@ -1,9 +1,10 @@ use super::clean::clean_plugins; +use super::pin::{internal_pin, PinArgs}; use crate::helpers::{create_progress_bar, disable_progress_bars}; use crate::shell; use clap::Args; use miette::IntoDiagnostic; -use proto_core::{load_tool, Id, Tool, UnresolvedVersionSpec}; +use proto_core::{load_tool, Id, PinType, Tool, UnresolvedVersionSpec, UserConfig}; use proto_pdk_api::{InstallHook, SyncShellProfileInput, SyncShellProfileOutput}; use starbase::{system, SystemResult}; use starbase_styles::color; @@ -37,9 +38,34 @@ pub struct InstallArgs { pub passthrough: Vec, } -pub fn pin_global(tool: &mut Tool) -> SystemResult { - tool.manifest.default_version = Some(tool.get_resolved_version().to_unresolved_spec()); - tool.manifest.save()?; +fn pin_version( + tool: &mut Tool, + initial_version: &UnresolvedVersionSpec, + global: bool, +) -> SystemResult { + let mut args = PinArgs { + id: tool.id.clone(), + spec: tool.get_resolved_version().to_unresolved_spec(), + global: false, + }; + + // via `--pin` arg + if global { + args.global = true; + + return internal_pin(tool, &args); + } + + // via `pin-latest` setting + if initial_version.is_latest() { + let user_config = UserConfig::load()?; + + if let Some(pin_type) = user_config.pin_latest { + args.global = matches!(pin_type, PinType::Global); + + return internal_pin(tool, &args); + } + } Ok(()) } @@ -56,9 +82,7 @@ pub async fn internal_install(args: InstallArgs) -> SystemResult { tool.disable_caching(); if !version.is_canary() && tool.is_setup(&version).await? { - if args.pin { - pin_global(&mut tool)?; - } + pin_version(&mut tool, &version, args.pin)?; info!( "{} has already been installed at {}", @@ -110,9 +134,7 @@ pub async fn internal_install(args: InstallArgs) -> SystemResult { return Ok(()); } - if args.pin { - pin_global(&mut tool)?; - } + pin_version(&mut tool, &version, args.pin)?; info!( "{} has been installed to {}!", @@ -134,7 +156,7 @@ pub async fn internal_install(args: InstallArgs) -> SystemResult { update_shell(tool, args.passthrough.clone())?; // Clean plugins - debug!("Auto-cleaning old plugins"); + debug!("Auto-cleaning plugins"); clean_plugins(7).await?; diff --git a/crates/cli/src/commands/pin.rs b/crates/cli/src/commands/pin.rs index 5fb192fed..58d60dbe7 100644 --- a/crates/cli/src/commands/pin.rs +++ b/crates/cli/src/commands/pin.rs @@ -2,30 +2,27 @@ use std::env; use std::path::PathBuf; use clap::Args; -use proto_core::{load_tool, Id, ToolsConfig, UnresolvedVersionSpec}; -use starbase::system; +use proto_core::{load_tool, Id, Tool, ToolsConfig, UnresolvedVersionSpec}; +use starbase::{system, SystemResult}; use starbase_styles::color; use tracing::{debug, info}; #[derive(Args, Clone, Debug)] pub struct PinArgs { #[arg(required = true, help = "ID of tool")] - id: Id, + pub id: Id, #[arg(required = true, help = "Version or alias of tool")] - spec: UnresolvedVersionSpec, + pub spec: UnresolvedVersionSpec, #[arg( long, help = "Add to the global user config instead of local .prototools" )] - global: bool, + pub global: bool, } -#[system] -pub async fn pin(args: ArgsRef) -> SystemResult { - let mut tool = load_tool(&args.id).await?; - +pub fn internal_pin(tool: &mut Tool, args: &PinArgs) -> SystemResult { if args.global { tool.manifest.default_version = Some(args.spec.clone()); tool.manifest.save()?; @@ -35,12 +32,6 @@ pub async fn pin(args: ArgsRef) -> SystemResult { manifest = ?tool.manifest.path, "Wrote the global version", ); - - info!( - "Set the global {} version to {}", - tool.get_name(), - color::hash(args.spec.to_string()) - ); } else { let local_path = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); @@ -53,11 +44,20 @@ pub async fn pin(args: ArgsRef) -> SystemResult { config = ?config.path, "Wrote the local version", ); - - info!( - "Set the local {} version to {}", - tool.get_name(), - color::hash(args.spec.to_string()) - ); } + + Ok(()) +} + +#[system] +pub async fn pin(args: ArgsRef) -> SystemResult { + let mut tool = load_tool(&args.id).await?; + + internal_pin(&mut tool, args)?; + + info!( + "Set the {} version to {}", + tool.get_name(), + color::hash(args.spec.to_string()) + ); } diff --git a/crates/cli/tests/install_uninstall_test.rs b/crates/cli/tests/install_uninstall_test.rs index f277a904b..954560144 100644 --- a/crates/cli/tests/install_uninstall_test.rs +++ b/crates/cli/tests/install_uninstall_test.rs @@ -1,6 +1,6 @@ mod utils; -use proto_core::{ToolManifest, UnresolvedVersionSpec, VersionSpec}; +use proto_core::{ToolManifest, ToolsConfig, UnresolvedVersionSpec, VersionSpec}; use starbase_sandbox::predicates::prelude::*; use std::collections::HashSet; use utils::*; @@ -231,4 +231,84 @@ mod install_uninstall { Some(UnresolvedVersionSpec::parse("19.0.0").unwrap()) ); } + + #[test] + fn can_pin_latest_locally_using_setting() { + let temp = create_empty_sandbox(); + temp.create_file("config.toml", "pin-latest = \"local\""); + + let manifest_file = temp.path().join("tools/node/manifest.json"); + + // We set a default version here to compare against later + let mut manifest = ToolManifest::load(&manifest_file).unwrap(); + manifest.default_version = Some(UnresolvedVersionSpec::parse("18.0.0").unwrap()); + manifest.save().unwrap(); + + let mut cmd = create_proto_command(temp.path()); + cmd.arg("install") + .arg("node") + .arg("--") + .arg("--no-bundled-npm") + .assert(); + + let manifest = ToolManifest::load(&manifest_file).unwrap(); + + assert_eq!( + manifest.default_version.unwrap(), + UnresolvedVersionSpec::parse("18.0.0").unwrap() + ); + + let tools = ToolsConfig::load_from(temp.path()).unwrap(); + + assert!(tools.tools.get("node").is_some()); + } + + #[test] + fn can_pin_latest_globally_using_setting() { + let temp = create_empty_sandbox(); + temp.create_file("config.toml", "pin-latest = \"global\""); + + let manifest_file = temp.path().join("tools/node/manifest.json"); + + // We set a default version here to compare against later + let mut manifest = ToolManifest::load(&manifest_file).unwrap(); + manifest.default_version = Some(UnresolvedVersionSpec::parse("18.0.0").unwrap()); + manifest.save().unwrap(); + + let mut cmd = create_proto_command(temp.path()); + cmd.arg("install") + .arg("node") + .arg("--") + .arg("--no-bundled-npm") + .assert(); + + let manifest = ToolManifest::load(&manifest_file).unwrap(); + + assert_ne!( + manifest.default_version.unwrap(), + UnresolvedVersionSpec::parse("18.0.0").unwrap() + ); + + let tools = ToolsConfig::load_from(temp.path()).unwrap(); + + assert!(tools.tools.get("node").is_none()); + } + + #[test] + fn doesnt_pin_using_setting_if_not_latest() { + let temp = create_empty_sandbox(); + temp.create_file("config.toml", "pin-latest = \"local\""); + + let mut cmd = create_proto_command(temp.path()); + cmd.arg("install") + .arg("node") + .arg("20.0.0") + .arg("--") + .arg("--no-bundled-npm") + .assert(); + + let tools = ToolsConfig::load_from(temp.path()).unwrap(); + + assert!(tools.tools.get("node").is_none()); + } } diff --git a/crates/core/src/user_config.rs b/crates/core/src/user_config.rs index ddcde0392..af51e1a6f 100644 --- a/crates/core/src/user_config.rs +++ b/crates/core/src/user_config.rs @@ -10,12 +10,20 @@ use warpgate::{HttpOptions, Id, PluginLocator}; pub const USER_CONFIG_NAME: &str = "config.toml"; +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum PinType { + Global, + Local, +} + #[derive(Debug, Deserialize, PartialEq, Serialize)] #[serde(default, rename_all = "kebab-case")] pub struct UserConfig { pub auto_clean: bool, pub auto_install: bool, pub node_intercept_globals: bool, + pub pin_latest: Option, pub http: HttpOptions, pub plugins: BTreeMap, @@ -85,8 +93,9 @@ impl Default for UserConfig { Self { auto_clean: from_var("PROTO_AUTO_CLEAN", false), auto_install: from_var("PROTO_AUTO_INSTALL", false), - node_intercept_globals: from_var("PROTO_NODE_INTERCEPT_GLOBALS", true), http: HttpOptions::default(), + node_intercept_globals: from_var("PROTO_NODE_INTERCEPT_GLOBALS", true), + pin_latest: None, plugins: BTreeMap::default(), path: PathBuf::new(), } diff --git a/crates/core/tests/user_config_test.rs b/crates/core/tests/user_config_test.rs index e1c25c94b..e34f2d965 100644 --- a/crates/core/tests/user_config_test.rs +++ b/crates/core/tests/user_config_test.rs @@ -1,4 +1,4 @@ -use proto_core::{UserConfig, USER_CONFIG_NAME}; +use proto_core::{PinType, UserConfig, USER_CONFIG_NAME}; use starbase_sandbox::create_empty_sandbox; use std::collections::BTreeMap; use std::env; @@ -19,6 +19,7 @@ mod user_config { auto_install: false, node_intercept_globals: true, http: HttpOptions::default(), + pin_latest: None, plugins: BTreeMap::default(), path: sandbox.path().join(USER_CONFIG_NAME), } @@ -26,7 +27,7 @@ mod user_config { } #[test] - fn can_set_booleans() { + fn can_set_values() { let sandbox = create_empty_sandbox(); sandbox.create_file( "config.toml", @@ -34,6 +35,7 @@ mod user_config { auto-clean = true auto-install = true node-intercept-globals = false +pin-latest = "global" "#, ); @@ -46,6 +48,7 @@ node-intercept-globals = false auto_install: true, node_intercept_globals: false, http: HttpOptions::default(), + pin_latest: Some(PinType::Global), plugins: BTreeMap::default(), path: sandbox.path().join(USER_CONFIG_NAME), } @@ -70,6 +73,7 @@ node-intercept-globals = false auto_install: true, node_intercept_globals: false, http: HttpOptions::default(), + pin_latest: None, plugins: BTreeMap::default(), path: sandbox.path().join(USER_CONFIG_NAME), } diff --git a/crates/version-spec/Cargo.toml b/crates/version-spec/Cargo.toml index c3982b88e..5d7c8500d 100644 --- a/crates/version-spec/Cargo.toml +++ b/crates/version-spec/Cargo.toml @@ -11,10 +11,5 @@ repository = "https://github.com/moonrepo/proto" human-sort = { workspace = true } once_cell = { workspace = true } regex = { workspace = true } -schematic = { version = "0.11.7", default-features = false, optional = true } semver = { workspace = true } serde = { workspace = true } - -[features] -default = [] -schematic = ["dep:schematic"] diff --git a/crates/version-spec/src/resolved_spec.rs b/crates/version-spec/src/resolved_spec.rs index e92b4ee5c..4f81db0b3 100644 --- a/crates/version-spec/src/resolved_spec.rs +++ b/crates/version-spec/src/resolved_spec.rs @@ -107,10 +107,3 @@ impl PartialEq for VersionSpec { } } } - -#[cfg(feature = "schematic")] -impl schematic::Schematic for VersionSpec { - fn generate_schema() -> schematic::SchemaType { - schematic::SchemaType::string() - } -} diff --git a/crates/version-spec/src/unresolved_spec.rs b/crates/version-spec/src/unresolved_spec.rs index d0f7d3bdd..e3e55a157 100644 --- a/crates/version-spec/src/unresolved_spec.rs +++ b/crates/version-spec/src/unresolved_spec.rs @@ -26,11 +26,18 @@ impl UnresolvedVersionSpec { matches!(self, UnresolvedVersionSpec::Canary) } + pub fn is_latest(&self) -> bool { + match self { + Self::Alias(alias) => alias == "latest", + _ => false, + } + } + pub fn to_resolved_spec(&self) -> VersionSpec { match self { - UnresolvedVersionSpec::Canary => VersionSpec::Alias("canary".to_owned()), - UnresolvedVersionSpec::Alias(alias) => VersionSpec::Alias(alias.to_owned()), - UnresolvedVersionSpec::Version(version) => VersionSpec::Version(version.to_owned()), + Self::Canary => VersionSpec::Alias("canary".to_owned()), + Self::Alias(alias) => VersionSpec::Alias(alias.to_owned()), + Self::Version(version) => VersionSpec::Version(version.to_owned()), _ => unreachable!(), } } @@ -141,10 +148,3 @@ impl PartialEq for UnresolvedVersionSpec { } } } - -#[cfg(feature = "schematic")] -impl schematic::Schematic for UnresolvedVersionSpec { - fn generate_schema() -> schematic::SchemaType { - schematic::SchemaType::string() - } -}