git_adr/cli/
onboard.rs

1//! Interactive onboarding wizard for new team members.
2
3use 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/// Arguments for the onboard command.
12#[derive(ClapArgs, Debug)]
13pub struct Args {
14    /// Show only accepted ADRs.
15    #[arg(long)]
16    pub accepted_only: bool,
17
18    /// Show ADRs by category/tag.
19    #[arg(long)]
20    pub by_tag: bool,
21
22    /// Skip interactive prompts.
23    #[arg(long)]
24    pub non_interactive: bool,
25
26    /// Limit number of ADRs to show.
27    #[arg(long, short, default_value = "10")]
28    pub limit: usize,
29}
30
31/// Run the onboard command.
32///
33/// # Errors
34///
35/// Returns an error if onboarding fails.
36pub 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    // Filter ADRs if needed
64    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    // Summary
82    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    // Interactive mode
91    if !args.non_interactive {
92        interactive_browse(&filtered_adrs, &notes)?;
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
109/// Print summary statistics.
110fn 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
133/// Show ADR overview.
134fn 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        // Show tags if present
157        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
182/// Show ADRs organized by tag.
183fn show_by_tag(adrs: &[&crate::core::Adr], _args: &Args) -> Result<()> {
184    // Collect ADRs by tag
185    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    // Sort tags by count
201    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
233/// Interactive browse mode.
234fn 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        // Find matching ADR
252        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                // Show body preview
274                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                // Try to get from notes manager directly
289                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}