use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use runx_runtime::ConfigError;
use runx_runtime::registry::{
AcquireOptions, FileRegistryStore, IngestSkillOptions, InstallCandidate,
InstallLocalSkillOptions, LocalRegistryClient, PublishSkillMarkdownOptions, RegistryClient,
RegistryManifestSourceAuthority, RegistryPackageFile, RegistryResolveOptions,
RegistrySearchOptions, RegistrySkillResolution, TrustTier, TrustedRegistryManifestKey,
install_local_skill, publish_skill_markdown, read_registry_skill, resolve_registry_skill,
search_registry_with_options,
};
use runx_runtime::scaffold::{InitGeneratedValues, ensure_runx_install_state};
mod output;
mod package;
mod remote_publish;
mod target;
pub(crate) use target::{
RegistryTarget, destination_root, official_skills_cache_root, registry_skills_cache_root,
registry_source_description, resolve_registry_target,
};
#[derive(Debug, Eq, PartialEq)]
pub enum RegistryAction {
Search,
Read,
Resolve,
Install,
Publish,
}
#[derive(Debug, Eq, PartialEq)]
pub struct RegistryPlan {
pub action: RegistryAction,
pub subject: String,
pub registry: Option<String>,
pub registry_dir: Option<PathBuf>,
pub version: Option<String>,
pub expected_digest: Option<String>,
pub destination: Option<PathBuf>,
pub owner: Option<String>,
pub profile: Option<PathBuf>,
pub trust_tier: Option<TrustTier>,
pub limit: Option<usize>,
pub upsert: bool,
pub json: bool,
}
pub fn run_native_registry(plan: RegistryPlan) -> ExitCode {
let json = plan.json;
match run_registry(plan) {
Ok(output) => crate::cli_io::write_stdout_code(&output.stdout, output.exit_code),
Err(error) => {
if json {
return crate::cli_io::write_stdout_code(
&crate::router::json_failure_output(&error.message, error.code()),
error.exit_code,
);
}
let _ignored = crate::cli_io::write_stderr(&format!("\n ✗ {}\n\n", error.message));
ExitCode::from(error.exit_code)
}
}
}
struct RegistryCliOutput {
stdout: String,
exit_code: u8,
}
fn run_registry(plan: RegistryPlan) -> Result<RegistryCliOutput, RegistryCliError> {
let env = env_map();
let cwd = env::current_dir().map_err(|error| internal_error(error.to_string()))?;
let target = resolve_registry_target(&plan, &env, &cwd);
match plan.action {
RegistryAction::Search => run_search(plan, target),
RegistryAction::Read => run_read(plan, target),
RegistryAction::Resolve => run_resolve(plan, target),
RegistryAction::Install => run_install(plan, target, &env, &cwd),
RegistryAction::Publish => run_publish(plan, target, &env, &cwd),
}
}
fn run_search(
plan: RegistryPlan,
target: RegistryTarget,
) -> Result<RegistryCliOutput, RegistryCliError> {
let source = target.label();
let query = plan.subject;
let results = match target {
RegistryTarget::Remote { registry_url } => RegistryClient::new(®istry_url)?
.search_with_limit(&query, plan.limit.unwrap_or(20))?,
RegistryTarget::Local {
registry_path,
registry_url,
..
} => search_registry_with_options(
&FileRegistryStore::new(registry_path),
&query,
RegistrySearchOptions {
limit: plan.limit,
registry_url,
},
)?,
};
let human = output::render_search(&query, source, &results);
output::write_output(
plan.json,
&output::RegistryEnvelope {
status: "success",
registry: output::RegistryPayload::Search {
source,
query: query.clone(),
results,
},
},
|| human,
)
}
fn run_read(
plan: RegistryPlan,
target: RegistryTarget,
) -> Result<RegistryCliOutput, RegistryCliError> {
let source = target.label();
let skill = match target {
RegistryTarget::Remote { registry_url } => RegistryClient::new(®istry_url)?
.read(&plan.subject, plan.version.as_deref())?
.ok_or_else(|| not_found(&plan.subject))?,
RegistryTarget::Local {
registry_path,
registry_url,
..
} => read_registry_skill(
&FileRegistryStore::new(registry_path),
&plan.subject,
plan.version.as_deref(),
registry_url.as_deref(),
)?
.ok_or_else(|| not_found(&plan.subject))?,
};
let human = output::render_read(source, &plan.subject, &skill);
output::write_output(
plan.json,
&output::RegistryEnvelope {
status: "success",
registry: output::RegistryPayload::Read {
source,
r#ref: plan.subject,
skill: Box::new(skill),
},
},
|| human,
)
}
fn run_resolve(
plan: RegistryPlan,
target: RegistryTarget,
) -> Result<RegistryCliOutput, RegistryCliError> {
let source = target.label();
let resolution = match target {
RegistryTarget::Remote { registry_url } => {
let client = RegistryClient::new(®istry_url)?;
let resolved = client
.resolve_ref(&plan.subject, plan.version.as_deref())?
.ok_or_else(|| not_found(&plan.subject))?;
let detail = client
.read(&resolved.skill_id, resolved.version.as_deref())?
.ok_or_else(|| not_found(&resolved.skill_id))?;
output::RemoteOrLocalResolution::Remote(Box::new(detail))
}
RegistryTarget::Local {
registry_path,
registry_url,
..
} => output::RemoteOrLocalResolution::Local(Box::new(
resolve_registry_skill(
&FileRegistryStore::new(registry_path),
&plan.subject,
RegistryResolveOptions {
version: plan.version,
registry_url,
},
)?
.ok_or_else(|| not_found(&plan.subject))?,
)),
};
let human = output::render_resolve(source, &plan.subject, &resolution);
output::write_output(
plan.json,
&output::RegistryEnvelope {
status: "success",
registry: output::RegistryPayload::Resolve {
source,
r#ref: plan.subject,
resolution,
},
},
|| human,
)
}
fn run_install(
plan: RegistryPlan,
target: RegistryTarget,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Result<RegistryCliOutput, RegistryCliError> {
let source = target.label();
let source_authority = target.manifest_source_authority();
let (candidate, acquisition) = install_candidate(&plan, target, env, cwd)?;
let install = install_local_skill(
&candidate,
&InstallLocalSkillOptions {
destination_root: destination_root(&plan, env, cwd),
expected_digest: plan.expected_digest,
trusted_manifest_keys: trusted_manifest_keys_from_env_for_source(
env,
source_authority,
)?,
},
)?;
let receipt_metadata = runx_runtime::registry_install_receipt_metadata(
runx_runtime::RegistryInstallMetadataInput {
candidate: &candidate,
install: &install,
acquisition: acquisition.as_ref(),
},
);
let human = output::render_install(
source,
&plan.subject,
&install,
candidate.signed_manifest.as_ref(),
);
output::write_output(
plan.json,
&output::RegistryEnvelope {
status: "success",
registry: output::RegistryPayload::Install {
source,
r#ref: plan.subject,
install: Box::new(install),
receipt_metadata,
},
},
|| human,
)
}
fn run_publish(
plan: RegistryPlan,
target: RegistryTarget,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Result<RegistryCliOutput, RegistryCliError> {
match target {
RegistryTarget::Remote { registry_url } => {
let package = package::read_skill_package(
&plan.subject,
plan.profile.as_deref(),
env,
cwd,
true,
)?;
let harness = package::run_publish_harness(
package.harness_path.as_deref(),
&package.harness_fixture_paths,
);
if let Some(temp_dir) = package.harness_temp_dir.as_ref() {
let _ignored = fs::remove_dir_all(temp_dir);
}
let harness = harness?;
let result = remote_publish::publish_remote_skill_package(
®istry_url,
&plan,
&package,
&harness,
env,
cwd,
)?;
output::write_output(
plan.json,
&output::RegistryEnvelope {
status: "success",
registry: output::RegistryPayload::Publish {
publish: output::PublishPayload::Hosted(Box::new(result)),
},
},
|| "\n registry publish success\n\n".to_owned(),
)
}
RegistryTarget::Local {
registry_path,
registry_url,
..
} => {
let package = package::read_skill_package(
&plan.subject,
plan.profile.as_deref(),
env,
cwd,
true,
)?;
let harness = package::run_publish_harness(
package.harness_path.as_deref(),
&package.harness_fixture_paths,
);
if let Some(temp_dir) = package.harness_temp_dir.as_ref() {
let _ignored = fs::remove_dir_all(temp_dir);
}
let harness = harness?;
let result = publish_skill_markdown(
&LocalRegistryClient::new(FileRegistryStore::new(registry_path)),
&package.markdown,
PublishSkillMarkdownOptions {
ingest: IngestSkillOptions {
owner: plan.owner,
version: plan.version,
profile_document: package.profile_document,
package_files: package.package_files.into_iter().map(Into::into).collect(),
trust_tier: plan.trust_tier,
upsert: plan.upsert,
..IngestSkillOptions::default()
},
registry_url,
harness,
},
)?;
output::write_output(
plan.json,
&output::RegistryEnvelope {
status: "success",
registry: output::RegistryPayload::Publish {
publish: output::PublishPayload::Local(Box::new(result)),
},
},
|| "\n registry publish success\n\n".to_owned(),
)
}
}
}
pub(crate) fn install_candidate(
plan: &RegistryPlan,
target: RegistryTarget,
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Result<
(
InstallCandidate,
Option<runx_runtime::registry::AcquiredRegistrySkill>,
),
RegistryCliError,
> {
let source_authority = target.manifest_source_authority();
match target {
RegistryTarget::Remote { registry_url } => {
let installation_id = remote_installation_id(env, cwd)?;
let acquired = RegistryClient::new(®istry_url)?.acquire(
&plan.subject,
AcquireOptions {
installation_id: &installation_id,
version: plan.version.as_deref(),
channel: Some("cli"),
},
)?;
Ok((
candidate_from_acquired(&plan.subject, &acquired, source_authority),
Some(acquired),
))
}
RegistryTarget::Local {
registry_path,
registry_url,
..
} => {
let resolution = resolve_registry_skill(
&FileRegistryStore::new(registry_path),
&plan.subject,
RegistryResolveOptions {
version: plan.version.clone(),
registry_url,
},
)?
.ok_or_else(|| not_found(&plan.subject))?;
Ok((
candidate_from_resolution(&plan.subject, resolution, source_authority),
None,
))
}
}
}
fn remote_installation_id(
env: &BTreeMap<String, String>,
cwd: &Path,
) -> Result<String, RegistryCliError> {
if let Some(installation_id) = env
.get("RUNX_INSTALLATION_ID")
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Ok(installation_id.to_owned());
}
let generated = InitGeneratedValues::generate();
let home = runx_runtime::resolve_runx_global_home_dir(env, cwd);
let state = ensure_runx_install_state(&home, &generated.installation_id, &generated.created_at)
.map_err(|error| usage_error(error.to_string()))?;
Ok(state.state.installation_id)
}
fn candidate_from_resolution(
registry_ref: &str,
resolution: RegistrySkillResolution,
source_authority: RegistryManifestSourceAuthority,
) -> InstallCandidate {
InstallCandidate {
markdown: resolution.markdown,
profile_document: resolution.profile_document,
package_files: resolution.package_files,
package_digest: resolution.package_digest,
source: resolution.source,
source_label: resolution.source_label,
r#ref: registry_ref.to_owned(),
skill_id: Some(resolution.skill_id),
version: Some(resolution.version),
signed_manifest: resolution.signed_manifest,
profile_digest: resolution.profile_digest,
runner_names: resolution.runner_names,
trust_tier: Some(resolution.trust_tier),
manifest_source_authority: Some(source_authority),
}
}
fn candidate_from_acquired(
registry_ref: &str,
acquired: &runx_runtime::registry::AcquiredRegistrySkill,
source_authority: RegistryManifestSourceAuthority,
) -> InstallCandidate {
InstallCandidate {
markdown: acquired.markdown.clone(),
profile_document: acquired.profile_document.clone(),
package_files: acquired.package_files.clone(),
package_digest: acquired.package_digest.clone(),
source: "runx-registry".to_owned(),
source_label: "runx registry".to_owned(),
r#ref: registry_ref.to_owned(),
skill_id: Some(acquired.skill_id.clone()),
version: Some(acquired.version.clone()),
signed_manifest: acquired.signed_manifest.clone(),
profile_digest: acquired.profile_digest.clone(),
runner_names: acquired.runner_names.clone(),
trust_tier: Some(acquired.trust_tier.clone()),
manifest_source_authority: Some(source_authority),
}
}
impl From<package::HostedSkillPackageFile> for RegistryPackageFile {
fn from(file: package::HostedSkillPackageFile) -> Self {
Self {
path: file.path,
content: file.content,
}
}
}
fn resolve_path(
path: &Path,
env: &BTreeMap<String, String>,
cwd: &Path,
prefer_existing: bool,
) -> PathBuf {
if path.is_absolute() {
return path.to_path_buf();
}
runx_runtime::resolve_path_from_user_input(
&path.display().to_string(),
env,
cwd,
prefer_existing,
)
}
pub(crate) fn trusted_manifest_keys_from_env_for_source(
env: &BTreeMap<String, String>,
source_authority: RegistryManifestSourceAuthority,
) -> Result<Vec<TrustedRegistryManifestKey>, RegistryCliError> {
runx_runtime::registry::trusted_registry_manifest_keys_from_env_with_source(
env,
Some(source_authority),
)
.map_err(trust_env_error)
}
fn trust_env_error(
error: runx_runtime::registry::RegistryManifestTrustEnvError,
) -> RegistryCliError {
match error {
runx_runtime::registry::RegistryManifestTrustEnvError::InvalidKey => {
internal_error(error.to_string())
}
runx_runtime::registry::RegistryManifestTrustEnvError::MissingKeyId => {
usage_error(error.to_string())
}
runx_runtime::registry::RegistryManifestTrustEnvError::MissingOwner => {
usage_error(error.to_string())
}
runx_runtime::registry::RegistryManifestTrustEnvError::MissingSource => {
usage_error(error.to_string())
}
}
}
pub(crate) fn env_map() -> BTreeMap<String, String> {
crate::cli_io::env_map()
}
pub(crate) struct RegistryCliError {
message: String,
exit_code: u8,
}
impl std::fmt::Display for RegistryCliError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::fmt::Debug for RegistryCliError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RegistryCliError")
.field("message", &self.message)
.field("exit_code", &self.exit_code)
.finish()
}
}
impl std::error::Error for RegistryCliError {}
impl RegistryCliError {
pub(crate) fn into_message(self) -> String {
self.message
}
fn code(&self) -> &'static str {
if self.exit_code == 64 {
"invalid_args"
} else {
"registry_error"
}
}
}
fn usage_error(message: impl Into<String>) -> RegistryCliError {
RegistryCliError {
message: message.into(),
exit_code: 64,
}
}
fn internal_error(message: impl Into<String>) -> RegistryCliError {
RegistryCliError {
message: message.into(),
exit_code: 1,
}
}
fn not_found(registry_ref: &str) -> RegistryCliError {
RegistryCliError {
message: format!("registry skill not found: {registry_ref}"),
exit_code: 1,
}
}
impl From<runx_runtime::registry::RegistryClientError> for RegistryCliError {
fn from(error: runx_runtime::registry::RegistryClientError) -> Self {
internal_error(error.to_string())
}
}
impl From<runx_runtime::registry::RegistryResolveError> for RegistryCliError {
fn from(error: runx_runtime::registry::RegistryResolveError) -> Self {
internal_error(error.to_string())
}
}
impl From<runx_runtime::registry::LocalRegistryError> for RegistryCliError {
fn from(error: runx_runtime::registry::LocalRegistryError) -> Self {
internal_error(error.to_string())
}
}
impl From<runx_runtime::registry::InstallError> for RegistryCliError {
fn from(error: runx_runtime::registry::InstallError) -> Self {
let error_kind = match &error {
runx_runtime::registry::InstallError::UnsignedManifest(_) => Some("unsigned_manifest"),
runx_runtime::registry::InstallError::UnknownManifestKey { .. } => Some("unknown_key"),
runx_runtime::registry::InstallError::InvalidManifestSignature { .. } => {
Some("invalid_signature")
}
runx_runtime::registry::InstallError::DigestMismatch { .. } => Some("digest_mismatch"),
_ => None,
};
match error_kind {
Some(kind) => internal_error(format!("registry install {kind}: {error}")),
None => internal_error(error.to_string()),
}
}
}
impl From<ConfigError> for RegistryCliError {
fn from(error: ConfigError) -> Self {
internal_error(error.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use runx_runtime::registry::{InstallLocalSkillResult, InstallStatus, TrustTier};
#[test]
fn registry_install_render_shows_direct_skill_run_command() {
let rendered = output::render_install(
"local",
"acme/echo@1.2.3",
&InstallLocalSkillResult {
status: InstallStatus::Installed,
destination: PathBuf::from("/tmp/runx/skills/acme/echo/SKILL.md"),
skill_name: "echo".to_owned(),
source: "local".to_owned(),
source_label: "local registry".to_owned(),
skill_id: Some("acme/echo".to_owned()),
version: Some("1.2.3".to_owned()),
digest: "sha256:abc".to_owned(),
profile_digest: None,
profile_state_path: None,
runner_names: Vec::new(),
trust_tier: Some(TrustTier::Community),
},
None,
);
assert!(rendered.contains("next runx skill acme/echo@1.2.3"));
}
}