Skip to main content

systemprompt_cli/commands/core/content/
publish.rs

1use super::types::{PublishPipelineOutput, StepResult};
2use crate::cli_settings::CliConfig;
3use crate::shared::CommandResult;
4use anyhow::Result;
5use clap::{Args, ValueEnum};
6use std::sync::Arc;
7use std::time::Instant;
8use systemprompt_content::ContentIngestionJob;
9use systemprompt_generator::{
10    generate_sitemap, prerender_content, prerender_homepage, CopyExtensionAssetsJob,
11};
12use systemprompt_runtime::AppContext;
13
14#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
15pub enum PublishStep {
16    /// Ingest markdown from configured sources
17    Ingest,
18    /// Copy extension assets (CSS, JS) to web dist
19    Assets,
20    /// Generate static HTML pages
21    Prerender,
22    /// Generate homepage
23    Homepage,
24    /// Generate sitemap.xml
25    Sitemap,
26    /// Run all steps (default)
27    All,
28}
29
30#[derive(Debug, Args)]
31pub struct PublishArgs {
32    #[arg(long, short = 's', value_enum, help = "Run specific step(s) only")]
33    pub step: Option<Vec<PublishStep>>,
34
35    #[arg(long, help = "Skip content ingestion step")]
36    pub skip_ingest: bool,
37
38    #[arg(long, help = "Show verbose output")]
39    pub verbose: bool,
40}
41
42impl PublishArgs {
43    fn should_run(&self, step: PublishStep) -> bool {
44        self.step.as_ref().map_or_else(
45            || {
46                if step == PublishStep::Ingest {
47                    !self.skip_ingest
48                } else {
49                    true
50                }
51            },
52            |steps| {
53                if steps.contains(&PublishStep::All) {
54                    if step == PublishStep::Ingest {
55                        !self.skip_ingest
56                    } else {
57                        true
58                    }
59                } else {
60                    steps.contains(&step)
61                }
62            },
63        )
64    }
65}
66
67pub async fn execute(
68    args: PublishArgs,
69    _config: &CliConfig,
70) -> Result<CommandResult<PublishPipelineOutput>> {
71    let start_time = Instant::now();
72    let verbose = args.verbose;
73
74    let ctx = AppContext::new().await?;
75    let db_pool = ctx.db_pool();
76
77    let mut steps: Vec<StepResult> = Vec::new();
78
79    if args.should_run(PublishStep::Ingest) {
80        let step_start = Instant::now();
81        if verbose {
82            tracing::info!("Starting content ingestion...");
83        }
84
85        let result = ContentIngestionJob::execute_ingestion(db_pool).await;
86        let duration_ms = step_start.elapsed().as_millis() as u64;
87
88        steps.push(StepResult {
89            step: "ingest".to_string(),
90            success: result.is_ok(),
91            duration_ms,
92            message: result.err().map(|e| e.to_string()),
93        });
94    }
95
96    if args.should_run(PublishStep::Assets) {
97        let step_start = Instant::now();
98        if verbose {
99            tracing::info!("Starting extension asset copy...");
100        }
101
102        let result = CopyExtensionAssetsJob::execute_copy().await;
103        let duration_ms = step_start.elapsed().as_millis() as u64;
104
105        steps.push(StepResult {
106            step: "assets".to_string(),
107            success: result.is_ok(),
108            duration_ms,
109            message: result.err().map(|e| e.to_string()),
110        });
111    }
112
113    if args.should_run(PublishStep::Prerender) {
114        let step_start = Instant::now();
115        if verbose {
116            tracing::info!("Starting content prerendering...");
117        }
118
119        let result = prerender_content(Arc::clone(db_pool)).await;
120        let duration_ms = step_start.elapsed().as_millis() as u64;
121
122        steps.push(StepResult {
123            step: "prerender".to_string(),
124            success: result.is_ok(),
125            duration_ms,
126            message: result.err().map(|e| e.to_string()),
127        });
128    }
129
130    if args.should_run(PublishStep::Homepage) {
131        let step_start = Instant::now();
132        if verbose {
133            tracing::info!("Starting homepage prerendering...");
134        }
135
136        let result = prerender_homepage(Arc::clone(db_pool)).await;
137        let duration_ms = step_start.elapsed().as_millis() as u64;
138
139        steps.push(StepResult {
140            step: "homepage".to_string(),
141            success: result.is_ok(),
142            duration_ms,
143            message: result.err().map(|e| e.to_string()),
144        });
145    }
146
147    if args.should_run(PublishStep::Sitemap) {
148        let step_start = Instant::now();
149        if verbose {
150            tracing::info!("Starting sitemap generation...");
151        }
152
153        let result = generate_sitemap(Arc::clone(db_pool)).await;
154        let duration_ms = step_start.elapsed().as_millis() as u64;
155
156        steps.push(StepResult {
157            step: "sitemap".to_string(),
158            success: result.is_ok(),
159            duration_ms,
160            message: result.err().map(|e| e.to_string()),
161        });
162    }
163
164    let total_duration_ms = start_time.elapsed().as_millis() as u64;
165    let succeeded = steps.iter().filter(|s| s.success).count();
166    let failed = steps.iter().filter(|s| !s.success).count();
167    let total_steps = steps.len();
168
169    let output = PublishPipelineOutput {
170        steps,
171        total_steps,
172        succeeded,
173        failed,
174        duration_ms: total_duration_ms,
175    };
176
177    let title = if failed == 0 {
178        "Content Published Successfully"
179    } else {
180        "Content Publish Completed with Errors"
181    };
182
183    Ok(CommandResult::card(output).with_title(title))
184}