Skip to main content

systemprompt_cli/commands/web/templates/
show.rs

1//! `web templates show` command listing template variables.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result, anyhow};
7use clap::Args;
8use regex::Regex;
9use std::collections::HashSet;
10use std::fs;
11use std::io::{BufRead, BufReader};
12use std::path::Path;
13
14use crate::CliConfig;
15use crate::interactive::{Prompter, resolve_required};
16use crate::shared::CommandOutput;
17
18use super::super::paths::WebPaths;
19use super::super::types::{TemplateDetailOutput, TemplatesConfig};
20use super::selection::prompt_template_selection;
21
22#[derive(Debug, Args)]
23pub struct ShowArgs {
24    #[arg(help = "Template name")]
25    pub name: Option<String>,
26
27    #[arg(long, help = "Number of preview lines", default_value = "20")]
28    pub preview_lines: usize,
29}
30
31pub(super) fn execute(
32    args: ShowArgs,
33    prompter: &dyn Prompter,
34    config: &CliConfig,
35) -> Result<CommandOutput> {
36    execute_in_dir(args, prompter, config, &WebPaths::resolve()?.templates)
37}
38
39pub fn execute_in_dir(
40    args: ShowArgs,
41    prompter: &dyn Prompter,
42    config: &CliConfig,
43    templates_dir: &Path,
44) -> Result<CommandOutput> {
45    let templates_yaml_path = templates_dir.join("templates.yaml");
46
47    let content = fs::read_to_string(&templates_yaml_path).with_context(|| {
48        format!(
49            "Failed to read templates config at {}",
50            templates_yaml_path.display()
51        )
52    })?;
53
54    let templates_config: TemplatesConfig = serde_yaml::from_str(&content).with_context(|| {
55        format!(
56            "Failed to parse templates config at {}",
57            templates_yaml_path.display()
58        )
59    })?;
60
61    let name = resolve_required(args.name, "name", config, || {
62        prompt_template_selection(prompter, &templates_config, "Select template")
63    })?;
64
65    let entry = templates_config
66        .templates
67        .get(&name)
68        .ok_or_else(|| anyhow!("Template '{}' not found", name))?;
69
70    let file_path = templates_dir.join(format!("{}.html", name));
71    let file_exists = file_path.exists();
72
73    let (variables, preview_lines) = if file_exists {
74        let file_content = fs::read_to_string(&file_path)
75            .with_context(|| format!("Failed to read template file at {}", file_path.display()))?;
76
77        let variables = extract_template_variables(&file_content);
78
79        let file = fs::File::open(&file_path)
80            .with_context(|| format!("Failed to open template file at {}", file_path.display()))?;
81        let reader = BufReader::new(file);
82        let preview: Vec<String> = reader
83            .lines()
84            .take(args.preview_lines)
85            .collect::<Result<Vec<_>, _>>()
86            .context("Failed to read preview lines")?;
87
88        (variables, preview)
89    } else {
90        (vec![], vec![])
91    };
92
93    let output = TemplateDetailOutput {
94        name: name.clone(),
95        content_types: entry.content_types.clone(),
96        file_path: file_path.to_string_lossy().to_string(),
97        file_exists,
98        variables,
99        preview_lines,
100    };
101
102    Ok(CommandOutput::card_value(
103        format!("Template: {}", name),
104        &output,
105    ))
106}
107
108fn extract_template_variables(content: &str) -> Vec<String> {
109    let Ok(re) = Regex::new(r"\{\{([A-Z_]+)\}\}") else {
110        return vec![];
111    };
112    let mut variables: HashSet<String> = HashSet::new();
113
114    for cap in re.captures_iter(content) {
115        if let Some(matched) = cap.get(1) {
116            variables.insert(matched.as_str().to_owned());
117        }
118    }
119
120    let mut sorted: Vec<String> = variables.into_iter().collect();
121    sorted.sort();
122    sorted
123}