Skip to main content

systemprompt_cli/commands/core/content/
verify.rs

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