use clap::Args;
use miette::{Context, IntoDiagnostic};
use std::collections::BTreeSet;
pub const AFTER_LONG_HELP: &str = "\
Examples:
$ aube ignored-builds
The following builds were ignored during install:
esbuild@0.20.2
puppeteer@22.8.0
# When nothing was skipped
$ aube ignored-builds
No ignored builds.
# Approve them for this project
$ aube approve-builds
";
#[derive(Debug, Args)]
pub struct IgnoredBuildsArgs {
#[arg(short = 'g', long)]
pub global: bool,
}
pub async fn run(args: IgnoredBuildsArgs) -> miette::Result<()> {
if args.global {
return run_global();
}
let cwd = crate::dirs::project_root()?;
let ignored = collect_ignored(&cwd)?;
if ignored.is_empty() {
println!("No ignored builds.");
return Ok(());
}
println!("The following builds were ignored during install:");
for entry in &ignored {
print_entry_line(" ", entry);
}
Ok(())
}
fn print_entry_line(indent: &str, entry: &IgnoredEntry) {
println!("{indent}{}@{}", entry.name, entry.version);
for sus in &entry.suspicions {
println!("{indent} ⚠ {} — {}", sus.hook, sus.kind.description());
}
}
fn run_global() -> miette::Result<()> {
let layout = super::global::GlobalLayout::resolve()?;
let mut installs = super::global::scan_packages(&layout.pkg_dir);
installs.sort_by(|a, b| a.install_dir.cmp(&b.install_dir));
let mut printed = false;
let mut seen = std::collections::BTreeSet::new();
for info in installs {
if !seen.insert(info.install_dir.clone()) {
continue;
}
let ignored = collect_ignored(&info.install_dir)?;
if ignored.is_empty() {
continue;
}
if !printed {
println!("The following global builds were ignored during install:");
printed = true;
}
println!(
" {} ({})",
info.aliases.join(", "),
info.install_dir.display()
);
for entry in &ignored {
print_entry_line(" ", entry);
}
}
if !printed {
println!("No ignored builds.");
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct IgnoredEntry {
pub name: String,
pub version: String,
pub suspicions: Vec<aube_scripts::Suspicion>,
}
pub(super) fn collect_ignored(project_dir: &std::path::Path) -> miette::Result<Vec<IgnoredEntry>> {
let manifest = super::load_manifest(&project_dir.join("package.json"))?;
let graph = match aube_lockfile::parse_lockfile(project_dir, &manifest) {
Ok(g) => g,
Err(aube_lockfile::Error::NotFound(_)) => return Ok(Vec::new()),
Err(e) => return Err(miette::Report::new(e)).wrap_err("failed to parse lockfile"),
};
let workspace = aube_manifest::WorkspaceConfig::load(project_dir)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
let (policy, _warnings) =
super::install::build_policy_from_sources(&manifest, &workspace, false);
let store = super::open_store(project_dir)?;
let mut seen: BTreeSet<(String, String)> = BTreeSet::new();
let mut out: Vec<IgnoredEntry> = Vec::new();
for pkg in graph.packages.values() {
if !seen.insert((pkg.name.clone(), pkg.version.clone())) {
continue;
}
if matches!(
policy.decide(pkg.registry_name(), &pkg.version),
aube_scripts::AllowDecision::Allow
) {
continue;
}
let Some(suspicions) = lifecycle_scripts_with_suspicions(
&store,
&pkg.name,
&pkg.version,
pkg.integrity.as_deref(),
) else {
continue;
};
out.push(IgnoredEntry {
name: pkg.name.clone(),
version: pkg.version.clone(),
suspicions,
});
}
out.sort();
Ok(out)
}
fn lifecycle_scripts_with_suspicions(
store: &aube_store::Store,
name: &str,
version: &str,
integrity: Option<&str>,
) -> Option<Vec<aube_scripts::Suspicion>> {
let index = store.load_index(name, version, integrity)?;
let stored = index.get("package.json")?;
let content = std::fs::read_to_string(&stored.store_path).ok()?;
let manifest = serde_json::from_str::<aube_manifest::PackageJson>(&content).ok()?;
let has_declared = aube_scripts::DEP_LIFECYCLE_HOOKS
.iter()
.any(|h| manifest.scripts.contains_key(h.script_name()));
let has_implicit =
aube_scripts::implicit_install_script(&manifest, index.contains_key("binding.gyp"))
.is_some();
if !has_declared && !has_implicit {
return None;
}
Some(aube_scripts::sniff_lifecycle(&manifest))
}