koi-net 0.4.1

Local network toolkit: service discovery, DNS, health monitoring, TLS proxy, and certificate mesh
//! CLI command dispatch — the async entry point. Routes each subcommand to its handler in
//! `commands::*`, then falls through to `daemon::daemon_mode`. Moved from main.rs (P07 step 6b).

use std::sync::Arc;

use crate::cli::{
    CertmeshSubcommand, Cli, Command, Config, DnsSubcommand, HealthSubcommand, MdnsSubcommand,
    ProxySubcommand, UdpSubcommand,
};
use crate::commands::status::try_daemon_status;
use crate::daemon::daemon_mode;
use crate::infra::{is_piped_stdin, print_top_level_help};
use crate::{adapters, commands, format, surface};

// ── Async entry point ────────────────────────────────────────────────

pub(crate) async fn run(cli: Cli, config: Config) -> anyhow::Result<()> {
    if let Some(command) = &cli.command {
        return match command {
            Command::Status => commands::status::status(&cli, &config),
            Command::Mdns(mdns_cmd) => {
                config.require_capability("mdns")?;
                match &mdns_cmd.command {
                    None => {
                        surface::print_category_catalog(surface::KoiCategory::Discovery, None)?;
                        Ok(())
                    }
                    Some(MdnsSubcommand::Admin(admin_cmd)) => match &admin_cmd.command {
                        Some(admin) => commands::mdns::admin(admin, &cli),
                        None => {
                            surface::print_category_catalog(
                                surface::KoiCategory::Discovery,
                                Some(surface::KoiScope::Admin),
                            )?;
                            Ok(())
                        }
                    },
                    Some(MdnsSubcommand::Discover { service_type }) => {
                        let mode = commands::detect_mode(&cli);
                        commands::mdns::discover(
                            service_type.as_deref(),
                            cli.json,
                            cli.timeout,
                            mode,
                        )
                        .await
                    }
                    Some(MdnsSubcommand::Announce {
                        name,
                        service_type,
                        port,
                        ip,
                        txt,
                    }) => {
                        let mode = commands::detect_mode(&cli);
                        commands::mdns::announce(
                            name,
                            service_type,
                            *port,
                            ip.as_deref(),
                            txt,
                            cli.json,
                            cli.timeout,
                            mode,
                        )
                        .await
                    }
                    Some(MdnsSubcommand::Unregister { id }) => {
                        let mode = commands::detect_mode(&cli);
                        commands::mdns::unregister(id, cli.json, mode).await
                    }
                    Some(MdnsSubcommand::Resolve { instance }) => {
                        let mode = commands::detect_mode(&cli);
                        commands::mdns::resolve(instance, cli.json, mode).await
                    }
                    Some(MdnsSubcommand::Subscribe { service_type }) => {
                        let mode = commands::detect_mode(&cli);
                        commands::mdns::subscribe(service_type, cli.json, cli.timeout, mode).await
                    }
                }
            }
            Command::Certmesh(cm_cmd) => {
                config.require_capability("certmesh")?;
                let ep = cli.endpoint.as_deref();
                match &cm_cmd.command {
                    None => {
                        surface::print_category_catalog(surface::KoiCategory::Trust, None)?;
                        Ok(())
                    }
                    Some(CertmeshSubcommand::Create {
                        profile,
                        operator,
                        enrollment,
                        require_approval,
                        passphrase,
                    }) => commands::certmesh::create(
                        profile.as_deref(),
                        operator.as_deref(),
                        enrollment.as_deref(),
                        *require_approval,
                        passphrase.as_deref(),
                        cli.json,
                        ep,
                    ),
                    Some(CertmeshSubcommand::Status) => commands::certmesh::status(cli.json, ep),
                    Some(CertmeshSubcommand::Log) => commands::certmesh::log(ep),
                    Some(CertmeshSubcommand::Compliance) => {
                        commands::certmesh::compliance(cli.json, ep)
                    }
                    Some(CertmeshSubcommand::Unlock) => commands::certmesh::unlock(ep),
                    Some(CertmeshSubcommand::SetHook { reload }) => {
                        commands::certmesh::set_hook(reload, cli.json, ep)
                    }
                    Some(CertmeshSubcommand::Join { endpoint }) => {
                        commands::certmesh::join(endpoint.as_deref(), cli.json, ep).await
                    }
                    Some(CertmeshSubcommand::Promote { endpoint }) => {
                        commands::certmesh::promote(endpoint.as_deref(), cli.json, ep).await
                    }
                    Some(CertmeshSubcommand::OpenEnrollment { until }) => {
                        commands::certmesh::open_enrollment(until.as_deref(), cli.json, ep)
                    }
                    Some(CertmeshSubcommand::CloseEnrollment) => {
                        commands::certmesh::close_enrollment(cli.json, ep)
                    }
                    Some(CertmeshSubcommand::SetPolicy {
                        domain,
                        subnet,
                        clear,
                    }) => commands::certmesh::set_policy(
                        domain.as_deref(),
                        subnet.as_deref(),
                        *clear,
                        cli.json,
                        ep,
                    ),
                    Some(CertmeshSubcommand::RotateAuth) => {
                        commands::certmesh::rotate_auth(cli.json, ep)
                    }
                    Some(CertmeshSubcommand::Backup { path }) => {
                        commands::certmesh::backup(path, cli.json, ep)
                    }
                    Some(CertmeshSubcommand::Restore { path }) => {
                        commands::certmesh::restore(path, cli.json, ep)
                    }
                    Some(CertmeshSubcommand::Revoke { hostname, reason }) => {
                        commands::certmesh::revoke(hostname, reason.as_deref(), cli.json, ep)
                    }
                    Some(CertmeshSubcommand::Destroy) => commands::certmesh::destroy(cli.json, ep),
                }
            }
            Command::Dns(dns_cmd) => {
                config.require_capability("dns")?;
                let mode = commands::detect_mode(&cli);
                match &dns_cmd.command {
                    None => {
                        surface::print_category_catalog(surface::KoiCategory::Dns, None)?;
                        Ok(())
                    }
                    Some(DnsSubcommand::Serve) => commands::dns::serve(&config, mode).await,
                    Some(DnsSubcommand::Stop) => commands::dns::stop(mode).await,
                    Some(DnsSubcommand::Status) => {
                        commands::dns::status(&config, mode, cli.json).await
                    }
                    Some(DnsSubcommand::Lookup { name, record_type }) => {
                        commands::dns::lookup(name, record_type, mode, cli.json, &config).await
                    }
                    Some(DnsSubcommand::Add { name, ip, ttl }) => {
                        commands::dns::add(name, ip, *ttl, mode, cli.json, &config.dns_zone)
                    }
                    Some(DnsSubcommand::Remove { name }) => {
                        commands::dns::remove(name, mode, cli.json, &config.dns_zone)
                    }
                    Some(DnsSubcommand::List) => commands::dns::list(mode, cli.json, &config).await,
                }
            }
            Command::Health(health_cmd) => {
                config.require_capability("health")?;
                let mode = commands::detect_mode(&cli);
                match &health_cmd.command {
                    None => {
                        surface::print_category_catalog(surface::KoiCategory::Health, None)?;
                        Ok(())
                    }
                    Some(HealthSubcommand::Status) => {
                        commands::health::status(&config, mode, cli.json).await
                    }
                    Some(HealthSubcommand::Watch { interval }) => {
                        commands::health::watch(&config, mode, *interval).await
                    }
                    Some(HealthSubcommand::Add {
                        name,
                        http,
                        tcp,
                        interval,
                        timeout,
                    }) => {
                        commands::health::add(
                            name,
                            http.as_deref(),
                            tcp.as_deref(),
                            *interval,
                            *timeout,
                            mode,
                            cli.json,
                            &config,
                        )
                        .await
                    }
                    Some(HealthSubcommand::Remove { name }) => {
                        commands::health::remove(name, mode, cli.json, &config).await
                    }
                    Some(HealthSubcommand::Log) => commands::health::log(),
                }
            }
            Command::Proxy(proxy_cmd) => {
                config.require_capability("proxy")?;
                let mode = commands::detect_mode(&cli);
                match &proxy_cmd.command {
                    None => {
                        surface::print_category_catalog(surface::KoiCategory::Proxy, None)?;
                        Ok(())
                    }
                    Some(ProxySubcommand::Add {
                        name,
                        listen,
                        backend,
                        backend_remote,
                    }) => {
                        commands::proxy::add(
                            name,
                            *listen,
                            backend,
                            *backend_remote,
                            mode,
                            cli.json,
                        )
                        .await
                    }
                    Some(ProxySubcommand::Remove { name }) => {
                        commands::proxy::remove(name, mode, cli.json).await
                    }
                    Some(ProxySubcommand::Status) => commands::proxy::status(mode, cli.json).await,
                    Some(ProxySubcommand::List) => commands::proxy::list(mode, cli.json).await,
                }
            }
            Command::Udp(udp_cmd) => {
                config.require_capability("udp")?;
                let mode = commands::detect_mode(&cli);
                match &udp_cmd.command {
                    None => {
                        surface::print_category_catalog(surface::KoiCategory::Udp, None)?;
                        Ok(())
                    }
                    Some(UdpSubcommand::Bind { port, addr, lease }) => {
                        commands::udp::bind(*port, addr, *lease, mode, cli.json).await
                    }
                    Some(UdpSubcommand::Unbind { id }) => {
                        commands::udp::unbind(id, mode, cli.json).await
                    }
                    Some(UdpSubcommand::Send { id, dest, payload }) => {
                        commands::udp::send(id, dest, payload, mode, cli.json).await
                    }
                    Some(UdpSubcommand::Status) => commands::udp::status(mode, cli.json).await,
                    Some(UdpSubcommand::Heartbeat { id }) => {
                        commands::udp::heartbeat(id, mode, cli.json).await
                    }
                }
            }
            Command::Token(token_cmd) => commands::token::run(token_cmd, cli.json),
            // Install, Uninstall, Version, Launch, FactoryReset handled before runtime
            Command::Install
            | Command::Uninstall
            | Command::Version
            | Command::Launch
            | Command::FactoryReset => Ok(()),
        };
    }

    // ── No subcommand provided ─────────────────────────────────────

    // Explicit daemon request: start services
    if cli.daemon {
        return daemon_mode(config).await;
    }

    // Piped CLI mode still works without a subcommand
    if is_piped_stdin() {
        if config.no_mdns {
            anyhow::bail!(
                "Piped mode requires the mDNS capability. \
                 Remove --no-mdns or unset KOI_NO_MDNS to enable it."
            );
        }
        let core = Arc::new(koi_mdns::MdnsCore::new()?);
        adapters::cli::start(core.clone()).await?;
        let _ = core.shutdown().await;
        return Ok(());
    }

    // Try to show daemon status if a healthy daemon is reachable; otherwise stay quiet
    if let Some(status_json) = try_daemon_status(&cli) {
        if cli.json {
            if let Ok(body) = serde_json::to_string_pretty(&status_json) {
                println!("{body}");
            }
        } else {
            print!("{}", format::unified_status(&status_json));
        }
    }

    // Always show available commands/help for discoverability
    let api_endpoint = cli
        .endpoint
        .clone()
        .or_else(koi_config::breadcrumb::read_breadcrumb_endpoint)
        .unwrap_or_else(|| "http://localhost:5641".to_string());
    print_top_level_help(&api_endpoint);
    Ok(())
}