systemprompt-cli 0.6.1

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::{Context, Result, anyhow};
use clap::Args;
use std::fs;

use super::create_files::{
    build_skill_config, build_skill_markdown, get_skills_path, sync_skill_to_db,
};
use super::create_prompts::{
    check_normalized_conflicts, prompt_description, prompt_display_name, prompt_name,
    resolve_instructions, title_case, validate_skill_name,
};
use super::types::SkillCreateOutput;
use crate::CliConfig;
use crate::interactive::resolve_required;
use crate::shared::CommandResult;
use systemprompt_identifiers::SkillId;
use systemprompt_logging::CliService;

#[derive(Debug, Args)]
pub struct CreateArgs {
    #[arg(long, help = "Skill name/slug (e.g., greeting_agent)")]
    pub name: Option<String>,

    #[arg(long, help = "Display name for the skill")]
    pub display_name: Option<String>,

    #[arg(long, help = "Description of the skill")]
    pub description: Option<String>,

    #[arg(long, help = "Skill instructions")]
    pub instructions: Option<String>,

    #[arg(long, help = "File containing skill instructions")]
    pub instructions_file: Option<String>,

    #[arg(long, help = "Comma-separated tags")]
    pub tags: Option<String>,

    #[arg(long, help = "Enable the skill (default: true)")]
    pub enabled: Option<bool>,

    #[arg(long, help = "Skip syncing to database after creation")]
    pub no_sync: bool,
}

pub async fn execute(
    args: CreateArgs,
    config: &CliConfig,
) -> Result<CommandResult<SkillCreateOutput>> {
    let name = resolve_required(args.name, "name", config, prompt_name)?;
    validate_skill_name(&name)?;

    let display_name = args.display_name.unwrap_or_else(|| {
        if config.is_interactive() {
            prompt_display_name(&name).unwrap_or_else(|_| title_case(&name))
        } else {
            title_case(&name)
        }
    });

    let description = args.description.unwrap_or_else(|| {
        if config.is_interactive() {
            prompt_description().unwrap_or_else(|e| {
                tracing::warn!(error = %e, "Failed to prompt for description");
                String::new()
            })
        } else {
            String::new()
        }
    });

    let instructions = resolve_instructions(
        args.instructions.as_deref(),
        args.instructions_file.as_deref(),
        config,
    )?;

    let tags: Vec<String> = args.tags.map_or_else(Vec::new, |t| {
        t.split(',').map(|s| s.trim().to_string()).collect()
    });

    let enabled = args.enabled.unwrap_or(true);

    CliService::info(&format!(
        "Creating skill '{}' (display: {})...",
        name, display_name
    ));

    let skills_path = get_skills_path()?;
    check_normalized_conflicts(&name, &skills_path)?;

    let skill_dir = skills_path.join(&name);

    if skill_dir.exists() {
        return Err(anyhow!(
            "Skill directory already exists: {}. Use 'skills edit' to modify.",
            skill_dir.display()
        ));
    }

    fs::create_dir_all(&skill_dir)
        .with_context(|| format!("Failed to create skill directory: {}", skill_dir.display()))?;

    let skill_path = skill_dir.join("SKILL.md");
    let content = build_skill_markdown(&description, &instructions);

    fs::write(&skill_path, &content)
        .with_context(|| format!("Failed to write skill file: {}", skill_path.display()))?;

    let config_content = build_skill_config(&name, &display_name, &description, enabled, &tags);
    let config_path = skill_dir.join("config.yaml");
    fs::write(&config_path, config_content)
        .with_context(|| format!("Failed to write config: {}", config_path.display()))?;

    CliService::success(&format!(
        "Skill '{}' created at {}",
        name,
        skill_path.display()
    ));

    let mut synced_to_db = false;
    if !args.no_sync {
        match sync_skill_to_db().await {
            Ok(()) => {
                CliService::success("Skill synced to database");
                synced_to_db = true;
            },
            Err(e) => {
                CliService::warning(&format!(
                    "Skill created but not synced to database: {}. Run 'skills sync' manually.",
                    e
                ));
            },
        }
    }

    let message = if synced_to_db {
        format!(
            "Skill '{}' created and synced to database at {}",
            name,
            skill_path.display()
        )
    } else {
        format!("Skill '{}' created at {}", name, skill_path.display())
    };

    let output = SkillCreateOutput {
        skill_id: SkillId::new(name.clone()),
        message,
        file_path: skill_path.to_string_lossy().to_string(),
    };

    Ok(CommandResult::text(output).with_title("Skill Created"))
}