systemprompt_cli/commands/web/templates/
show.rs1use 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}