1use anyhow::Result;
4use clap::Args as ClapArgs;
5use colored::Colorize;
6use std::collections::HashMap;
7use std::io::{self, Write};
8
9use crate::core::{AdrStatus, ConfigManager, Git, NotesManager};
10
11#[derive(ClapArgs, Debug)]
13pub struct Args {
14 #[arg(long)]
16 pub accepted_only: bool,
17
18 #[arg(long)]
20 pub by_tag: bool,
21
22 #[arg(long)]
24 pub non_interactive: bool,
25
26 #[arg(long, short, default_value = "10")]
28 pub limit: usize,
29}
30
31pub fn run(args: Args) -> Result<()> {
37 let git = Git::new();
38 git.check_repository()?;
39
40 let config = ConfigManager::new(git.clone()).load()?;
41 let notes = NotesManager::new(git, config);
42
43 let adrs = notes.list()?;
44
45 if adrs.is_empty() {
46 println!();
47 println!(
48 "{}",
49 "Welcome to Architecture Decision Records!".bold().cyan()
50 );
51 println!();
52 println!("This repository doesn't have any ADRs yet.");
53 println!();
54 println!("Get started by creating your first ADR:");
55 println!(
56 " {} git adr new \"Use git-adr for architecture decisions\"",
57 "→".blue()
58 );
59 println!();
60 return Ok(());
61 }
62
63 let filtered_adrs: Vec<_> = if args.accepted_only {
65 adrs.iter()
66 .filter(|a| matches!(a.frontmatter.status, AdrStatus::Accepted))
67 .collect()
68 } else {
69 adrs.iter().collect()
70 };
71
72 println!();
73 println!("{}", "═══════════════════════════════════════════".dimmed());
74 println!(
75 "{}",
76 " Architecture Decision Records - Onboarding".bold().cyan()
77 );
78 println!("{}", "═══════════════════════════════════════════".dimmed());
79 println!();
80
81 print_summary(&adrs);
83
84 if args.by_tag {
85 show_by_tag(&filtered_adrs, &args)?;
86 } else {
87 show_overview(&filtered_adrs, &args)?;
88 }
89
90 if !args.non_interactive {
92 interactive_browse(&filtered_adrs, ¬es)?;
93 }
94
95 println!();
96 println!("{}", "Quick Reference:".bold());
97 println!(" {} List all ADRs ", "git adr list".cyan());
98 println!(" {} View specific ADR ", "git adr show <id>".cyan());
99 println!(
100 " {} Search ADRs ",
101 "git adr search <query>".cyan()
102 );
103 println!(" {} Create new ADR ", "git adr new <title>".cyan());
104 println!();
105
106 Ok(())
107}
108
109fn print_summary(adrs: &[crate::core::Adr]) {
111 let mut status_counts: HashMap<&AdrStatus, usize> = HashMap::new();
112 for adr in adrs {
113 *status_counts.entry(&adr.frontmatter.status).or_insert(0) += 1;
114 }
115
116 println!("{}", "Summary".bold());
117 println!(" Total ADRs: {}", adrs.len().to_string().cyan());
118
119 let accepted = status_counts.get(&AdrStatus::Accepted).unwrap_or(&0);
120 let proposed = status_counts.get(&AdrStatus::Proposed).unwrap_or(&0);
121 let deprecated = status_counts.get(&AdrStatus::Deprecated).unwrap_or(&0);
122 let superseded = status_counts.get(&AdrStatus::Superseded).unwrap_or(&0);
123
124 println!(
125 " {} accepted, {} proposed, {} deprecated/superseded",
126 accepted.to_string().green(),
127 proposed.to_string().yellow(),
128 (deprecated + superseded).to_string().dimmed()
129 );
130 println!();
131}
132
133fn show_overview(adrs: &[&crate::core::Adr], args: &Args) -> Result<()> {
135 println!("{}", "Key Decisions".bold());
136 println!();
137
138 let display_count = args.limit.min(adrs.len());
139
140 for adr in adrs.iter().take(display_count) {
141 let status_color = match adr.frontmatter.status {
142 AdrStatus::Accepted => "accepted".green(),
143 AdrStatus::Proposed => "proposed".yellow(),
144 AdrStatus::Deprecated => "deprecated".dimmed(),
145 AdrStatus::Superseded => "superseded".dimmed(),
146 AdrStatus::Rejected => "rejected".red(),
147 };
148
149 println!(
150 " {} {} [{}]",
151 adr.id.cyan(),
152 adr.frontmatter.title,
153 status_color
154 );
155
156 if !adr.frontmatter.tags.is_empty() {
158 let tags: Vec<&str> = adr
159 .frontmatter
160 .tags
161 .iter()
162 .take(3)
163 .map(String::as_str)
164 .collect();
165 println!(" Tags: {}", tags.join(", ").dimmed());
166 }
167 }
168
169 if adrs.len() > display_count {
170 println!();
171 println!(
172 " {} ... and {} more (use --limit to show more)",
173 "→".dimmed(),
174 (adrs.len() - display_count).to_string().dimmed()
175 );
176 }
177
178 println!();
179 Ok(())
180}
181
182fn show_by_tag(adrs: &[&crate::core::Adr], _args: &Args) -> Result<()> {
184 let mut by_tag: HashMap<String, Vec<&crate::core::Adr>> = HashMap::new();
186
187 for adr in adrs {
188 if adr.frontmatter.tags.is_empty() {
189 by_tag
190 .entry("uncategorized".to_string())
191 .or_default()
192 .push(adr);
193 } else {
194 for tag in &adr.frontmatter.tags {
195 by_tag.entry(tag.clone()).or_default().push(adr);
196 }
197 }
198 }
199
200 let mut tags: Vec<_> = by_tag.iter().collect();
202 tags.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
203
204 println!("{}", "Decisions by Category".bold());
205 println!();
206
207 for (tag, tag_adrs) in tags.iter().take(10) {
208 println!(" {} ({} ADRs)", tag.bold().cyan(), tag_adrs.len());
209
210 for adr in tag_adrs.iter().take(3) {
211 let status = match adr.frontmatter.status {
212 AdrStatus::Accepted => "✓".green(),
213 AdrStatus::Proposed => "○".yellow(),
214 _ => "·".dimmed(),
215 };
216 println!(
217 " {} {} {}",
218 status,
219 adr.id.dimmed(),
220 adr.frontmatter.title
221 );
222 }
223
224 if tag_adrs.len() > 3 {
225 println!(" {} ... {} more", "→".dimmed(), tag_adrs.len() - 3);
226 }
227 println!();
228 }
229
230 Ok(())
231}
232
233fn interactive_browse(adrs: &[&crate::core::Adr], notes: &NotesManager) -> Result<()> {
235 println!("{}", "Interactive Browse".bold());
236 println!(" Enter an ADR ID to view details, or press Enter to skip.");
237 println!();
238
239 loop {
240 print!(" {} ", "ADR ID (or 'q' to quit):".dimmed());
241 io::stdout().flush()?;
242
243 let mut input = String::new();
244 io::stdin().read_line(&mut input)?;
245 let input = input.trim();
246
247 if input.is_empty() || input.eq_ignore_ascii_case("q") {
248 break;
249 }
250
251 let matching = adrs
253 .iter()
254 .find(|a| a.id.eq_ignore_ascii_case(input) || a.id.contains(input));
255
256 match matching {
257 Some(adr) => {
258 println!();
259 println!("{}", "─".repeat(50).dimmed());
260 println!("{} {}", adr.id.bold().cyan(), adr.frontmatter.title.bold());
261 println!("{}", "─".repeat(50).dimmed());
262 println!();
263 println!("{}: {}", "Status".bold(), adr.frontmatter.status);
264
265 if let Some(date) = &adr.frontmatter.date {
266 println!("{}: {}", "Date".bold(), date.0.format("%Y-%m-%d"));
267 }
268
269 if !adr.frontmatter.tags.is_empty() {
270 println!("{}: {}", "Tags".bold(), adr.frontmatter.tags.join(", "));
271 }
272
273 let body_preview: String = adr.body.chars().take(500).collect();
275 if !body_preview.is_empty() {
276 println!();
277 println!("{}", body_preview);
278 if adr.body.len() > 500 {
279 println!("{}...", "".dimmed());
280 }
281 }
282
283 println!();
284 println!(" {} View full: git adr show {}", "→".blue(), adr.id.cyan());
285 println!();
286 },
287 None => {
288 if let Ok(adr) = notes.get(input) {
290 println!();
291 println!("{}: {}", adr.id.cyan(), adr.frontmatter.title);
292 println!("Status: {}", adr.frontmatter.status);
293 println!();
294 } else {
295 println!(" {} ADR not found: {}", "!".yellow(), input);
296 }
297 },
298 }
299 }
300
301 Ok(())
302}