1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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}"))
}