//! Sync command: unified orchestrator for ingest -> generate-docs -> embed. use console::style; use serde::Serialize; use tracing::{info, warn}; use crate::Config; use crate::core::error::Result; use super::embed::run_embed; use super::generate_docs::run_generate_docs; use super::ingest::{IngestDisplay, run_ingest}; /// Options for the sync command. #[derive(Debug, Default)] pub struct SyncOptions { pub full: bool, pub force: bool, pub no_embed: bool, pub no_docs: bool, pub robot_mode: bool, } /// Result of the sync command. #[derive(Debug, Default, Serialize)] pub struct SyncResult { pub issues_updated: usize, pub mrs_updated: usize, pub discussions_fetched: usize, pub documents_regenerated: usize, pub documents_embedded: usize, } /// Run the full sync pipeline: ingest -> generate-docs -> embed. pub async fn run_sync(config: &Config, options: SyncOptions) -> Result { let mut result = SyncResult::default(); let ingest_display = if options.robot_mode { IngestDisplay::silent() } else { IngestDisplay::progress_only() }; // Stage 1: Ingest issues info!("Sync stage 1/4: ingesting issues"); let issues_result = run_ingest(config, "issues", None, options.force, options.full, ingest_display).await?; result.issues_updated = issues_result.issues_upserted; result.discussions_fetched += issues_result.discussions_fetched; // Stage 2: Ingest MRs info!("Sync stage 2/4: ingesting merge requests"); let mrs_result = run_ingest(config, "mrs", None, options.force, options.full, ingest_display).await?; result.mrs_updated = mrs_result.mrs_upserted; result.discussions_fetched += mrs_result.discussions_fetched; // Stage 3: Generate documents (unless --no-docs) if options.no_docs { info!("Sync stage 3/4: skipping document generation (--no-docs)"); } else { info!("Sync stage 3/4: generating documents"); let docs_result = run_generate_docs(config, false, None)?; result.documents_regenerated = docs_result.regenerated; } // Stage 4: Embed documents (unless --no-embed) if options.no_embed { info!("Sync stage 4/4: skipping embedding (--no-embed)"); } else { info!("Sync stage 4/4: embedding documents"); match run_embed(config, false).await { Ok(embed_result) => { result.documents_embedded = embed_result.embedded; } Err(e) => { // Graceful degradation: Ollama down is a warning, not an error warn!(error = %e, "Embedding stage failed (Ollama may be unavailable), continuing"); } } } info!( issues = result.issues_updated, mrs = result.mrs_updated, discussions = result.discussions_fetched, docs = result.documents_regenerated, embedded = result.documents_embedded, "Sync pipeline complete" ); Ok(result) } /// Print human-readable sync summary. pub fn print_sync(result: &SyncResult, elapsed: std::time::Duration) { println!( "{} Sync complete:", style("done").green().bold(), ); println!(" Issues updated: {}", result.issues_updated); println!(" MRs updated: {}", result.mrs_updated); println!(" Discussions fetched: {}", result.discussions_fetched); println!(" Documents regenerated: {}", result.documents_regenerated); println!(" Documents embedded: {}", result.documents_embedded); println!( " Elapsed: {:.1}s", elapsed.as_secs_f64() ); } /// JSON output for sync. #[derive(Serialize)] struct SyncJsonOutput<'a> { ok: bool, data: &'a SyncResult, meta: SyncMeta, } #[derive(Serialize)] struct SyncMeta { elapsed_ms: u64, } /// Print JSON robot-mode sync output. pub fn print_sync_json(result: &SyncResult, elapsed_ms: u64) { let output = SyncJsonOutput { ok: true, data: result, meta: SyncMeta { elapsed_ms }, }; println!("{}", serde_json::to_string(&output).unwrap()); }