netsky 0.2.0

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
use std::env;

use netsky_prompts::skills::{InjectFormat, SkillSummary, inject_skills, list_skills, read_skill};
use serde_json::json;

use crate::cli::{SkillCommand, SkillInjectFormat};

pub fn run(cmd: SkillCommand) -> netsky_core::Result<()> {
    match cmd {
        SkillCommand::Ls { json, verbose } => list(json, verbose),
        SkillCommand::Cat { name } => cat(&name),
        SkillCommand::Inject { csv, format } => inject(&csv, format),
    }
}

fn list(json_output: bool, verbose: bool) -> netsky_core::Result<()> {
    let cwd = env::current_dir()?;
    let skills = prompt_result(list_skills(&cwd))?;
    if json_output {
        println!("{}", serde_json::to_string_pretty(&skills)?);
        return Ok(());
    }
    print_list(&skills, verbose);
    Ok(())
}

fn cat(name: &str) -> netsky_core::Result<()> {
    let cwd = env::current_dir()?;
    let doc = prompt_result(read_skill(&cwd, name))?;
    print!("{}", doc.body);
    Ok(())
}

fn inject(csv: &str, format: SkillInjectFormat) -> netsky_core::Result<()> {
    let cwd = env::current_dir()?;
    let names = parse_csv(csv);
    if format == SkillInjectFormat::Json {
        let injected = prompt_result(inject_skills(&cwd, &names, InjectFormat::Plain))?;
        let envelope = json!({
            "names": injected.names,
            "bytes": injected.bytes,
            "estimated_tokens": injected.estimated_tokens,
            "text": injected.text,
        });
        println!("{}", serde_json::to_string_pretty(&envelope)?);
        return Ok(());
    }

    let injected = prompt_result(inject_skills(&cwd, &names, to_inject_format(format)))?;
    print!("{}", injected.text);
    Ok(())
}

fn prompt_result<T>(result: anyhow::Result<T>) -> netsky_core::Result<T> {
    result.map_err(|err| netsky_core::Error::msg(err.to_string()))
}

fn parse_csv(csv: &str) -> Vec<String> {
    csv.split(',')
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .map(ToOwned::to_owned)
        .collect()
}

fn to_inject_format(format: SkillInjectFormat) -> InjectFormat {
    match format {
        SkillInjectFormat::Plain => InjectFormat::Plain,
        SkillInjectFormat::Claude => InjectFormat::Claude,
        SkillInjectFormat::Codex => InjectFormat::Codex,
        SkillInjectFormat::Json => InjectFormat::Plain,
    }
}

fn print_list(skills: &[SkillSummary], verbose: bool) {
    if skills.is_empty() {
        return;
    }
    if verbose {
        let name_width = skills
            .iter()
            .map(|skill| skill.name.len())
            .max()
            .unwrap_or(4);
        for skill in skills {
            println!(
                "{name:<name_width$}  {tokens:>6} tok  {bytes:>6} B  {support:>2} support  {path}  {description}",
                name = skill.name,
                tokens = skill.estimated_tokens,
                bytes = skill.bytes,
                support = skill.support_files.len(),
                path = skill.path.display(),
                description = skill.description,
            );
        }
        return;
    }

    let name_width = skills
        .iter()
        .map(|skill| skill.name.len())
        .max()
        .unwrap_or(4);
    for skill in skills {
        println!(
            "{name:<name_width$}  {tokens:>6} tok  {bytes:>6} B  {description}",
            name = skill.name,
            tokens = skill.estimated_tokens,
            bytes = skill.bytes,
            description = skill.description,
        );
    }
}