devpulse 1.0.0

Developer diagnostics: HTTP timing, build artifact cleanup, environment health checks, port scanning, PATH analysis, and config format conversion
//! devpulse — Take the pulse of your dev environment.
//!
//! Entry point: Windows VT bootstrap, CLI parsing, subcommand dispatch,
//! TUI dashboard launch, and exit code mapping.
//! All logic lives in feature modules.

mod cli;
mod convert;
mod doctor;
mod env;
mod error;
mod http;
mod ports;
mod sweep;
mod tui;
mod utils;

use std::io::{IsTerminal, Write};
use std::process::ExitCode;

use clap::{CommandFactory, Parser};
use clap_complete::generate;
use colored::Colorize;

use cli::{Cli, ColorChoice, Commands};

fn main() -> ExitCode {
    // Enable ANSI color support in Windows legacy consoles (PowerShell 5.1 / conhost)
    #[cfg(windows)]
    {
        colored::control::set_virtual_terminal(true).ok();
    }

    // Check if we should launch the TUI (no args or just "tui")
    let raw_args: Vec<String> = wild::args_os()
        .map(|a| a.to_string_lossy().to_string())
        .collect();

    // Launch TUI if: no subcommand args, or explicit "tui" subcommand
    // Only if stdin is connected to a real terminal (not piped/redirected)
    let is_tty = std::io::stdin().is_terminal();
    if is_tty
        && (raw_args.len() <= 1
            || (raw_args.len() == 2 && raw_args[1].eq_ignore_ascii_case("tui")))
    {
        match tui::run() {
            Ok(()) => return ExitCode::SUCCESS,
            Err(e) => {
                let _ = writeln!(std::io::stderr(), "{} TUI error: {e}", "error:".red().bold());
                return ExitCode::FAILURE;
            }
        }
    }

    // Parse CLI using wild::args_os() for Windows glob expansion
    let cli = Cli::parse_from(wild::args_os());

    // Apply color settings based on --color flag and NO_COLOR env var
    match cli.color {
        ColorChoice::Never => colored::control::set_override(false),
        ColorChoice::Always => colored::control::set_override(true),
        ColorChoice::Auto => {
            if std::env::var_os("NO_COLOR").is_some() {
                colored::control::set_override(false);
            }
        }
    }

    // Dispatch to the appropriate subcommand handler
    let command = match cli.command {
        Some(cmd) => cmd,
        None => {
            // No subcommand: launch TUI if terminal, else show help
            if std::io::stdin().is_terminal() {
                match tui::run() {
                    Ok(()) => return ExitCode::SUCCESS,
                    Err(e) => {
                        let _ = writeln!(std::io::stderr(), "{} TUI error: {e}", "error:".red().bold());
                        return ExitCode::FAILURE;
                    }
                }
            } else {
                // Non-interactive: print help
                Cli::command().print_help().ok();
                println!();
                return ExitCode::SUCCESS;
            }
        }
    };

    let result = match command {
        Commands::Http {
            url,
            method,
            headers,
            data,
        } => {
            http::run(&url, &method, &headers, &data, cli.json).map_err(error::DevpulseError::Http)
        }

        Commands::Sweep {
            path,
            yes,
            min_size,
        } => sweep::run(path.as_deref(), yes, &min_size, cli.json)
            .map_err(error::DevpulseError::Sweep),

        Commands::Doctor => doctor::run(cli.json).map_err(error::DevpulseError::Doctor),

        Commands::Ports { port, scan_range } => {
            // If --scan-range is provided, run the range scan and display results
            if let Some(ref range) = scan_range {
                if let Some((start_str, end_str)) = range.split_once('-') {
                    match (start_str.trim().parse::<u16>(), end_str.trim().parse::<u16>()) {
                        (Ok(start), Ok(end)) if start <= end => {
                            let results = ports::scan_range("127.0.0.1", start, end);
                            let open: Vec<_> = results.iter().filter(|e| e.open).collect();
                            if cli.json {
                                let json_str = serde_json::to_string_pretty(&results)
                                    .unwrap_or_else(|_| "[]".to_string());
                                println!("{json_str}");
                            } else {
                                println!();
                                println!(
                                    "  {} {} {} {} {}-{}",
                                    "devpulse".bold(),
                                    "──".dimmed(),
                                    "Port Range Scan".bold(),
                                    "──".dimmed(),
                                    start,
                                    end
                                );
                                println!();
                                if open.is_empty() {
                                    println!("  No open ports found in range {start}-{end}.");
                                } else {
                                    println!(
                                        "  {:<8} {:<16} {:<10} {}",
                                        "Port".bold(),
                                        "Service".bold(),
                                        "Latency".bold(),
                                        "Banner".bold()
                                    );
                                    println!("  {}", "".repeat(65).dimmed());
                                    for e in &open {
                                        let lat = e.latency_ms.map(|ms| format!("{ms}ms")).unwrap_or_default();
                                        let banner = e.banner.as_deref().unwrap_or("");
                                        println!(
                                            "  {:<8} {:<16} {:<10} {}",
                                            e.port.to_string().bold().white(),
                                            e.service.green(),
                                            lat.dimmed(),
                                            banner.dimmed()
                                        );
                                    }
                                }
                                println!();
                                println!(
                                    "  {} open / {} scanned",
                                    open.len().to_string().bold().green(),
                                    results.len().to_string().bold()
                                );
                                println!();
                            }
                            Ok(())
                        }
                        _ => Err(error::DevpulseError::Ports(ports::PortsError::CommandFailed(
                            format!("Invalid range: {range} — use START-END (e.g., 1-1024)"),
                        ))),
                    }
                } else {
                    Err(error::DevpulseError::Ports(ports::PortsError::CommandFailed(
                        format!("Invalid range format: {range} — use START-END (e.g., 1-1024)"),
                    )))
                }
            } else {
                ports::run(port, cli.json).map_err(error::DevpulseError::Ports)
            }
        }

        Commands::Env { filter } => {
            env::run(filter.as_deref(), cli.json).map_err(error::DevpulseError::Env)
        }

        Commands::Convert { input, to, from, output } => {
            convert::run(&input, &to, from.as_deref(), output.as_deref(), cli.json)
                .map_err(error::DevpulseError::Convert)
        }

        Commands::Completions { shell } => {
            let mut cmd = Cli::command();
            generate(shell, &mut cmd, "devpulse", &mut std::io::stdout());
            Ok(())
        }
    };

    // Map errors to stderr output and non-zero exit code
    match result {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            let _ = writeln!(std::io::stderr(), "{} {e}", "error:".red().bold());
            ExitCode::FAILURE
        }
    }
}