use std::sync::Arc;
use anyhow::{Context as _, Result};
use tracing::{info, warn};
use trusty_review::{
config::ReviewConfig,
integrations::{
search_client::HttpSearchClient, subprocess_analyze_client::SubprocessAnalyzeClient,
},
llm::build_provider,
pipeline::enforce_verifier_liveness,
service::{AppState, DEFAULT_PORT, serve as serve_http},
};
use crate::cli_verify;
#[derive(Debug, clap::Parser)]
pub struct ServeArgs {
#[arg(long, default_value_t = DEFAULT_PORT, value_name = "PORT")]
pub port: u16,
#[arg(long, default_value = "127.0.0.1", value_name = "ADDR")]
pub bind: String,
#[cfg(feature = "mcp")]
#[arg(long, default_value_t = false)]
pub stdio: bool,
}
pub async fn cmd_serve(config: ReviewConfig, args: ServeArgs) -> Result<()> {
let state = build_app_state(config.clone()).await?;
#[cfg(feature = "mcp")]
if args.stdio {
info!("trusty-review MCP stdio service starting");
return trusty_review::mcp::run(state).await;
}
use std::net::SocketAddr;
let addr: SocketAddr = format!("{}:{}", args.bind, args.port)
.parse()
.with_context(|| format!("invalid bind address {}:{}", args.bind, args.port))?;
info!(
port = args.port,
bind = %args.bind,
reviewer_model = %config.role_models.reviewer.model,
dry_run = config.dry_run,
"trusty-review serve starting"
);
serve_http(state, addr).await
}
async fn build_app_state(mut config: ReviewConfig) -> Result<AppState> {
let reviewer_model = config.role_models.reviewer.model.clone();
let default_provider = config.role_models.reviewer.provider.clone();
let llm = build_provider(
&reviewer_model,
&default_provider,
&config.openrouter_api_key,
)
.await
.map_err(|e| anyhow::anyhow!("failed to build LLM provider: {e}"))?;
let verifier = cli_verify::build_verifier_for_serve(&config).await?;
enforce_verifier_liveness(&config, verifier.as_ref())
.await
.map_err(|reason| anyhow::anyhow!(reason))?;
let search = HttpSearchClient::from_config(&config)
.map_err(|e| anyhow::anyhow!("failed to build search HTTP client: {e}"))?;
config.resolve_index(&search).await;
let analyze = SubprocessAnalyzeClient::from_config(&config)
.map_err(|e| anyhow::anyhow!("failed to build analyze HTTP client: {e}"))?;
let dedup_path = config.log_dir.join("dedup.redb");
let dedup = match trusty_review::store::DedupStore::open(&dedup_path) {
Ok(store) => {
info!(path = %dedup_path.display(), "dedup store opened");
Some(Arc::new(store))
}
Err(e) => {
warn!(
path = %dedup_path.display(),
"failed to open dedup store (continuing without it): {e}"
);
None
}
};
Ok(AppState::with_verifier_and_dedup(
config,
llm,
verifier,
Arc::new(search),
Some(Arc::new(analyze)),
dedup,
))
}