use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use runx_runtime::registry::RegistryManifestSourceAuthority;
use url::Url;
use super::RegistryPlan;
#[derive(Clone, Debug)]
pub(crate) enum RegistryTarget {
Remote {
registry_url: String,
},
Local {
registry_path: PathBuf,
registry_url: Option<String>,
source_kind: LocalRegistrySourceKind,
},
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum LocalRegistrySourceKind {
Local,
File,
}
impl RegistryTarget {
pub(crate) fn label(&self) -> &'static str {
match self {
Self::Remote { .. } => "remote",
Self::Local { source_kind, .. } => match source_kind {
LocalRegistrySourceKind::Local => "local",
LocalRegistrySourceKind::File => "file",
},
}
}
pub(crate) fn fingerprint_source(&self) -> String {
match self {
Self::Remote { registry_url } => {
format!("remote:{}", canonical_remote_registry_url(registry_url))
}
Self::Local {
registry_path,
source_kind,
..
} => {
let absolute =
fs::canonicalize(registry_path).unwrap_or_else(|_| registry_path.to_path_buf());
match source_kind {
LocalRegistrySourceKind::Local => format!("local:{}", absolute.display()),
LocalRegistrySourceKind::File => format!("file:{}", absolute.display()),
}
}
}
}
pub(crate) fn manifest_source_authority(&self) -> RegistryManifestSourceAuthority {
match self {
Self::Remote { registry_url } => {
runx_runtime::registry::registry_manifest_source_authority_from_registry_url(
registry_url,
)
}
Self::Local {
registry_path,
source_kind,
..
} => {
let absolute =
fs::canonicalize(registry_path).unwrap_or_else(|_| registry_path.to_path_buf());
RegistryManifestSourceAuthority::RegistrySource(match source_kind {
LocalRegistrySourceKind::Local => format!("local:{}", absolute.display()),
LocalRegistrySourceKind::File => format!("file:{}", absolute.display()),
})
}
}
}
}
pub(crate) fn resolve_registry_target(
plan: &RegistryPlan,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> RegistryTarget {
let configured_registry = plan
.registry
.as_deref()
.or_else(|| env.get("RUNX_REGISTRY_URL").map(String::as_str));
if let Some(target) = explicit_registry_target(plan, env, cwd) {
return target;
}
if let Some(target) = explicit_registry_dir_target(plan, configured_registry, env, cwd) {
return target;
}
if let Some(target) = env_registry_dir_target(configured_registry, env, cwd) {
return target;
}
if let Some(registry) = configured_registry.filter(|value| is_remote_registry_url(value)) {
return RegistryTarget::Remote {
registry_url: registry.to_owned(),
};
}
default_local_registry_target(configured_registry, env, cwd)
}
fn explicit_registry_target(
plan: &RegistryPlan,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Option<RegistryTarget> {
let registry = plan.registry.as_ref()?;
if is_remote_registry_url(registry) {
return Some(RegistryTarget::Remote {
registry_url: registry.clone(),
});
}
Some(RegistryTarget::Local {
registry_path: registry_path_from_value(registry, env, cwd),
registry_url: env
.get("RUNX_REGISTRY_URL")
.filter(|value| is_remote_registry_url(value))
.cloned(),
source_kind: registry_source_kind(registry),
})
}
fn explicit_registry_dir_target(
plan: &RegistryPlan,
configured_registry: Option<&str>,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Option<RegistryTarget> {
let registry_dir = plan.registry_dir.as_ref()?;
Some(local_registry_target(
super::resolve_path(registry_dir, env, cwd, false),
remote_registry_url(configured_registry),
))
}
fn env_registry_dir_target(
configured_registry: Option<&str>,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Option<RegistryTarget> {
let registry_dir = env.get("RUNX_REGISTRY_DIR")?;
Some(local_registry_target(
runx_runtime::resolve_path_from_user_input(registry_dir, env, cwd, false),
remote_registry_url(configured_registry),
))
}
fn default_local_registry_target(
configured_registry: Option<&str>,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> RegistryTarget {
local_registry_target(
runx_runtime::resolve_runx_global_home_dir(env, cwd).join("registry"),
configured_registry,
)
}
fn local_registry_target(
registry_path: PathBuf,
configured_registry: Option<&str>,
) -> RegistryTarget {
RegistryTarget::Local {
registry_path,
registry_url: configured_registry.map(ToOwned::to_owned),
source_kind: LocalRegistrySourceKind::Local,
}
}
pub(crate) fn destination_root(
plan: &RegistryPlan,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> PathBuf {
plan.destination
.as_ref()
.map(|path| super::resolve_path(path, env, cwd, false))
.unwrap_or_else(|| runx_runtime::resolve_runx_workspace_base(env, cwd).join("skills"))
}
pub(crate) fn official_skills_cache_root(env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
env.get("RUNX_OFFICIAL_SKILLS_DIR")
.map(|value| runx_runtime::resolve_path_from_user_input(value, env, cwd, false))
.unwrap_or_else(|| {
runx_runtime::resolve_runx_global_home_dir(env, cwd).join("official-skills")
})
}
pub(crate) fn registry_skills_cache_root(env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
runx_runtime::resolve_runx_global_home_dir(env, cwd).join("registry-skills")
}
pub(crate) fn registry_source_description(target: &RegistryTarget) -> String {
match target {
RegistryTarget::Remote { registry_url } => {
format!("remote {}", canonical_remote_registry_url(registry_url))
}
RegistryTarget::Local {
registry_path,
source_kind,
..
} => match source_kind {
LocalRegistrySourceKind::Local => format!("local {}", registry_path.display()),
LocalRegistrySourceKind::File => format!("file {}", registry_path.display()),
},
}
}
fn registry_path_from_value(value: &str, env: &BTreeMap<String, String>, cwd: &Path) -> PathBuf {
if let Some(path) = file_url_path(value) {
return path;
}
runx_runtime::resolve_path_from_user_input(value, env, cwd, false)
}
fn registry_source_kind(registry: &str) -> LocalRegistrySourceKind {
if file_url_path(registry).is_some() {
LocalRegistrySourceKind::File
} else {
LocalRegistrySourceKind::Local
}
}
fn remote_registry_url(value: Option<&str>) -> Option<&str> {
value.filter(|entry| is_remote_registry_url(entry))
}
fn file_url_path(value: &str) -> Option<PathBuf> {
let url = Url::parse(value).ok()?;
if url.scheme() != "file" {
return None;
}
url.to_file_path().ok()
}
fn canonical_remote_registry_url(value: &str) -> String {
let without_fragment = value.split_once('#').map_or(value, |(prefix, _)| prefix);
let without_query = without_fragment
.split_once('?')
.map_or(without_fragment, |(prefix, _)| prefix);
let Some((scheme, rest)) = without_query.split_once("://") else {
return without_query.trim_end_matches('/').to_owned();
};
let (authority, path) = rest
.split_once('/')
.map_or((rest, ""), |(authority, path)| (authority, path));
let authority = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host);
if path.is_empty() {
format!("{scheme}://{authority}")
} else {
format!("{scheme}://{authority}/{}", path.trim_end_matches('/'))
}
}
fn is_remote_registry_url(value: &str) -> bool {
value.starts_with("https://") || value.starts_with("http://")
}