offline-cli 0.3.3

The Offline Protocol developer CLI
//! Offline Protocol developer CLI.

mod api;
mod auth;
mod commands;
mod config;
mod output;
mod setup;

use std::future::Future;

use clap::{CommandFactory, Parser, Subcommand};

#[derive(Parser)]
#[command(
    name = "offline",
    version,
    about = "Build offline-first apps on the Offline Protocol ecosystem",
    long_about = "The Offline Protocol developer CLI: authenticate with the developer \
portal, scaffold production-ready apps from ecosystem knowledge, and serve \
that knowledge to AI agents over MCP.\n\nAgent-friendly: every informational \
command supports --json, and `offline mcp serve` exposes the full registry \
as MCP tools/resources."
)]
struct Cli {
    /// Organization to use for this command (id or name), overriding the default from `offline org use`
    #[arg(long, global = true, value_name = "ORG")]
    org: Option<String>,
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Authenticate with the Offline Protocol developer portal
    Login(commands::login::LoginArgs),
    /// Revoke the stored API key and clear local credentials
    Logout(commands::login::LogoutArgs),
    /// Show the currently authenticated developer
    Whoami(commands::login::WhoamiArgs),
    /// Provision org, application, and default API key (needs a developer session)
    ///
    /// Needs the developer session `offline login` stores. An app key from
    /// `offline login --api-key` (CI) can't run it. Reuses the API key this
    /// machine already created for the application.
    Setup(commands::setup::SetupArgs),
    /// Manage organizations: list, create, and `use` one as the default (needs a developer session)
    ///
    /// Needs the developer session `offline login` stores. An app key from
    /// `offline login --api-key` (CI) can't run it.
    #[command(subcommand, alias = "org")]
    Orgs(commands::orgs::OrgsCommand),
    /// Manage applications: native apps, origins, features, status (needs a developer session)
    ///
    /// Needs the developer session `offline login` stores (or
    /// OFFLINE_ACCESS_TOKEN). An app key from `offline login --api-key` (CI)
    /// can't run it.
    #[command(subcommand)]
    Apps(commands::apps::AppsCommand),
    /// Converge the application to the "app" block of offline.config.json (native apps, origins, features)
    ///
    /// Additive and idempotent: registers what is missing and never removes a
    /// registration. With --detect it also registers the native apps the
    /// project ships (app.json, Xcode, Gradle, assetlinks.json).
    Apply(commands::apply::ApplyArgs),
    /// Manage application API keys (needs a developer session)
    ///
    /// Needs the developer session `offline login` stores. An app key from
    /// `offline login --api-key` (CI) can't run it.
    #[command(subcommand)]
    Keys(commands::keys::KeysCommand),
    /// Create a new project from a workflow, template, or natural-language prompt
    Create(commands::create::CreateArgs),
    /// Wire Offline Protocol config + agent guidance into an existing project
    Init(commands::init::InitArgs),
    /// Add an Offline Protocol package to an existing project
    Add(commands::add::AddArgs),
    /// Plan an app from a prompt: workflow, packages, architecture documents
    Plan(commands::plan::PlanArgs),
    /// Check your environment for everything Offline Protocol development needs
    Doctor(commands::doctor::DoctorArgs),
    /// Check for a newer release of this CLI
    Update(commands::update::UpdateArgs),
    /// Query the ecosystem registry (packages, workflows, skills, templates)
    #[command(subcommand)]
    Registry(commands::registry::RegistryCommand),
    /// Read integration skills (deep guides with accurate SDK usage)
    #[command(subcommand)]
    Skills(commands::skills::SkillsCommand),
    /// Model Context Protocol server for AI agents
    #[command(subcommand)]
    Mcp(commands::mcp::McpCommand),
    /// Generate shell completions
    Completions {
        /// Shell to generate completions for
        #[arg(value_enum)]
        shell: clap_complete::Shell,
    },
}

/// Restore SIG_DFL for SIGPIPE so piping to `head` does not panic.
#[cfg(unix)]
fn reset_sigpipe() {
    // SAFETY: setting a signal disposition to SIG_DFL is async-signal-safe
    // and has no preconditions.
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }
}

#[cfg(not(unix))]
fn reset_sigpipe() {}

fn run_async<F>(future: F) -> anyhow::Result<()>
where
    F: Future<Output = anyhow::Result<()>>,
{
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .expect("tokio runtime")
        .block_on(future)
}

fn main() {
    reset_sigpipe();
    let cli = Cli::parse();
    // `--org` overrides the stored default for this command, like OFFLINE_ORG_ID.
    if let Some(org) = cli.org.as_deref().filter(|o| !o.trim().is_empty()) {
        std::env::set_var(config::storage::ENV_ORG_ID, org.trim());
    }
    let result = match cli.command {
        Command::Login(args) => run_async(commands::login::login(args)),
        Command::Logout(args) => run_async(commands::login::logout(args)),
        Command::Whoami(args) => run_async(commands::login::whoami(args)),
        Command::Setup(args) => run_async(commands::setup::run(args)),
        Command::Orgs(cmd) => run_async(commands::orgs::run(cmd)),
        Command::Apps(cmd) => run_async(commands::apps::run(cmd)),
        Command::Apply(args) => run_async(commands::apply::run(args)),
        Command::Keys(cmd) => run_async(commands::keys::run(cmd)),
        Command::Create(args) => run_async(commands::create::run(args)),
        Command::Init(args) => commands::init::run(args),
        Command::Add(args) => commands::add::run(args),
        Command::Plan(args) => commands::plan::run(args),
        Command::Doctor(args) => run_async(commands::doctor::run(args)),
        Command::Update(args) => commands::update::run(args),
        Command::Registry(cmd) => commands::registry::run(cmd),
        Command::Skills(cmd) => commands::skills::run(cmd),
        Command::Mcp(cmd) => commands::mcp::run(cmd),
        Command::Completions { shell } => {
            let mut cmd = Cli::command();
            clap_complete::generate(shell, &mut cmd, "offline", &mut std::io::stdout());
            Ok(())
        }
    };

    if let Err(err) = result {
        eprintln!("{} {err:#}", console::style("error:").red().bold());
        std::process::exit(1);
    }
}