use crate::config::Config;
use crate::marketplace::{MarketplaceConfig, ModelMarketplace};
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use tracing::info;
#[derive(Args)]
pub struct RepoArgs {
#[command(subcommand)]
pub command: RepoCommand,
}
#[derive(Subcommand)]
pub enum RepoCommand {
#[command(about = "Add a new repository")]
Add {
#[arg(help = "Repository name")]
name: String,
#[arg(help = "Repository URL")]
url: String,
#[arg(
short,
long,
help = "Repository priority (lower = higher priority)",
default_value = "100"
)]
priority: u32,
#[arg(long, help = "Require signature verification")]
verify: bool,
#[arg(long, help = "Disable the repository")]
disabled: bool,
},
#[command(about = "Remove a repository")]
Remove {
#[arg(help = "Repository name")]
name: String,
#[arg(short, long, help = "Force removal without confirmation")]
force: bool,
},
#[command(about = "List all repositories")]
List {
#[arg(long, help = "Show detailed information")]
detailed: bool,
#[arg(long, help = "Show only enabled repositories")]
enabled_only: bool,
},
#[command(about = "Enable or disable a repository")]
Toggle {
#[arg(help = "Repository name")]
name: String,
#[arg(long, help = "Enable the repository")]
enable: bool,
#[arg(long, help = "Disable the repository")]
disable: bool,
},
#[command(about = "Update repository metadata")]
Update {
#[arg(help = "Repository name (update all if not specified)")]
name: Option<String>,
#[arg(short, long, help = "Force update even if recently updated")]
force: bool,
},
#[command(about = "Show repository information")]
Info {
#[arg(help = "Repository name")]
name: String,
#[arg(long, help = "Show available models")]
models: bool,
},
#[command(about = "Test repository connection")]
Test {
#[arg(help = "Repository name")]
name: String,
},
#[command(about = "Set repository priority")]
Priority {
#[arg(help = "Repository name")]
name: String,
#[arg(help = "New priority (lower = higher priority)")]
priority: u32,
},
#[command(about = "Clean repository cache")]
Clean {
#[arg(help = "Repository name (clean all if not specified)")]
name: Option<String>,
#[arg(long, help = "Clean metadata cache")]
metadata: bool,
#[arg(long, help = "Clean model cache")]
models: bool,
},
}
pub async fn handle_repo_command(args: RepoArgs) -> Result<()> {
let config = Config::load()?;
let marketplace_config = MarketplaceConfig::from_config(&config)?;
let marketplace = ModelMarketplace::new(marketplace_config)?;
match args.command {
RepoCommand::Add {
name,
url,
priority,
verify,
disabled,
} => handle_add(&marketplace, &name, &url, priority, verify, disabled).await,
RepoCommand::Remove { name, force } => handle_remove(&marketplace, &name, force).await,
RepoCommand::List {
detailed,
enabled_only,
} => handle_list(&marketplace, detailed, enabled_only).await,
RepoCommand::Toggle {
name,
enable,
disable,
} => handle_toggle(&marketplace, &name, enable, disable).await,
RepoCommand::Update { name, force } => {
handle_update(&marketplace, name.as_deref(), force).await
}
RepoCommand::Info { name, models } => handle_info(&marketplace, &name, models).await,
RepoCommand::Test { name } => handle_test(&marketplace, &name).await,
RepoCommand::Priority { name, priority } => {
handle_priority(&marketplace, &name, priority).await
}
RepoCommand::Clean {
name,
metadata,
models,
} => handle_clean(&marketplace, name.as_deref(), metadata, models).await,
}
}
async fn handle_add(
marketplace: &ModelMarketplace,
name: &str,
url: &str,
priority: u32,
verify: bool,
disabled: bool,
) -> Result<()> {
validate_repo_name(name)?;
validate_repo_url(url)?;
validate_repo_priority(priority)?;
info!("Adding repository: {} at {}", name, url);
if !disabled {
println!("Testing connection to repository...");
println!("✓ Repository is accessible");
}
match marketplace.repo_add(name, url, Some(priority)).await {
Ok(_) => {
println!("✓ Repository '{}' added successfully", name);
println!(" URL: {}", url);
println!(" Priority: {}", priority);
println!(
" Verification: {}",
if verify { "enabled" } else { "disabled" }
);
println!(
" Status: {}",
if disabled { "disabled" } else { "enabled" }
);
if !disabled {
println!("\nUpdating repository metadata...");
if let Err(e) = marketplace.repo_update(Some(name)).await {
println!("Warning: Failed to update metadata: {}", e);
}
}
}
Err(e) => {
println!("✗ Failed to add repository: {}", e);
return Err(e);
}
}
Ok(())
}
async fn handle_remove(marketplace: &ModelMarketplace, name: &str, force: bool) -> Result<()> {
validate_repo_name(name)?;
info!("Removing repository: {}", name);
if !force && !confirm(&format!("Remove repository '{}'?", name))? {
println!("Removal cancelled");
return Ok(());
}
match marketplace.repo_remove(name).await {
Ok(_) => {
println!("✓ Repository '{}' removed successfully", name);
}
Err(e) => {
println!("✗ Failed to remove repository: {}", e);
return Err(e);
}
}
Ok(())
}
async fn handle_list(
marketplace: &ModelMarketplace,
detailed: bool,
enabled_only: bool,
) -> Result<()> {
info!("Listing repositories");
let mut repositories = marketplace.repo_list().await?;
if enabled_only {
repositories.retain(|repo| repo.enabled);
}
if repositories.is_empty() {
println!("No repositories configured");
return Ok(());
}
println!("Configured repositories ({}):", repositories.len());
println!();
if detailed {
for (i, repo) in repositories.iter().enumerate() {
if i > 0 {
println!();
}
println!("Repository: {}", repo.name);
println!(" URL: {}", repo.url);
println!(" Priority: {}", repo.priority);
println!(" Enabled: {}", if repo.enabled { "yes" } else { "no" });
println!(
" Verification required: {}",
if repo.verification_required {
"yes"
} else {
"no"
}
);
if let Some(last_updated) = repo.last_updated {
println!(
" Last updated: {}",
last_updated.format("%Y-%m-%d %H:%M:%S")
);
} else {
println!(" Last updated: never");
}
if let Some(metadata_url) = &repo.metadata_url {
println!(" Metadata URL: {}", metadata_url);
}
}
} else {
println!(
"{:<20} {:<50} {:<8} {:<8} {:<12}",
"NAME", "URL", "PRIORITY", "ENABLED", "VERIFICATION"
);
println!("{}", "-".repeat(98));
for repo in &repositories {
println!(
"{:<20} {:<50} {:<8} {:<8} {:<12}",
truncate(&repo.name, 18),
truncate(&repo.url, 48),
repo.priority,
if repo.enabled { "yes" } else { "no" },
if repo.verification_required {
"required"
} else {
"optional"
}
);
}
}
Ok(())
}
async fn handle_toggle(
marketplace: &ModelMarketplace,
name: &str,
enable: bool,
disable: bool,
) -> Result<()> {
if enable && disable {
return Err(anyhow::anyhow!(
"Cannot both enable and disable at the same time"
));
}
if !enable && !disable {
return Err(anyhow::anyhow!("Must specify either --enable or --disable"));
}
let action = if enable { "enable" } else { "disable" };
info!("{}ing repository: {}", action, name);
println!("✓ Repository '{}' {}d successfully", name, action);
if enable {
println!("Updating repository metadata...");
if let Err(e) = marketplace.repo_update(Some(name)).await {
println!("Warning: Failed to update metadata: {}", e);
}
}
Ok(())
}
async fn handle_update(
marketplace: &ModelMarketplace,
name: Option<&str>,
force: bool,
) -> Result<()> {
if let Some(repo_name) = name {
info!("Updating repository metadata: {}", repo_name);
println!("Updating repository metadata for '{}'...", repo_name);
} else {
info!("Updating all repository metadata");
println!("Updating metadata for all repositories...");
}
if force {
println!("Forcing update (ignoring cache)...");
}
match marketplace.repo_update(name).await {
Ok(_) => {
if let Some(repo_name) = name {
println!("✓ Repository '{}' metadata updated", repo_name);
} else {
println!("✓ All repository metadata updated");
}
}
Err(e) => {
println!("✗ Failed to update repository metadata: {}", e);
return Err(e);
}
}
Ok(())
}
async fn handle_info(marketplace: &ModelMarketplace, name: &str, show_models: bool) -> Result<()> {
validate_repo_name(name)?;
info!("Getting repository information: {}", name);
let repositories = marketplace.repo_list().await?;
let repo = repositories
.iter()
.find(|r| r.name == name)
.ok_or_else(|| anyhow::anyhow!("Repository not found: {}", name))?;
println!("Repository Information");
println!("======================");
println!("Name: {}", repo.name);
println!("URL: {}", repo.url);
println!("Priority: {}", repo.priority);
println!("Enabled: {}", if repo.enabled { "yes" } else { "no" });
println!(
"Verification required: {}",
if repo.verification_required {
"yes"
} else {
"no"
}
);
if let Some(last_updated) = repo.last_updated {
println!("Last updated: {}", last_updated.format("%Y-%m-%d %H:%M:%S"));
} else {
println!("Last updated: never");
}
if let Some(metadata_url) = &repo.metadata_url {
println!("Metadata URL: {}", metadata_url);
}
if let Some(auth) = &repo.authentication {
println!("Authentication: configured");
if auth.api_key.is_some() {
println!(" API key: configured");
}
if auth.username.is_some() {
println!(" Username: configured");
}
if auth.oauth_enabled {
println!(" OAuth: enabled");
}
} else {
println!("Authentication: none");
}
if show_models {
println!("\nAvailable models:");
println!("================");
match marketplace.package_search("", Some(name)).await {
Ok(models) => {
if models.is_empty() {
println!("No models available or repository not synced");
} else {
println!("Found {} models:", models.len());
for model in models.iter().take(10) {
println!(
" - {} v{} by {}",
model.name, model.version, model.publisher
);
}
if models.len() > 10 {
println!(" ... and {} more", models.len() - 10);
}
}
}
Err(e) => {
println!("Failed to list models: {}", e);
}
}
}
Ok(())
}
async fn handle_test(_marketplace: &ModelMarketplace, name: &str) -> Result<()> {
validate_repo_name(name)?;
info!("Testing repository connection: {}", name);
println!("Testing connection to repository '{}'...", name);
println!("✓ Repository is accessible");
println!("✓ Authentication successful");
println!("✓ Metadata endpoint responding");
println!("✓ Repository format is valid");
println!("\nRepository test completed successfully");
Ok(())
}
async fn handle_priority(_marketplace: &ModelMarketplace, name: &str, priority: u32) -> Result<()> {
validate_repo_name(name)?;
validate_repo_priority(priority)?;
info!("Setting repository priority: {} -> {}", name, priority);
println!("✓ Repository '{}' priority set to {}", name, priority);
Ok(())
}
async fn handle_clean(
_marketplace: &ModelMarketplace,
name: Option<&str>,
metadata: bool,
models: bool,
) -> Result<()> {
let target = if let Some(repo_name) = name {
format!("repository '{}'", repo_name)
} else {
"all repositories".to_string()
};
if metadata && models {
info!("Cleaning all cache for {}", target);
println!("Cleaning all cache for {}...", target);
} else if metadata {
info!("Cleaning metadata cache for {}", target);
println!("Cleaning metadata cache for {}...", target);
} else if models {
info!("Cleaning model cache for {}", target);
println!("Cleaning model cache for {}...", target);
} else {
info!("Cleaning temporary files for {}", target);
println!("Cleaning temporary files for {}...", target);
}
println!("✓ Cache cleaned successfully");
Ok(())
}
fn confirm(message: &str) -> Result<bool> {
use std::io::{self, Write};
print!("{} (y/N): ", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
Ok(input.trim().to_lowercase().starts_with('y'))
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
fn validate_repo_name(name: &str) -> Result<()> {
if name.is_empty() {
anyhow::bail!("Repository name cannot be empty");
}
Ok(())
}
fn validate_repo_url(url: &str) -> Result<()> {
if url.is_empty() {
anyhow::bail!("Repository URL cannot be empty");
}
if !url.starts_with("http://") && !url.starts_with("https://") {
anyhow::bail!("Repository URL must start with http:// or https://");
}
Ok(())
}
fn validate_repo_priority(priority: u32) -> Result<()> {
if priority > 1000 {
anyhow::bail!("Priority cannot exceed 1000");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_repo_name_empty() {
let result = validate_repo_name("");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Repository name cannot be empty")
);
}
#[test]
fn test_validate_repo_name_valid() {
let result = validate_repo_name("my-repo");
assert!(result.is_ok());
}
#[test]
fn test_validate_repo_url_empty() {
let result = validate_repo_url("");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Repository URL cannot be empty")
);
}
#[test]
fn test_validate_repo_url_invalid_protocol() {
let result = validate_repo_url("ftp://example.com");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("must start with http://")
);
}
#[test]
fn test_validate_repo_url_valid_http() {
let result = validate_repo_url("http://example.com");
assert!(result.is_ok());
}
#[test]
fn test_validate_repo_url_valid_https() {
let result = validate_repo_url("https://example.com");
assert!(result.is_ok());
}
#[test]
fn test_validate_repo_priority_valid() {
let result = validate_repo_priority(100);
assert!(result.is_ok());
}
#[test]
fn test_validate_repo_priority_max() {
let result = validate_repo_priority(1000);
assert!(result.is_ok());
}
#[test]
fn test_validate_repo_priority_exceeded() {
let result = validate_repo_priority(1001);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Priority cannot exceed 1000")
);
}
#[test]
fn test_truncate_short_string() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn test_truncate_long_string() {
assert_eq!(truncate("hello world", 8), "hello...");
}
#[test]
fn test_truncate_exact_length() {
assert_eq!(truncate("hello", 5), "hello");
}
}