Skip to main content

greentic_setup/cli_commands/
inspect.rs

1//! Bundle inspection commands: build, list, status.
2
3use anyhow::{Context, Result};
4
5use crate::bundle;
6use crate::cli_args::*;
7use crate::cli_helpers::{copy_dir_recursive, resolve_bundle_dir};
8use crate::cli_i18n::CliI18n;
9
10/// Build a portable bundle archive.
11pub fn build(args: BundleBuildArgs, i18n: &CliI18n) -> Result<()> {
12    use crate::gtbundle;
13
14    let bundle_dir = resolve_bundle_dir(args.bundle)?;
15
16    bundle::validate_bundle_exists(&bundle_dir).context(i18n.t("cli.error.invalid_bundle"))?;
17
18    let out_str = args.out.to_string_lossy();
19    let is_archive = out_str.ends_with(".gtbundle");
20
21    println!("{}", i18n.t("cli.bundle.build.building"));
22    println!(
23        "{}",
24        i18n.tf(
25            "cli.bundle.add.bundle",
26            &[&bundle_dir.display().to_string()]
27        )
28    );
29    println!(
30        "{}",
31        i18n.tf(
32            "cli.bundle.build.output",
33            &[&args.out.display().to_string()]
34        )
35    );
36    println!(
37        "  Format: {}",
38        if is_archive {
39            "archive (.gtbundle)"
40        } else {
41            "directory"
42        }
43    );
44
45    if let Some(ref tenant) = args.tenant {
46        println!("{}", i18n.tf("cli.bundle.add.tenant", &[tenant]));
47    }
48
49    if args.doctor && !args.skip_doctor {
50        println!("\n{}", i18n.t("cli.bundle.build.running_doctor"));
51    }
52
53    if is_archive {
54        gtbundle::create_gtbundle(&bundle_dir, &args.out)
55            .context("failed to create .gtbundle archive")?;
56    } else {
57        std::fs::create_dir_all(&args.out).context("failed to create output directory")?;
58        copy_dir_recursive(&bundle_dir, &args.out, args.only_used_providers)
59            .context("failed to copy bundle")?;
60    }
61
62    println!(
63        "\n{}",
64        i18n.tf(
65            "cli.bundle.build.success",
66            &[&args.out.display().to_string()]
67        )
68    );
69
70    Ok(())
71}
72
73/// List packs in a bundle.
74pub fn list(args: BundleListArgs, i18n: &CliI18n) -> Result<()> {
75    let bundle_dir = resolve_bundle_dir(args.bundle)?;
76
77    bundle::validate_bundle_exists(&bundle_dir).context(i18n.t("cli.error.invalid_bundle"))?;
78
79    let mut packs = Vec::new();
80    let providers_dir = bundle_dir.join("providers");
81    let packs_dir = bundle_dir.join("packs");
82
83    let domain_dir = providers_dir.join(&args.domain);
84    if domain_dir.exists()
85        && let Ok(entries) = std::fs::read_dir(&domain_dir)
86    {
87        for entry in entries.flatten() {
88            let path = entry.path();
89            if path.extension().is_some_and(|e| e == "gtpack")
90                && let Some(name) = path.file_stem().and_then(|n| n.to_str())
91            {
92                packs.push((name.to_string(), args.domain.clone()));
93            }
94        }
95    }
96
97    if packs_dir.exists()
98        && let Ok(entries) = std::fs::read_dir(&packs_dir)
99    {
100        for entry in entries.flatten() {
101            let path = entry.path();
102            if path.extension().is_some_and(|e| e == "gtpack")
103                && let Some(name) = path.file_stem().and_then(|n| n.to_str())
104            {
105                packs.push((name.to_string(), "pack".to_string()));
106            }
107        }
108    }
109
110    if let Some(ref pack_filter) = args.pack {
111        packs.retain(|(name, _)| name.contains(pack_filter));
112    }
113
114    if args.format == "json" {
115        let output = serde_json::json!({
116            "bundle": bundle_dir.display().to_string(),
117            "domain": args.domain,
118            "pack_count": packs.len(),
119            "packs": packs.iter().map(|(name, domain)| {
120                serde_json::json!({
121                    "name": name,
122                    "domain": domain,
123                })
124            }).collect::<Vec<_>>(),
125        });
126        println!("{}", serde_json::to_string_pretty(&output)?);
127    } else {
128        println!(
129            "{}",
130            i18n.tf(
131                "cli.bundle.list.bundle",
132                &[&bundle_dir.display().to_string()]
133            )
134        );
135        println!("{}", i18n.tf("cli.bundle.list.domain", &[&args.domain]));
136        println!(
137            "{}",
138            i18n.tf("cli.bundle.list.packs_found", &[&packs.len().to_string()])
139        );
140
141        for (name, domain) in &packs {
142            println!("  - {} ({})", name, domain);
143        }
144    }
145
146    Ok(())
147}
148
149/// Show bundle status.
150pub fn status(args: BundleStatusArgs, i18n: &CliI18n) -> Result<()> {
151    let bundle_dir = resolve_bundle_dir(args.bundle)?;
152
153    if !bundle_dir.exists() {
154        if args.format == "json" {
155            println!(r#"{{"exists": false, "path": "{}"}}"#, bundle_dir.display());
156        } else {
157            println!(
158                "{}",
159                i18n.tf(
160                    "cli.bundle.status.not_found",
161                    &[&bundle_dir.display().to_string()]
162                )
163            );
164        }
165        return Ok(());
166    }
167
168    let is_valid = bundle::is_bundle_root(&bundle_dir);
169
170    let providers_dir = bundle_dir.join("providers");
171    let packs_dir = bundle_dir.join("packs");
172    let mut pack_count = 0;
173    let mut packs = Vec::new();
174
175    if providers_dir.exists() {
176        for domain in &[
177            "messaging",
178            "events",
179            "oauth",
180            "secrets",
181            "mcp",
182            "state",
183            "other",
184        ] {
185            let domain_dir = providers_dir.join(domain);
186            if domain_dir.exists()
187                && let Ok(entries) = std::fs::read_dir(&domain_dir)
188            {
189                for entry in entries.flatten() {
190                    let path = entry.path();
191                    if path.extension().is_some_and(|e| e == "gtpack") {
192                        pack_count += 1;
193                        if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
194                            packs.push(format!("providers/{}/{}", domain, name));
195                        }
196                    }
197                }
198            }
199        }
200    }
201
202    if packs_dir.exists()
203        && let Ok(entries) = std::fs::read_dir(&packs_dir)
204    {
205        for entry in entries.flatten() {
206            let path = entry.path();
207            if path.extension().is_some_and(|e| e == "gtpack") {
208                pack_count += 1;
209                if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
210                    packs.push(format!("packs/{}", name));
211                }
212            }
213        }
214    }
215
216    let tenants_dir = bundle_dir.join("tenants");
217    let mut tenant_count = 0;
218    let mut tenants = Vec::new();
219
220    if tenants_dir.exists()
221        && let Ok(entries) = std::fs::read_dir(&tenants_dir)
222    {
223        for entry in entries.flatten() {
224            if entry.path().is_dir() {
225                tenant_count += 1;
226                if let Some(name) = entry.file_name().to_str() {
227                    tenants.push(name.to_string());
228                }
229            }
230        }
231    }
232
233    if args.format == "json" {
234        let status = serde_json::json!({
235            "exists": true,
236            "valid": is_valid,
237            "path": bundle_dir.display().to_string(),
238            "pack_count": pack_count,
239            "packs": packs,
240            "tenant_count": tenant_count,
241            "tenants": tenants,
242        });
243        println!("{}", serde_json::to_string_pretty(&status)?);
244    } else {
245        println!(
246            "{}",
247            i18n.tf(
248                "cli.bundle.status.bundle_label",
249                &[&bundle_dir.display().to_string()]
250            )
251        );
252        let valid_status = if is_valid {
253            i18n.t("cli.bundle.status.valid_yes")
254        } else {
255            i18n.t("cli.bundle.status.valid_no")
256        };
257        println!("Valid: {}", valid_status);
258        println!(
259            "{}",
260            i18n.tf("cli.bundle.status.packs", &[&pack_count.to_string()])
261        );
262        for pack in &packs {
263            println!("  - {}", pack);
264        }
265        println!(
266            "{}",
267            i18n.tf("cli.bundle.status.tenants", &[&tenant_count.to_string()])
268        );
269        for tenant in &tenants {
270            println!("  - {}", tenant);
271        }
272    }
273
274    Ok(())
275}