systemprompt-cli 0.2.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 regex::Regex;
use std::collections::HashSet;
use std::fs;
use std::io::{BufRead, BufReader};

use crate::CliConfig;
use crate::interactive::resolve_required;
use crate::shared::CommandResult;
use dialoguer::Select;
use dialoguer::theme::ColorfulTheme;

use super::super::paths::WebPaths;
use super::super::types::{TemplateDetailOutput, TemplatesConfig};

#[derive(Debug, Args)]
pub struct ShowArgs {
    #[arg(help = "Template name")]
    pub name: Option<String>,

    #[arg(long, help = "Number of preview lines", default_value = "20")]
    pub preview_lines: usize,
}

pub fn execute(args: ShowArgs, config: &CliConfig) -> Result<CommandResult<TemplateDetailOutput>> {
    let web_paths = WebPaths::resolve()?;
    let templates_dir = &web_paths.templates;
    let templates_yaml_path = templates_dir.join("templates.yaml");

    let content = fs::read_to_string(&templates_yaml_path).with_context(|| {
        format!(
            "Failed to read templates config at {}",
            templates_yaml_path.display()
        )
    })?;

    let templates_config: TemplatesConfig = serde_yaml::from_str(&content).with_context(|| {
        format!(
            "Failed to parse templates config at {}",
            templates_yaml_path.display()
        )
    })?;

    let name = resolve_required(args.name, "name", config, || {
        prompt_template_selection(&templates_config)
    })?;

    let entry = templates_config
        .templates
        .get(&name)
        .ok_or_else(|| anyhow!("Template '{}' not found", name))?;

    let file_path = templates_dir.join(format!("{}.html", name));
    let file_exists = file_path.exists();

    let (variables, preview_lines) = if file_exists {
        let file_content = fs::read_to_string(&file_path)
            .with_context(|| format!("Failed to read template file at {}", file_path.display()))?;

        let variables = extract_template_variables(&file_content);

        let file = fs::File::open(&file_path)?;
        let reader = BufReader::new(file);
        let preview: Vec<String> = reader
            .lines()
            .take(args.preview_lines)
            .collect::<Result<Vec<_>, _>>()
            .context("Failed to read preview lines")?;

        (variables, preview)
    } else {
        (vec![], vec![])
    };

    let output = TemplateDetailOutput {
        name: name.clone(),
        content_types: entry.content_types.clone(),
        file_path: file_path.to_string_lossy().to_string(),
        file_exists,
        variables,
        preview_lines,
    };

    Ok(CommandResult::card(output).with_title(format!("Template: {}", name)))
}

fn extract_template_variables(content: &str) -> Vec<String> {
    let Ok(re) = Regex::new(r"\{\{([A-Z_]+)\}\}") else {
        return vec![];
    };
    let mut variables: HashSet<String> = HashSet::new();

    for cap in re.captures_iter(content) {
        if let Some(matched) = cap.get(1) {
            variables.insert(matched.as_str().to_string());
        }
    }

    let mut sorted: Vec<String> = variables.into_iter().collect();
    sorted.sort();
    sorted
}

fn prompt_template_selection(config: &TemplatesConfig) -> Result<String> {
    let mut names: Vec<&String> = config.templates.keys().collect();
    names.sort();

    if names.is_empty() {
        return Err(anyhow!("No templates configured"));
    }

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

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