Skip to main content

systemprompt_cli/commands/core/content/
status.rs

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