211 lines
7.2 KiB
Rust
211 lines
7.2 KiB
Rust
|
|
mod cli;
|
||
|
|
mod config;
|
||
|
|
mod download;
|
||
|
|
mod marketplace;
|
||
|
|
mod skill;
|
||
|
|
|
||
|
|
use clap::Parser;
|
||
|
|
use cli::{Cli, Command, MarketplaceCommand};
|
||
|
|
use config::InstallMode;
|
||
|
|
|
||
|
|
fn main() {
|
||
|
|
let cli = Cli::parse();
|
||
|
|
|
||
|
|
if let Err(e) = run(cli) {
|
||
|
|
eprintln!("Error: {e}");
|
||
|
|
std::process::exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(cli: Cli) -> Result<(), String> {
|
||
|
|
match cli.command {
|
||
|
|
Command::Marketplace(cmd) => run_marketplace(cmd),
|
||
|
|
Command::Search(args) => {
|
||
|
|
let results = marketplace::search(args.query.as_deref())?;
|
||
|
|
if results.is_empty() {
|
||
|
|
println!("No skills found.");
|
||
|
|
if args.query.is_none() {
|
||
|
|
println!(
|
||
|
|
"No marketplaces configured, or all marketplaces are empty."
|
||
|
|
);
|
||
|
|
println!("Use `ccsm marketplace add <url>` to add a marketplace.");
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
println!("Found {} skill(s):\n", results.len());
|
||
|
|
for (market, skill) in &results {
|
||
|
|
println!(" {} ({})", skill.name, market);
|
||
|
|
println!(" {}\n", skill.description);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
Command::Install(args) => {
|
||
|
|
let mode = if args.link {
|
||
|
|
Some(InstallMode::Link)
|
||
|
|
} else if args.copy {
|
||
|
|
Some(InstallMode::Copy)
|
||
|
|
} else {
|
||
|
|
None
|
||
|
|
};
|
||
|
|
|
||
|
|
let mode_label = match &mode {
|
||
|
|
Some(InstallMode::Link) => " (link)",
|
||
|
|
Some(InstallMode::Copy) => " (copy)",
|
||
|
|
None => "",
|
||
|
|
};
|
||
|
|
|
||
|
|
let s = skill::install(&args.name, args.marketplace.as_deref(), mode)?;
|
||
|
|
println!(
|
||
|
|
"Installed skill '{}'{}",
|
||
|
|
s.name, mode_label
|
||
|
|
);
|
||
|
|
println!(" Store: {}", s.path.display());
|
||
|
|
println!(" Claude: {}", config::claude_skills_dir().join(&s.name).display());
|
||
|
|
println!(" Remote: {}", s.remote_url);
|
||
|
|
println!(" Branch: {}", s.branch);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
Command::List => {
|
||
|
|
let skills = skill::list()?;
|
||
|
|
if skills.is_empty() {
|
||
|
|
println!("No skills installed.");
|
||
|
|
println!("Store: {}", config::skills_store_dir().display());
|
||
|
|
println!(
|
||
|
|
"Use `ccsm search` to browse available skills, then `ccsm install <name>`."
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
println!("Installed skills ({})\n", skills.len());
|
||
|
|
for s in &skills {
|
||
|
|
let has_manifest = has_skill_manifest(&s.path);
|
||
|
|
let marker = if has_manifest { " " } else { "?" };
|
||
|
|
let mode_str = match &s.mode {
|
||
|
|
Some(InstallMode::Link) => "[link]".to_string(),
|
||
|
|
Some(InstallMode::Copy) => "[copy]".to_string(),
|
||
|
|
None => String::new(),
|
||
|
|
};
|
||
|
|
println!(" {marker} {} {mode_str}", s.name);
|
||
|
|
}
|
||
|
|
if skills
|
||
|
|
.iter()
|
||
|
|
.any(|s| !has_skill_manifest(&s.path))
|
||
|
|
{
|
||
|
|
println!(
|
||
|
|
"\n (?) No SKILL.md or CLAUDE.md found — these may not be valid skill repos."
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
Command::Remove(args) => {
|
||
|
|
skill::remove(&args.name)?;
|
||
|
|
println!("Removed skill '{}'", args.name);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
Command::Update(args) => {
|
||
|
|
let results = skill::update(args.name.as_deref())?;
|
||
|
|
if results.is_empty() {
|
||
|
|
println!("No skills to update.");
|
||
|
|
}
|
||
|
|
for (name, msg) in &results {
|
||
|
|
println!("{name}: {msg}");
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
Command::Info(args) => {
|
||
|
|
let s = skill::info(&args.name)?;
|
||
|
|
println!("Skill: {}", s.name);
|
||
|
|
println!(" Store: {}", s.path.display());
|
||
|
|
println!(
|
||
|
|
" Claude: {}",
|
||
|
|
config::claude_skills_dir().join(&s.name).display()
|
||
|
|
);
|
||
|
|
|
||
|
|
let deploy_status = match &s.mode {
|
||
|
|
Some(InstallMode::Link) => "linked",
|
||
|
|
Some(InstallMode::Copy) => "copied",
|
||
|
|
None => "not deployed",
|
||
|
|
};
|
||
|
|
println!(" Status: {deploy_status}");
|
||
|
|
|
||
|
|
let skill_md = s.path.join("SKILL.md");
|
||
|
|
let claude_md = s.path.join("CLAUDE.md");
|
||
|
|
if skill_md.exists() {
|
||
|
|
println!(" Manifest: SKILL.md");
|
||
|
|
} else if claude_md.exists() {
|
||
|
|
println!(" Manifest: CLAUDE.md");
|
||
|
|
} else {
|
||
|
|
println!(" Manifest: none found");
|
||
|
|
}
|
||
|
|
|
||
|
|
let entries = std::fs::read_dir(&s.path)
|
||
|
|
.map_err(|e| format!("Cannot read skill directory: {e}"))?;
|
||
|
|
let mut files = Vec::new();
|
||
|
|
for entry in entries {
|
||
|
|
let entry = entry.map_err(|e| format!("I/O error: {e}"))?;
|
||
|
|
let fname = entry.file_name().to_string_lossy().to_string();
|
||
|
|
if !fname.starts_with('.') && fname != "target" {
|
||
|
|
let ft = if entry.path().is_dir() { "/" } else { "" };
|
||
|
|
files.push(format!("{fname}{ft}"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !files.is_empty() {
|
||
|
|
files.sort();
|
||
|
|
println!(" Files:");
|
||
|
|
for f in &files {
|
||
|
|
println!(" - {f}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run_marketplace(cmd: MarketplaceCommand) -> Result<(), String> {
|
||
|
|
match cmd {
|
||
|
|
MarketplaceCommand::Add(args) => {
|
||
|
|
marketplace::add(&args.url, args.name.as_deref())?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
MarketplaceCommand::Remove(args) => {
|
||
|
|
marketplace::remove(&args.name)?;
|
||
|
|
println!("Removed marketplace '{}'", args.name);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
MarketplaceCommand::List => {
|
||
|
|
let markets = marketplace::list()?;
|
||
|
|
if markets.is_empty() {
|
||
|
|
println!("No marketplaces configured.");
|
||
|
|
println!(
|
||
|
|
"Use `ccsm marketplace add <url>` to add a skill marketplace."
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
println!("Marketplaces ({})\n", markets.len());
|
||
|
|
for (entry, count) in &markets {
|
||
|
|
let branch = entry.branch.as_deref().unwrap_or("?");
|
||
|
|
println!(
|
||
|
|
" {} ({} skills, branch: {})",
|
||
|
|
entry.name, count, branch
|
||
|
|
);
|
||
|
|
println!(" {}\n", entry.url);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
MarketplaceCommand::Update(args) => {
|
||
|
|
let results = marketplace::update(args.name.as_deref())?;
|
||
|
|
for (name, msg) in &results {
|
||
|
|
println!("{name}: {msg}");
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn has_skill_manifest(path: &std::path::Path) -> bool {
|
||
|
|
path.join("SKILL.md").exists()
|
||
|
|
|| path.join("skill.md").exists()
|
||
|
|
|| path.join("CLAUDE.md").exists()
|
||
|
|
|| path.join("claude.md").exists()
|
||
|
|
}
|