use std::sync::Arc;
use anyhow::{Context as _, Result};
use tracing::warn;
use trusty_review::{
config::{ReviewConfig, RoleCliOverrides},
integrations::{
github::{AuthStrategy, GithubClient, RunMode},
search_client::HttpSearchClient,
subprocess_analyze_client::SubprocessAnalyzeClient,
},
llm::build_provider,
pipeline::{DiffSource, ReviewDeps, ReviewInput, TriggerDecision, log_json_path, run_review},
};
use crate::cli_verify;
#[derive(Debug, clap::Parser)]
pub struct RunArgs {
#[arg(value_name = "OWNER")]
pub owner: Option<String>,
#[arg(value_name = "REPO")]
pub repo: Option<String>,
#[arg(value_name = "PR")]
pub pr: Option<u64>,
#[arg(long, value_name = "SLUG")]
pub reviewer_model: Option<String>,
#[arg(long, value_name = "PROVIDER")]
pub provider: Option<String>,
#[arg(long, value_name = "PATH")]
pub local_diff: Option<std::path::PathBuf>,
#[arg(long = "no-log", action = clap::ArgAction::SetFalse, default_value = "true")]
pub write_log: bool,
}
pub async fn cmd_run(config: ReviewConfig, args: RunArgs) -> Result<()> {
let diff_source = resolve_diff_source_run(&config, &args).await?;
let overrides = RoleCliOverrides {
reviewer_model: args.reviewer_model.clone(),
provider: args.provider.clone(),
..Default::default()
};
let mut config_with_overrides = ReviewConfig::from_env_and_file(None, Some(&overrides));
let reviewer_model = config_with_overrides.role_models.reviewer.model.clone();
let default_provider = config_with_overrides.role_models.reviewer.provider.clone();
let search_for_resolve = HttpSearchClient::from_config(&config_with_overrides)
.map_err(|e| anyhow::anyhow!("failed to build search HTTP client: {e}"))?;
config_with_overrides
.resolve_index(&search_for_resolve)
.await;
let deps = build_deps_async(&config_with_overrides, &reviewer_model, &default_provider).await?;
let input = ReviewInput {
diff_source,
reviewer_model: reviewer_model.clone(),
write_log: args.write_log,
print_result: true,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: true,
};
let result = run_review(&config_with_overrides, input, deps).await;
if args.write_log {
let log_path = log_json_path(&result, &config_with_overrides.log_dir);
eprintln!("\nLog written to: {}", log_path.display());
}
if result.status.is_skipped() {
anyhow::bail!(
"review skipped — {}",
result
.error
.as_deref()
.unwrap_or("required code-context dependency unavailable")
);
}
Ok(())
}
pub async fn resolve_diff_source_run(config: &ReviewConfig, args: &RunArgs) -> Result<DiffSource> {
if let Some(ref path) = args.local_diff {
return Ok(DiffSource::LocalFile { path: path.clone() });
}
let owner = args
.owner
.as_deref()
.context("OWNER is required (or use --local-diff)")?
.to_string();
let repo = args
.repo
.as_deref()
.context("REPO is required (or use --local-diff)")?
.to_string();
let pr = args
.pr
.context("PR number is required (or use --local-diff)")?;
let client = GithubClient::new()
.map_err(|e| anyhow::anyhow!("failed to build GitHub HTTP client: {e}"))?;
let token = AuthStrategy::select(RunMode::Cli, None)
.resolve_token(&client, config, &owner)
.await
.map_err(|e| {
warn!(
"GitHub token resolution failed: {e} — set GITHUB_TOKEN/GH_TOKEN or run `gh auth login`"
);
anyhow::anyhow!("GitHub authentication failed: {e}")
})?;
Ok(DiffSource::Github {
owner,
repo,
pr,
token,
})
}
pub async fn build_deps_async(
config: &ReviewConfig,
model: &str,
default_provider: &trusty_review::config::Provider,
) -> Result<ReviewDeps> {
let llm = build_provider(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_opt(config).await;
let search = HttpSearchClient::from_config(config)
.map_err(|e| anyhow::anyhow!("failed to build search HTTP client: {e}"))?;
let analyze = SubprocessAnalyzeClient::from_config(config)
.map_err(|e| anyhow::anyhow!("failed to build analyze HTTP client: {e}"))?;
Ok(ReviewDeps {
llm,
verifier,
search: Arc::new(search),
analyze: Some(Arc::new(analyze)),
dedup: None,
})
}