chub_cli/commands/
context_cmd.rs1use clap::Args;
2use owo_colors::OwoColorize;
3
4use chub_core::team::context;
5
6#[derive(Args)]
7pub struct ContextArgs {
8 query: Option<String>,
10
11 #[arg(long)]
13 list: bool,
14}
15
16pub fn run(args: ContextArgs, json: bool) {
17 if args.list || args.query.is_none() {
18 run_list(json);
19 return;
20 }
21
22 let query = args.query.as_deref().unwrap_or("");
24 let docs = context::discover_context_docs();
25
26 if docs.is_empty() {
27 if json {
28 println!("{}", serde_json::json!({"docs": [], "query": query}));
29 } else {
30 eprintln!(
31 "{}",
32 "No project context docs found in .chub/context/".dimmed()
33 );
34 }
35 return;
36 }
37
38 let query_lower = query.to_lowercase();
40 let query_words: Vec<&str> = query_lower.split_whitespace().collect();
41
42 let mut scored: Vec<(&context::ContextDoc, f64)> = docs
43 .iter()
44 .map(|doc| {
45 let mut score = 0.0f64;
46 let name_lower = doc.name.to_lowercase();
47 let desc_lower = doc.description.to_lowercase();
48
49 for word in &query_words {
50 if name_lower.contains(word) {
51 score += 10.0;
52 }
53 if desc_lower.contains(word) {
54 score += 5.0;
55 }
56 for tag in &doc.tags {
57 if tag.to_lowercase().contains(word) {
58 score += 8.0;
59 }
60 }
61 }
62 (doc, score)
63 })
64 .collect();
65
66 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
67
68 if json {
70 let items: Vec<serde_json::Value> = scored
71 .iter()
72 .map(|(doc, score)| {
73 let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
74 serde_json::json!({
75 "id": format!("project/{}", stem),
76 "name": doc.name,
77 "description": doc.description,
78 "relevance": score,
79 })
80 })
81 .collect();
82 println!(
83 "{}",
84 serde_json::to_string_pretty(&serde_json::json!({
85 "query": query,
86 "docs": items,
87 }))
88 .unwrap_or_default()
89 );
90 } else {
91 eprintln!(
92 "Context for \"{}\": {} doc(s)\n",
93 query.bold(),
94 scored.len()
95 );
96 for (doc, score) in &scored {
97 let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
98 let relevance = if *score > 0.0 {
99 format!(" (relevance: {:.0})", score).dimmed().to_string()
100 } else {
101 String::new()
102 };
103 eprintln!(
104 " {} {}{}",
105 format!("project/{}", stem).cyan(),
106 doc.description.dimmed(),
107 relevance,
108 );
109 }
110 }
111}
112
113fn run_list(json: bool) {
114 let docs = context::list_context_docs();
115
116 if json {
117 let items: Vec<serde_json::Value> = docs
118 .iter()
119 .map(|doc| {
120 let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
121 serde_json::json!({
122 "id": format!("project/{}", stem),
123 "name": doc.name,
124 "description": doc.description,
125 "tags": doc.tags,
126 "file": doc.file,
127 })
128 })
129 .collect();
130 println!(
131 "{}",
132 serde_json::to_string_pretty(&serde_json::json!({
133 "docs": items,
134 "total": docs.len(),
135 }))
136 .unwrap_or_default()
137 );
138 } else {
139 if docs.is_empty() {
140 eprintln!(
141 "{}",
142 "No project context docs. Add .md files to .chub/context/".dimmed()
143 );
144 return;
145 }
146 eprintln!(
147 "{}",
148 format!("{} project context docs:\n", docs.len()).bold()
149 );
150 for doc in &docs {
151 let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
152 eprintln!(" {}", format!("project/{}", stem).cyan());
153 if !doc.description.is_empty() {
154 eprintln!(" {}", doc.description.dimmed());
155 }
156 }
157 }
158}