Skip to main content

systemprompt_cli/commands/core/content/
status.rs

1//! `core content status` command summarising sources.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use super::types::{ContentStatusRow, StatusOutput};
7use crate::cli_settings::CliConfig;
8use crate::shared::CommandOutput;
9use anyhow::Result;
10use clap::Args;
11use std::path::PathBuf;
12use systemprompt_content::ContentRepository;
13use systemprompt_database::DbPool;
14use systemprompt_identifiers::{LocaleCode, SourceId};
15
16use crate::context::CommandContext;
17
18#[derive(Debug, Args)]
19pub struct StatusArgs {
20    #[arg(long, help = "Filter by source ID")]
21    pub source: 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 = "URL pattern (e.g., /{source}/{slug})")]
27    pub url_pattern: Option<String>,
28
29    #[arg(long, default_value = "50")]
30    pub limit: i64,
31}
32
33pub async fn execute(args: StatusArgs, ctx: &CommandContext) -> Result<CommandOutput> {
34    execute_with_pool(args, &ctx.db_pool().await?, &ctx.cli).await
35}
36
37pub async fn execute_with_pool(
38    args: StatusArgs,
39    pool: &DbPool,
40    _config: &CliConfig,
41) -> Result<CommandOutput> {
42    let repo = ContentRepository::new(pool)?;
43
44    let source = SourceId::new(args.source.clone());
45    let contents = repo.list_by_source(&source, &LocaleCode::new("en")).await?;
46
47    let url_pattern = args
48        .url_pattern
49        .unwrap_or_else(|| format!("/{}/{{}}", args.source));
50
51    let mut items = Vec::with_capacity(contents.len().min(args.limit as usize));
52    let mut healthy = 0i64;
53    let mut issues = 0i64;
54
55    for content in contents.into_iter().take(args.limit as usize) {
56        let expected_url = url_pattern.replace("{slug}", &content.slug);
57        let expected_url = expected_url.replace("{}", &content.slug);
58
59        let prerendered = args.web_dist.as_ref().map(|dist_dir| {
60            let html_path = dist_dir.join(format!(
61                "{}/index.html",
62                expected_url.trim_start_matches('/')
63            ));
64            html_path.exists()
65        });
66
67        let is_healthy = content.public && prerendered.unwrap_or(true);
68        if is_healthy {
69            healthy += 1;
70        } else {
71            issues += 1;
72        }
73
74        items.push(ContentStatusRow {
75            slug: content.slug,
76            title: content.title,
77            in_database: true,
78            is_public: content.public,
79            prerendered,
80            http_status: None,
81            last_updated: content.updated_at,
82        });
83    }
84
85    let total = items.len() as i64;
86
87    let output = StatusOutput {
88        items,
89        source_id: source,
90        total,
91        healthy,
92        issues,
93    };
94
95    Ok(CommandOutput::table_of(
96        vec!["slug", "title", "is_public", "prerendered", "last_updated"],
97        &output.items,
98    )
99    .with_title("Content Status"))
100}