bmo 0.6.0

Local-first SQLite-backed CLI issue tracker for AI agents
Documentation
use clap::{Parser, Subcommand};

pub mod agent_init;
pub mod board;
pub mod config;
pub mod export;
pub mod import;
pub mod init;
pub mod issue;
pub mod next;
pub mod plan;
pub mod stats;
pub mod truncate;
pub mod version;
pub mod web;

#[derive(Parser)]
#[command(name = "bmo", about = "Local-first issue tracker for AI agents")]
pub struct Cli {
    /// Output results as JSON
    #[arg(long, global = true)]
    pub json: bool,

    /// Path to the bmo database file
    #[arg(long, global = true, env = "BMO_DB")]
    pub db: Option<String>,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    // ── Non-issue top-level commands ──────────────────────────────────────────
    /// Initialize a new bmo project in the current directory
    Init(init::InitArgs),
    /// Show or modify project configuration
    Config(config::ConfigArgs),
    /// Print the bmo version
    Version,
    /// Show issue statistics
    Stats,
    /// Export all issues to JSON
    Export(export::ExportArgs),
    /// Import issues from a JSON export
    Import(import::ImportArgs),
    /// Show a Kanban board of all issues
    Board(board::BoardArgs),
    /// Show next work-ready issues
    Next(next::NextArgs),
    /// Show a phased execution plan
    Plan(plan::PlanArgs),
    /// Start the local web UI
    Web(web::WebArgs),
    /// Delete issues in bulk
    Truncate(truncate::TruncateArgs),
    /// One-shot session orientation: init + board + next + stats + cheat sheet
    AgentInit(agent_init::AgentInitArgs),

    // ── Issue commands (short form: `bmo <cmd>` instead of `bmo issue <cmd>`) ─
    /// Atomically claim an issue (sets status=in-progress and assignee)
    Claim(issue::claim::ClaimArgs),
    /// Create a new issue
    Create(issue::create::CreateArgs),
    /// List issues
    #[command(alias = "ls")]
    List(issue::list::ListArgs),
    /// Show issue details
    Show(issue::show::ShowArgs),
    /// Edit an issue
    Edit(issue::edit::EditArgs),
    /// Change an issue's status
    Move(issue::move_cmd::MoveArgs),
    /// Mark an issue as done
    Close(issue::close::CloseArgs),
    /// Reopen a closed issue
    Reopen(issue::reopen::ReopenArgs),
    /// Delete an issue
    Delete(issue::delete::DeleteArgs),
    /// Show issue activity log
    Log(issue::log_cmd::LogArgs),
    /// Show issue dependency graph
    Graph(issue::graph::GraphArgs),
    /// Manage comments
    #[command(subcommand)]
    Comment(issue::comment::CommentCommands),
    /// Manage labels
    #[command(subcommand)]
    Label(issue::label::LabelCommands),
    /// Manage issue relations
    #[command(subcommand)]
    Link(issue::link::LinkCommands),
    /// Manage attached files
    #[command(subcommand)]
    File(issue::file_cmd::FileCommands),

    // ── Long form (backward compatible): `bmo issue <cmd>` ───────────────────
    /// Manage issues (use `bmo <cmd>` directly for brevity)
    #[command(subcommand)]
    Issue(issue::IssueCommands),
}

/// Parse an issue ID that may be in "42" or "BMO-42" format.
#[allow(dead_code)]
pub fn parse_id(s: &str) -> anyhow::Result<i64> {
    let stripped = s
        .trim()
        .to_uppercase()
        .strip_prefix("BMO-")
        .map(|s| s.to_string())
        .unwrap_or_else(|| s.trim().to_string());
    stripped
        .parse::<i64>()
        .map_err(|_| anyhow::anyhow!("invalid issue ID: {s}"))
}