systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::{Context, Result, anyhow};
use clap::Args;
use dialoguer::Select;
use dialoguer::theme::ColorfulTheme;
use std::fs;
use std::path::Path;

use super::types::SkillDeleteOutput;
use crate::CliConfig;
use crate::interactive::{require_confirmation, resolve_required};
use crate::shared::CommandResult;
use systemprompt_logging::CliService;
use systemprompt_models::ProfileBootstrap;

#[derive(Debug, Args)]
pub struct DeleteArgs {
    #[arg(help = "Skill name to delete")]
    pub name: Option<String>,

    #[arg(long, help = "Delete all skills")]
    pub all: bool,

    #[arg(short = 'y', long, help = "Skip confirmation prompts")]
    pub yes: bool,
}

pub fn execute(args: DeleteArgs, config: &CliConfig) -> Result<CommandResult<SkillDeleteOutput>> {
    let skills_path = get_skills_path()?;

    let skills_to_delete: Vec<String> = if args.all {
        list_all_skills(&skills_path)?
    } else {
        let name = resolve_required(args.name, "name", config, || {
            prompt_skill_selection(&skills_path)
        })?;

        let skill_dir = find_skill_dir(&skills_path, &name);
        if skill_dir.is_none() {
            return Err(anyhow!("Skill '{}' not found", name));
        }

        vec![name]
    };

    if skills_to_delete.is_empty() {
        return Ok(CommandResult::text(SkillDeleteOutput {
            deleted: vec![],
            message: "No skills to delete".to_string(),
        })
        .with_title("Delete Skill"));
    }

    let confirm_message = if args.all {
        format!("Delete ALL {} skills?", skills_to_delete.len())
    } else {
        format!("Delete skill '{}'?", skills_to_delete[0])
    };

    require_confirmation(&confirm_message, args.yes, config)?;

    let mut deleted = Vec::new();

    for skill_name in &skills_to_delete {
        CliService::info(&format!("Deleting skill '{}'...", skill_name));

        match delete_skill(&skills_path, skill_name) {
            Ok(()) => {
                CliService::success(&format!("Skill '{}' deleted", skill_name));
                deleted.push(skill_name.clone());
            },
            Err(e) => {
                CliService::error(&format!("Failed to delete skill '{}': {}", skill_name, e));
            },
        }
    }

    let message = match deleted.len() {
        0 => "No skills were deleted".to_string(),
        1 => format!("Skill '{}' deleted successfully", deleted[0]),
        n => format!("{} skill(s) deleted successfully", n),
    };

    let output = SkillDeleteOutput { deleted, message };
    Ok(CommandResult::text(output).with_title("Delete Skill"))
}

fn get_skills_path() -> Result<std::path::PathBuf> {
    let profile = ProfileBootstrap::get().context("Failed to get profile")?;
    Ok(std::path::PathBuf::from(profile.paths.skills()))
}

fn find_skill_dir(skills_path: &Path, name: &str) -> Option<std::path::PathBuf> {
    let direct = skills_path.join(name);
    if direct.exists() && direct.is_dir() {
        return Some(direct);
    }
    None
}

fn delete_skill(skills_path: &Path, name: &str) -> Result<()> {
    let skill_dir =
        find_skill_dir(skills_path, name).ok_or_else(|| anyhow!("Skill '{}' not found", name))?;

    fs::remove_dir_all(&skill_dir)
        .with_context(|| format!("Failed to remove skill directory: {}", skill_dir.display()))
}

fn list_all_skills(skills_path: &Path) -> Result<Vec<String>> {
    if !skills_path.exists() {
        return Ok(Vec::new());
    }

    let mut skills = Vec::new();

    for entry in fs::read_dir(skills_path)? {
        let entry = entry?;
        let path = entry.path();

        if !path.is_dir() {
            continue;
        }

        let has_skill_file = path.join("SKILL.md").exists();

        if has_skill_file {
            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                skills.push(name.to_string());
            }
        }
    }

    skills.sort();
    Ok(skills)
}

fn prompt_skill_selection(skills_path: &Path) -> Result<String> {
    let skills = list_all_skills(skills_path)?;

    if skills.is_empty() {
        return Err(anyhow!("No skills found"));
    }

    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Select skill to delete")
        .items(&skills)
        .default(0)
        .interact()
        .context("Failed to get skill selection")?;

    Ok(skills[selection].clone())
}