skillctl 0.1.2

CLI to manage your personal agent skills library across projects
use anyhow::Result;
use serde_json::json;

use crate::cli::ListArgs;
use crate::commands::shared::matches_tags;
use crate::config;
use crate::context::Context;
use crate::error::AppError;
use crate::git;
use crate::skill;

pub fn run(args: ListArgs, ctx: &Context) -> Result<()> {
    let cfg = config::load()?;
    let library = cfg.library.ok_or_else(|| {
        AppError::Config("no library configured — run `skillctl init<github-url>` first".into())
    })?;

    let repo =
        config::library_cache_path(&library.url).map_err(|e| AppError::Config(e.to_string()))?;
    if !repo.exists() {
        return Err(AppError::Config(format!(
            "library cache not found at {} — run `skillctl init{}` again",
            repo.display(),
            library.url
        ))
        .into());
    }

    if let Err(e) = git::fetch_and_fast_forward(&repo)
        && !ctx.json
    {
        eprintln!("warning: could not refresh library cache ({e}); using cached version");
    }

    let skills = skill::discover(&repo)?;
    let filtered: Vec<_> = skills
        .into_iter()
        .filter(|s| matches_tags(&s.tags, &args.tags, args.all_tags))
        .collect();

    if ctx.json {
        let entries: Vec<_> = filtered
            .iter()
            .map(|s| {
                json!({
                    "name": s.name,
                    "path": s.path.strip_prefix(&repo).unwrap_or(&s.path).display().to_string(),
                    "description": s.description,
                    "tags": s.tags,
                })
            })
            .collect();
        let out = json!({
            "command": "list",
            "library": library.url,
            "skills": entries,
        });
        println!("{out}");
        return Ok(());
    }

    if filtered.is_empty() {
        if args.tags.is_empty() {
            println!("no skills found in {}", library.url);
        } else {
            println!(
                "no skills match the requested tag(s): {}",
                args.tags.join(", ")
            );
        }
        return Ok(());
    }

    for s in filtered {
        let tags_suffix = if s.tags.is_empty() {
            String::new()
        } else {
            format!(" [{}]", s.tags.join(", "))
        };
        match &s.description {
            Some(desc) => println!("  {}{tags_suffix}{desc}", s.name),
            None => println!("  {}{tags_suffix}", s.name),
        }
    }
    Ok(())
}