use anyhow::{bail, Result};
use clap::{Args, Subcommand, ValueEnum};
use offline_registry::{llms, Registry};
use serde_json::json;
use crate::output;
#[derive(Subcommand)]
pub enum RegistryCommand {
List(ListArgs),
Search(SearchArgs),
Show(ShowArgs),
ExportLlms(ExportArgs),
}
#[derive(Args)]
pub struct ListArgs {
#[arg(value_enum, default_value = "packages")]
pub kind: Kind,
#[arg(long)]
pub json: bool,
}
#[derive(Clone, Copy, ValueEnum)]
pub enum Kind {
Packages,
Workflows,
Skills,
Templates,
Capabilities,
}
#[derive(Args)]
pub struct SearchArgs {
pub query: String,
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct ShowArgs {
pub name: String,
#[arg(long)]
pub json: bool,
}
#[derive(Args)]
pub struct ExportArgs {
#[arg(long)]
pub full: bool,
#[arg(long)]
pub out: Option<std::path::PathBuf>,
}
pub fn run(cmd: RegistryCommand) -> Result<()> {
let registry = Registry::load_default()?;
match cmd {
RegistryCommand::List(args) => list(®istry, args),
RegistryCommand::Search(args) => search(®istry, args),
RegistryCommand::Show(args) => show(®istry, args),
RegistryCommand::ExportLlms(args) => export(®istry, args),
}
}
fn list(registry: &Registry, args: ListArgs) -> Result<()> {
match args.kind {
Kind::Packages => {
if args.json {
return output::json(®istry.packages().collect::<Vec<_>>());
}
output::heading("Packages");
for p in registry.packages() {
output::item(
&p.name,
&format!(
"v{} [{}] — {}",
p.version,
p.platforms.join(", "),
p.description
),
);
}
}
Kind::Workflows => {
if args.json {
return output::json(®istry.workflows().collect::<Vec<_>>());
}
output::heading("Workflows");
for w in registry.workflows() {
output::item(&w.name, &w.description);
}
}
Kind::Skills => {
if args.json {
let skills: Vec<_> = registry
.skills()
.map(
|s| json!({"name": s.name, "title": s.title, "description": s.description}),
)
.collect();
return output::json(&skills);
}
output::heading("Skills");
for s in registry.skills() {
output::item(&s.name, &s.description);
}
}
Kind::Templates => {
if args.json {
return output::json(®istry.templates().collect::<Vec<_>>());
}
output::heading("Templates");
for t in registry.templates() {
output::item(&t.name, &format!("[{}] {}", t.platform, t.description));
}
}
Kind::Capabilities => {
let caps = registry.capabilities();
if args.json {
return output::json(&caps);
}
output::heading("Capabilities");
for c in caps {
output::item(&c.name, &format!("packages: {}", c.packages.join(", ")));
}
}
}
Ok(())
}
fn search(registry: &Registry, args: SearchArgs) -> Result<()> {
let hits = registry.search_all(&args.query, 20);
if args.json {
return output::json(&hits);
}
if hits.is_empty() {
output::warn(&format!("no matches for `{}`", args.query));
return Ok(());
}
output::heading(&format!("Results for `{}`", args.query));
for h in hits {
output::item(
&format!("[{}] {}", h.kind, h.name),
&truncate(&h.description, 100),
);
}
Ok(())
}
fn show(registry: &Registry, args: ShowArgs) -> Result<()> {
if let Some(p) = registry
.package(&args.name)
.or_else(|| registry.package(&format!("@offline-protocol/{}", args.name)))
{
if args.json {
return output::json(p);
}
println!("{}", serde_json::to_string_pretty(p)?);
return Ok(());
}
if let Some(w) = registry.workflow(&args.name) {
if args.json {
return output::json(w);
}
println!("{}", serde_json::to_string_pretty(w)?);
return Ok(());
}
if let Some(t) = registry.template(&args.name) {
if args.json {
return output::json(t);
}
println!("{}", serde_json::to_string_pretty(t)?);
return Ok(());
}
bail!(
"`{}` not found — try `offline registry search {}`",
args.name,
args.name
);
}
fn export(registry: &Registry, args: ExportArgs) -> Result<()> {
let text = if args.full {
llms::llms_full_txt(registry)
} else {
llms::llms_txt(registry)
};
match args.out {
Some(path) => {
std::fs::write(&path, text)?;
output::success(&format!("wrote {}", path.display()));
}
None => print!("{text}"),
}
Ok(())
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let cut: String = s.chars().take(max).collect();
format!("{cut}…")
}
}