Skip to main content

crate_checker/
cli.rs

1//! Command-line interface for the crate checker application
2
3use crate::client::CrateClient;
4use crate::config::{AppConfig, EnvironmentConfig};
5use crate::error::Result;
6use crate::server::start_server;
7use crate::types::*;
8use crate::utils::{
9    create_example_batch_inputs, format_download_count, parse_json_file, parse_json_input,
10    parse_timeout, truncate_text, validate_batch_input,
11};
12use crate::DEFAULT_SERVER_PORT;
13use clap::{Parser, Subcommand, ValueEnum};
14use serde::Serialize;
15use serde_json;
16use std::path::PathBuf;
17use tabled::{Table, Tabled};
18use tracing::{error, info, warn};
19
20/// Crate Checker - A comprehensive Rust crate information retrieval tool
21#[derive(Parser)]
22#[command(
23    name = "crate-checker",
24    version = "1.0.0",
25    about = "Check crate existence, versions, dependencies and more from crates.io",
26    long_about = "A comprehensive tool for retrieving information about Rust crates from crates.io. 
27Supports checking crate existence, getting version information, searching crates, 
28batch operations, and running as an HTTP API server."
29)]
30pub struct Cli {
31    /// Output format
32    #[arg(short, long, global = true, value_enum, default_value = "table")]
33    pub format: OutputFormat,
34
35    /// Enable verbose output
36    #[arg(long, global = true)]
37    pub verbose: bool,
38
39    /// Enable quiet mode (only errors)
40    #[arg(short, long, global = true)]
41    pub quiet: bool,
42
43    /// Configuration file path
44    #[arg(long, long, global = true)]
45    pub config: Option<PathBuf>,
46
47    /// Timeout for requests (e.g. 30s, 2m, 1h)
48    #[arg(long, global = true)]
49    pub timeout: Option<String>,
50
51    /// Custom crates.io API URL
52    #[arg(long, global = true)]
53    pub api_url: Option<String>,
54
55    #[command(subcommand)]
56    pub command: Commands,
57}
58
59/// Available commands
60#[derive(Subcommand)]
61pub enum Commands {
62    /// Check if a crate exists
63    Check {
64        /// Name of the crate to check
65        crate_name: String,
66
67        /// Specific version to check (optional)
68        #[arg(short, long)]
69        version: Option<String>,
70    },
71
72    /// Check multiple crates at once with merged output
73    CheckMultiple {
74        /// Names of the crates to check (space-separated)
75        crate_names: Vec<String>,
76
77        /// Show only summary (don't list individual results)
78        #[arg(short, long)]
79        summary_only: bool,
80
81        /// Exit with error code if any crate doesn't exist
82        #[arg(long)]
83        fail_on_missing: bool,
84    },
85
86    /// Get detailed information about a crate
87    Info {
88        /// Name of the crate
89        crate_name: String,
90
91        /// Include dependency information
92        #[arg(short, long)]
93        deps: bool,
94
95        /// Include download statistics
96        #[arg(short, long)]
97        stats: bool,
98    },
99
100    /// List all versions of a crate
101    Versions {
102        /// Name of the crate
103        crate_name: String,
104
105        /// Show only non-yanked versions
106        #[arg(long)]
107        no_yanked: bool,
108
109        /// Limit number of versions to show
110        #[arg(short, long)]
111        limit: Option<usize>,
112    },
113
114    /// Search for crates by name or keywords
115    Search {
116        /// Search query
117        query: String,
118
119        /// Maximum number of results
120        #[arg(short, long, default_value = "10")]
121        limit: usize,
122
123        /// Show only exact matches
124        #[arg(short, long)]
125        exact: bool,
126    },
127
128    /// Show dependencies for a crate version
129    Deps {
130        /// Name of the crate
131        crate_name: String,
132
133        /// Version (defaults to latest)
134        #[arg(short, long)]
135        version: Option<String>,
136
137        /// Show only runtime dependencies
138        #[arg(long)]
139        runtime_only: bool,
140    },
141
142    /// Show download statistics for a crate
143    Stats {
144        /// Name of the crate
145        crate_name: String,
146
147        /// Show version-specific stats
148        #[arg(short, long)]
149        versions: bool,
150    },
151
152    /// Process multiple crates at once
153    Batch {
154        /// JSON string with batch input
155        #[arg(long, long, conflicts_with = "file")]
156        json: Option<String>,
157
158        /// JSON file with batch input
159        #[arg(long, long, conflicts_with = "json")]
160        file: Option<PathBuf>,
161
162        /// Process requests in parallel
163        #[arg(short, long)]
164        parallel: bool,
165    },
166
167    /// Start HTTP API server
168    Server {
169        /// Port to bind to
170        #[arg(short, long, default_value_t = DEFAULT_SERVER_PORT)]
171        port: u16,
172
173        /// Host to bind to
174        #[arg(long, default_value = "0.0.0.0")]
175        host: String,
176
177        /// Enable CORS
178        #[arg(long)]
179        cors: bool,
180
181        /// Configuration file for server
182        #[arg(short, long)]
183        config: Option<PathBuf>,
184    },
185
186    /// Generate sample configuration file
187    Config {
188        /// Output file (prints to stdout if not specified)
189        #[arg(short, long)]
190        output: Option<PathBuf>,
191    },
192
193    /// Show examples of JSON batch input formats
194    Examples,
195}
196
197/// Output format options
198#[derive(ValueEnum, Clone, Debug, Default)]
199pub enum OutputFormat {
200    /// Human-readable table format
201    #[default]
202    Table,
203    /// JSON format
204    Json,
205    /// YAML format
206    Yaml,
207    /// Compact text format
208    Compact,
209    /// CSV format
210    Csv,
211}
212
213/// Tabled display for crate information
214#[derive(Tabled)]
215struct CrateInfoDisplay {
216    #[tabled(rename = "Name")]
217    name: String,
218    #[tabled(rename = "Version")]
219    version: String,
220    #[tabled(rename = "Downloads")]
221    downloads: String,
222    #[tabled(rename = "Description")]
223    description: String,
224}
225
226/// Tabled display for version information
227#[derive(Tabled)]
228struct VersionDisplay {
229    #[tabled(rename = "Version")]
230    version: String,
231    #[tabled(rename = "Downloads")]
232    downloads: String,
233    #[tabled(rename = "Published")]
234    published: String,
235    #[tabled(rename = "Yanked")]
236    yanked: String,
237}
238
239/// Tabled display for search results
240#[derive(Tabled)]
241struct SearchResultDisplay {
242    #[tabled(rename = "Name")]
243    name: String,
244    #[tabled(rename = "Version")]
245    version: String,
246    #[tabled(rename = "Downloads")]
247    downloads: String,
248    #[tabled(rename = "Description")]
249    description: String,
250}
251
252/// Tabled display for dependencies
253#[derive(Tabled)]
254struct DependencyDisplay {
255    #[tabled(rename = "Name")]
256    name: String,
257    #[tabled(rename = "Version")]
258    version: String,
259    #[tabled(rename = "Kind")]
260    kind: String,
261    #[tabled(rename = "Optional")]
262    optional: String,
263}
264
265/// Tabled display for multi-check results
266#[derive(Tabled)]
267struct MultiCheckDisplay {
268    #[tabled(rename = "Crate")]
269    name: String,
270    #[tabled(rename = "Status")]
271    status: String,
272    #[tabled(rename = "Latest Version")]
273    version: String,
274}
275
276/// Summary for multi-check results
277#[derive(Serialize)]
278struct MultiCheckSummary {
279    total_checked: usize,
280    existing: usize,
281    missing: usize,
282    existing_crates: Vec<String>,
283    missing_crates: Vec<String>,
284}
285
286/// Run the CLI application
287pub async fn run() -> Result<()> {
288    let cli = Cli::parse();
289
290    // Initialize logging
291    init_logging(cli.verbose, cli.quiet, &cli.format);
292
293    // Load configuration
294    let config = if let Some(config_path) = &cli.config {
295        AppConfig::load_from_file(Some(config_path))?
296    } else {
297        AppConfig::load()?
298    };
299
300    // Apply environment overrides
301    let env_config = EnvironmentConfig::detect();
302    let mut final_config = config;
303    env_config.apply_overrides(&mut final_config);
304
305    // Create client with configuration
306    let mut client_builder = CrateClient::builder();
307
308    if let Some(url) = &cli.api_url {
309        client_builder = client_builder.base_url(url);
310    } else {
311        client_builder = client_builder.base_url(&final_config.crates_io.api_url);
312    }
313
314    if let Some(timeout_str) = &cli.timeout {
315        let timeout = parse_timeout(timeout_str)?;
316        client_builder = client_builder.timeout(timeout);
317    } else {
318        client_builder = client_builder.timeout(std::time::Duration::from_secs(
319            final_config.crates_io.timeout_seconds,
320        ));
321    }
322
323    let client = client_builder.build()?;
324
325    // Execute command
326    match cli.command {
327        Commands::Check {
328            crate_name,
329            version,
330        } => {
331            handle_check(client, &crate_name, version.as_deref(), &cli.format).await?;
332        }
333        Commands::CheckMultiple {
334            crate_names,
335            summary_only,
336            fail_on_missing,
337        } => {
338            handle_check_multiple(
339                client,
340                crate_names,
341                summary_only,
342                fail_on_missing,
343                &cli.format,
344            )
345            .await?;
346        }
347        Commands::Info {
348            crate_name,
349            deps,
350            stats,
351        } => {
352            handle_info(client, &crate_name, deps, stats, &cli.format).await?;
353        }
354        Commands::Versions {
355            crate_name,
356            no_yanked,
357            limit,
358        } => {
359            handle_versions(client, &crate_name, no_yanked, limit, &cli.format).await?;
360        }
361        Commands::Search {
362            query,
363            limit,
364            exact,
365        } => {
366            handle_search(client, &query, limit, exact, &cli.format).await?;
367        }
368        Commands::Deps {
369            crate_name,
370            version,
371            runtime_only,
372        } => {
373            handle_deps(
374                client,
375                &crate_name,
376                version.as_deref(),
377                runtime_only,
378                &cli.format,
379            )
380            .await?;
381        }
382        Commands::Stats {
383            crate_name,
384            versions,
385        } => {
386            handle_stats(client, &crate_name, versions, &cli.format).await?;
387        }
388        Commands::Batch {
389            json,
390            file,
391            parallel,
392        } => {
393            handle_batch(
394                client,
395                json.as_deref(),
396                file.as_deref(),
397                parallel,
398                &cli.format,
399            )
400            .await?;
401        }
402        Commands::Server {
403            port,
404            host,
405            cors,
406            config,
407        } => {
408            let mut server_config = final_config;
409            server_config.server.port = port;
410            server_config.server.host = host;
411            server_config.server.enable_cors = cors;
412
413            if let Some(config_path) = config {
414                server_config = AppConfig::load_from_file(Some(config_path))?;
415            }
416
417            start_server(server_config).await?;
418        }
419        Commands::Config { output } => {
420            handle_config(output.as_deref())?;
421        }
422        Commands::Examples => {
423            handle_examples();
424        }
425    }
426
427    Ok(())
428}
429
430/// Handle the check command
431async fn handle_check(
432    client: CrateClient,
433    crate_name: &str,
434    version: Option<&str>,
435    format: &OutputFormat,
436) -> Result<()> {
437    if let Some(version) = version {
438        // Check specific version
439        let versions = client.get_all_versions(crate_name).await?;
440        let version_exists = versions.iter().any(|v| v.num == version);
441
442        let result = serde_json::json!({
443            "crate": crate_name,
444            "version": version,
445            "exists": version_exists
446        });
447
448        output_result(&serde_json::to_value(result)?, format)?;
449
450        if !version_exists {
451            std::process::exit(1);
452        }
453    } else {
454        // Check crate existence
455        let exists = client.crate_exists(crate_name).await?;
456        let result = serde_json::json!({
457            "crate": crate_name,
458            "exists": exists
459        });
460
461        output_result(&serde_json::to_value(&result)?, format)?;
462
463        if !exists {
464            std::process::exit(1);
465        }
466    }
467
468    Ok(())
469}
470
471/// Handle the check multiple command
472async fn handle_check_multiple(
473    client: CrateClient,
474    crate_names: Vec<String>,
475    summary_only: bool,
476    fail_on_missing: bool,
477    format: &OutputFormat,
478) -> Result<()> {
479    use crate::error::CrateCheckerError;
480
481    if crate_names.is_empty() {
482        return Err(CrateCheckerError::ValidationError(
483            "At least one crate name must be provided".to_string(),
484        ));
485    }
486
487    info!("Checking {} crates", crate_names.len());
488
489    let mut existing_crates = Vec::new();
490    let mut missing_crates = Vec::new();
491    let mut results = Vec::new();
492
493    // Check each crate
494    for crate_name in &crate_names {
495        match client.crate_exists(crate_name).await {
496            Ok(exists) => {
497                let version = if exists {
498                    match client.get_latest_version(crate_name).await {
499                        Ok(v) => v,
500                        Err(_) => "unknown".to_string(),
501                    }
502                } else {
503                    "N/A".to_string()
504                };
505
506                let status = if exists { "EXISTS" } else { "MISSING" };
507
508                results.push(MultiCheckDisplay {
509                    name: crate_name.clone(),
510                    status: status.to_string(),
511                    version,
512                });
513
514                if exists {
515                    existing_crates.push(crate_name.clone());
516                } else {
517                    missing_crates.push(crate_name.clone());
518                }
519            }
520            Err(e) => {
521                error!("Error checking crate '{}': {}", crate_name, e);
522                results.push(MultiCheckDisplay {
523                    name: crate_name.clone(),
524                    status: "ERROR".to_string(),
525                    version: "N/A".to_string(),
526                });
527                missing_crates.push(crate_name.clone());
528            }
529        }
530    }
531
532    // Create summary
533    let summary = MultiCheckSummary {
534        total_checked: crate_names.len(),
535        existing: existing_crates.len(),
536        missing: missing_crates.len(),
537        existing_crates: existing_crates.clone(),
538        missing_crates: missing_crates.clone(),
539    };
540
541    // Output results based on format and options
542    match format {
543        OutputFormat::Table => {
544            if !summary_only {
545                println!("{}", Table::new(results));
546                println!();
547            }
548
549            // Always show summary for table format
550            println!("=== SUMMARY ===");
551            println!("Total checked: {}", summary.total_checked);
552            println!(
553                "Existing: {} ({}%)",
554                summary.existing,
555                (summary.existing as f32 / summary.total_checked as f32 * 100.0).round()
556            );
557            println!(
558                "Missing: {} ({}%)",
559                summary.missing,
560                (summary.missing as f32 / summary.total_checked as f32 * 100.0).round()
561            );
562
563            if !summary.existing_crates.is_empty() {
564                println!("\nExisting crates:");
565                for crate_name in &summary.existing_crates {
566                    println!("  ✓ {}", crate_name);
567                }
568            }
569
570            if !summary.missing_crates.is_empty() {
571                println!("\nMissing crates:");
572                for crate_name in &summary.missing_crates {
573                    println!("  ✗ {}", crate_name);
574                }
575            }
576        }
577        _ => {
578            let output_data = if summary_only {
579                serde_json::to_value(&summary)?
580            } else {
581                serde_json::json!({
582                    "results": results.into_iter().map(|r| serde_json::json!({
583                        "crate": r.name,
584                        "status": r.status,
585                        "version": r.version
586                    })).collect::<Vec<_>>(),
587                    "summary": summary
588                })
589            };
590            output_result(&output_data, format)?;
591        }
592    }
593
594    // Exit with error if requested and there are missing crates
595    if fail_on_missing && !missing_crates.is_empty() {
596        std::process::exit(1);
597    }
598
599    Ok(())
600}
601
602/// Handle the info command
603async fn handle_info(
604    client: CrateClient,
605    crate_name: &str,
606    include_deps: bool,
607    include_stats: bool,
608    format: &OutputFormat,
609) -> Result<()> {
610    let info = client.get_crate_info(crate_name).await?;
611
612    match format {
613        OutputFormat::Table => {
614            let display = CrateInfoDisplay {
615                name: info.name.clone(),
616                version: info.newest_version.clone(),
617                downloads: format_download_count(info.downloads),
618                description: info.description.as_deref().unwrap_or("N/A").to_string(),
619            };
620            println!("{}", Table::new([display]));
621
622            if !info.keywords.is_empty() {
623                println!("\nKeywords: {}", info.keywords.join(", "));
624            }
625            if !info.categories.is_empty() {
626                println!("Categories: {}", info.categories.join(", "));
627            }
628            if let Some(repo) = &info.repository {
629                println!("Repository: {}", repo);
630            }
631            if let Some(homepage) = &info.homepage {
632                println!("Homepage: {}", homepage);
633            }
634        }
635        _ => {
636            let mut result = serde_json::to_value(&info)?;
637
638            if include_deps {
639                if let Ok(deps) = client
640                    .get_crate_dependencies(crate_name, &info.newest_version)
641                    .await
642                {
643                    result["dependencies"] = serde_json::to_value(deps)?;
644                }
645            }
646
647            if include_stats {
648                if let Ok(stats) = client.get_download_stats(crate_name).await {
649                    result["download_stats"] = serde_json::to_value(stats)?;
650                }
651            }
652
653            output_result(&result, format)?;
654        }
655    }
656
657    Ok(())
658}
659
660/// Handle the versions command
661async fn handle_versions(
662    client: CrateClient,
663    crate_name: &str,
664    no_yanked: bool,
665    limit: Option<usize>,
666    format: &OutputFormat,
667) -> Result<()> {
668    let mut versions = client.get_all_versions(crate_name).await?;
669
670    if no_yanked {
671        versions.retain(|v| !v.yanked);
672    }
673
674    if let Some(limit) = limit {
675        versions.truncate(limit);
676    }
677
678    match format {
679        OutputFormat::Table => {
680            let displays: Vec<VersionDisplay> = versions
681                .into_iter()
682                .map(|v| VersionDisplay {
683                    version: v.num,
684                    downloads: format_download_count(v.downloads),
685                    published: v.created_at.format("%Y-%m-%d").to_string(),
686                    yanked: if v.yanked { "Yes" } else { "No" }.to_string(),
687                })
688                .collect();
689            println!("{}", Table::new(displays));
690        }
691        _ => {
692            output_result(&serde_json::to_value(&versions)?, format)?;
693        }
694    }
695
696    Ok(())
697}
698
699/// Handle the search command
700async fn handle_search(
701    client: CrateClient,
702    query: &str,
703    limit: usize,
704    exact: bool,
705    format: &OutputFormat,
706) -> Result<()> {
707    let mut results = client.search_crates(query, Some(limit)).await?;
708
709    if exact {
710        results.retain(|r| r.exact_match);
711    }
712
713    match format {
714        OutputFormat::Table => {
715            let displays: Vec<SearchResultDisplay> = results
716                .into_iter()
717                .map(|r| SearchResultDisplay {
718                    name: r.name,
719                    version: r.newest_version,
720                    downloads: format_download_count(r.downloads),
721                    description: truncate_text(r.description.as_deref().unwrap_or("N/A"), 50),
722                })
723                .collect();
724            println!("{}", Table::new(displays));
725        }
726        _ => {
727            output_result(&serde_json::to_value(&results)?, format)?;
728        }
729    }
730
731    Ok(())
732}
733
734/// Handle the deps command
735async fn handle_deps(
736    client: CrateClient,
737    crate_name: &str,
738    version: Option<&str>,
739    runtime_only: bool,
740    format: &OutputFormat,
741) -> Result<()> {
742    let version = if let Some(v) = version {
743        v.to_string()
744    } else {
745        client.get_latest_version(crate_name).await?
746    };
747
748    let mut deps = client.get_crate_dependencies(crate_name, &version).await?;
749
750    if runtime_only {
751        deps.retain(|d| d.kind == "normal");
752    }
753
754    match format {
755        OutputFormat::Table => {
756            let displays: Vec<DependencyDisplay> = deps
757                .into_iter()
758                .map(|d| {
759                    DependencyDisplay {
760                        name: d.name,
761                        version: d.req, // Use req field directly
762                        kind: d.kind,
763                        optional: if d.optional { "Yes" } else { "No" }.to_string(),
764                    }
765                })
766                .collect();
767            println!("{}", Table::new(displays));
768        }
769        _ => {
770            output_result(&serde_json::to_value(&deps)?, format)?;
771        }
772    }
773
774    Ok(())
775}
776
777/// Handle the stats command
778async fn handle_stats(
779    client: CrateClient,
780    crate_name: &str,
781    show_versions: bool,
782    format: &OutputFormat,
783) -> Result<()> {
784    let stats = client.get_download_stats(crate_name).await?;
785
786    match format {
787        OutputFormat::Table => {
788            println!("Download Statistics for '{}':", crate_name);
789            println!("Total Downloads: {}", format_download_count(stats.total));
790
791            if show_versions && !stats.versions.is_empty() {
792                println!("\nVersion Downloads:");
793                let version_displays: Vec<_> = stats
794                    .versions
795                    .into_iter()
796                    .take(10)
797                    .map(|v| (v.version, format_download_count(v.downloads)))
798                    .collect();
799
800                for (version, downloads) in version_displays {
801                    println!("  {}: {}", version, downloads);
802                }
803            }
804        }
805        _ => {
806            output_result(&serde_json::to_value(&stats)?, format)?;
807        }
808    }
809
810    Ok(())
811}
812
813/// Handle the batch command
814async fn handle_batch(
815    client: CrateClient,
816    json: Option<&str>,
817    file: Option<&std::path::Path>,
818    parallel: bool,
819    format: &OutputFormat,
820) -> Result<()> {
821    let batch_input = if let Some(json_str) = json {
822        parse_json_input(json_str)?
823    } else if let Some(file_path) = file {
824        parse_json_file(file_path)?
825    } else {
826        return Err(crate::error::CrateCheckerError::ValidationError(
827            "Either --json or --file must be provided".to_string(),
828        ));
829    };
830
831    validate_batch_input(&batch_input)?;
832
833    info!(
834        "Processing batch request with {} mode",
835        if parallel { "parallel" } else { "sequential" }
836    );
837
838    let result = match batch_input {
839        BatchInput::CrateVersionMap(map) => client.process_crate_version_map(map).await?,
840        BatchInput::CrateList { crates } => {
841            let results = client.process_crate_list(crates).await?;
842            BatchResult {
843                results,
844                total_processed: 0,
845                successful: 0,
846                failed: 0,
847                processing_time_ms: 0,
848            }
849        }
850        BatchInput::Operations { operations } => {
851            client.process_batch_operations(operations).await?.result
852        }
853    };
854
855    output_result(&serde_json::to_value(&result)?, format)?;
856
857    Ok(())
858}
859
860/// Handle the config command
861fn handle_config(output: Option<&std::path::Path>) -> Result<()> {
862    let sample_config = AppConfig::create_sample_config();
863
864    if let Some(path) = output {
865        std::fs::write(path, sample_config)?;
866        println!("Configuration written to: {}", path.display());
867    } else {
868        println!("{}", sample_config);
869    }
870
871    Ok(())
872}
873
874/// Handle the examples command
875fn handle_examples() {
876    println!("JSON Batch Input Examples:\n");
877
878    let examples = create_example_batch_inputs();
879    for (title, example) in examples {
880        println!("{}:", title);
881        println!("{}\n", example);
882    }
883
884    println!("Usage:");
885    println!("  crate-checker batch --json '<json_string>'");
886    println!("  crate-checker batch --file input.json");
887}
888
889/// Output a result in the specified format
890fn output_result(value: &serde_json::Value, format: &OutputFormat) -> Result<()> {
891    match format {
892        OutputFormat::Json => {
893            println!("{}", serde_json::to_string_pretty(value)?);
894        }
895        OutputFormat::Yaml => {
896            println!("{}", serde_yaml::to_string(value)?);
897        }
898        OutputFormat::Compact => {
899            println!("{}", serde_json::to_string(value)?);
900        }
901        OutputFormat::Csv => {
902            // Simple CSV output for basic structures
903            if let Some(array) = value.as_array() {
904                if let Some(first) = array.first() {
905                    if let Some(obj) = first.as_object() {
906                        // Print headers
907                        let headers: Vec<String> = obj.keys().map(|k| k.to_string()).collect();
908                        println!("{}", headers.join(","));
909
910                        // Print rows
911                        for item in array {
912                            if let Some(obj) = item.as_object() {
913                                let values: Vec<_> = headers
914                                    .iter()
915                                    .map(|h| obj.get(h).and_then(|v| v.as_str()).unwrap_or("N/A"))
916                                    .collect();
917                                println!("{}", values.join(","));
918                            }
919                        }
920                    }
921                }
922            } else {
923                warn!("CSV format is only supported for array structures");
924                println!("{}", serde_json::to_string_pretty(value)?);
925            }
926        }
927        OutputFormat::Table => {
928            // Table format should be handled by the individual command handlers
929            println!("{}", serde_json::to_string_pretty(value)?);
930        }
931    }
932
933    Ok(())
934}
935
936/// Initialize logging based on CLI flags
937fn init_logging(verbose: bool, quiet: bool, format: &OutputFormat) {
938    // For structured output formats (JSON, YAML, CSV), suppress logging to stdout
939    // or set to quiet mode automatically to avoid interfering with output parsing
940    let should_suppress = matches!(
941        format,
942        OutputFormat::Json | OutputFormat::Yaml | OutputFormat::Csv | OutputFormat::Compact
943    );
944
945    let level = if quiet || should_suppress {
946        tracing::Level::ERROR
947    } else if verbose {
948        tracing::Level::DEBUG
949    } else {
950        tracing::Level::INFO
951    };
952
953    // Configure logging to stderr to not interfere with stdout output
954    tracing_subscriber::fmt()
955        .with_max_level(level)
956        .with_target(false)
957        .with_writer(std::io::stderr) // Always write logs to stderr
958        .init();
959}