diff --git a/CHANGELOG.md b/CHANGELOG.md index b65831f19..b739283b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,21 @@ ## Unreleased +#### 💥 Breaking + +- WASM API + - Changed `SyncManifestOutput` `versions` field to the `VersionSpec` type instead of `Version`. + - Changed `LoadVersionsOutput` `canary`, `latest`, and `aliases` fields to the `UnresolvedVersionSpec` type instead of `Version`. + - Changed `LoadVersionsOutput` `versions` fields to the `VersionSpec` type instead of `Version`. + - Renamed `VersionSpec::Version` to `VersionSpec::Semantic`. The inner `Version` must also be wrapped in a `SemVer` type. + +#### 🚀 Updates + +- Added experimental support for the [calver](https://calver.org) (calendar versioning) specification. For example: 2024-04, 2024-06-10, etc. + - There are some caveats to this approach. Please refer to the documentation. +- WASM API + - Added `VersionSpec::Calendar` and `UnresolvedVersionSpec::Calendar` variant types. + #### ⚙️ Internal - Improved command execution. May see some slight performance gains. diff --git a/Cargo.lock b/Cargo.lock index 08f2bcf1c..93179717f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3971,6 +3971,7 @@ dependencies = [ "schematic", "semver", "serde", + "thiserror", ] [[package]] diff --git a/crates/cli/src/commands/clean.rs b/crates/cli/src/commands/clean.rs index 6768c9555..75a021c7b 100644 --- a/crates/cli/src/commands/clean.rs +++ b/crates/cli/src/commands/clean.rs @@ -81,10 +81,11 @@ pub async fn clean_tool(mut tool: Tool, now: u128, days: u8, yes: bool) -> miett continue; } - let version = VersionSpec::parse(&dir_name).map_err(|error| ProtoError::Semver { - version: dir_name, - error: Box::new(error), - })?; + let version = + VersionSpec::parse(&dir_name).map_err(|error| ProtoError::VersionSpec { + version: dir_name, + error: Box::new(error), + })?; if !tool.inventory.manifest.versions.contains_key(&version) { debug!( diff --git a/crates/cli/src/commands/outdated.rs b/crates/cli/src/commands/outdated.rs index 6a57f50fb..2f96979f3 100644 --- a/crates/cli/src/commands/outdated.rs +++ b/crates/cli/src/commands/outdated.rs @@ -56,7 +56,10 @@ pub struct OutdatedItem { fn get_in_major_range(spec: &UnresolvedVersionSpec) -> UnresolvedVersionSpec { match spec { - UnresolvedVersionSpec::Version(version) => UnresolvedVersionSpec::Req( + UnresolvedVersionSpec::Calendar(version) => UnresolvedVersionSpec::Req( + VersionReq::parse(format!("~{}", version.major).as_str()).unwrap(), + ), + UnresolvedVersionSpec::Semantic(version) => UnresolvedVersionSpec::Req( VersionReq::parse(format!("~{}", version.major).as_str()).unwrap(), ), _ => spec.clone(), diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 2b66e4717..face43a0a 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -154,10 +154,10 @@ pub enum ProtoError { #[diagnostic(code(proto::version::invalid))] #[error("Invalid version or requirement {}.", .version.style(Style::Hash))] - Semver { + VersionSpec { version: String, #[source] - error: Box, + error: Box, }, #[diagnostic(code(proto::shim::create_failed))] diff --git a/crates/core/src/tool.rs b/crates/core/src/tool.rs index 7bfd538c2..61202c0b3 100644 --- a/crates/core/src/tool.rs +++ b/crates/core/src/tool.rs @@ -318,8 +318,7 @@ impl Tool { let mut entries = FxHashMap::default(); let mut installed = FxHashSet::default(); - for version in versions { - let key = VersionSpec::Version(version); + for key in versions { let value = manifest.versions.get(&key).cloned().unwrap_or_default(); installed.insert(key.clone()); @@ -415,7 +414,11 @@ impl Tool { // If we have a fully qualified semantic version, // exit early and assume the version is legitimate! // Also canary is a special type that we can simply just use. - if short_circuit && matches!(initial_version, UnresolvedVersionSpec::Version(_)) + if short_circuit + && matches!( + initial_version, + UnresolvedVersionSpec::Calendar(_) | UnresolvedVersionSpec::Semantic(_) + ) || matches!(initial_version, UnresolvedVersionSpec::Canary) { let version = initial_version.to_resolved_spec(); @@ -568,7 +571,7 @@ impl Tool { result.version.unwrap() } else { - UnresolvedVersionSpec::parse(&content).map_err(|error| ProtoError::Semver { + UnresolvedVersionSpec::parse(&content).map_err(|error| ProtoError::VersionSpec { version: content, error: Box::new(error), })? diff --git a/crates/core/src/version_detector.rs b/crates/core/src/version_detector.rs index f853fbe9d..011635340 100644 --- a/crates/core/src/version_detector.rs +++ b/crates/core/src/version_detector.rs @@ -136,7 +136,7 @@ pub async fn detect_version( return Ok( UnresolvedVersionSpec::parse(&session_version).map_err(|error| { - ProtoError::Semver { + ProtoError::VersionSpec { version: session_version, error: Box::new(error), } diff --git a/crates/core/src/version_resolver.rs b/crates/core/src/version_resolver.rs index 8cd81216a..7e3c1a1a7 100644 --- a/crates/core/src/version_resolver.rs +++ b/crates/core/src/version_resolver.rs @@ -1,8 +1,7 @@ use crate::proto_config::ProtoToolConfig; use crate::tool_manifest::ToolManifest; use proto_pdk_api::LoadVersionsOutput; -use rustc_hash::FxHashSet; -use semver::{Version, VersionReq}; +use semver::VersionReq; use std::collections::BTreeMap; use tracing::trace; use version_spec::*; @@ -10,7 +9,7 @@ use version_spec::*; #[derive(Default)] pub struct VersionResolver<'tool> { pub aliases: BTreeMap, - pub versions: Vec, + pub versions: Vec, manifest: Option<&'tool ToolManifest>, config: Option<&'tool ProtoToolConfig>, @@ -21,26 +20,21 @@ impl<'tool> VersionResolver<'tool> { let mut resolver = Self::default(); resolver.versions.extend(output.versions); - for (alias, version) in output.aliases { - resolver - .aliases - .insert(alias, UnresolvedVersionSpec::Version(version)); + for (alias, spec) in output.aliases { + resolver.aliases.insert(alias, spec); } if let Some(latest) = output.latest { - resolver - .aliases - .insert("latest".into(), UnresolvedVersionSpec::Version(latest)); + resolver.aliases.insert("latest".into(), latest); } // Sort from newest to oldest resolver.versions.sort_by(|a, d| d.cmp(a)); if !resolver.aliases.contains_key("latest") && !resolver.versions.is_empty() { - resolver.aliases.insert( - "latest".into(), - UnresolvedVersionSpec::Version(resolver.versions[0].clone()), - ); + resolver + .aliases + .insert("latest".into(), resolver.versions[0].to_unresolved_spec()); } resolver @@ -72,41 +66,32 @@ impl<'tool> VersionResolver<'tool> { } } -pub fn match_highest_version(req: &VersionReq, versions: &[&Version]) -> Option { - let mut highest_match: Option = None; +pub fn match_highest_version(req: &VersionReq, specs: &[&VersionSpec]) -> Option { + let mut highest_match: Option = None; - for version in versions { - if req.matches(version) - && (highest_match.is_none() || highest_match.as_ref().is_some_and(|v| *version > v)) - { - highest_match = Some((*version).clone()); + for spec in specs { + if let Some(version) = spec.as_version() { + if req.matches(version) + && (highest_match.is_none() || highest_match.as_ref().is_some_and(|v| *spec > v)) + { + highest_match = Some((*spec).clone()); + } } } - highest_match.map(VersionSpec::Version) -} - -// Filter out aliases because they cannot be matched against -fn extract_installed_versions(installed: &FxHashSet) -> Vec<&Version> { - installed - .iter() - .filter_map(|item| match item { - VersionSpec::Version(v) => Some(v), - _ => None, - }) - .collect() + highest_match } pub fn resolve_version( candidate: &UnresolvedVersionSpec, - versions: &[Version], + versions: &[VersionSpec], aliases: &BTreeMap, manifest: Option<&ToolManifest>, config: Option<&ProtoToolConfig>, ) -> Option { let remote_versions = versions.iter().collect::>(); let installed_versions = if let Some(manifest) = manifest { - extract_installed_versions(&manifest.installed_versions) + Vec::from_iter(&manifest.installed_versions) } else { vec![] }; @@ -226,8 +211,11 @@ pub fn resolve_version( "No match for range, trying others", ); } - UnresolvedVersionSpec::Version(ver) => { - let version_string = ver.to_string(); + // Calendar + // Semantic + _ => { + let version_string = candidate.to_string(); + let resolved_spec = candidate.to_resolved_spec(); trace!( version = &version_string, @@ -235,24 +223,24 @@ pub fn resolve_version( ); // Check locally installed versions first - if installed_versions.contains(&ver) { + if installed_versions.contains(&&resolved_spec) { trace!( version = &version_string, "Resolved to locally installed version" ); - return Some(VersionSpec::Version(ver.to_owned())); + return Some(resolved_spec); } // Otherwise we'll need to download from remote for version in versions { - if ver == version { + if &resolved_spec == version { trace!( version = &version_string, "Resolved to remote available version" ); - return Some(VersionSpec::Version(ver.to_owned())); + return Some(resolved_spec); } } diff --git a/crates/core/tests/version_resolver_test.rs b/crates/core/tests/version_resolver_test.rs index cf01a7b17..67347d501 100644 --- a/crates/core/tests/version_resolver_test.rs +++ b/crates/core/tests/version_resolver_test.rs @@ -1,5 +1,5 @@ use proto_core::{ - resolve_version, ProtoToolConfig, ToolManifest, UnresolvedVersionSpec, VersionSpec, + resolve_version, ProtoToolConfig, SemVer, ToolManifest, UnresolvedVersionSpec, VersionSpec, }; use semver::Version; use std::collections::BTreeMap; @@ -7,17 +7,17 @@ use std::collections::BTreeMap; mod version_resolver { use super::*; - fn create_versions() -> Vec { + fn create_versions() -> Vec { vec![ - Version::new(1, 0, 0), - Version::new(1, 2, 3), - Version::new(1, 1, 1), - Version::new(1, 5, 9), - Version::new(1, 10, 5), - Version::new(4, 5, 6), - Version::new(7, 8, 9), - Version::new(8, 0, 0), - Version::new(10, 0, 0), + VersionSpec::Semantic(SemVer(Version::new(1, 0, 0))), + VersionSpec::Semantic(SemVer(Version::new(1, 2, 3))), + VersionSpec::Semantic(SemVer(Version::new(1, 1, 1))), + VersionSpec::Semantic(SemVer(Version::new(1, 5, 9))), + VersionSpec::Semantic(SemVer(Version::new(1, 10, 5))), + VersionSpec::Semantic(SemVer(Version::new(4, 5, 6))), + VersionSpec::Semantic(SemVer(Version::new(7, 8, 9))), + VersionSpec::Semantic(SemVer(Version::new(8, 0, 0))), + VersionSpec::Semantic(SemVer(Version::new(10, 0, 0))), ] } @@ -25,7 +25,7 @@ mod version_resolver { BTreeMap::from_iter([ ( "latest".into(), - UnresolvedVersionSpec::Version(Version::new(10, 0, 0)), + UnresolvedVersionSpec::Semantic(SemVer(Version::new(10, 0, 0))), ), ( "stable".into(), @@ -33,7 +33,7 @@ mod version_resolver { ), ( "no-version".into(), - UnresolvedVersionSpec::Version(Version::new(20, 0, 0)), + UnresolvedVersionSpec::Semantic(SemVer(Version::new(20, 0, 0))), ), ( "no-alias".into(), @@ -60,7 +60,7 @@ mod version_resolver { config.aliases.insert( "latest-manifest".into(), - UnresolvedVersionSpec::Version(Version::new(8, 0, 0)), + UnresolvedVersionSpec::Semantic(SemVer(Version::new(8, 0, 0))), ); config.aliases.insert( "stable-manifest".into(), @@ -187,7 +187,7 @@ mod version_resolver { assert_eq!( resolve_version( - &UnresolvedVersionSpec::Version(Version::new(1, 10, 5)), + &UnresolvedVersionSpec::Semantic(SemVer(Version::new(1, 10, 5))), &versions, &aliases, None, @@ -199,7 +199,7 @@ mod version_resolver { assert_eq!( resolve_version( - &UnresolvedVersionSpec::Version(Version::new(8, 0, 0)), + &UnresolvedVersionSpec::Semantic(SemVer(Version::new(8, 0, 0))), &versions, &aliases, None, @@ -219,7 +219,7 @@ mod version_resolver { assert_eq!( resolve_version( - &UnresolvedVersionSpec::Version(Version::new(3, 0, 0)), + &UnresolvedVersionSpec::Semantic(SemVer(Version::new(3, 0, 0))), &versions, &aliases, Some(&manifest), @@ -341,7 +341,7 @@ mod version_resolver { let aliases = create_aliases(); resolve_version( - &UnresolvedVersionSpec::Version(Version::new(20, 0, 0)), + &UnresolvedVersionSpec::Semantic(SemVer(Version::new(20, 0, 0))), &versions, &aliases, None, diff --git a/crates/core/tests/version_test.rs b/crates/core/tests/version_test.rs deleted file mode 100644 index 2ff7cad0c..000000000 --- a/crates/core/tests/version_test.rs +++ /dev/null @@ -1,116 +0,0 @@ -use proto_core::UnresolvedVersionSpec; -use semver::{Version, VersionReq}; -use std::str::FromStr; - -mod version_type { - use super::*; - - #[test] - fn parses_alias() { - assert_eq!( - UnresolvedVersionSpec::from_str("stable").unwrap(), - UnresolvedVersionSpec::Alias("stable".to_owned()) - ); - assert_eq!( - UnresolvedVersionSpec::from_str("latest").unwrap(), - UnresolvedVersionSpec::Alias("latest".to_owned()) - ); - assert_eq!( - UnresolvedVersionSpec::from_str("lts-2014").unwrap(), - UnresolvedVersionSpec::Alias("lts-2014".to_owned()) - ); - } - - #[test] - fn parses_req() { - for req in ["=1.2.3", "^1.2", "~1", ">1.2.0", "<1", "*", ">1, <=1.5"] { - assert_eq!( - UnresolvedVersionSpec::from_str(req).unwrap(), - UnresolvedVersionSpec::Req(VersionReq::parse(req).unwrap()) - ); - } - } - - #[test] - fn parses_req_spaces() { - assert_eq!( - UnresolvedVersionSpec::from_str("> 10").unwrap(), - UnresolvedVersionSpec::Req(VersionReq::parse(">10").unwrap()) - ); - assert_eq!( - UnresolvedVersionSpec::from_str("1.2 , 2").unwrap(), - UnresolvedVersionSpec::Req(VersionReq::parse("1.2, 2").unwrap()) - ); - assert_eq!( - UnresolvedVersionSpec::from_str(">= 1.2 < 2").unwrap(), - UnresolvedVersionSpec::Req(VersionReq::parse(">=1.2, <2").unwrap()) - ); - } - - #[test] - fn parses_req_any() { - assert_eq!( - UnresolvedVersionSpec::from_str("^1 || ~2 || =3").unwrap(), - UnresolvedVersionSpec::ReqAny(vec![ - VersionReq::parse("~2").unwrap(), - VersionReq::parse("^1").unwrap(), - VersionReq::parse("=3").unwrap(), - ]) - ); - } - - #[test] - fn sorts_any_req() { - assert_eq!( - UnresolvedVersionSpec::from_str("^1 || ^2 || ^3").unwrap(), - UnresolvedVersionSpec::ReqAny(vec![ - VersionReq::parse("^3").unwrap(), - VersionReq::parse("^2").unwrap(), - VersionReq::parse("^1").unwrap(), - ]) - ); - assert_eq!( - UnresolvedVersionSpec::from_str("^1.1 || ^1.10 || ^1.10.1 || ^1.2").unwrap(), - UnresolvedVersionSpec::ReqAny(vec![ - VersionReq::parse("^1.10.1").unwrap(), - VersionReq::parse("^1.10").unwrap(), - VersionReq::parse("^1.2").unwrap(), - VersionReq::parse("^1.1").unwrap(), - ]) - ); - } - - #[test] - fn parses_version() { - for req in ["1.2.3", "4.5.6", "7.8.9-alpha", "10.11.12+build"] { - assert_eq!( - UnresolvedVersionSpec::from_str(req).unwrap(), - UnresolvedVersionSpec::Version(Version::parse(req).unwrap()) - ); - } - } - - #[test] - fn parses_version_with_v() { - assert_eq!( - UnresolvedVersionSpec::from_str("v1.2.3").unwrap(), - UnresolvedVersionSpec::Version(Version::parse("1.2.3").unwrap()) - ); - } - - #[test] - fn no_patch_becomes_req() { - assert_eq!( - UnresolvedVersionSpec::from_str("1.2").unwrap(), - UnresolvedVersionSpec::Req(VersionReq::parse("~1.2").unwrap()) - ); - } - - #[test] - fn no_minor_becomes_req() { - assert_eq!( - UnresolvedVersionSpec::from_str("1").unwrap(), - UnresolvedVersionSpec::Req(VersionReq::parse("~1").unwrap()) - ); - } -} diff --git a/crates/pdk-api/src/api/mod.rs b/crates/pdk-api/src/api/mod.rs index 00bce39a7..a320a8a80 100644 --- a/crates/pdk-api/src/api/mod.rs +++ b/crates/pdk-api/src/api/mod.rs @@ -3,7 +3,7 @@ mod build_source; use crate::shapes::StringOrVec; use rustc_hash::FxHashMap; use std::path::PathBuf; -use version_spec::{UnresolvedVersionSpec, VersionSpec}; +use version_spec::{CalVer, SemVer, SpecError, UnresolvedVersionSpec, VersionSpec}; use warpgate_api::*; pub use build_source::*; @@ -380,51 +380,63 @@ api_struct!( pub struct LoadVersionsOutput { /// Latest canary version. #[serde(skip_serializing_if = "Option::is_none")] - pub canary: Option, + pub canary: Option, /// Latest stable version. #[serde(skip_serializing_if = "Option::is_none")] - pub latest: Option, + pub latest: Option, /// Mapping of aliases (channels, etc) to a version. #[serde(skip_serializing_if = "FxHashMap::is_empty")] - pub aliases: FxHashMap, + pub aliases: FxHashMap, /// List of available production versions to install. #[serde(skip_serializing_if = "Vec::is_empty")] - pub versions: Vec, + pub versions: Vec, } ); impl LoadVersionsOutput { /// Create the output from a list of strings that'll be parsed as versions. /// The latest version will be the highest version number. - pub fn from(values: Vec) -> Result { + pub fn from(values: Vec) -> Result { let mut versions = vec![]; for value in values { - versions.push(Version::parse(&value)?); + versions.push(VersionSpec::parse(&value)?); } Ok(Self::from_versions(versions)) } - /// Create the output from a list of versions. + /// Create the output from a list of version specifications. /// The latest version will be the highest version number. - pub fn from_versions(versions: Vec) -> Self { + pub fn from_versions(versions: Vec) -> Self { let mut output = LoadVersionsOutput::default(); let mut latest = Version::new(0, 0, 0); + let mut calver = false; for version in versions { - if version.pre.is_empty() && version.build.is_empty() && version > latest { - latest = version.clone(); + if let Some(inner) = version.as_version() { + if inner.pre.is_empty() && inner.build.is_empty() && inner > &latest { + inner.clone_into(&mut latest); + calver = matches!(version, VersionSpec::Calendar(_)); + } } output.versions.push(version); } - output.latest = Some(latest.clone()); - output.aliases.insert("latest".into(), latest); + output.latest = Some(if calver { + UnresolvedVersionSpec::Calendar(CalVer(latest)) + } else { + UnresolvedVersionSpec::Semantic(SemVer(latest)) + }); + + output + .aliases + .insert("latest".into(), output.latest.clone().unwrap()); + output } } @@ -469,7 +481,7 @@ api_struct!( /// List of versions that are currently installed. Will replace /// what is currently in the manifest. #[serde(skip_serializing_if = "Option::is_none")] - pub versions: Option>, + pub versions: Option>, /// Whether to skip the syncing process or not. pub skip_sync: bool, diff --git a/crates/version-spec/Cargo.toml b/crates/version-spec/Cargo.toml index 0e379bcf0..dbe476ec3 100644 --- a/crates/version-spec/Cargo.toml +++ b/crates/version-spec/Cargo.toml @@ -3,7 +3,7 @@ name = "version_spec" version = "0.5.2" edition = "2021" license = "MIT" -description = "A specification for working with partial, full, or aliased versions." +description = "A specification for working with partial, full, or aliased versions. Supports semver and calver." homepage = "https://moonrepo.dev/proto" repository = "https://github.com/moonrepo/proto" @@ -13,6 +13,7 @@ regex = { workspace = true } schematic = { workspace = true, optional = true, features = ["schema"] } semver = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } [features] default = [] diff --git a/crates/version-spec/README.md b/crates/version-spec/README.md index 7dddad9ee..538c56960 100644 --- a/crates/version-spec/README.md +++ b/crates/version-spec/README.md @@ -1,5 +1,8 @@ # version_spec -![Crates.io](https://img.shields.io/crates/v/version_spec) ![Crates.io](https://img.shields.io/crates/d/version_spec) +![Crates.io](https://img.shields.io/crates/v/version_spec) +![Crates.io](https://img.shields.io/crates/d/version_spec) -Enums and utilities for working with partial, full, and aliased semantic versions, known as a version specification. It primarily handles the states of an unresoled version candidate (requirement, range, alias, partial, etc) to a resolved version (version, alias). +Enums and utilities for working with partial, full, and aliased versions, known as a version specification. It primarily handles the states of an unresoled version candidate (requirement, range, alias, partial, etc) to a resolved version (version, alias). + +Supports both semantic versions (semver) and calendar versions (calver). diff --git a/crates/version-spec/src/lib.rs b/crates/version-spec/src/lib.rs index 6de3d3a13..b02e69a48 100644 --- a/crates/version-spec/src/lib.rs +++ b/crates/version-spec/src/lib.rs @@ -1,8 +1,17 @@ mod resolved_spec; +mod spec_error; +mod unresolved_parser; mod unresolved_spec; +mod version_types; pub use resolved_spec::*; +pub use spec_error::*; +pub use unresolved_parser::*; pub use unresolved_spec::*; +pub use version_types::*; + +use regex::Regex; +use std::sync::OnceLock; /// Returns true if the provided value is an alias. An alias is a word that /// maps to version, for example, "latest" -> "1.2.3". @@ -26,96 +35,60 @@ pub fn is_alias_name>(value: T) -> bool { }) } -/// Cleans a potential version string by removing a leading `v` or `V`, -/// removing each occurence of `.*`, and removing invalid spaces. -pub fn clean_version_string>(value: T) -> String { - let value = value.as_ref().trim(); +/// Returns true if the provided value is a calendar version string. +pub fn is_calver>(value: T) -> bool { + get_calver_regex().is_match(value.as_ref()) +} - if value.contains("||") { - return value - .split("||") - .map(clean_version_string) - .collect::>() - .join(" || "); - } +/// Returns true if the provided value is a semantic version string. +pub fn is_semver>(value: T) -> bool { + get_semver_regex().is_match(value.as_ref()) +} - let mut version = value.replace(".*", "").replace("&&", ","); +/// Cleans a potential version string by removing a leading `v` or `V`. +pub fn clean_version_string>(value: T) -> String { + let mut version = value.as_ref().trim(); - // Remove a leading "v" or "V" from a version string. + // Remove a leading "v" or "V" from a version string #[allow(clippy::assigning_clones)] - if version.starts_with('v') || version.starts_with('V') { - version = version[1..].to_owned(); + if (version.starts_with('v') || version.starts_with('V')) + && version.as_bytes()[1].is_ascii_digit() + { + version = &version[1..]; } - // Remove invalid space after <, <=, >, >=. - let version = regex::Regex::new(r"([><]=?)[ ]*v?([0-9])") - .unwrap() - .replace_all(&version, "$1$2"); - - // Replace spaces with commas - regex::Regex::new("[, ]+") - .unwrap() - .replace_all(&version, ",") - .to_string() + version.to_owned() } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn checks_alias() { - assert!(is_alias_name("foo")); - assert!(is_alias_name("foo.bar")); - assert!(is_alias_name("foo/bar")); - assert!(is_alias_name("foo-bar")); - assert!(is_alias_name("foo_bar-baz")); - assert!(is_alias_name("alpha.1")); - assert!(is_alias_name("beta-0")); - assert!(is_alias_name("rc-1.2.3")); - assert!(is_alias_name("next-2023")); - - assert!(!is_alias_name("1.2.3")); - assert!(!is_alias_name("1.2")); - assert!(!is_alias_name("1")); - assert!(!is_alias_name("1-3")); - } - - #[test] - fn cleans_string() { - assert_eq!(clean_version_string("v1.2.3"), "1.2.3"); - assert_eq!(clean_version_string("V1.2.3"), "1.2.3"); - - assert_eq!(clean_version_string("1.2.*"), "1.2"); - assert_eq!(clean_version_string("1.*.*"), "1"); - assert_eq!(clean_version_string("*"), "*"); +/// Cleans a version requirement string by removing * version parts, +/// and correcting AND operators. +pub fn clean_version_req_string>(value: T) -> String { + value + .as_ref() + .trim() + .replace(".*", "") + .replace("-*", "") + .replace("&&", ",") +} - assert_eq!(clean_version_string(">= 1.2.3"), ">=1.2.3"); - assert_eq!(clean_version_string("> 1.2.3"), ">1.2.3"); - assert_eq!(clean_version_string("<1.2.3"), "<1.2.3"); - assert_eq!(clean_version_string("<= 1.2.3"), "<=1.2.3"); +static CALVER_REGEX: OnceLock = OnceLock::new(); - assert_eq!(clean_version_string(">= v1.2.3"), ">=1.2.3"); - assert_eq!(clean_version_string("> v1.2.3"), ">1.2.3"); - assert_eq!(clean_version_string(" &'static Regex { + CALVER_REGEX.get_or_init(|| { + Regex::new(r"^(?[0-9]{1,4})-(?((0?[1-9]{1})|10|11|12))(-(?(0?[1-9]{1}|[1-3]{1}[0-9]{1})))?((_|\.)(?[0-9]+))?(?
-[a-zA-Z]{1}[-0-9a-zA-Z.]+)?$").unwrap()
+    })
+}
 
-        assert_eq!(clean_version_string("1.2, 3"), "1.2,3");
-        assert_eq!(clean_version_string("1,3, 4"), "1,3,4");
-        assert_eq!(clean_version_string("1 2"), "1,2");
-        assert_eq!(clean_version_string("1 && 2"), "1,2");
-    }
+static SEMVER_REGEX: OnceLock = OnceLock::new();
 
-    #[test]
-    fn handles_commas() {
-        assert_eq!(clean_version_string("1 2"), "1,2");
-        assert_eq!(clean_version_string("1  2"), "1,2");
-        assert_eq!(clean_version_string("1   2"), "1,2");
-        assert_eq!(clean_version_string("1,2"), "1,2");
-        assert_eq!(clean_version_string("1 ,2"), "1,2");
-        assert_eq!(clean_version_string("1, 2"), "1,2");
-        assert_eq!(clean_version_string("1 , 2"), "1,2");
-        assert_eq!(clean_version_string("1  , 2"), "1,2");
-        assert_eq!(clean_version_string("1,  2"), "1,2");
-    }
+/// Get a regex pattern that matches semantic versions (semver).
+/// For example: 1.2.3, 6.5.4, 7.8.9-alpha, etc.
+pub fn get_semver_regex() -> &'static Regex {
+    // https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
+    SEMVER_REGEX.get_or_init(|| {
+        Regex::new(r"^(?[0-9]+).(?[0-9]+).(?[0-9]+)(?
-[-0-9a-zA-Z.]+)?(?\+[-0-9a-zA-Z.]+)?$",)
+        .unwrap()
+    })
 }
diff --git a/crates/version-spec/src/resolved_spec.rs b/crates/version-spec/src/resolved_spec.rs
index e4978cdc5..d0579c5b5 100644
--- a/crates/version-spec/src/resolved_spec.rs
+++ b/crates/version-spec/src/resolved_spec.rs
@@ -1,9 +1,11 @@
 #![allow(clippy::from_over_into)]
 
-use crate::{clean_version_string, is_alias_name, UnresolvedVersionSpec};
-use semver::{Error, Version};
+use crate::spec_error::SpecError;
+use crate::{clean_version_string, is_alias_name, is_calver, UnresolvedVersionSpec};
+use crate::{is_semver, version_types::*};
+use semver::Version;
 use serde::{Deserialize, Serialize};
-use std::fmt::{Debug, Display};
+use std::fmt;
 use std::str::FromStr;
 
 /// Represents a resolved version or alias.
@@ -14,8 +16,10 @@ pub enum VersionSpec {
     Canary,
     /// An alias that is used as a map to a version.
     Alias(String),
+    /// A fully-qualified calendar version.
+    Calendar(CalVer),
     /// A fully-qualified semantic version.
-    Version(Version),
+    Semantic(SemVer),
 }
 
 impl VersionSpec {
@@ -25,10 +29,19 @@ impl VersionSpec {
     /// - If the value "canary", map as `Canary` variant.
     /// - If an alpha-numeric value that starts with a character, map as `Alias`.
     /// - Else parse with [`Version`], and map as `Version`.
-    pub fn parse>(value: T) -> Result {
+    pub fn parse>(value: T) -> Result {
         Self::from_str(value.as_ref())
     }
 
+    /// Return the specification as a resolved [`Version`].
+    pub fn as_version(&self) -> Option<&Version> {
+        match self {
+            Self::Calendar(inner) => Some(&inner.0),
+            Self::Semantic(inner) => Some(&inner.0),
+            _ => None,
+        }
+    }
+
     /// Return true if the provided alias matches the current specification.
     pub fn is_alias>(&self, name: A) -> bool {
         match self {
@@ -59,7 +72,8 @@ impl VersionSpec {
         match self {
             Self::Canary => UnresolvedVersionSpec::Canary,
             Self::Alias(alias) => UnresolvedVersionSpec::Alias(alias.to_owned()),
-            Self::Version(version) => UnresolvedVersionSpec::Version(version.to_owned()),
+            Self::Calendar(version) => UnresolvedVersionSpec::Calendar(version.to_owned()),
+            Self::Semantic(version) => UnresolvedVersionSpec::Semantic(version.to_owned()),
         }
     }
 }
@@ -84,7 +98,7 @@ impl Default for VersionSpec {
 }
 
 impl FromStr for VersionSpec {
-    type Err = Error;
+    type Err = SpecError;
 
     fn from_str(value: &str) -> Result {
         if value == "canary" {
@@ -97,12 +111,20 @@ impl FromStr for VersionSpec {
             return Ok(VersionSpec::Alias(value));
         }
 
-        Ok(VersionSpec::Version(Version::parse(&value)?))
+        if is_calver(&value) {
+            return Ok(VersionSpec::Calendar(CalVer::parse(&value)?));
+        }
+
+        if is_semver(&value) {
+            return Ok(VersionSpec::Semantic(SemVer::parse(&value)?));
+        }
+
+        Err(SpecError::ResolvedUnknownFormat(value.to_owned()))
     }
 }
 
 impl TryFrom for VersionSpec {
-    type Error = Error;
+    type Error = SpecError;
 
     fn try_from(value: String) -> Result {
         Self::from_str(&value)
@@ -115,19 +137,20 @@ impl Into for VersionSpec {
     }
 }
 
-impl Debug for VersionSpec {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl fmt::Debug for VersionSpec {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         // Debug version as a string instead of a struct
         write!(f, "{}", self)
     }
 }
 
-impl Display for VersionSpec {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl fmt::Display for VersionSpec {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             Self::Canary => write!(f, "canary"),
             Self::Alias(alias) => write!(f, "{}", alias),
-            Self::Version(version) => write!(f, "{}", version),
+            Self::Calendar(version) => write!(f, "{}", version),
+            Self::Semantic(version) => write!(f, "{}", version),
         }
     }
 }
@@ -137,15 +160,16 @@ impl PartialEq<&str> for VersionSpec {
         match self {
             Self::Canary => "canary" == *other,
             Self::Alias(alias) => alias == other,
-            Self::Version(version) => version.to_string() == *other,
+            _ => &self.to_string() == other,
         }
     }
 }
 
-impl PartialEq for VersionSpec {
-    fn eq(&self, other: &Version) -> bool {
+impl PartialEq for VersionSpec {
+    fn eq(&self, other: &semver::Version) -> bool {
         match self {
-            Self::Version(version) => version == other,
+            Self::Calendar(version) => &version.0 == other,
+            Self::Semantic(version) => &version.0 == other,
             _ => false,
         }
     }
diff --git a/crates/version-spec/src/spec_error.rs b/crates/version-spec/src/spec_error.rs
new file mode 100644
index 000000000..191b5e594
--- /dev/null
+++ b/crates/version-spec/src/spec_error.rs
@@ -0,0 +1,20 @@
+#[derive(thiserror::Error, Debug)]
+pub enum SpecError {
+    #[error("Invalid calver (calendar version) format.")]
+    CalverInvalidFormat,
+
+    #[error("Unknown version format `{0}`. Must be a semantic or calendar based format.")]
+    ResolvedUnknownFormat(String),
+
+    #[error("Requirement operator found in an invalid position")]
+    ParseInvalidReq,
+
+    #[error("Unknown character `{0}` in version string!")]
+    ParseUnknownChar(char),
+
+    #[error("Missing major number for semantic versions, or year for calendar versions.")]
+    ParseMissingMajorPart,
+
+    #[error(transparent)]
+    Semver(#[from] semver::Error),
+}
diff --git a/crates/version-spec/src/unresolved_parser.rs b/crates/version-spec/src/unresolved_parser.rs
new file mode 100644
index 000000000..d1a192c60
--- /dev/null
+++ b/crates/version-spec/src/unresolved_parser.rs
@@ -0,0 +1,364 @@
+use crate::spec_error::SpecError;
+use human_sort::compare;
+
+#[derive(Debug, Default, PartialEq)]
+pub enum ParseKind {
+    #[default]
+    Unknown,
+    Req,
+    Cal,
+    Sem,
+}
+
+#[derive(Debug, Default, PartialEq)]
+pub enum ParsePart {
+    #[default]
+    Start,
+    ReqPrefix,
+    MajorYear,
+    MinorMonth,
+    PatchDay,
+    PreId,
+    BuildSuffix,
+}
+
+impl ParsePart {
+    pub fn is_prefix(&self) -> bool {
+        matches!(self, Self::Start | Self::ReqPrefix)
+    }
+
+    pub fn is_suffix(&self) -> bool {
+        matches!(self, Self::PreId | Self::BuildSuffix)
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct UnresolvedParser {
+    // States
+    kind: ParseKind,
+    in_part: ParsePart,
+    is_and: bool,
+    req_op: String,
+    major_year: String,
+    minor_month: String,
+    patch_day: String,
+    pre_id: String,
+    build_id: String,
+
+    // Final result
+    results: Vec,
+}
+
+impl UnresolvedParser {
+    pub fn parse(mut self, input: impl AsRef) -> Result<(String, ParseKind), SpecError> {
+        let input = input.as_ref().trim();
+
+        if input.is_empty() || input == "*" {
+            return Ok(("*".to_owned(), ParseKind::Req));
+        }
+
+        for ch in input.chars() {
+            match ch {
+                // Requirement operator
+                '=' | '~' | '^' | '>' | '<' => {
+                    if self.in_part != ParsePart::Start && self.in_part != ParsePart::ReqPrefix {
+                        return Err(SpecError::ParseInvalidReq);
+                    }
+
+                    self.in_part = ParsePart::ReqPrefix;
+                    self.req_op.push(ch);
+                }
+                // Wildcard operator
+                '*' => {
+                    // Ignore entirely
+                }
+                // Version part
+                '0'..='9' => {
+                    let part_str = match self.in_part {
+                        ParsePart::Start | ParsePart::ReqPrefix | ParsePart::MajorYear => {
+                            self.in_part = ParsePart::MajorYear;
+                            &mut self.major_year
+                        }
+                        ParsePart::MinorMonth => &mut self.minor_month,
+                        ParsePart::PatchDay => &mut self.patch_day,
+                        ParsePart::PreId => &mut self.pre_id,
+                        ParsePart::BuildSuffix => &mut self.build_id,
+                    };
+
+                    part_str.push(ch);
+                }
+                // Suffix part
+                'a'..='z' | 'A'..='Z' => match self.in_part {
+                    ParsePart::PreId => {
+                        self.pre_id.push(ch);
+                    }
+                    ParsePart::BuildSuffix => {
+                        self.build_id.push(ch);
+                    }
+                    _ => {
+                        // Remove leading v
+                        if ch == 'v' || ch == 'V' {
+                            continue;
+                        } else {
+                            unreachable!()
+                        }
+                    }
+                },
+                // Part separator
+                '.' | '-' => {
+                    // Determine version type based on separator
+                    if self.kind == ParseKind::Unknown {
+                        if ch == '-' {
+                            self.kind = ParseKind::Cal;
+                        } else {
+                            self.kind = ParseKind::Sem;
+                        }
+                    }
+
+                    // Continue to the next part
+                    if ch == '-' {
+                        if self.kind == ParseKind::Sem {
+                            match self.in_part {
+                                ParsePart::MajorYear
+                                | ParsePart::MinorMonth
+                                | ParsePart::PatchDay => {
+                                    self.in_part = ParsePart::PreId;
+                                }
+                                ParsePart::PreId => {
+                                    self.pre_id.push('-');
+                                }
+                                ParsePart::BuildSuffix => {
+                                    self.build_id.push('-');
+                                }
+                                _ => unreachable!(),
+                            };
+                        } else if self.kind == ParseKind::Cal {
+                            match self.in_part {
+                                ParsePart::MajorYear => {
+                                    self.in_part = ParsePart::MinorMonth;
+                                }
+                                ParsePart::MinorMonth => {
+                                    self.in_part = ParsePart::PatchDay;
+                                }
+                                ParsePart::PatchDay | ParsePart::BuildSuffix => {
+                                    self.in_part = ParsePart::PreId;
+                                }
+                                ParsePart::PreId => {
+                                    self.pre_id.push('-');
+                                }
+                                _ => unreachable!(),
+                            };
+                        }
+                    } else if ch == '.' {
+                        if self.kind == ParseKind::Sem {
+                            match self.in_part {
+                                ParsePart::MajorYear => {
+                                    self.in_part = ParsePart::MinorMonth;
+                                }
+                                ParsePart::MinorMonth => {
+                                    self.in_part = ParsePart::PatchDay;
+                                }
+                                ParsePart::PatchDay => {
+                                    self.in_part = ParsePart::PreId;
+                                }
+                                ParsePart::PreId => {
+                                    self.pre_id.push('.');
+                                }
+                                ParsePart::BuildSuffix => {
+                                    self.build_id.push('.');
+                                }
+                                _ => unreachable!(),
+                            };
+                        } else if self.kind == ParseKind::Cal {
+                            match self.in_part {
+                                ParsePart::MajorYear
+                                | ParsePart::MinorMonth
+                                | ParsePart::PatchDay => {
+                                    self.in_part = ParsePart::BuildSuffix;
+                                }
+                                ParsePart::PreId => {
+                                    self.pre_id.push('.');
+                                }
+                                ParsePart::BuildSuffix => {
+                                    self.build_id.push('.');
+                                }
+                                _ => unreachable!(),
+                            };
+                        }
+                    }
+                }
+                // Build separator
+                '_' | '+' => {
+                    if ch == '+' {
+                        if self.kind == ParseKind::Sem {
+                            self.in_part = ParsePart::BuildSuffix;
+                        } else {
+                            unreachable!();
+                        }
+                    } else if self.kind == ParseKind::Cal {
+                        self.in_part = ParsePart::BuildSuffix;
+                    } else {
+                        unreachable!();
+                    }
+                }
+                // AND separator
+                ',' => {
+                    self.is_and = true;
+                    self.build_result()?;
+                    self.reset_state();
+                }
+                // Whitespace
+                ' ' => {
+                    if self.in_part.is_prefix() {
+                        // Skip
+                    } else {
+                        // Possible AND sequence?
+                        self.is_and = true;
+                        self.build_result()?;
+                        self.reset_state();
+                    }
+                }
+                _ => {
+                    return Err(SpecError::ParseUnknownChar(ch));
+                }
+            }
+        }
+
+        self.build_result()?;
+
+        let result = self.get_result();
+        let is_req = result.contains(',');
+
+        Ok((result, if is_req { ParseKind::Req } else { self.kind }))
+    }
+
+    fn get_result(&self) -> String {
+        self.results.join(",")
+    }
+
+    fn get_part<'p>(&self, value: &'p str) -> &'p str {
+        let value = value.trim_start_matches('0');
+
+        if value.is_empty() {
+            return "0";
+        }
+
+        value
+    }
+
+    fn build_result(&mut self) -> Result<(), SpecError> {
+        if self.in_part.is_prefix() {
+            return Ok(());
+        }
+
+        let mut output = String::new();
+        let was_calver = self.kind == ParseKind::Cal;
+
+        if self.req_op.is_empty() {
+            if self.minor_month.is_empty() || self.patch_day.is_empty() {
+                self.kind = ParseKind::Req;
+
+                if !self.is_and {
+                    output.push('~');
+                }
+            }
+        } else {
+            self.kind = ParseKind::Req;
+            output.push_str(&self.req_op);
+        }
+
+        let separator = if self.kind == ParseKind::Cal && !self.is_and {
+            '-'
+        } else {
+            '.'
+        };
+
+        // Major/year
+        if was_calver {
+            let year = self.get_part(&self.major_year);
+
+            if year.len() < 4 {
+                let mut year: usize = year.parse().unwrap();
+                year += 2000;
+
+                output.push_str(&year.to_string());
+            } else {
+                output.push_str(year);
+            }
+        } else if self.major_year.is_empty() {
+            return Err(SpecError::ParseMissingMajorPart);
+        } else {
+            output.push_str(self.get_part(&self.major_year));
+        }
+
+        // Minor/month
+        if !self.minor_month.is_empty() {
+            output.push(separator);
+            output.push_str(self.get_part(&self.minor_month));
+        }
+
+        // Patch/day
+        if !self.patch_day.is_empty() {
+            output.push(separator);
+            output.push_str(self.get_part(&self.patch_day));
+        }
+
+        // Pre ID
+        if !self.pre_id.is_empty() {
+            output.push('-');
+            output.push_str(&self.pre_id);
+        }
+
+        // Build metadata
+        if !self.build_id.is_empty() {
+            output.push('+');
+            output.push_str(&self.build_id);
+        }
+
+        self.results.push(output);
+
+        Ok(())
+    }
+
+    fn reset_state(&mut self) {
+        self.kind = ParseKind::Unknown;
+        self.in_part = ParsePart::Start;
+        self.req_op.truncate(0);
+        self.major_year.truncate(0);
+        self.minor_month.truncate(0);
+        self.patch_day.truncate(0);
+        self.pre_id.truncate(0);
+        self.build_id.truncate(0);
+    }
+}
+
+/// Parse the provided string as a list of version requirements,
+/// as separated by `||`. Each requirement will be parsed
+/// individually with [`parse`].
+pub fn parse_multi(input: impl AsRef) -> Result, SpecError> {
+    let input = input.as_ref();
+    let mut results = vec![];
+
+    if input.contains("||") {
+        let mut parts = input.split("||").map(|p| p.trim()).collect::>();
+
+        // Try and sort from highest to lowest range
+        parts.sort_by(|a, d| compare(d, a));
+
+        for part in parts {
+            results.push(parse(part)?.0);
+        }
+    } else {
+        results.push(parse(input)?.0);
+    }
+
+    Ok(results)
+}
+
+/// Parse the provided string and determine the output format.
+/// Since an unresolved version can be many things, such as an
+/// alias, version requirement, semver, or calver, we need to
+/// parse this manually to determine the correct output.
+pub fn parse(input: impl AsRef) -> Result<(String, ParseKind), SpecError> {
+    UnresolvedParser::default().parse(input)
+}
diff --git a/crates/version-spec/src/unresolved_spec.rs b/crates/version-spec/src/unresolved_spec.rs
index fec14bbf3..892b47d66 100644
--- a/crates/version-spec/src/unresolved_spec.rs
+++ b/crates/version-spec/src/unresolved_spec.rs
@@ -1,14 +1,16 @@
 #![allow(clippy::from_over_into)]
 
-use crate::{clean_version_string, is_alias_name, VersionSpec};
-use human_sort::compare;
-use semver::{Error, Version, VersionReq};
+use crate::spec_error::SpecError;
+use crate::unresolved_parser::*;
+use crate::version_types::*;
+use crate::{clean_version_req_string, clean_version_string, is_alias_name, VersionSpec};
+use semver::VersionReq;
 use serde::{Deserialize, Serialize};
 use std::fmt::{Debug, Display};
 use std::str::FromStr;
 
 /// Represents an unresolved version or alias that must be resolved
-/// to a fully-qualified and semantic result.
+/// to a fully-qualified version.
 #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
 #[serde(untagged, into = "String", try_from = "String")]
 pub enum UnresolvedVersionSpec {
@@ -20,8 +22,10 @@ pub enum UnresolvedVersionSpec {
     Req(VersionReq),
     /// A list of requirements to match any against (joined by `||`).
     ReqAny(Vec),
+    /// A fully-qualified calendar version.
+    Calendar(CalVer),
     /// A fully-qualified semantic version.
-    Version(Version),
+    Semantic(SemVer),
 }
 
 impl UnresolvedVersionSpec {
@@ -35,8 +39,8 @@ impl UnresolvedVersionSpec {
     /// - If contains `,` or ` ` (space), parse with [`VersionReq`], and map as `Req`.
     /// - If starts with `=`, `^`, `~`, `>`, `<`, or `*`, parse with [`VersionReq`],
     ///   and map as `Req`.
-    /// - Else parse with [`Version`], and map as `Version`.
-    pub fn parse>(value: T) -> Result {
+    /// - Else parse as `Semantic` or `Calendar` types.
+    pub fn parse>(value: T) -> Result {
         Self::from_str(value.as_ref())
     }
 
@@ -75,7 +79,8 @@ impl UnresolvedVersionSpec {
         match self {
             Self::Canary => VersionSpec::Canary,
             Self::Alias(alias) => VersionSpec::Alias(alias.to_owned()),
-            Self::Version(version) => VersionSpec::Version(version.to_owned()),
+            Self::Calendar(version) => VersionSpec::Calendar(version.to_owned()),
+            Self::Semantic(version) => VersionSpec::Semantic(version.to_owned()),
             _ => unreachable!(),
         }
     }
@@ -88,7 +93,7 @@ impl schematic::Schematic for UnresolvedVersionSpec {
     }
 
     fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
-        schema.set_description("Represents an unresolved version or alias that must be resolved to a fully-qualified and semantic result.");
+        schema.set_description("Represents an unresolved version or alias that must be resolved to a fully-qualified version.");
         schema.string_default()
     }
 }
@@ -101,7 +106,7 @@ impl Default for UnresolvedVersionSpec {
 }
 
 impl FromStr for UnresolvedVersionSpec {
-    type Err = Error;
+    type Err = SpecError;
 
     fn from_str(value: &str) -> Result {
         if value == "canary" {
@@ -114,46 +119,32 @@ impl FromStr for UnresolvedVersionSpec {
             return Ok(UnresolvedVersionSpec::Alias(value));
         }
 
-        // OR requirements (Node.js)
-        if value.contains("||") {
-            let mut any = vec![];
-            let mut parts = value.split("||").map(|p| p.trim()).collect::>();
+        let value = clean_version_req_string(&value);
 
-            // Try and sort from highest to lowest range
-            parts.sort_by(|a, d| compare(d, a));
+        // OR requirements
+        if value.contains("||") {
+            let mut reqs = vec![];
 
-            for req in parts {
-                any.push(VersionReq::parse(req)?);
+            for result in parse_multi(&value)? {
+                reqs.push(VersionReq::parse(&result)?);
             }
 
-            return Ok(UnresolvedVersionSpec::ReqAny(any));
+            return Ok(UnresolvedVersionSpec::ReqAny(reqs));
         }
 
-        // AND requirements
-        if value.contains(',') {
-            return Ok(UnresolvedVersionSpec::Req(VersionReq::parse(&value)?));
-        }
+        // Version or requirement
+        let (result, kind) = parse(value)?;
 
-        Ok(match value.chars().next().unwrap() {
-            '=' | '^' | '~' | '>' | '<' | '*' => {
-                UnresolvedVersionSpec::Req(VersionReq::parse(&value)?)
-            }
-            _ => {
-                let dot_count = value.match_indices('.').collect::>().len();
-
-                // If not fully qualified, match using a requirement
-                if dot_count < 2 {
-                    UnresolvedVersionSpec::Req(VersionReq::parse(&format!("~{value}"))?)
-                } else {
-                    UnresolvedVersionSpec::Version(Version::parse(&value)?)
-                }
-            }
+        Ok(match kind {
+            ParseKind::Req => UnresolvedVersionSpec::Req(VersionReq::parse(&result)?),
+            ParseKind::Cal => UnresolvedVersionSpec::Calendar(CalVer::parse(&result)?),
+            _ => UnresolvedVersionSpec::Semantic(SemVer::parse(&result)?),
         })
     }
 }
 
 impl TryFrom for UnresolvedVersionSpec {
-    type Error = Error;
+    type Error = SpecError;
 
     fn try_from(value: String) -> Result {
         Self::from_str(&value)
@@ -180,7 +171,8 @@ impl Display for UnresolvedVersionSpec {
                     .collect::>()
                     .join(" || ")
             ),
-            Self::Version(version) => write!(f, "{}", version),
+            Self::Calendar(version) => write!(f, "{}", version),
+            Self::Semantic(version) => write!(f, "{}", version),
         }
     }
 }
@@ -190,7 +182,8 @@ impl PartialEq for UnresolvedVersionSpec {
         match (self, other) {
             (Self::Canary, VersionSpec::Alias(a)) => a == "canary",
             (Self::Alias(a1), VersionSpec::Alias(a2)) => a1 == a2,
-            (Self::Version(v1), VersionSpec::Version(v2)) => v1 == v2,
+            (Self::Calendar(v1), VersionSpec::Calendar(v2)) => v1 == v2,
+            (Self::Semantic(v1), VersionSpec::Semantic(v2)) => v1 == v2,
             _ => false,
         }
     }
diff --git a/crates/version-spec/src/version_types.rs b/crates/version-spec/src/version_types.rs
new file mode 100644
index 000000000..cdc5f044c
--- /dev/null
+++ b/crates/version-spec/src/version_types.rs
@@ -0,0 +1,116 @@
+use crate::get_calver_regex;
+use crate::spec_error::SpecError;
+use semver::Version;
+use serde::{Deserialize, Serialize};
+use std::fmt;
+use std::ops::Deref;
+
+/// Container for a semantic version.
+#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
+pub struct SemVer(pub Version);
+
+impl SemVer {
+    /// Parse the string into a [`semver::Version`] type.
+    pub fn parse(value: &str) -> Result {
+        Ok(Self(Version::parse(value)?))
+    }
+}
+
+impl Deref for SemVer {
+    type Target = Version;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl fmt::Display for SemVer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+/// Container for a calendar version.
+#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
+pub struct CalVer(pub Version);
+
+impl CalVer {
+    /// If the provided value is a calver-like version string,
+    /// parse and convert it to a semver compatible version string,
+    /// so that we can utilize the [`semver::Version`] type.
+    pub fn parse(value: &str) -> Result {
+        let Some(caps) = get_calver_regex().captures(value) else {
+            return Err(SpecError::CalverInvalidFormat);
+        };
+
+        // Short years (less than 4 characters) are relative
+        // from the year 2000, so let's enforce it. Is this correct?
+        // https://calver.org/#scheme
+        let year = caps
+            .name("year")
+            .map(|cap| cap.as_str().trim_start_matches('0'))
+            .unwrap_or("0");
+        let mut year_no: usize = year.parse().unwrap();
+
+        if year.len() < 4 {
+            year_no += 2000;
+        }
+
+        // Strip leading zeros from months and days. If the value is
+        // not provided, fallback to a zero, as calver is 1-index based
+        // and we can use this 0 for comparison.
+        let month = caps
+            .name("month")
+            .map(|cap| cap.as_str().trim_start_matches('0'))
+            .unwrap_or("0");
+
+        let day = caps
+            .name("day")
+            .map(|cap| cap.as_str().trim_start_matches('0'))
+            .unwrap_or("0");
+
+        let mut version = format!("{year_no}.{month}.{day}");
+
+        if let Some(pre) = caps.name("pre") {
+            version.push_str(pre.as_str());
+        }
+
+        if let Some(micro) = caps.name("micro") {
+            version.push('+');
+            version.push_str(micro.as_str());
+        }
+
+        Ok(Self(Version::parse(&version)?))
+    }
+}
+
+impl Deref for CalVer {
+    type Target = Version;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl fmt::Display for CalVer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let version = &self.0;
+
+        write!(f, "{:0>4}-{:0>2}", version.major, version.minor)?;
+
+        if version.patch > 0 {
+            write!(f, "-{:0>2}", version.patch)?;
+        }
+
+        // micro
+        if !version.build.is_empty() {
+            write!(f, ".{}", version.build)?;
+        }
+
+        if !version.pre.is_empty() {
+            write!(f, "-{}", version.pre)?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/version-spec/tests/calver_test.rs b/crates/version-spec/tests/calver_test.rs
new file mode 100644
index 000000000..d80557c7c
--- /dev/null
+++ b/crates/version-spec/tests/calver_test.rs
@@ -0,0 +1,188 @@
+use semver::{BuildMetadata, Prerelease, Version};
+use version_spec::{is_calver, CalVer};
+
+mod calver {
+    use super::*;
+
+    #[test]
+    fn matches() {
+        let years = [
+            "2024", // 4 digit
+            "224",  // 3 digit
+            "24",   // 2 digit
+            "4",    // 1 digit
+            "04",   // zero padded
+        ];
+        let months = [
+            "3",  // without zero
+            "03", // zero padded
+            "12", // 2 digit
+        ];
+        // let weeks = ["1", "25", "52", "05"];
+        let days = ["1", "18", "30", "09"];
+
+        for year in years {
+            for month in months {
+                assert!(is_calver(format!("{year}-{month}")));
+                assert!(is_calver(format!("{year}-{month}-rc.1")));
+                assert!(is_calver(format!("{year}-{month}_456")));
+                assert!(is_calver(format!("{year}-{month}_456-rc.2")));
+                assert!(is_calver(format!("{year}-{month}.456")));
+
+                for day in days {
+                    assert!(is_calver(format!("{year}-{month}-{day}")));
+                    assert!(is_calver(format!("{year}-{month}-{day}-beta.1")));
+                    assert!(is_calver(format!("{year}-{month}-{day}_123")));
+                    assert!(is_calver(format!("{year}-{month}-{day}.123")));
+                    assert!(is_calver(format!("{year}-{month}-{day}.123-beta.1")));
+                }
+            }
+        }
+    }
+
+    #[test]
+    fn doesnt_match() {
+        // invalid
+        assert!(!is_calver("24"));
+        assert!(!is_calver("2024"));
+
+        // invalid months
+        assert!(!is_calver("2024-0"));
+        assert!(!is_calver("2024-00"));
+        assert!(!is_calver("2024-13"));
+        assert!(!is_calver("2024-20"));
+        assert!(!is_calver("2024-010"));
+
+        // invalid days
+        assert!(!is_calver("2024-10-0"));
+        assert!(!is_calver("2024-10-00"));
+        assert!(!is_calver("2024-10-123"));
+        assert!(!is_calver("2024-10-023"));
+        assert!(!is_calver("2024-10-40"));
+        assert!(!is_calver("2024-10-50"));
+
+        // invalid micro
+        assert!(!is_calver("2024_abc"));
+        assert!(!is_calver("2024-10_abc"));
+        assert!(!is_calver("2024-1-1_abc"));
+    }
+
+    #[test]
+    fn parse_year_month() {
+        for (month, actual) in [("1", "01"), ("05", "05"), ("10", "10"), ("12", "12")] {
+            let ver = CalVer::parse(&format!("2024-{month}")).unwrap();
+
+            assert_eq!(
+                ver.0,
+                Version {
+                    major: 2024,
+                    minor: actual.parse().unwrap(),
+                    patch: 0,
+                    pre: Prerelease::EMPTY,
+                    build: BuildMetadata::EMPTY,
+                }
+            );
+            assert_eq!(ver.to_string(), format!("2024-{actual}"));
+        }
+
+        // build
+        let ver = CalVer::parse("2024-5_123").unwrap();
+
+        assert_eq!(
+            ver.0,
+            Version {
+                major: 2024,
+                minor: 5,
+                patch: 0,
+                pre: Prerelease::EMPTY,
+                build: BuildMetadata::new("123").unwrap(),
+            }
+        );
+        assert_eq!(ver.to_string(), "2024-05.123");
+
+        // pre
+        let ver = CalVer::parse("2024-05-alpha.1").unwrap();
+
+        assert_eq!(
+            ver.0,
+            Version {
+                major: 2024,
+                minor: 5,
+                patch: 0,
+                pre: Prerelease::new("alpha.1").unwrap(),
+                build: BuildMetadata::EMPTY,
+            }
+        );
+        assert_eq!(ver.to_string(), "2024-05-alpha.1");
+
+        // pre + build
+        let ver = CalVer::parse("2024-05_123-alpha.1").unwrap();
+
+        assert_eq!(
+            ver.0,
+            Version {
+                major: 2024,
+                minor: 5,
+                patch: 0,
+                pre: Prerelease::new("alpha.1").unwrap(),
+                build: BuildMetadata::new("123").unwrap(),
+            }
+        );
+        assert_eq!(ver.to_string(), "2024-05.123-alpha.1");
+    }
+
+    #[test]
+    fn parse_year_month_day() {
+        for (day, actual) in [
+            ("1", "01"),
+            ("05", "05"),
+            ("10", "10"),
+            ("22", "22"),
+            ("31", "31"),
+        ] {
+            let ver = CalVer::parse(&format!("2024-1-{day}")).unwrap();
+
+            assert_eq!(
+                ver.0,
+                Version {
+                    major: 2024,
+                    minor: 1,
+                    patch: actual.parse().unwrap(),
+                    pre: Prerelease::EMPTY,
+                    build: BuildMetadata::EMPTY,
+                }
+            );
+            assert_eq!(ver.to_string(), format!("2024-01-{actual}"));
+        }
+
+        // build
+        let ver = CalVer::parse("2024-5-23_123").unwrap();
+
+        assert_eq!(
+            ver.0,
+            Version {
+                major: 2024,
+                minor: 5,
+                patch: 23,
+                pre: Prerelease::EMPTY,
+                build: BuildMetadata::new("123").unwrap(),
+            }
+        );
+        assert_eq!(ver.to_string(), "2024-05-23.123");
+
+        // pre
+        let ver = CalVer::parse("2024-05-1-alpha.1").unwrap();
+
+        assert_eq!(
+            ver.0,
+            Version {
+                major: 2024,
+                minor: 5,
+                patch: 1,
+                pre: Prerelease::new("alpha.1").unwrap(),
+                build: BuildMetadata::EMPTY,
+            }
+        );
+        assert_eq!(ver.to_string(), "2024-05-01-alpha.1");
+    }
+}
diff --git a/crates/version-spec/tests/helpers_test.rs b/crates/version-spec/tests/helpers_test.rs
new file mode 100644
index 000000000..e90490f5b
--- /dev/null
+++ b/crates/version-spec/tests/helpers_test.rs
@@ -0,0 +1,38 @@
+use version_spec::{clean_version_req_string, clean_version_string, is_alias_name};
+
+#[test]
+fn checks_alias() {
+    assert!(is_alias_name("foo"));
+    assert!(is_alias_name("foo.bar"));
+    assert!(is_alias_name("foo/bar"));
+    assert!(is_alias_name("foo-bar"));
+    assert!(is_alias_name("foo_bar-baz"));
+    assert!(is_alias_name("alpha.1"));
+    assert!(is_alias_name("beta-0"));
+    assert!(is_alias_name("rc-1.2.3"));
+    assert!(is_alias_name("next-2023"));
+    assert!(is_alias_name("ver-2023"));
+
+    assert!(!is_alias_name("1.2.3"));
+    assert!(!is_alias_name("1.2"));
+    assert!(!is_alias_name("1"));
+    assert!(!is_alias_name("1-3"));
+}
+
+#[test]
+fn cleans_version() {
+    assert_eq!(clean_version_string("1.2.3"), "1.2.3");
+    assert_eq!(clean_version_string("v1.2.3"), "1.2.3");
+    assert_eq!(clean_version_string("V1.2.3"), "1.2.3");
+}
+
+#[test]
+fn cleans_req() {
+    assert_eq!(clean_version_req_string("1.2.*"), "1.2");
+    assert_eq!(clean_version_req_string("1.*.*"), "1");
+
+    assert_eq!(clean_version_req_string("1-2-*"), "1-2");
+    assert_eq!(clean_version_req_string("1-*-*"), "1");
+
+    assert_eq!(clean_version_req_string("1 && 2"), "1 , 2");
+}
diff --git a/crates/version-spec/tests/resolved_spec_test.rs b/crates/version-spec/tests/resolved_spec_test.rs
index f23a9f14c..4fd678dd0 100644
--- a/crates/version-spec/tests/resolved_spec_test.rs
+++ b/crates/version-spec/tests/resolved_spec_test.rs
@@ -1,5 +1,5 @@
 use semver::Version;
-use version_spec::VersionSpec;
+use version_spec::{CalVer, SemVer, VersionSpec};
 
 mod resolved_spec {
     use super::*;
@@ -33,28 +33,38 @@ mod resolved_spec {
     fn versions() {
         assert_eq!(
             VersionSpec::parse("v1.2.3").unwrap(),
-            VersionSpec::Version(Version::new(1, 2, 3))
+            VersionSpec::Semantic(SemVer(Version::new(1, 2, 3)))
         );
         assert_eq!(
             VersionSpec::parse("1.2.3").unwrap(),
-            VersionSpec::Version(Version::new(1, 2, 3))
+            VersionSpec::Semantic(SemVer(Version::new(1, 2, 3)))
+        );
+        assert_eq!(
+            VersionSpec::parse("1.2.3-0").unwrap(),
+            VersionSpec::Semantic(SemVer(Version::parse("1.2.3-0").unwrap()))
+        );
+        assert_eq!(
+            VersionSpec::parse("1.2.3-alpha").unwrap(),
+            VersionSpec::Semantic(SemVer(Version::parse("1.2.3-alpha").unwrap()))
+        );
+        assert_eq!(
+            VersionSpec::parse("1.2.3-alpha.1").unwrap(),
+            VersionSpec::Semantic(SemVer(Version::parse("1.2.3-alpha.1").unwrap()))
         );
-    }
-
-    #[test]
-    #[should_panic(expected = "unexpected end of input while parsing minor version number")]
-    fn error_missing_patch() {
-        VersionSpec::parse("1.2").unwrap();
-    }
 
-    #[test]
-    #[should_panic(expected = "unexpected end of input while parsing major version number")]
-    fn error_missing_minor() {
-        VersionSpec::parse("1").unwrap();
+        // calver
+        assert_eq!(
+            VersionSpec::parse("2024-02").unwrap(),
+            VersionSpec::Calendar(CalVer(Version::new(2024, 2, 0)))
+        );
+        assert_eq!(
+            VersionSpec::parse("2024-2-26").unwrap(),
+            VersionSpec::Calendar(CalVer(Version::new(2024, 2, 26)))
+        );
     }
 
     #[test]
-    #[should_panic(expected = "unexpected character '%' while parsing major version number")]
+    #[should_panic(expected = "ResolvedUnknownFormat")]
     fn error_invalid_char() {
         VersionSpec::parse("%").unwrap();
     }
diff --git a/crates/version-spec/tests/unresolved_parser_test.rs b/crates/version-spec/tests/unresolved_parser_test.rs
new file mode 100644
index 000000000..d6131849e
--- /dev/null
+++ b/crates/version-spec/tests/unresolved_parser_test.rs
@@ -0,0 +1,241 @@
+use version_spec::{parse, ParseKind};
+
+mod unresolved_parser {
+    use super::*;
+
+    #[test]
+    fn parses_reqs() {
+        assert_eq!(parse("").unwrap(), ("*".to_owned(), ParseKind::Req));
+        assert_eq!(parse("*").unwrap(), ("*".to_owned(), ParseKind::Req));
+
+        // semver
+        assert_eq!(parse("1").unwrap(), ("~1".to_owned(), ParseKind::Req));
+        assert_eq!(parse("1.2").unwrap(), ("~1.2".to_owned(), ParseKind::Req));
+        assert_eq!(parse("1.02").unwrap(), ("~1.2".to_owned(), ParseKind::Req));
+        assert_eq!(parse("v1").unwrap(), ("~1".to_owned(), ParseKind::Req));
+        assert_eq!(parse("v1.2").unwrap(), ("~1.2".to_owned(), ParseKind::Req));
+        assert_eq!(parse("1.*").unwrap(), ("~1".to_owned(), ParseKind::Req));
+        assert_eq!(parse("1.*.*").unwrap(), ("~1".to_owned(), ParseKind::Req));
+        assert_eq!(parse("1.2.*").unwrap(), ("~1.2".to_owned(), ParseKind::Req));
+
+        // calver
+        assert_eq!(parse("2000").unwrap(), ("~2000".to_owned(), ParseKind::Req));
+        assert_eq!(
+            parse("2000-2").unwrap(),
+            ("~2000.2".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("2000-02").unwrap(),
+            ("~2000.2".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("v2000").unwrap(),
+            ("~2000".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("v2000-2").unwrap(),
+            ("~2000.2".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("2000-*").unwrap(),
+            ("~2000".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("2000-*-*").unwrap(),
+            ("~2000".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("2000-2-*").unwrap(),
+            ("~2000.2".to_owned(), ParseKind::Req)
+        );
+
+        // calver (short years)
+        assert_eq!(
+            parse("1-2").unwrap(),
+            ("~2001.2".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("12-2").unwrap(),
+            ("~2012.2".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse("123-2").unwrap(),
+            ("~2123.2".to_owned(), ParseKind::Req)
+        );
+
+        for op in ["=", "<", "<=", ">", ">=", "~", "^"] {
+            // semver
+            assert_eq!(
+                parse(format!("{op}1")).unwrap(),
+                (format!("{op}1"), ParseKind::Req)
+            );
+            assert_eq!(
+                parse(format!("{op} 1.2")).unwrap(),
+                (format!("{op}1.2"), ParseKind::Req)
+            );
+            assert_eq!(
+                parse(format!("{op}1")).unwrap(),
+                (format!("{op}1"), ParseKind::Req)
+            );
+            assert_eq!(
+                parse(format!("  {op}  v1.2.3  ")).unwrap(),
+                (format!("{op}1.2.3"), ParseKind::Req)
+            );
+
+            // calver
+            assert_eq!(
+                parse(format!("{op}2000")).unwrap(),
+                (format!("{op}2000"), ParseKind::Req)
+            );
+            assert_eq!(
+                parse(format!("{op} 2000-10")).unwrap(),
+                (format!("{op}2000.10"), ParseKind::Req)
+            );
+            assert_eq!(
+                parse(format!("  {op}  v2000-10-03  ")).unwrap(),
+                (format!("{op}2000.10.3"), ParseKind::Req)
+            );
+        }
+    }
+
+    #[test]
+    fn parses_reqs_special() {
+        assert_eq!(
+            parse("1.2, 4.5").unwrap(),
+            ("1.2,4.5".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse(">=1.2.7 <1.3.0").unwrap(),
+            (">=1.2.7,<1.3.0".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse(">=1.2.0, <1.3.0").unwrap(),
+            (">=1.2.0,<1.3.0".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(parse("1.2.*").unwrap(), ("~1.2".to_owned(), ParseKind::Req));
+        assert_eq!(
+            parse(">= 1.2, < 1.5").unwrap(),
+            (">=1.2,<1.5".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse(">=1.2.3 <2.4.0-0").unwrap(),
+            (">=1.2.3,<2.4.0-0".to_owned(), ParseKind::Req)
+        );
+        assert_eq!(
+            parse(">=1.2.3, <2.4.0-0").unwrap(),
+            (">=1.2.3,<2.4.0-0".to_owned(), ParseKind::Req)
+        );
+    }
+
+    #[test]
+    fn parses_reqs_semver() {
+        assert_eq!(
+            parse("1.2.3").unwrap(),
+            ("1.2.3".to_owned(), ParseKind::Sem)
+        );
+        assert_eq!(
+            parse("01.02.03").unwrap(),
+            ("1.2.3".to_owned(), ParseKind::Sem)
+        );
+        assert_eq!(
+            parse("v1.2.3").unwrap(),
+            ("1.2.3".to_owned(), ParseKind::Sem)
+        );
+
+        // pre
+        assert_eq!(
+            parse("1.2.3-alpha").unwrap(),
+            ("1.2.3-alpha".to_owned(), ParseKind::Sem)
+        );
+        assert_eq!(
+            parse("1.2.3-rc.0").unwrap(),
+            ("1.2.3-rc.0".to_owned(), ParseKind::Sem)
+        );
+        assert_eq!(
+            parse("v1.2.3-a-b-c").unwrap(),
+            ("1.2.3-a-b-c".to_owned(), ParseKind::Sem)
+        );
+
+        // build
+        assert_eq!(
+            parse("1.2.3+alpha").unwrap(),
+            ("1.2.3+alpha".to_owned(), ParseKind::Sem)
+        );
+        assert_eq!(
+            parse("1.2.3+rc.0").unwrap(),
+            ("1.2.3+rc.0".to_owned(), ParseKind::Sem)
+        );
+        assert_eq!(
+            parse("v1.2.3+a-b-c").unwrap(),
+            ("1.2.3+a-b-c".to_owned(), ParseKind::Sem)
+        );
+    }
+
+    #[test]
+    fn parses_reqs_calver() {
+        assert_eq!(
+            parse("0-2-3").unwrap(),
+            ("2000-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("00-2-3").unwrap(),
+            ("2000-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("000-2-3").unwrap(),
+            ("2000-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("1-2-3").unwrap(),
+            ("2001-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("12-2-03").unwrap(),
+            ("2012-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("123-2-31").unwrap(),
+            ("2123-2-31".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("2000-2-3").unwrap(),
+            ("2000-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("2000-02-03").unwrap(),
+            ("2000-2-3".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("v12-2-3").unwrap(),
+            ("2012-2-3".to_owned(), ParseKind::Cal)
+        );
+
+        // pre
+        assert_eq!(
+            parse("0-2-3-rc.0").unwrap(),
+            ("2000-2-3-rc.0".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("v12-2-3-alpha-5").unwrap(),
+            ("2012-2-3-alpha-5".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("12-2-3-beta").unwrap(),
+            ("2012-2-3-beta".to_owned(), ParseKind::Cal)
+        );
+
+        // build
+        assert_eq!(
+            parse("0-2-3_123").unwrap(),
+            ("2000-2-3+123".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("v12-2-3_0").unwrap(),
+            ("2012-2-3+0".to_owned(), ParseKind::Cal)
+        );
+        assert_eq!(
+            parse("12-2-3.789").unwrap(),
+            ("2012-2-3+789".to_owned(), ParseKind::Cal)
+        );
+    }
+}
diff --git a/crates/version-spec/tests/unresolved_spec_test.rs b/crates/version-spec/tests/unresolved_spec_test.rs
index df96a24b8..ef5170128 100644
--- a/crates/version-spec/tests/unresolved_spec_test.rs
+++ b/crates/version-spec/tests/unresolved_spec_test.rs
@@ -1,9 +1,7 @@
-use semver::Version;
-use version_spec::UnresolvedVersionSpec;
+use semver::{Version, VersionReq};
+use version_spec::{CalVer, SemVer, UnresolvedVersionSpec};
 
 mod unresolved_spec {
-    use semver::VersionReq;
-
     use super::*;
 
     #[test]
@@ -38,11 +36,21 @@ mod unresolved_spec {
     fn versions() {
         assert_eq!(
             UnresolvedVersionSpec::parse("v1.2.3").unwrap(),
-            UnresolvedVersionSpec::Version(Version::new(1, 2, 3))
+            UnresolvedVersionSpec::Semantic(SemVer(Version::new(1, 2, 3)))
         );
         assert_eq!(
             UnresolvedVersionSpec::parse("1.2.3").unwrap(),
-            UnresolvedVersionSpec::Version(Version::new(1, 2, 3))
+            UnresolvedVersionSpec::Semantic(SemVer(Version::new(1, 2, 3)))
+        );
+
+        // calver
+        assert_eq!(
+            UnresolvedVersionSpec::parse("2024-02").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~2024.2").unwrap())
+        );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("2024-2-26").unwrap(),
+            UnresolvedVersionSpec::Calendar(CalVer(Version::new(2024, 2, 26)))
         );
     }
 
@@ -52,30 +60,58 @@ mod unresolved_spec {
             UnresolvedVersionSpec::parse("1.2").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("~1.2").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("~2000-2").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~2000.2").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse("1").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("~1").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("2000").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~2000").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse("1.2.*").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("~1.2").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("2000.02.*").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~2000.2").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse("1.*").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("~1").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("2000.*").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~2000").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse(">1").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse(">1").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse(">2000-10").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse(">2000.10").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse("<=1").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("<=1").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("<=2000-12-12").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("<=2000.12.12").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse("1, 2").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("1, 2").unwrap())
         );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("2000-05, 3000-01").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("2000.5, 3000.1").unwrap())
+        );
         assert_eq!(
             UnresolvedVersionSpec::parse("1,2").unwrap(),
             UnresolvedVersionSpec::Req(VersionReq::parse("1,2").unwrap())
@@ -96,5 +132,123 @@ mod unresolved_spec {
                 VersionReq::parse("3,4").unwrap(),
             ])
         );
+
+        assert_eq!(
+            UnresolvedVersionSpec::parse("^2000-10 || ~1000 || 3000-05-12,4000-09-09").unwrap(),
+            UnresolvedVersionSpec::ReqAny(vec![
+                VersionReq::parse("~1000").unwrap(),
+                VersionReq::parse("^2000.10").unwrap(),
+                VersionReq::parse("3000.5.12,4000.9.9").unwrap(),
+            ])
+        );
+    }
+
+    #[test]
+    fn parses_alias() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("stable").unwrap(),
+            UnresolvedVersionSpec::Alias("stable".to_owned())
+        );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("latest").unwrap(),
+            UnresolvedVersionSpec::Alias("latest".to_owned())
+        );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("lts-2014").unwrap(),
+            UnresolvedVersionSpec::Alias("lts-2014".to_owned())
+        );
+    }
+
+    #[test]
+    fn parses_req() {
+        for req in ["=1.2.3", "^1.2", "~1", ">1.2.0", "<1", "*", ">1, <=1.5"] {
+            assert_eq!(
+                UnresolvedVersionSpec::parse(req).unwrap(),
+                UnresolvedVersionSpec::Req(VersionReq::parse(req).unwrap())
+            );
+        }
+    }
+
+    #[test]
+    fn parses_req_spaces() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("> 10").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse(">10").unwrap())
+        );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("1.2 , 2").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("1.2, 2").unwrap())
+        );
+        assert_eq!(
+            UnresolvedVersionSpec::parse(">= 1.2 < 2").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse(">=1.2, <2").unwrap())
+        );
+    }
+
+    #[test]
+    fn parses_req_any() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("^1 || ~2 || =3").unwrap(),
+            UnresolvedVersionSpec::ReqAny(vec![
+                VersionReq::parse("~2").unwrap(),
+                VersionReq::parse("^1").unwrap(),
+                VersionReq::parse("=3").unwrap(),
+            ])
+        );
+    }
+
+    #[test]
+    fn sorts_any_req() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("^1 || ^2 || ^3").unwrap(),
+            UnresolvedVersionSpec::ReqAny(vec![
+                VersionReq::parse("^3").unwrap(),
+                VersionReq::parse("^2").unwrap(),
+                VersionReq::parse("^1").unwrap(),
+            ])
+        );
+        assert_eq!(
+            UnresolvedVersionSpec::parse("^1.1 || ^1.10 || ^1.10.1 || ^1.2").unwrap(),
+            UnresolvedVersionSpec::ReqAny(vec![
+                VersionReq::parse("^1.10.1").unwrap(),
+                VersionReq::parse("^1.10").unwrap(),
+                VersionReq::parse("^1.2").unwrap(),
+                VersionReq::parse("^1.1").unwrap(),
+            ])
+        );
+    }
+
+    #[test]
+    fn parses_version() {
+        for req in ["1.2.3", "4.5.6", "7.8.9-alpha", "10.11.12+build"] {
+            assert_eq!(
+                UnresolvedVersionSpec::parse(req).unwrap(),
+                UnresolvedVersionSpec::Semantic(SemVer(Version::parse(req).unwrap()))
+            );
+        }
+    }
+
+    #[test]
+    fn parses_version_with_v() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("v1.2.3").unwrap(),
+            UnresolvedVersionSpec::Semantic(SemVer(Version::parse("1.2.3").unwrap()))
+        );
+    }
+
+    #[test]
+    fn no_patch_becomes_req() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("1.2").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~1.2").unwrap())
+        );
+    }
+
+    #[test]
+    fn no_minor_becomes_req() {
+        assert_eq!(
+            UnresolvedVersionSpec::parse("1").unwrap(),
+            UnresolvedVersionSpec::Req(VersionReq::parse("~1").unwrap())
+        );
     }
 }
diff --git a/plugins/Cargo.lock b/plugins/Cargo.lock
index 8f6fe21c0..564819c97 100644
--- a/plugins/Cargo.lock
+++ b/plugins/Cargo.lock
@@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
 [[package]]
 name = "ahash"
-version = "0.8.6"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
  "getrandom",
@@ -145,7 +145,7 @@ dependencies = [
  "cfg-if",
  "libc",
  "miniz_oxide",
- "object",
+ "object 0.32.1",
  "rustc-demangle",
 ]
 
@@ -161,20 +161,11 @@ version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
-[[package]]
-name = "bincode"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "binstall-tar"
-version = "0.4.39"
+version = "0.4.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01db907e07c37309ea816c183ffe548daaa66ef640a291408f232d6ca4089dbb"
+checksum = "e3620d72763b5d8df3384f3b2ec47dc5885441c2abbd94dd32197167d08b014a"
 dependencies = [
  "filetime",
  "libc",
@@ -239,9 +230,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
 
 [[package]]
 name = "cached"
-version = "0.51.3"
+version = "0.51.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd93a9f06ec296ca66b4c26fafa9ed63f32c473d7a708a5f28563ee64c948515"
+checksum = "0feb64151eed3da6107fddd2d717a6ca4b9dbd74e43784c55c841d1abfe5a295"
 dependencies = [
  "ahash",
  "cached_proc_macro",
@@ -272,9 +263,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
 
 [[package]]
 name = "cap-fs-ext"
-version = "2.0.1"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88e341d15ac1029aadce600be764a1a1edafe40e03cde23285bc1d261b3a4866"
+checksum = "2fc2d2954524be4866aaa720f008fba9995de54784957a1b0e0119992d6d5e52"
 dependencies = [
  "cap-primitives",
  "cap-std",
@@ -282,23 +273,11 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "cap-net-ext"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "434168fe6533055f0f4204039abe3ff6d7db338ef46872a5fa39e9d5ad5ab7a9"
-dependencies = [
- "cap-primitives",
- "cap-std",
- "rustix",
- "smallvec",
-]
-
 [[package]]
 name = "cap-primitives"
-version = "2.0.1"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe16767ed8eee6d3f1f00d6a7576b81c226ab917eb54b96e5f77a5216ef67abb"
+checksum = "00172660727e2d7f808e7cc2bfffd093fdb3ea2ff2ef819289418a3c3ffab5ac"
 dependencies = [
  "ambient-authority",
  "fs-set-times",
@@ -313,9 +292,9 @@ dependencies = [
 
 [[package]]
 name = "cap-rand"
-version = "2.0.1"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20e5695565f0cd7106bc3c7170323597540e772bb73e0be2cd2c662a0f8fa4ca"
+checksum = "270f1d341a2afc62604f8f688bee4e444d052b7a74c1458dd3aa7efb47d4077f"
 dependencies = [
  "ambient-authority",
  "rand",
@@ -323,9 +302,9 @@ dependencies = [
 
 [[package]]
 name = "cap-std"
-version = "2.0.1"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "593db20e4c51f62d3284bae7ee718849c3214f93a3b94ea1899ad85ba119d330"
+checksum = "8cd9187bb3f7478a4c135ea10473a41a5f029d2ac800c1adf64f35ec7d4c8603"
 dependencies = [
  "cap-primitives",
  "io-extras",
@@ -335,9 +314,9 @@ dependencies = [
 
 [[package]]
 name = "cap-time-ext"
-version = "2.0.1"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03261630f291f425430a36f38c847828265bc928f517cdd2004c56f4b02f002b"
+checksum = "91666f31e30c85b1d2ee8432c90987f752c45f5821f5638027b41e73e16a395b"
 dependencies = [
  "ambient-authority",
  "cap-primitives",
@@ -396,6 +375,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aaa6b4b263a5d737e9bf6b7c09b72c41a5480aec4d7219af827f6564e950b6a5"
 
+[[package]]
+name = "cobs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+
 [[package]]
 name = "command-group"
 version = "5.0.1"
@@ -478,9 +463,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
 [[package]]
 name = "cpp_demangle"
-version = "0.3.5"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f"
+checksum = "7e8227005286ec39567949b33df9896bcadfa6051bccca2488129f108ca23119"
 dependencies = [
  "cfg-if",
 ]
@@ -496,18 +481,18 @@ dependencies = [
 
 [[package]]
 name = "cranelift-bforest"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c22542c0b95bd3302f7ed6839869c561f2324bac2fd5e7e99f5cfa65fdc8b92"
+checksum = "29daf137addc15da6bab6eae2c4a11e274b1d270bf2759508e62f6145e863ef6"
 dependencies = [
  "cranelift-entity",
 ]
 
 [[package]]
 name = "cranelift-codegen"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b3db903ef2e9c8a4de2ea6db5db052c7857282952f9df604aa55d169e6000d8"
+checksum = "de619867d5de4c644b7fd9904d6e3295269c93d8a71013df796ab338681222d4"
 dependencies = [
  "bumpalo",
  "cranelift-bforest",
@@ -520,39 +505,40 @@ dependencies = [
  "hashbrown 0.14.3",
  "log",
  "regalloc2",
+ "rustc-hash",
  "smallvec",
  "target-lexicon",
 ]
 
 [[package]]
 name = "cranelift-codegen-meta"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6590feb5a1d6438f974bf6a5ac4dddf69fca14e1f07f3265d880f69e61a94463"
+checksum = "29f5cf277490037d8dae9513d35e0ee8134670ae4a964a5ed5b198d4249d7c10"
 dependencies = [
  "cranelift-codegen-shared",
 ]
 
 [[package]]
 name = "cranelift-codegen-shared"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7239038c56fafe77fddc8788fc8533dd6c474dc5bdc5637216404f41ba807330"
+checksum = "8c3e22ecad1123343a3c09ac6ecc532bb5c184b6fcb7888df0ea953727f79924"
 
 [[package]]
 name = "cranelift-control"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7dc9c595341404d381d27a3d950160856b35b402275f0c3990cd1ad683c8053"
+checksum = "53ca3ec6d30bce84ccf59c81fead4d16381a3ef0ef75e8403bc1e7385980da09"
 dependencies = [
  "arbitrary",
 ]
 
 [[package]]
 name = "cranelift-entity"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44e3ee532fc4776c69bcedf7e62f9632cbb3f35776fa9a525cdade3195baa3f7"
+checksum = "7eabb8d36b0ca8906bec93c78ea516741cac2d7e6b266fa7b0ffddcc09004990"
 dependencies = [
  "serde",
  "serde_derive",
@@ -560,9 +546,9 @@ dependencies = [
 
 [[package]]
 name = "cranelift-frontend"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a612c94d09e653662ec37681dc2d6fd2b9856e6df7147be0afc9aabb0abf19df"
+checksum = "44b42630229e49a8cfcae90bdc43c8c4c08f7a7aa4618b67f79265cd2f996dd2"
 dependencies = [
  "cranelift-codegen",
  "log",
@@ -572,15 +558,15 @@ dependencies = [
 
 [[package]]
 name = "cranelift-isle"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85db9830abeb1170b7d29b536ffd55af1d4d26ac8a77570b5d1aca003bf225cc"
+checksum = "918d1e36361805dfe0b6cdfd5a5ffdb5d03fa796170c5717d2727cbe623b93a0"
 
 [[package]]
 name = "cranelift-native"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301ef0edafeaeda5771a5d2db64ac53e1818ae3111220a185677025fe91db4a1"
+checksum = "75aea85a0d7e1800b14ce9d3f53adf8ad4d1ee8a9e23b0269bdc50285e93b9b3"
 dependencies = [
  "cranelift-codegen",
  "libc",
@@ -589,14 +575,14 @@ dependencies = [
 
 [[package]]
 name = "cranelift-wasm"
-version = "0.103.0"
+version = "0.108.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "380f0abe8264e4570ac615fc31cef32a3b90a77f7eb97b08331f9dd357b1f500"
+checksum = "dac491fd3473944781f0cf9528c90cc899d18ad438da21961a839a3a44d57dfb"
 dependencies = [
  "cranelift-codegen",
  "cranelift-entity",
  "cranelift-frontend",
- "itertools 0.10.5",
+ "itertools 0.12.1",
  "log",
  "smallvec",
  "wasmparser",
@@ -814,6 +800,12 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
 
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
 [[package]]
 name = "encode_unicode"
 version = "0.3.6"
@@ -847,9 +839,9 @@ dependencies = [
 
 [[package]]
 name = "extism"
-version = "1.3.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d04edf6dcef24f6b28de8d709c73fabf3577323a8774e30cd03a1a00f35240e1"
+checksum = "772bff1d1a09e26152a7fc08d44bad463a69ae29d85e652dc851a9a2ebc5b7f1"
 dependencies = [
  "anyhow",
  "cbindgen",
@@ -860,21 +852,21 @@ dependencies = [
  "serde",
  "serde_json",
  "sha2",
- "toml 0.8.13",
+ "toml 0.8.14",
  "tracing",
  "tracing-subscriber",
  "ureq",
  "url",
  "uuid",
+ "wasi-common",
  "wasmtime",
- "wasmtime-wasi",
 ]
 
 [[package]]
 name = "extism-convert"
-version = "1.3.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b96dda334f4d05d02504c7c6fd8b2a5fb9caf77aea956fd5a25e467d6df4e813"
+checksum = "2e5a978634c28e4b150213cc8265c211f24421a8ee5dea4126ece0dc60cbc709"
 dependencies = [
  "anyhow",
  "base64 0.22.1",
@@ -888,9 +880,9 @@ dependencies = [
 
 [[package]]
 name = "extism-convert-macros"
-version = "1.3.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f542da6bda406e348633328bf13b71c15b08dc14a92b62fb724ac17f0081fe1"
+checksum = "c40464260bcb3982b9e1967e2446ebea4a4637772c1b39f29b6410f555367092"
 dependencies = [
  "manyhow",
  "proc-macro-crate",
@@ -901,9 +893,9 @@ dependencies = [
 
 [[package]]
 name = "extism-manifest"
-version = "1.3.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97da8e6e2803cc3ac8cde2529e027da013413555c421edfabddbf8a637f52548"
+checksum = "a60b6ea31edc0831e28665b4e808175dd5acd4602bfcc5d31f09c97f334238d1"
 dependencies = [
  "base64 0.22.1",
  "serde",
@@ -1018,28 +1010,14 @@ dependencies = [
 
 [[package]]
 name = "fs4"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73969b81e8bc90a3828d913dd3973d80771bfb9d7fbe1a78a79122aad456af15"
+checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8"
 dependencies = [
  "rustix",
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "futures"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
 [[package]]
 name = "futures-channel"
 version = "0.3.30"
@@ -1047,7 +1025,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
 dependencies = [
  "futures-core",
- "futures-sink",
 ]
 
 [[package]]
@@ -1056,12 +1033,6 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
 
-[[package]]
-name = "futures-io"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
-
 [[package]]
 name = "futures-sink"
 version = "0.3.30"
@@ -1081,7 +1052,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
 dependencies = [
  "futures-core",
- "futures-sink",
  "futures-task",
  "pin-project-lite",
  "pin-utils",
@@ -1378,6 +1348,124 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
 [[package]]
 name = "id-arena"
 version = "2.2.1"
@@ -1392,12 +1480,14 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
 [[package]]
 name = "idna"
-version = "0.5.0"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
 dependencies = [
- "unicode-bidi",
- "unicode-normalization",
+ "icu_normalizer",
+ "icu_properties",
+ "smallvec",
+ "utf8_iter",
 ]
 
 [[package]]
@@ -1488,18 +1578,18 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
 
 [[package]]
 name = "itertools"
-version = "0.10.5"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
 dependencies = [
  "either",
 ]
 
 [[package]]
 name = "itertools"
-version = "0.11.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
 dependencies = [
  "either",
 ]
@@ -1568,9 +1658,15 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
 [[package]]
 name = "libc"
-version = "0.2.150"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "libm"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
 [[package]]
 name = "libredox"
@@ -1595,6 +1691,12 @@ version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
 
+[[package]]
+name = "litemap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
+
 [[package]]
 name = "lock_api"
 version = "0.4.11"
@@ -1629,10 +1731,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "mach"
-version = "0.3.2"
+name = "mach2"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
 dependencies = [
  "libc",
 ]
@@ -1821,6 +1923,15 @@ name = "object"
 version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "object"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d"
 dependencies = [
  "crc32fast",
  "hashbrown 0.14.3",
@@ -1952,6 +2063,17 @@ dependencies = [
  "nom",
 ]
 
+[[package]]
+name = "postcard"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8"
+dependencies = [
+ "cobs",
+ "embedded-io",
+ "serde",
+]
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.17"
@@ -2020,9 +2142,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.84"
+version = "1.0.85"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
+checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
 dependencies = [
  "unicode-ident",
 ]
@@ -2052,7 +2174,7 @@ dependencies = [
 
 [[package]]
 name = "proto_core"
-version = "0.35.4"
+version = "0.36.3"
 dependencies = [
  "cached",
  "indexmap 2.2.6",
@@ -2073,7 +2195,7 @@ dependencies = [
  "starbase_archive",
  "starbase_events",
  "starbase_styles 0.4.0",
- "starbase_utils 0.7.2",
+ "starbase_utils 0.7.5",
  "thiserror",
  "tracing",
  "url",
@@ -2084,7 +2206,7 @@ dependencies = [
 
 [[package]]
 name = "proto_pdk"
-version = "0.19.1"
+version = "0.20.0"
 dependencies = [
  "extism-pdk",
  "proto_pdk_api",
@@ -2095,7 +2217,7 @@ dependencies = [
 
 [[package]]
 name = "proto_pdk_api"
-version = "0.19.1"
+version = "0.20.0"
 dependencies = [
  "rustc-hash",
  "semver",
@@ -2109,19 +2231,19 @@ dependencies = [
 
 [[package]]
 name = "proto_pdk_test_utils"
-version = "0.23.1"
+version = "0.24.1"
 dependencies = [
  "proto_core",
  "proto_pdk_api",
  "serde",
  "serde_json",
- "starbase_sandbox 0.6.1",
+ "starbase_sandbox 0.6.2",
  "warpgate",
 ]
 
 [[package]]
 name = "proto_shim"
-version = "0.3.1"
+version = "0.4.1"
 dependencies = [
  "command-group",
  "dirs 5.0.1",
@@ -2242,9 +2364,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.10.4"
+version = "1.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -2384,9 +2506,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustix"
-version = "0.38.26"
+version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
  "bitflags 2.4.1",
  "errno",
@@ -2505,9 +2627,9 @@ dependencies = [
 
 [[package]]
 name = "schematic"
-version = "0.16.3"
+version = "0.16.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240dcf6dca558d33591d44e53983414d96193c3a980f36826cc7ad518b746abb"
+checksum = "a2a97356d9a387d6963940ab05de6ac9dbee0c3c97090247954c3d4c2ddc5b8d"
 dependencies = [
  "garde",
  "indexmap 2.2.6",
@@ -2518,15 +2640,15 @@ dependencies = [
  "serde_path_to_error",
  "starbase_styles 0.4.0",
  "thiserror",
- "toml 0.8.13",
+ "toml 0.8.14",
  "tracing",
 ]
 
 [[package]]
 name = "schematic_macros"
-version = "0.16.3"
+version = "0.16.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b72625710565d1a516aed853db8e1e3dffc00ee89f73a610dc7b500e49785c2"
+checksum = "906eff11b3963e20f5d33fb6dfc67d84351769917970c0d7ebf86e1782d869b5"
 dependencies = [
  "convert_case",
  "darling",
@@ -2543,7 +2665,8 @@ checksum = "788660c0972a2aab386e2b4b8bd304e0b21c21751e2722cead28d059597d3dad"
 dependencies = [
  "indexmap 2.2.6",
  "serde_json",
- "toml 0.8.13",
+ "toml 0.8.14",
+ "url",
 ]
 
 [[package]]
@@ -2732,6 +2855,9 @@ name = "smallvec"
 version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "socket2"
@@ -2763,28 +2889,28 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
 [[package]]
 name = "starbase_archive"
-version = "0.7.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6c534fa921df0a5ee7d59029a53fa9f91db6dadfaf4e8cfd15785079e1ec7cb"
+checksum = "1159e924d28043b3eb045d9a70e30312a50d31c7271ee0720865a32ba36042bf"
 dependencies = [
  "binstall-tar",
  "flate2",
  "miette",
  "rustc-hash",
  "starbase_styles 0.4.0",
- "starbase_utils 0.7.2",
+ "starbase_utils 0.7.5",
  "thiserror",
  "tracing",
  "xz2",
  "zip",
- "zstd 0.13.1",
+ "zstd",
 ]
 
 [[package]]
 name = "starbase_events"
-version = "0.6.0"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1b23db0b8074346e689895c7a18e97517c3492ed4cd1c330b3eeb765a861346"
+checksum = "c96e6586ad2f10fcbc6d30fe330b7ea88709de3d55db6259a64e309937016b4e"
 dependencies = [
  "async-trait",
  "miette",
@@ -2794,9 +2920,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_macros"
-version = "0.6.2"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72e11e236b44c4d9304e1652142f6606670e540e81d0e4110e3a4897713c0aed"
+checksum = "052d4a67b75ca00709992b99a332679b21d621d3aad441a11d89dc4e1d42bd14"
 dependencies = [
  "darling",
  "proc-macro2",
@@ -2822,9 +2948,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_sandbox"
-version = "0.6.1"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e87bd5565afbfbe20f19efa79db83d714a93fc21557dbdb4ccd8b02c08b295a"
+checksum = "9dc8cc21fc2c389549297a2049074bf22a34aceb8ed8d31be6e1df42648abcb8"
 dependencies = [
  "assert_cmd",
  "assert_fs",
@@ -2833,7 +2959,7 @@ dependencies = [
  "once_cell",
  "predicates",
  "pretty_assertions",
- "starbase_utils 0.7.2",
+ "starbase_utils 0.7.5",
 ]
 
 [[package]]
@@ -2874,9 +3000,9 @@ dependencies = [
 
 [[package]]
 name = "starbase_utils"
-version = "0.7.2"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8c1967c1604450d267dc796e6f23f371b6f65d907573316eed17c109c08a9e1"
+checksum = "06fac1efba629ebe53fd2363b0af2a7b4f9a2a79540e53b1f0bbdd08c3cb6fbf"
 dependencies = [
  "dirs 5.0.1",
  "fs4",
@@ -2890,7 +3016,7 @@ dependencies = [
  "starbase_styles 0.4.0",
  "thiserror",
  "tokio",
- "toml 0.8.13",
+ "toml 0.8.14",
  "tracing",
  "url",
  "wax",
@@ -2951,6 +3077,17 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
 
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
 [[package]]
 name = "system-configuration"
 version = "0.5.1"
@@ -2974,9 +3111,9 @@ dependencies = [
 
 [[package]]
 name = "system-interface"
-version = "0.26.1"
+version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0682e006dd35771e392a6623ac180999a9a854b1d4a6c12fb2e804941c2b1f58"
+checksum = "b858526d22750088a9b3cf2e3c2aacebd5377f13adeec02860c30d09113010a6"
 dependencies = [
  "bitflags 2.4.1",
  "cap-fs-ext",
@@ -3001,9 +3138,9 @@ dependencies = [
 
 [[package]]
 name = "target-lexicon"
-version = "0.12.12"
+version = "0.12.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
+checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
 
 [[package]]
 name = "tempfile"
@@ -3055,25 +3192,20 @@ dependencies = [
 ]
 
 [[package]]
-name = "tinyvec"
-version = "1.6.0"
+name = "tinystr"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
 dependencies = [
- "tinyvec_macros",
+ "displaydoc",
+ "zerovec",
 ]
 
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
 [[package]]
 name = "tokio"
-version = "1.37.0"
+version = "1.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
 dependencies = [
  "backtrace",
  "bytes",
@@ -3090,9 +3222,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3135,14 +3267,14 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.8.13"
+version = "0.8.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
+checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
 dependencies = [
  "serde",
  "serde_spanned",
  "toml_datetime",
- "toml_edit 0.22.13",
+ "toml_edit 0.22.14",
 ]
 
 [[package]]
@@ -3167,9 +3299,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.22.13"
+version = "0.22.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
+checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
 dependencies = [
  "indexmap 2.2.6",
  "serde",
@@ -3279,27 +3411,12 @@ version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
-[[package]]
-name = "unicode-bidi"
-version = "0.3.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
-
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
-[[package]]
-name = "unicode-normalization"
-version = "0.1.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
-dependencies = [
- "tinyvec",
-]
-
 [[package]]
 name = "unicode-segmentation"
 version = "1.10.1"
@@ -3342,15 +3459,28 @@ dependencies = [
 
 [[package]]
 name = "url"
-version = "2.5.0"
+version = "2.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
 dependencies = [
  "form_urlencoded",
  "idna",
  "percent-encoding",
+ "serde",
 ]
 
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
 [[package]]
 name = "uuid"
 version = "1.8.0"
@@ -3374,7 +3504,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
 name = "version_spec"
-version = "0.5.0"
+version = "0.5.2"
 dependencies = [
  "human-sort",
  "regex",
@@ -3413,7 +3543,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate"
-version = "0.14.1"
+version = "0.15.0"
 dependencies = [
  "extism",
  "miette",
@@ -3427,7 +3557,7 @@ dependencies = [
  "sha2",
  "starbase_archive",
  "starbase_styles 0.4.0",
- "starbase_utils 0.7.2",
+ "starbase_utils 0.7.5",
  "system_env",
  "thiserror",
  "tracing",
@@ -3436,7 +3566,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate_api"
-version = "0.7.1"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "rustc-hash",
@@ -3449,7 +3579,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate_pdk"
-version = "0.5.1"
+version = "0.6.0"
 dependencies = [
  "extism-pdk",
  "serde",
@@ -3463,13 +3593,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
-name = "wasi-cap-std-sync"
-version = "16.0.0"
+name = "wasi-common"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "154528979a211aa28d969846e883df75705809ed9bcc70aba61460683ea7355b"
+checksum = "3f1ff7fb4a1ce516d349598c62cc95e077b7016a2cc6471548ab066cc3849078"
 dependencies = [
  "anyhow",
- "async-trait",
+ "bitflags 2.4.1",
  "cap-fs-ext",
  "cap-rand",
  "cap-std",
@@ -3477,32 +3607,15 @@ dependencies = [
  "fs-set-times",
  "io-extras",
  "io-lifetimes",
+ "log",
  "once_cell",
  "rustix",
  "system-interface",
- "tracing",
- "wasi-common",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "wasi-common"
-version = "16.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d888b611fee7d273dd057dc009d2dd3132736f36710ffd65657ac83628d1e3b"
-dependencies = [
- "anyhow",
- "bitflags 2.4.1",
- "cap-rand",
- "cap-std",
- "io-extras",
- "log",
- "rustix",
  "thiserror",
  "tracing",
  "wasmtime",
  "wiggle",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -3573,28 +3686,40 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
 
 [[package]]
 name = "wasm-encoder"
-version = "0.38.1"
+version = "0.207.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f"
+checksum = "d996306fb3aeaee0d9157adbe2f670df0236caf19f6728b221e92d0f27b3fe17"
+dependencies = [
+ "leb128",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.210.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e3764d9d6edabd8c9e16195e177be0d20f6ab942ad18af52860f12f82bc59a"
 dependencies = [
  "leb128",
 ]
 
 [[package]]
 name = "wasmparser"
-version = "0.118.1"
+version = "0.207.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ee9723b928e735d53000dec9eae7b07a60e490c85ab54abb66659fc61bfcd9"
+checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c"
 dependencies = [
+ "ahash",
+ "bitflags 2.4.1",
+ "hashbrown 0.14.3",
  "indexmap 2.2.6",
  "semver",
 ]
 
 [[package]]
 name = "wasmprinter"
-version = "0.2.75"
+version = "0.207.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d027eb8294904fc715ac0870cebe6b0271e96b90605ee21511e7565c4ce568c"
+checksum = "9c2d8a7b4dabb460208e6b4334d9db5766e84505038b2529e69c3d07ac619115"
 dependencies = [
  "anyhow",
  "wasmparser",
@@ -3602,77 +3727,94 @@ dependencies = [
 
 [[package]]
 name = "wasmtime"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8e539fded2495422ea3c4dfa7beeddba45904eece182cf315294009e1a323bf"
+checksum = "f92a1370c66a0022e6d92dcc277e2c84f5dece19569670b8ce7db8162560d8b6"
 dependencies = [
+ "addr2line",
  "anyhow",
  "async-trait",
- "bincode",
  "bumpalo",
+ "cc",
  "cfg-if",
  "encoding_rs",
  "fxprof-processed-profile",
+ "gimli",
+ "hashbrown 0.14.3",
  "indexmap 2.2.6",
+ "ittapi",
  "libc",
+ "libm",
  "log",
- "object",
+ "mach2",
+ "memfd",
+ "memoffset",
+ "object 0.33.0",
  "once_cell",
  "paste",
+ "postcard",
+ "psm",
  "rayon",
+ "rustix",
+ "semver",
  "serde",
  "serde_derive",
  "serde_json",
+ "smallvec",
+ "sptr",
  "target-lexicon",
- "wasm-encoder",
+ "wasm-encoder 0.207.0",
  "wasmparser",
+ "wasmtime-asm-macros",
  "wasmtime-cache",
  "wasmtime-component-macro",
  "wasmtime-component-util",
  "wasmtime-cranelift",
  "wasmtime-environ",
  "wasmtime-fiber",
- "wasmtime-jit",
- "wasmtime-runtime",
+ "wasmtime-jit-debug",
+ "wasmtime-jit-icache-coherence",
+ "wasmtime-slab",
+ "wasmtime-versioned-export-macros",
  "wasmtime-winch",
  "wat",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "wasmtime-asm-macros"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "660ba9143e15a2acd921820df221b73aee256bd3ca2d208d73d8adc9587ccbb9"
+checksum = "6dee8679c974a7f258c03d60d3c747c426ed219945b6d08cbc77fd2eab15b2d1"
 dependencies = [
  "cfg-if",
 ]
 
 [[package]]
 name = "wasmtime-cache"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3ce373743892002f9391c6741ef0cb0335b55ec899d874f311222b7e36f4594"
+checksum = "b00103ffaf7ee980f4e750fe272b6ada79d9901659892e457c7ca316b16df9ec"
 dependencies = [
  "anyhow",
  "base64 0.21.5",
- "bincode",
  "directories-next",
  "log",
+ "postcard",
  "rustix",
  "serde",
  "serde_derive",
  "sha2",
- "toml 0.5.11",
- "windows-sys 0.48.0",
- "zstd 0.11.2+zstd.1.5.2",
+ "toml 0.8.14",
+ "windows-sys 0.52.0",
+ "zstd",
 ]
 
 [[package]]
 name = "wasmtime-component-macro"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12ef32643324e564e1c359e9044daa06cbf90d7e2d6c99a738d17a12959f01a5"
+checksum = "32cae30035f1cf97dcc6657c979cf39f99ce6be93583675eddf4aeaa5548509c"
 dependencies = [
  "anyhow",
  "proc-macro2",
@@ -3685,15 +3827,15 @@ dependencies = [
 
 [[package]]
 name = "wasmtime-component-util"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c87d06c18d21a4818f354c00a85f4ebc62b2270961cd022968452b0e4dbed9d"
+checksum = "f7ae611f08cea620c67330925be28a96115bf01f8f393a6cbdf4856a86087134"
 
 [[package]]
 name = "wasmtime-cranelift"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d648c8b4064a7911093b02237cd5569f71ca171d3a0a486bf80600b19e1cba2"
+checksum = "b2909406a6007e28be964067167890bca4574bd48a9ff18f1fa9f4856d89ea40"
 dependencies = [
  "anyhow",
  "cfg-if",
@@ -3705,48 +3847,33 @@ dependencies = [
  "cranelift-wasm",
  "gimli",
  "log",
- "object",
+ "object 0.33.0",
  "target-lexicon",
  "thiserror",
  "wasmparser",
- "wasmtime-cranelift-shared",
  "wasmtime-environ",
  "wasmtime-versioned-export-macros",
 ]
 
-[[package]]
-name = "wasmtime-cranelift-shared"
-version = "16.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290a89027688782da8ff60b12bb95695494b1874e0d0ba2ba387d23dace6d70c"
-dependencies = [
- "anyhow",
- "cranelift-codegen",
- "cranelift-control",
- "cranelift-native",
- "gimli",
- "object",
- "target-lexicon",
- "wasmtime-environ",
-]
-
 [[package]]
 name = "wasmtime-environ"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61eb64fb3e0da883e2df4a13a81d6282e072336e6cb6295021d0f7ab2e352754"
+checksum = "40e227f9ed2f5421473723d6c0352b5986e6e6044fde5410a274a394d726108f"
 dependencies = [
  "anyhow",
+ "cpp_demangle",
  "cranelift-entity",
  "gimli",
  "indexmap 2.2.6",
  "log",
- "object",
+ "object 0.33.0",
+ "postcard",
+ "rustc-demangle",
  "serde",
  "serde_derive",
  "target-lexicon",
- "thiserror",
- "wasm-encoder",
+ "wasm-encoder 0.207.0",
  "wasmparser",
  "wasmprinter",
  "wasmtime-component-util",
@@ -3755,9 +3882,9 @@ dependencies = [
 
 [[package]]
 name = "wasmtime-fiber"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ecf1d3a838b0956b71ad3f8cb80069a228339775bf02dd35d86a5a68bbe443"
+checksum = "42edb392586d07038c1638e854382db916b6ca7845a2e6a7f8dc49e08907acdd"
 dependencies = [
  "anyhow",
  "cc",
@@ -3765,43 +3892,16 @@ dependencies = [
  "rustix",
  "wasmtime-asm-macros",
  "wasmtime-versioned-export-macros",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "wasmtime-jit"
-version = "16.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f485336add49267d8859e8f8084d2d4b9a4b1564496b6f30ba5b168d50c10ceb"
-dependencies = [
- "addr2line",
- "anyhow",
- "bincode",
- "cfg-if",
- "cpp_demangle",
- "gimli",
- "ittapi",
- "log",
- "object",
- "rustc-demangle",
- "rustix",
- "serde",
- "serde_derive",
- "target-lexicon",
- "wasmtime-environ",
- "wasmtime-jit-debug",
- "wasmtime-jit-icache-coherence",
- "wasmtime-runtime",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "wasmtime-jit-debug"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65e119affec40edb2fab9044f188759a00c2df9c3017278d047012a2de1efb4f"
+checksum = "95b26ef7914af0c0e3ca811bdc32f5f66fbba0fd21e1f8563350e8a7951e3598"
 dependencies = [
- "object",
+ "object 0.33.0",
  "once_cell",
  "rustix",
  "wasmtime-versioned-export-macros",
@@ -3809,126 +3909,68 @@ dependencies = [
 
 [[package]]
 name = "wasmtime-jit-icache-coherence"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b6d197fcc34ad32ed440e1f9552fd57d1f377d9699d31dee1b5b457322c1f8a"
+checksum = "afe088f9b56bb353adaf837bf7e10f1c2e1676719dd5be4cac8e37f2ba1ee5bc"
 dependencies = [
+ "anyhow",
  "cfg-if",
  "libc",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
-name = "wasmtime-runtime"
-version = "16.0.0"
+name = "wasmtime-slab"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "794b2bb19b99ef8322ff0dd9fe1ba7e19c41036dfb260b3f99ecce128c42ff92"
-dependencies = [
- "anyhow",
- "cc",
- "cfg-if",
- "encoding_rs",
- "indexmap 2.2.6",
- "libc",
- "log",
- "mach",
- "memfd",
- "memoffset",
- "paste",
- "psm",
- "rustix",
- "sptr",
- "wasm-encoder",
- "wasmtime-asm-macros",
- "wasmtime-environ",
- "wasmtime-fiber",
- "wasmtime-jit-debug",
- "wasmtime-versioned-export-macros",
- "wasmtime-wmemcheck",
- "windows-sys 0.48.0",
-]
+checksum = "4ff75cafffe47b04b036385ce3710f209153525b0ed19d57b0cf44a22d446460"
 
 [[package]]
 name = "wasmtime-types"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d995db8bb56f2cd8d2dc0ed5ffab94ffb435283b0fe6747f80f7aab40b2d06a1"
+checksum = "2f2fa462bfea3220711c84e2b549f147e4df89eeb49b8a2a3d89148f6cc4a8b1"
 dependencies = [
  "cranelift-entity",
  "serde",
  "serde_derive",
- "thiserror",
+ "smallvec",
  "wasmparser",
 ]
 
 [[package]]
 name = "wasmtime-versioned-export-macros"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55c5565959287c21dd0f4277ae3518dd2ae62679f655ee2dbc4396e19d210db"
+checksum = "d4cedc5bfef3db2a85522ee38564b47ef3b7fc7c92e94cacbce99808e63cdd47"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn 2.0.66",
 ]
 
-[[package]]
-name = "wasmtime-wasi"
-version = "16.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccd8370078149d49a3a47e93741553fd79b700421464b6a27ca32718192ab130"
-dependencies = [
- "anyhow",
- "async-trait",
- "bitflags 2.4.1",
- "bytes",
- "cap-fs-ext",
- "cap-net-ext",
- "cap-rand",
- "cap-std",
- "cap-time-ext",
- "fs-set-times",
- "futures",
- "io-extras",
- "io-lifetimes",
- "libc",
- "log",
- "once_cell",
- "rustix",
- "system-interface",
- "thiserror",
- "tokio",
- "tracing",
- "url",
- "wasi-cap-std-sync",
- "wasi-common",
- "wasmtime",
- "wiggle",
- "windows-sys 0.48.0",
-]
-
 [[package]]
 name = "wasmtime-winch"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6f945ff9bad96e0a69973d74f193c19f627c8adbf250e7cb73ae7564b6cc8a"
+checksum = "97b27054fed6be4f3800aba5766f7ef435d4220ce290788f021a08d4fa573108"
 dependencies = [
  "anyhow",
  "cranelift-codegen",
  "gimli",
- "object",
+ "object 0.33.0",
  "target-lexicon",
  "wasmparser",
- "wasmtime-cranelift-shared",
+ "wasmtime-cranelift",
  "wasmtime-environ",
  "winch-codegen",
 ]
 
 [[package]]
 name = "wasmtime-wit-bindgen"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f328b2d4a690270324756e886ed5be3a4da4c00be0eea48253f4595ad068062b"
+checksum = "c936a52ce69c28de2aa3b5fb4f2dbbb2966df304f04cccb7aca4ba56d915fda0"
 dependencies = [
  "anyhow",
  "heck",
@@ -3936,12 +3978,6 @@ dependencies = [
  "wit-parser",
 ]
 
-[[package]]
-name = "wasmtime-wmemcheck"
-version = "16.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67761d8f8c0b3c13a5d34356274b10a40baba67fe9cfabbfc379a8b414e45de2"
-
 [[package]]
 name = "wast"
 version = "35.0.2"
@@ -3953,23 +3989,24 @@ dependencies = [
 
 [[package]]
 name = "wast"
-version = "69.0.1"
+version = "210.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1ee37317321afde358e4d7593745942c48d6d17e0e6e943704de9bbee121e7a"
+checksum = "aa835c59bd615e00f16be65705d85517d40b44b3c831d724e450244685176c3c"
 dependencies = [
+ "bumpalo",
  "leb128",
  "memchr",
  "unicode-width",
- "wasm-encoder",
+ "wasm-encoder 0.210.0",
 ]
 
 [[package]]
 name = "wat"
-version = "1.0.82"
+version = "1.210.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aeb338ee8dee4d4cd05e6426683f21c5087dc7cfc8903e839ccf48d43332da3c"
+checksum = "67faece8487996430c6812be7f8776dc563ca0efcd3db77f8839070480c0d1a6"
 dependencies = [
- "wast 69.0.1",
+ "wast 210.0.0",
 ]
 
 [[package]]
@@ -4005,9 +4042,9 @@ checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
 
 [[package]]
 name = "wiggle"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0afb26cd3269289bb314a361ff0a6685e5ce793b62181a9fe3f81ace15051697"
+checksum = "a89ea6f74ece6d1cfbd089783006b8eb69a0219ca83cad22068f0d9fa9df3f91"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -4020,9 +4057,9 @@ dependencies = [
 
 [[package]]
 name = "wiggle-generate"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cef2868fed7584d2b552fa317104858ded80021d23b073b2d682d3c932a027bd"
+checksum = "36beda94813296ecaf0d91b7ada9da073fd41865ba339bdd3b7764e2e785b8e9"
 dependencies = [
  "anyhow",
  "heck",
@@ -4035,9 +4072,9 @@ dependencies = [
 
 [[package]]
 name = "wiggle-macro"
-version = "16.0.0"
+version = "21.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31ae1ec11a17ea481539ee9a5719a278c9790d974060fbf71db4b2c05378780b"
+checksum = "0b47d2b4442ce93106dba5d1a9c59d5f85b5732878bb3d0598d3c93c0d01b16b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4078,9 +4115,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "winch-codegen"
-version = "0.14.0"
+version = "0.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58e58c236a6abdd9ab454552b4f29e16cfa837a86897c1503313b2e62e7609ec"
+checksum = "1dc69899ccb2da7daa4df31426dcfd284b104d1a85e1dae35806df0c46187f87"
 dependencies = [
  "anyhow",
  "cranelift-codegen",
@@ -4089,6 +4126,7 @@ dependencies = [
  "smallvec",
  "target-lexicon",
  "wasmparser",
+ "wasmtime-cranelift",
  "wasmtime-environ",
 ]
 
@@ -4339,9 +4377,9 @@ dependencies = [
 
 [[package]]
 name = "wit-parser"
-version = "0.13.0"
+version = "0.207.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15df6b7b28ce94b8be39d8df5cb21a08a4f3b9f33b631aedb4aa5776f785ead3"
+checksum = "78c83dab33a9618d86cfe3563cc864deffd08c17efc5db31a3b7cd1edeffe6e1"
 dependencies = [
  "anyhow",
  "id-arena",
@@ -4352,6 +4390,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "unicode-xid",
+ "wasmparser",
 ]
 
 [[package]]
@@ -4366,13 +4405,27 @@ dependencies = [
  "wast 35.0.2",
 ]
 
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
 [[package]]
 name = "xattr"
-version = "0.2.3"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
 dependencies = [
  "libc",
+ "linux-raw-sys",
+ "rustix",
 ]
 
 [[package]]
@@ -4390,37 +4443,104 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
+[[package]]
+name = "yoke"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+ "synstructure",
+]
+
 [[package]]
 name = "zerocopy"
-version = "0.7.29"
+version = "0.7.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d075cf85bbb114e933343e087b92f2146bac0d55b534cbb8188becf0039948e"
+checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
 dependencies = [
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.7.29"
+version = "0.7.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86cd5ca076997b97ef09d3ad65efe811fa68c9e874cb636ccb211223a813b0c2"
+checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn 2.0.66",
 ]
 
+[[package]]
+name = "zerofrom"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+ "synstructure",
+]
+
 [[package]]
 name = "zeroize"
 version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
 
+[[package]]
+name = "zerovec"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.66",
+]
+
 [[package]]
 name = "zip"
-version = "2.1.0"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2568cd0f20e86cd9a7349fe05178f7bd22f22724678448ae5a9bac266df2689"
+checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39"
 dependencies = [
  "arbitrary",
  "crc32fast",
@@ -4447,32 +4567,13 @@ dependencies = [
  "simd-adler32",
 ]
 
-[[package]]
-name = "zstd"
-version = "0.11.2+zstd.1.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
-dependencies = [
- "zstd-safe 5.0.2+zstd.1.5.2",
-]
-
 [[package]]
 name = "zstd"
 version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a"
 dependencies = [
- "zstd-safe 7.1.0",
-]
-
-[[package]]
-name = "zstd-safe"
-version = "5.0.2+zstd.1.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
-dependencies = [
- "libc",
- "zstd-sys",
+ "zstd-safe",
 ]
 
 [[package]]
diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml
index 836f5848f..6346ca777 100644
--- a/plugins/Cargo.toml
+++ b/plugins/Cargo.toml
@@ -5,4 +5,4 @@ members = ["wasm-test"]
 [workspace.dependencies]
 extism-pdk = "1.2.0"
 serde = { version = "1.0.203", features = ["derive"] }
-tokio = { version = "1.37.0", features = ["full"] }
+tokio = { version = "1.38.0", features = ["full"] }
diff --git a/plugins/wasm-test/src/lib.rs b/plugins/wasm-test/src/lib.rs
index 7e999b3cb..3d5304ecc 100644
--- a/plugins/wasm-test/src/lib.rs
+++ b/plugins/wasm-test/src/lib.rs
@@ -208,10 +208,10 @@ pub fn load_versions(Json(_): Json) -> FnResult