Skip to main content

systemprompt_cli/commands/core/content/
verify.rs

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