skillhub 0.2.0

CLI for SkillHub - AI Agent Skill Registry
use crate::client::ApiClient;
use crate::config::load;
use crate::error::Result;
use clap::Parser;
use colored::Colorize;
use std::path::PathBuf;

#[derive(Parser, Debug)]
pub struct InstallArgs {
    pub skill_spec: String,

    #[arg(short, long)]
    pub output: Option<PathBuf>,
}

fn parse_skill_spec(spec: &str) -> Result<(String, String, Option<String>)> {
    let parts: Vec<&str> = spec.split('/').collect();
    if parts.len() < 2 {
        return Err(crate::error::CliError::Parse(format!(
            "Invalid skill spec. Expected: namespace/slug[@version], got: {}",
            spec
        )));
    }

    let namespace = parts[0].to_string();
    let slug_and_ver = parts[1];
    let (slug, version) = if let Some(idx) = slug_and_ver.find('@') {
        (
            slug_and_ver[..idx].to_string(),
            Some(slug_and_ver[idx + 1..].to_string()),
        )
    } else {
        (slug_and_ver.to_string(), None)
    };

    Ok((namespace, slug, version))
}

pub async fn run(args: InstallArgs) -> Result<()> {
    let config = load()?;
    let token = config.auth.token.ok_or(crate::error::CliError::NotAuthenticated)?;

    let (namespace, slug, version_opt) = parse_skill_spec(&args.skill_spec)?;

    let client = ApiClient::new(config.registry.url, Some(token));

    let version = if let Some(v) = version_opt {
        v
    } else {
        let detail = client.get_skill_detail(&namespace, &slug).await?;
        detail["latest_version"]
            .as_str()
            .ok_or_else(|| crate::error::CliError::Parse("No latest version found".to_string()))?
            .to_string()
    };

    println!(
        "{}",
        format!(
            "Downloading {} {} {}...",
            namespace.cyan(),
            slug.bright_white(),
            format!("(v{})", version.dimmed())
        )
        .bright_cyan()
    );

    let bytes = client
        .download_bundle(&namespace, &slug, &version)
        .await?;

    let output_dir = args.output.unwrap_or_else(|| PathBuf::from("."));
    let filename = format!("{}-{}-{}.zip", namespace, slug, version);
    let output_path = output_dir.join(&filename);

    std::fs::write(&output_path, bytes)?;

    println!(
        "{} {}",
        "Downloaded to:".green(),
        output_path.display().to_string().bright_white()
    );

    Ok(())
}