use clap::Parser;
use deps_cli::cli::{Cli, Command, OutputFormat};
use deps_cli::config::{self, CliConfig};
use deps_cli::exit::exit_code;
use deps_cli::report::{CheckContext, CheckReport, FailOnPolicy, check_manifest};
use deps_cli::{format, walk};
use deps_core::osv::OsvClient;
use deps_core::{EcosystemRegistry, HttpCache};
use deps_engine::setup::{EcosystemRuntime, register_ecosystems};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;
const MAX_MANIFEST_FILE_SIZE: u64 = 10_000_000;
fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
let cli = Cli::parse();
let Command::Check(args) = cli.command;
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(error) => {
eprintln!("deps-cli: failed to start async runtime: {error}");
return ExitCode::from(2);
}
};
let walk_paths = args.walk_paths();
let default_config_dir = config_default_dir(&walk_paths);
let cli_config = match config::load(args.config.as_deref(), &default_config_dir) {
Ok(config) => config::apply_overrides(config, args.offline, args.cooldown),
Err(error) => {
eprintln!("deps-cli: {error}");
return ExitCode::from(2);
}
};
let fail_on = if args.fail_on.is_empty() {
FailOnPolicy::default_categories()
} else {
FailOnPolicy::new(args.fail_on.clone())
};
let (report, had_execution_error) = runtime.block_on(run_check(
walk_paths,
cli_config,
args.respect_gitignore,
args.follow_symlinks,
));
let rendered = match args.format {
OutputFormat::Table => format::table::render(&report),
OutputFormat::Json => match format::json::render(&report) {
Ok(json) => json,
Err(error) => {
eprintln!("deps-cli: failed to render JSON report: {error}");
return ExitCode::from(2);
}
},
OutputFormat::Sarif => match format::sarif::render(&report) {
Ok(sarif) => sarif,
Err(error) => {
eprintln!("deps-cli: failed to render SARIF report: {error}");
return ExitCode::from(2);
}
},
};
print!("{rendered}");
ExitCode::from(u8::try_from(exit_code(&report, &fail_on, had_execution_error)).unwrap_or(2))
}
async fn run_check(
paths: Vec<PathBuf>,
cli_config: CliConfig,
respect_gitignore: bool,
follow_symlinks: bool,
) -> (CheckReport, bool) {
let policy = cli_config.policy;
let ecosystem_runtime = EcosystemRuntime::from_policy(&policy);
let cache = Arc::new(HttpCache::with_policy(Arc::clone(
&ecosystem_runtime.policy,
)));
cache.set_offline(policy.network.offline);
let ecosystem_registry = EcosystemRegistry::new();
let _workspace_registry_ecosystems =
register_ecosystems(&ecosystem_registry, Arc::clone(&cache), &ecosystem_runtime);
let ctx = CheckContext {
cache: Arc::clone(&cache),
osv: Arc::new(OsvClient::new(Arc::clone(&cache))),
lockfile_cache: Arc::new(deps_core::lockfile::LockFileCache::new()),
policy,
};
let walk_outcome = walk::walk(
&paths,
&ecosystem_registry,
respect_gitignore,
follow_symlinks,
);
let mut had_execution_error = false;
for error in &walk_outcome.walk_errors {
eprintln!("deps-cli: warning: {error}");
had_execution_error = true;
}
if walk_outcome.truncated {
eprintln!(
"deps-cli: warning: walk truncated at {} entries; some manifests may be missing from this report",
walk::MAX_WALKED_FILES
);
had_execution_error = true;
}
for path in &walk_outcome.unrecognized_explicit_paths {
eprintln!(
"deps-cli: warning: {} is not recognized by any ecosystem",
path.display()
);
}
for path in &walk_outcome.ignored_manifests {
eprintln!(
"deps-cli: warning: {} looks like a manifest but was excluded from the scan (a .gitignore/.ignore rule, a pruned directory such as vendor/build/dist, or a symlink not followed — see --follow-symlinks)",
path.display()
);
had_execution_error = true;
}
if walk_outcome.manifests.is_empty() {
eprintln!("deps-cli: warning: no manifests were discovered under the given path(s)");
had_execution_error = true;
}
let mut findings = Vec::new();
for manifest in walk_outcome.manifests {
match deps_core::fs_probe::read_to_string_capped(&manifest.path, MAX_MANIFEST_FILE_SIZE) {
Ok(Some(content)) => {
match check_manifest(
&manifest.ecosystem,
&manifest.uri_path,
&manifest.display_path,
&content,
&ctx,
)
.await
{
Ok(result) => {
had_execution_error |= result.registry_unreachable;
findings.extend(result.findings);
}
Err(error) => {
eprintln!("deps-cli: warning: {error}");
had_execution_error = true;
}
}
}
Ok(None) => {
eprintln!(
"deps-cli: warning: {} exceeds the manifest size cap, skipping",
manifest.display_path.display()
);
had_execution_error = true;
}
Err(error) => {
eprintln!(
"deps-cli: warning: could not read {}: {error}",
manifest.display_path.display()
);
had_execution_error = true;
}
}
}
(CheckReport { findings }, had_execution_error)
}
fn config_default_dir(paths: &[PathBuf]) -> PathBuf {
match paths {
[only] if only.is_dir() => only.clone(),
[only] => only
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf),
_ => PathBuf::from("."),
}
}