git_adr/cli/
artifacts.rs

1//! List artifacts attached to an ADR.
2
3use anyhow::Result;
4use clap::Args as ClapArgs;
5use colored::Colorize;
6
7use crate::core::{ConfigManager, Git, NotesManager, ARTIFACTS_NOTES_REF};
8
9/// Arguments for the artifacts command.
10#[derive(ClapArgs, Debug)]
11pub struct Args {
12    /// ADR ID.
13    pub adr_id: String,
14
15    /// Output format (text, json).
16    #[arg(long, short, default_value = "text")]
17    pub format: String,
18
19    /// Extract artifact to file.
20    #[arg(long)]
21    pub extract: Option<String>,
22
23    /// Remove artifact from ADR.
24    #[arg(long)]
25    pub remove: bool,
26}
27
28/// Run the artifacts command.
29///
30/// # Errors
31///
32/// Returns an error if listing fails.
33pub fn run(args: Args) -> Result<()> {
34    let git = Git::new();
35    git.check_repository()?;
36
37    let config = ConfigManager::new(git.clone()).load()?;
38    let notes = NotesManager::new(git.clone(), config);
39
40    // Find the ADR
41    let adrs = notes.list()?;
42    let adr = adrs
43        .into_iter()
44        .find(|a| a.id == args.adr_id || a.id.contains(&args.adr_id))
45        .ok_or_else(|| anyhow::anyhow!("ADR not found: {}", args.adr_id))?;
46
47    // Get artifacts for this ADR's commit
48    let artifact_content = git.notes_show(ARTIFACTS_NOTES_REF, &adr.commit)?;
49
50    match artifact_content {
51        Some(content) => {
52            // Parse artifact JSON
53            let artifact: serde_json::Value = serde_json::from_str(&content)?;
54
55            if args.remove {
56                // Remove the artifact from this ADR
57                git.notes_remove(ARTIFACTS_NOTES_REF, &adr.commit)?;
58                eprintln!(
59                    "{} Removed artifact {} from ADR {}",
60                    "✓".green(),
61                    artifact["filename"].as_str().unwrap_or("unknown").cyan(),
62                    adr.id.cyan()
63                );
64            } else if let Some(extract_name) = &args.extract {
65                // Extract the artifact to a file
66                use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
67
68                let encoded = artifact["content"]
69                    .as_str()
70                    .ok_or_else(|| anyhow::anyhow!("No content in artifact"))?;
71
72                let decoded = BASE64.decode(encoded)?;
73                std::fs::write(extract_name, decoded)?;
74
75                eprintln!(
76                    "{} Extracted {} ({} bytes)",
77                    "✓".green(),
78                    extract_name.cyan(),
79                    artifact["size"]
80                );
81            } else if args.format.as_str() == "json" {
82                // Remove content field for listing
83                let mut listing = artifact;
84                if let Some(obj) = listing.as_object_mut() {
85                    obj.remove("content");
86                }
87                println!("{}", serde_json::to_string_pretty(&listing)?);
88            } else {
89                eprintln!("{} Artifacts for ADR {}:", "→".blue(), adr.id.cyan());
90                println!();
91                println!(
92                    "  {} {}",
93                    "Filename:".bold(),
94                    artifact["filename"].as_str().unwrap_or("unknown").cyan()
95                );
96                println!(
97                    "  {} {} bytes",
98                    "Size:".bold(),
99                    artifact["size"].as_u64().unwrap_or(0)
100                );
101                if let Some(desc) = artifact["description"].as_str() {
102                    if !desc.is_empty() {
103                        println!("  {} {}", "Description:".bold(), desc);
104                    }
105                }
106            }
107        },
108        None => {
109            eprintln!("{} No artifacts found for ADR {}", "→".yellow(), adr.id);
110        },
111    }
112
113    Ok(())
114}