use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
pub struct Args {
#[command(subcommand)]
command: MarketplaceCommands,
}
#[derive(Subcommand, Debug)]
pub(crate) enum MarketplaceCommands {
List {
#[arg(long)]
registry: Option<String>,
},
Install {
name: String,
#[arg(long)]
registry: Option<String>,
},
Search {
query: String,
},
Info {
name: String,
},
AddRegistry {
url: String,
},
RemoveRegistry {
url: String,
},
ListRegistries,
}
#[allow(dead_code)]
struct MarketSkill {
name: &'static str,
description: &'static str,
url: &'static str,
author: &'static str,
tags: &'static [&'static str],
}
const MARKET_SKILLS: &[MarketSkill] = &[
MarketSkill {
name: "rust-expert",
description: "Advanced Rust patterns, unsafe guidelines, async best practices",
url: "https://github.com/ekhodzitsky/omk-skill-rust",
author: "@ekhodzitsky",
tags: &["rust", "systems"],
},
MarketSkill {
name: "web-dev",
description: "Full-stack web development with React, Node, and TypeScript",
url: "https://github.com/ekhodzitsky/omk-skill-web",
author: "@ekhodzitsky",
tags: &["web", "typescript", "react"],
},
MarketSkill {
name: "devops",
description: "Docker, Kubernetes, CI/CD pipelines, infrastructure as code",
url: "https://github.com/ekhodzitsky/omk-skill-devops",
author: "@ekhodzitsky",
tags: &["devops", "docker", "k8s"],
},
MarketSkill {
name: "security",
description: "Security audit patterns, vulnerability assessment, secure coding",
url: "https://github.com/ekhodzitsky/omk-skill-security",
author: "@ekhodzitsky",
tags: &["security", "audit"],
},
];
pub(crate) async fn run(args: Args) -> Result<()> {
match args.command {
MarketplaceCommands::List { registry } => list_skills(registry).await,
MarketplaceCommands::Install { name, registry } => install_skill(&name, registry).await,
MarketplaceCommands::Search { query } => search_skills(&query).await,
MarketplaceCommands::Info { name } => info_skill(&name).await,
MarketplaceCommands::AddRegistry { url } => add_registry(&url).await,
MarketplaceCommands::RemoveRegistry { url } => remove_registry(&url).await,
MarketplaceCommands::ListRegistries => list_registries().await,
}
}
async fn list_skills(registry_override: Option<String>) -> Result<()> {
println!("🛒 omk Marketplace\n");
println!("Built-in skills:");
println!("{:<4} {:<20} {:<50} Tags", "#", "Name", "Description");
println!("{}", "─".repeat(100));
for (i, skill) in MARKET_SKILLS.iter().enumerate() {
let tags = skill.tags.join(", ");
println!(
"{:<4} {:<20} {:<50} {}",
i + 1,
skill.name,
skill.description.chars().take(48).collect::<String>(),
tags
);
}
let config = crate::runtime::config::load_config().await?;
let registries = if let Some(url) = registry_override {
vec![url]
} else {
config.registries.clone()
};
if !registries.is_empty() {
println!("\nExternal registries:");
match crate::marketplace::load_all_skills(®istries).await {
Ok(skills) => {
if skills.is_empty() {
println!(" (no skills found)");
} else {
for (i, (registry_name, skill)) in skills.iter().enumerate() {
let tags = skill.tags.join(", ");
println!(
" {:<4} {:<20} {:<40} {} (from {})",
i + 1,
skill.name,
skill.description.chars().take(38).collect::<String>(),
tags,
registry_name
);
}
}
}
Err(e) => {
println!(" Error loading registries: {}", e);
}
}
}
println!("\nInstall with: omk marketplace install <name>");
Ok(())
}
async fn install_skill(name: &str, registry_override: Option<String>) -> Result<()> {
let builtin = if let Ok(idx) = name.parse::<usize>() {
MARKET_SKILLS.get(idx.saturating_sub(1))
} else {
MARKET_SKILLS.iter().find(|s| s.name == name)
};
if let Some(skill) = builtin {
println!("Installing '{}' from built-in marketplace...", skill.name);
return crate::cli::skill::run(crate::cli::skill::Args {
command: crate::cli::skill::SkillCommands::Install {
url: skill.url.to_string(),
name: Some(skill.name.to_string()),
},
})
.await;
}
let config = crate::runtime::config::load_config().await?;
let registries = if let Some(url) = registry_override {
vec![url]
} else {
config.registries.clone()
};
if !registries.is_empty() {
let skills = crate::marketplace::load_all_skills(®istries).await?;
if let Some((_, skill)) = skills.into_iter().find(|(_, s)| s.name == name) {
println!("Installing '{}' from external registry...", skill.name);
return crate::cli::skill::run(crate::cli::skill::Args {
command: crate::cli::skill::SkillCommands::Install {
url: skill.url,
name: Some(skill.name),
},
})
.await;
}
}
anyhow::bail!(
"Skill '{}' not found in marketplace or configured registries",
name
)
}
async fn search_skills(query: &str) -> Result<()> {
let query_lower = query.to_lowercase();
let builtin_matches: Vec<_> = MARKET_SKILLS
.iter()
.filter(|s| {
s.name.to_lowercase().contains(&query_lower)
|| s.description.to_lowercase().contains(&query_lower)
|| s.tags
.iter()
.any(|t| t.to_lowercase().contains(&query_lower))
})
.collect();
let config = crate::runtime::config::load_config().await?;
let external_matches = if !config.registries.is_empty() {
match crate::marketplace::load_all_skills(&config.registries).await {
Ok(skills) => skills
.into_iter()
.filter(|(_, s)| {
s.name.to_lowercase().contains(&query_lower)
|| s.description.to_lowercase().contains(&query_lower)
|| s.tags
.iter()
.any(|t| t.to_lowercase().contains(&query_lower))
})
.collect(),
Err(_) => vec![],
}
} else {
vec![]
};
let total = builtin_matches.len() + external_matches.len();
if total == 0 {
println!("No skills found for '{}'", query);
return Ok(());
}
println!("Found {} skill(s) for '{}':\n", total, query);
for (i, skill) in builtin_matches.iter().enumerate() {
println!(
" {}. {} — {} [built-in]",
i + 1,
skill.name,
skill.description
);
}
for (i, (registry, skill)) in external_matches.iter().enumerate() {
println!(
" {}. {} — {} [{}]",
builtin_matches.len() + i + 1,
skill.name,
skill.description,
registry
);
}
Ok(())
}
async fn info_skill(name: &str) -> Result<()> {
if let Some(skill) = MARKET_SKILLS.iter().find(|s| s.name == name) {
println!("Skill: {} [built-in]", skill.name);
println!("Description: {}", skill.description);
println!("Author: {}", skill.author);
println!("URL: {}", skill.url);
println!("Tags: {}", skill.tags.join(", "));
return Ok(());
}
let config = crate::runtime::config::load_config().await?;
if !config.registries.is_empty() {
let skills = crate::marketplace::load_all_skills(&config.registries).await?;
if let Some((registry, skill)) = skills.into_iter().find(|(_, s)| s.name == name) {
println!("Skill: {}", skill.name);
println!("Registry: {}", registry);
println!("Description: {}", skill.description);
println!("Author: {}", skill.author);
println!("URL: {}", skill.url);
println!("Tags: {}", skill.tags.join(", "));
return Ok(());
}
}
anyhow::bail!(
"Skill '{}' not found in marketplace or configured registries",
name
)
}
async fn add_registry(url: &str) -> Result<()> {
if !url.starts_with("http://")
&& !url.starts_with("https://")
&& !url.starts_with('/')
&& !url.starts_with("./")
{
anyhow::bail!(
"Invalid registry URL '{}'. Must start with http://, https://, or be an absolute/relative file path",
url
);
}
let mut config = crate::runtime::config::load_config().await?;
if config.registries.contains(&url.to_string()) {
println!("Registry '{}' is already configured.", url);
return Ok(());
}
println!("Validating registry '{}'...", url);
let _ = if url.starts_with("http://") || url.starts_with("https://") {
crate::marketplace::MarketplaceRegistry::fetch(url).await
} else {
crate::marketplace::MarketplaceRegistry::fetch_file(std::path::Path::new(url)).await
}
.context(
"Failed to validate registry. Make sure the URL is accessible and returns valid JSON.",
)?;
config.registries.push(url.to_string());
let config_path = crate::runtime::config::config_dir().join("config.toml");
let content = toml::to_string_pretty(&config).context("Failed to serialize config")?;
crate::runtime::atomic::atomic_write(&config_path, content.as_bytes()).await?;
println!("✓ Added registry '{}'", url);
Ok(())
}
async fn remove_registry(url: &str) -> Result<()> {
let mut config = crate::runtime::config::load_config().await?;
let before = config.registries.len();
config.registries.retain(|r| r != url);
if config.registries.len() == before {
println!("Registry '{}' was not found in configuration.", url);
return Ok(());
}
let config_path = crate::runtime::config::config_dir().join("config.toml");
let content = toml::to_string_pretty(&config).context("Failed to serialize config")?;
crate::runtime::atomic::atomic_write(&config_path, content.as_bytes()).await?;
println!("✓ Removed registry '{}'", url);
Ok(())
}
async fn list_registries() -> Result<()> {
let config = crate::runtime::config::load_config().await?;
if config.registries.is_empty() {
println!("No external registries configured.");
println!("Add one with: omk marketplace add-registry <url>");
return Ok(());
}
println!("Configured registries:\n");
for (i, url) in config.registries.iter().enumerate() {
println!(" {}. {}", i + 1, url);
}
Ok(())
}