Skip to main content

systemprompt_cli/commands/core/content/
verify.rs

1use super::types::VerifyOutput;
2use crate::context::CommandContext;
3use crate::shared::CommandOutput;
4use anyhow::{Result, anyhow};
5use clap::Args;
6use std::path::PathBuf;
7use systemprompt_content::ContentRepository;
8use systemprompt_identifiers::{ContentId, LocaleCode, SourceId};
9
10#[derive(Debug, Args)]
11pub struct VerifyArgs {
12    #[arg(help = "Content slug or ID")]
13    pub identifier: String,
14
15    #[arg(long, help = "Source ID (required when using slug)")]
16    pub source: Option<String>,
17
18    #[arg(long, help = "Web dist directory to check for prerendered HTML")]
19    pub web_dist: Option<PathBuf>,
20
21    #[arg(long, help = "Base URL to check HTTP status")]
22    pub base_url: Option<String>,
23
24    #[arg(long, help = "URL pattern (e.g., /{source}/{slug})")]
25    pub url_pattern: Option<String>,
26}
27
28pub async fn execute(args: VerifyArgs, ctx: &CommandContext) -> Result<CommandOutput> {
29    let pool = ctx.db_pool().await?;
30    let repo = ContentRepository::new(&pool)?;
31
32    let content = if uuid::Uuid::parse_str(&args.identifier).is_ok() {
33        let id = ContentId::new(args.identifier.clone());
34        repo.get_by_id(&id)
35            .await?
36            .ok_or_else(|| anyhow!("Content not found: {}", args.identifier))?
37    } else {
38        let source_id = args
39            .source
40            .as_ref()
41            .ok_or_else(|| anyhow!("--source required when using slug"))?;
42        let source = SourceId::new(source_id.clone());
43        repo.get_by_source_and_slug(&source, &args.identifier, &LocaleCode::new("en"))
44            .await?
45            .ok_or_else(|| {
46                anyhow!(
47                    "Content not found: {} in source {}",
48                    args.identifier,
49                    source_id
50                )
51            })?
52    };
53
54    let url_pattern = args
55        .url_pattern
56        .unwrap_or_else(|| format!("/{}/{{}}", content.source_id.as_str()));
57    let expected_url = url_pattern.replace("{slug}", &content.slug);
58    let expected_url = expected_url.replace("{}", &content.slug);
59
60    let (prerendered, prerender_path) = args.web_dist.as_ref().map_or((None, None), |dist_dir| {
61        let html_path = dist_dir.join(format!(
62            "{}/index.html",
63            expected_url.trim_start_matches('/')
64        ));
65        let exists = html_path.exists();
66        (Some(exists), Some(html_path.to_string_lossy().to_string()))
67    });
68
69    let http_status = match &args.base_url {
70        Some(base_url) => {
71            let full_url = format!("{}{}", base_url.trim_end_matches('/'), expected_url);
72            reqwest::Client::new()
73                .head(&full_url)
74                .timeout(std::time::Duration::from_secs(5))
75                .send()
76                .await
77                .ok()
78                .map(|response| response.status().as_u16())
79        },
80        None => None,
81    };
82
83    let output = VerifyOutput {
84        content_id: content.id.clone(),
85        slug: content.slug,
86        source_id: content.source_id.clone(),
87        in_database: true,
88        is_public: content.public,
89        url: expected_url,
90        prerendered,
91        prerender_path,
92        http_status,
93        template: Some(content.kind),
94        last_updated: content.updated_at,
95    };
96
97    Ok(CommandOutput::card_value("Content Verification", &output))
98}