use crate::analysis::run as r;
use crate::engine::{RunContext, run_scan};
use crate::error::{PassengerError, Result};
use crate::passenger::command::{PassengerCmd, passenger};
use crate::{commands, packs, plans};
use clap::{Parser, Subcommand};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(name = "code-passenger")]
#[command(about = "Scan a Rust crate for feature/dependency usage and generate feature gating headers.", long_about = None)]
pub struct Cli {
#[arg(long, default_value = ".")]
pub root: PathBuf,
#[arg(long, default_value = "src")]
pub src: String,
#[arg(long)]
pub manifest: Option<PathBuf>,
#[arg(long, default_value = "rust")]
pub lang: String,
#[arg(long, default_value = "verbose")]
pub plan: String,
#[arg(long)]
pub json: bool,
#[command(subcommand)]
pub cmd: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Scan,
Annotate {
#[arg(long)]
write: bool,
#[arg(long)]
check: bool,
},
Scaffold {
#[arg(long)]
kind: String,
#[arg(long)]
name: String,
#[arg(long, default_value = "")]
path: String,
#[arg(long, default_value = "default")]
scaffold_plan: String,
#[arg(long)]
write: bool,
},
Passenger {
#[clap(subcommand)]
cmd: PassengerCmd,
},
Gate {
#[arg(long)]
spec: PathBuf,
#[arg(long)]
package: Option<String>,
#[arg(long)]
write: bool,
#[arg(long)]
check: bool,
#[arg(long, default_value_t = 3)]
max_rounds: usize,
#[arg(long)]
no_cargo: bool,
},
FeatureMap {
#[arg(long)]
package: Option<String>,
},
FeatureSites {
target: String,
#[arg(long)]
package: Option<String>,
},
FeatureProbe {
target: String,
#[arg(long)]
package: Option<String>,
#[arg(long)]
spec: Option<PathBuf>,
},
Receipt {
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
kg: Option<PathBuf>,
},
Lint {
#[arg(long)]
policy: Option<PathBuf>,
},
Patterns {
#[arg(long, default_value_t = 16)]
min_tokens: usize,
#[arg(long, default_value_t = 20)]
top: usize,
},
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
let manifest_path = cli
.manifest
.clone()
.unwrap_or_else(|| cli.root.join("Cargo.toml"));
let ctx = RunContext {
root: cli.root.clone(),
src_rel: cli.src.clone(),
manifest_path,
lang: cli.lang.clone(),
};
match cli.cmd {
Command::Scan => {
let out = run_scan(&ctx)?;
if cli.json {
let a = r(&out);
let file = std::fs::File::create("report.json")?;
let writer = std::io::BufWriter::new(file);
let _ = serde_json::to_writer_pretty(writer, &a);
println!("Saved report to report.json");
} else {
let top_n = 5;
for r in &out.reports {
println!("== {} ==", r.document.relative_path);
print_set("used_deps", &r.used.packages); print_set("used_mods", &r.used.modules); print_set("corpus_features", &r.corpus_features);
println!(
"use_sites: external={}, internal={}",
r.external_use_sites.len(),
r.internal_use_sites.len()
);
print_counts("external_symbols", &r.external_dep_symbol_counts, top_n);
print_counts("internal_symbols", &r.internal_dep_symbol_counts, top_n);
println!();
}
}
}
Command::Annotate { write, check } => {
let out = run_scan(&ctx)?;
let pack = packs::get_pack(&cli.lang)?;
let plan = plans::get_plan(&cli.plan)?;
let mut changed_any = false;
for r in &out.reports {
let header = pack.render_header(plan.as_ref(), r);
let file_path = cli.root.join(&cli.src).join(&r.document.relative_path);
let original = fs::read_to_string(&file_path)?;
let updated = pack.apply_header(&original, &header);
if updated != original {
changed_any = true;
if write {
fs::write(&file_path, updated)?;
}
}
}
if check && changed_any {
return Err(PassengerError::ChangesNeeded);
}
}
Command::Scaffold {
kind,
name,
path,
scaffold_plan,
write,
} => {
let pack = packs::get_pack(&cli.lang)?;
let plan = crate::scaffolds::get_scaffold_plan(&scaffold_plan)?;
let kind = crate::model::ScaffoldKind::parse(&kind).ok_or_else(|| {
PassengerError::Unsupported(format!("unknown scaffold kind '{kind}'"))
})?;
let module_path: Vec<String> = path
.split('/')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
let out = pack.scaffold(
plan.as_ref(),
crate::model::ScaffoldRequest {
kind,
name,
module_path,
},
)?;
if out.files.is_empty() {
return Err(PassengerError::Unsupported(
"scaffold produced no files".into(),
));
}
for (rel, content) in out.files {
let abs = cli.root.join(rel);
if write {
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&abs, content)?;
} else {
println!("--- {}", abs.display());
println!("{content}");
}
}
}
Command::Passenger { cmd } => { passenger(cmd,ctx)? }
Command::Gate { spec, package, write, check, max_rounds, no_cargo } => {
let opts = commands::gate::GateOptions { spec, package, write, check, max_rounds, no_cargo };
let out = commands::gate::run(&cli.root, &opts)?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&out.plan)?);
} else {
print!("{}", commands::gate::render(&out));
}
if out.exit_code != 0 {
std::process::exit(out.exit_code);
}
}
Command::FeatureMap { package } => {
let m = commands::features::feature_map(&cli.root, package.as_deref())?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&m)?);
} else {
print!("{}", commands::features::render_map(&m));
}
}
Command::FeatureSites { target, package } => {
let s = commands::features::feature_sites(&cli.root, package.as_deref(), &target)?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&s)?);
} else {
print!("{}", commands::features::render_sites(&s));
}
}
Command::FeatureProbe { target, package, spec } => {
let p = commands::features::feature_probe(&cli.root, package.as_deref(), &target, spec.as_deref())?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&p)?);
} else {
print!("{}", commands::features::render_probe(&p));
}
}
Command::Receipt { out, kg } => {
let out = out.unwrap_or_else(|| cli.root.join("receipt.json"));
let w = commands::receipt::write(&cli.root, &out, kg.as_deref())?;
println!("wrote {}", w.receipt.display());
if let Some(k) = &w.knowledge {
println!("wrote {}", k.display());
}
if let Some(c) = &w.challenges {
println!("wrote {}", c.display());
}
}
Command::Lint { policy } => {
let v = commands::lint::run(&cli.root, policy.as_deref())?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&v)?);
} else {
println!("{}", v.summary());
}
if v.exit_code != 0 {
std::process::exit(v.exit_code);
}
}
Command::Patterns { min_tokens, top } => {
let r = commands::patterns::run(&cli.root, &klepto::MiningOptions { min_tokens, min_members: 2 })?;
if cli.json {
println!("{}", serde_json::to_string_pretty(&r)?);
} else {
print!("{}", commands::patterns::render(&r, top));
}
}
}
Ok(())
}
use std::collections::BTreeMap;
fn preview_syms(syms: &BTreeMap<String, usize>, top_n: usize) -> String {
let mut v: Vec<(&String, &usize)> = syms.iter().collect();
v.sort_by(|(ka, ca), (kb, cb)| {
cb.cmp(ca).then_with(|| ka.cmp(kb))
});
v.into_iter()
.take(top_n)
.map(|(k, c)| format!("{k}({c})"))
.collect::<Vec<_>>()
.join(", ")
}
fn print_counts(title: &str, counts: &BTreeMap<String, BTreeMap<String, usize>>, top_n: usize) {
if counts.is_empty() {
return;
}
println!("{title}:");
for (dep, syms) in counts {
if syms.is_empty() {
continue;
}
let preview = preview_syms(syms, top_n);
if preview.is_empty() {
continue;
}
println!(" {dep}: {preview}");
}
}
fn print_set<T: std::fmt::Debug>(label: &str, set: &T) {
println!("{label}: {set:?}");
}