nolgia-cli 0.2.3

CLI for the Nolgia generative media platform (image, audio, video)
mod auth;
mod commands;
mod output;
mod update_check;

use anyhow::Result;
use clap::{Parser, Subcommand};
use commands::{
    CommandContext, account, assets, billing, r#gen, models, pat, skills, status, wait,
};
use nolgia_client::{Client, ClientBuilder};
use output::OutputFormat;

const DEFAULT_BASE_URL: &str = "https://api.nolgia.ai";

#[derive(Parser, Debug)]
#[command(
    name = "nolgia",
    version,
    about = "Nolgia CLI",
    propagate_version = true
)]
pub struct Cli {
    #[arg(long, global = true, help = "Emit machine-readable JSON")]
    pub json: bool,
    #[arg(long, global = true, env = "NOLGIA_API_URL", default_value = DEFAULT_BASE_URL)]
    pub api_url: String,
    #[arg(long, global = true, env = "NOLGIA_TOKEN")]
    pub token: Option<String>,
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    #[command(subcommand, about = "Authenticate this machine")]
    Auth(auth::AuthCommand),
    #[command(subcommand, about = "Generate images, video, or audio")]
    Gen(r#gen::GenCommand),
    #[command(about = "Show current job status")]
    Status(status::StatusArgs),
    #[command(about = "Wait for a job to finish")]
    Wait(wait::WaitArgs),
    #[command(subcommand, about = "List and manage generated assets")]
    Assets(assets::AssetsCommand),
    #[command(subcommand, about = "Inspect account details and usage")]
    Account(account::AccountCommand),
    #[command(subcommand, about = "Inspect billing state and portal links")]
    Billing(billing::BillingCommand),
    #[command(subcommand, about = "Manage personal access tokens")]
    Pat(pat::PatCommand),
    #[command(subcommand, about = "Bundled AI-agent skills (list, show, install)")]
    Skills(skills::SkillsCommand),
    #[command(subcommand, about = "Live model catalog with capabilities and pricing")]
    Models(models::ModelsCommand),
    #[command(about = "Generate shell completions (bash, zsh, fish, powershell)")]
    Completion(CompletionArgs),
}

#[derive(clap::Args, Debug)]
pub struct CompletionArgs {
    #[arg(value_enum)]
    pub shell: clap_complete::Shell,
}

/// Detect the calling surface for the X-Nolgia-Surface header. Override
/// with NOLGIA_SURFACE.
fn detect_surface() -> String {
    if let Ok(s) = std::env::var("NOLGIA_SURFACE") {
        return s;
    }
    let has = |k: &str| std::env::var_os(k).is_some();
    if has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_AGENT_SDK_VERSION") || has("CLAUDECODE") {
        return "claude-code".into();
    }
    if has("CODEX_SANDBOX") || has("CODEX_THREAD_ID") {
        return "codex".into();
    }
    if has("HERMES_HOME") && has("HERMES_DASHBOARD") {
        return "hermes".into();
    }
    if has("AI_AGENT") {
        return "agent".into();
    }
    "cli".into()
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();
    let update = update_check::start(cli.json);
    let result = run_cli(cli).await;
    update.finish().await;
    result
}

pub async fn run_cli(cli: Cli) -> Result<()> {
    let format = OutputFormat::from_json_flag(cli.json);
    if let Commands::Auth(command) = cli.command {
        return auth::run(command, format, &cli.api_url, cli.token).await;
    }
    if let Commands::Skills(command) = cli.command {
        return skills::run(command, format);
    }
    if let Commands::Completion(args) = cli.command {
        let mut cmd = <Cli as clap::CommandFactory>::command();
        clap_complete::generate(args.shell, &mut cmd, "nolgia", &mut std::io::stdout());
        return Ok(());
    }

    let token = cli.token.or_else(auth::load_token).unwrap_or_default();
    let client = build_client(&cli.api_url, token)?;
    let ctx = CommandContext::new(client, format);

    match cli.command {
        Commands::Auth(_) => unreachable!("auth handled before client construction"),
        Commands::Gen(command) => r#gen::run(command, &ctx).await,
        Commands::Status(args) => status::run(args, &ctx).await,
        Commands::Wait(args) => wait::run(args, &ctx).await,
        Commands::Assets(command) => assets::run(command, &ctx).await,
        Commands::Account(command) => account::run(command, &ctx).await,
        Commands::Billing(command) => billing::run(command, &ctx).await,
        Commands::Pat(command) => pat::run(command, &ctx).await,
        Commands::Skills(_) => unreachable!("skills handled before client construction"),
        Commands::Completion(_) => unreachable!("completion handled before client construction"),
        Commands::Models(command) => models::run(command, &ctx).await,
    }
}

fn build_client(base_url: &str, token: String) -> Result<Client> {
    let builder = ClientBuilder::new(base_url).surface(detect_surface());
    let builder = if token.is_empty() {
        builder
    } else {
        builder.pat(token)
    };
    Ok(builder.build()?)
}