chub_cli/commands/
detect.rs1use clap::Args;
2use owo_colors::OwoColorize;
3
4use chub_core::registry::MergedRegistry;
5use chub_core::team::detect;
6
7use crate::output;
8
9#[derive(Args)]
10pub struct DetectArgs {
11 #[arg(long)]
13 pin: bool,
14}
15
16pub fn run(args: DetectArgs, json: bool, merged: &MergedRegistry) {
17 let cwd = std::env::current_dir().unwrap_or_default();
18 let deps = detect::detect_dependencies(&cwd);
19
20 if deps.is_empty() {
21 if json {
22 println!(
23 "{}",
24 serde_json::json!({ "dependencies": [], "matches": [] })
25 );
26 } else {
27 eprintln!(
28 "{}",
29 "No dependency files found (package.json, Cargo.toml, requirements.txt, etc.)."
30 .yellow()
31 );
32 }
33 return;
34 }
35
36 let doc_ids: Vec<(String, String)> = merged
38 .docs
39 .iter()
40 .map(|e| (e.id().to_string(), e.name().to_string()))
41 .collect();
42
43 let matches = detect::match_deps_to_docs(&deps, &doc_ids);
44
45 if json {
46 println!(
47 "{}",
48 serde_json::to_string_pretty(&serde_json::json!({
49 "dependencies": deps.len(),
50 "matches": matches.iter().map(|m| serde_json::json!({
51 "dependency": m.dep.name,
52 "language": m.dep.language,
53 "version": m.dep.version,
54 "doc_id": m.doc_id,
55 "doc_name": m.doc_name,
56 "confidence": m.confidence,
57 })).collect::<Vec<_>>(),
58 "unmatched": deps.iter()
59 .filter(|d| !matches.iter().any(|m| m.dep.name == d.name))
60 .map(|d| &d.name)
61 .collect::<Vec<_>>(),
62 }))
63 .unwrap_or_default()
64 );
65 } else {
66 eprintln!(
67 "Detected {} dependencies with {} available docs:\n",
68 deps.len(),
69 matches.len()
70 );
71
72 for m in &matches {
73 let ver = m
74 .dep
75 .version
76 .as_deref()
77 .map(|v| format!(" ({})", v))
78 .unwrap_or_default();
79 let conf = if m.confidence >= 0.8 {
80 "[pinnable]".green().to_string()
81 } else {
82 "[partial match]".yellow().to_string()
83 };
84 eprintln!(
85 " {} {}{} → {} {}",
86 m.dep.name.bold(),
87 m.dep.language.dimmed(),
88 ver.dimmed(),
89 m.doc_id.cyan(),
90 conf,
91 );
92 }
93
94 let unmatched: Vec<&str> = deps
96 .iter()
97 .filter(|d| !matches.iter().any(|m| m.dep.name == d.name))
98 .map(|d| d.name.as_str())
99 .collect();
100 if !unmatched.is_empty() {
101 eprintln!();
102 for name in &unmatched {
103 eprintln!(" {} {}", "✗".red(), name.dimmed());
104 }
105 }
106
107 if !matches.is_empty() {
108 eprintln!("\nPin all? {}", "chub detect --pin".bold());
109 }
110 }
111
112 if args.pin {
114 let mut pinned = 0;
115 for m in &matches {
116 if m.confidence >= 0.5 {
117 let lang = if m.dep.language == "javascript" || m.dep.language == "python" {
118 Some(m.dep.language.clone())
119 } else {
120 None
121 };
122 if chub_core::team::pins::add_pin(&m.doc_id, lang, None, None, None).is_ok() {
123 pinned += 1;
124 }
125 }
126 }
127 if !json {
128 output::success(&format!("Pinned {} docs.", pinned));
129 }
130 }
131}