use clap::builder::StyledStr;
use clap::{Args, Parser, Subcommand, ValueEnum};
use clap_complete::engine::{ArgValueCompleter, CompletionCandidate};
use fsqlite::Connection;
use fsqlite_types::SqliteValue;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::config;
use crate::format::truncate_title;
use crate::model::{IssueType, Status};
pub mod commands;
#[derive(Clone, Copy)]
enum IssueCompletionFilter {
Any,
Open,
Closed,
}
impl IssueCompletionFilter {
fn matches(self, status: &Status) -> bool {
match self {
Self::Any => !matches!(status, Status::Tombstone),
Self::Open => !status.is_terminal(),
Self::Closed => matches!(status, Status::Closed),
}
}
}
#[derive(Deserialize, Debug)]
struct CompletionIssue {
id: String,
title: String,
#[serde(default)]
status: Status,
#[serde(default)]
issue_type: IssueType,
#[serde(default)]
labels: Vec<String>,
#[serde(default)]
assignee: Option<String>,
#[serde(default)]
owner: Option<String>,
}
#[derive(Default, Debug)]
struct CompletionIndex {
issues: Vec<CompletionIssue>,
labels: Vec<String>,
assignees: Vec<String>,
owners: Vec<String>,
types: Vec<String>,
}
#[derive(Default, Debug)]
struct CompletionConfigIndex {
config_keys: Vec<String>,
saved_queries: Vec<String>,
}
static COMPLETION_INDEX: OnceLock<CompletionIndex> = OnceLock::new();
static CONFIG_INDEX: OnceLock<CompletionConfigIndex> = OnceLock::new();
const STATUS_CANDIDATES: &[(&str, &str)] = &[
("open", "Open issue"),
("in_progress", "In progress"),
("blocked", "Blocked"),
("deferred", "Deferred"),
("draft", "Draft"),
("closed", "Closed"),
("tombstone", "Deleted"),
("pinned", "Pinned"),
];
const STATUS_WITH_ALL_CANDIDATES: &[(&str, &str)] = &[
("all", "All statuses"),
("open", "Open issue"),
("in_progress", "In progress"),
("blocked", "Blocked"),
("deferred", "Deferred"),
("draft", "Draft"),
("closed", "Closed"),
("tombstone", "Deleted"),
("pinned", "Pinned"),
];
const ISSUE_TYPE_CANDIDATES: &[(&str, &str)] = &[
("task", "Task"),
("bug", "Bug"),
("feature", "Feature"),
("epic", "Epic"),
("chore", "Chore"),
("docs", "Docs"),
("question", "Question"),
];
const PRIORITY_CANDIDATES: &[(&str, &str)] = &[
("0", "Critical (P0)"),
("1", "High (P1)"),
("2", "Medium (P2)"),
("3", "Low (P3)"),
("4", "Backlog (P4)"),
("P0", "Critical (0)"),
("P1", "High (1)"),
("P2", "Medium (2)"),
("P3", "Low (3)"),
("P4", "Backlog (4)"),
];
const PRIORITY_NUMERIC_CANDIDATES: &[(&str, &str)] = &[
("0", "Critical (P0)"),
("1", "High (P1)"),
("2", "Medium (P2)"),
("3", "Low (P3)"),
("4", "Backlog (P4)"),
];
const DEP_TYPE_CANDIDATES: &[(&str, &str)] = &[
("blocks", "Blocks (default)"),
("parent-child", "Parent child"),
("conditional-blocks", "Conditional blocks"),
("waits-for", "Waits for"),
("related", "Related"),
("discovered-from", "Discovered from"),
("replies-to", "Replies to"),
("relates-to", "Relates to"),
("duplicates", "Duplicates"),
("supersedes", "Supersedes"),
("caused-by", "Caused by"),
];
const SORT_KEY_CANDIDATES: &[(&str, &str)] = &[
("priority", "Priority"),
("created_at", "Created at"),
("updated_at", "Updated at"),
("title", "Title"),
("created", "Alias for created_at"),
("updated", "Alias for updated_at"),
];
const DEP_TREE_FORMAT_CANDIDATES: &[(&str, &str)] =
&[("text", "Text output"), ("mermaid", "Mermaid graph")];
const CSV_FIELD_CANDIDATES: &[(&str, &str)] = &[
("id", "Issue ID"),
("title", "Title"),
("description", "Description"),
("status", "Status"),
("priority", "Priority"),
("issue_type", "Issue type"),
("assignee", "Assignee"),
("owner", "Owner"),
("created_at", "Created at"),
("updated_at", "Updated at"),
("closed_at", "Closed at"),
("due_at", "Due at"),
("defer_until", "Defer until"),
("notes", "Notes"),
("external_ref", "External ref"),
];
const EXPORT_ERROR_POLICY_CANDIDATES: &[(&str, &str)] = &[
("strict", "Abort export on any error (default)"),
(
"best-effort",
"Skip problematic records, export what we can",
),
("partial", "Export valid records, report failures"),
(
"required-core",
"Only export core issues, tolerate non-core errors",
),
];
const ORPHAN_MODE_CANDIDATES: &[(&str, &str)] = &[
("strict", "Fail if any issue references a missing parent"),
("resurrect", "Attempt to resurrect missing parents if found"),
("skip", "Skip orphaned issues"),
("allow", "Allow orphans (no parent validation)"),
];
const SAVED_QUERY_PREFIX: &str = "saved_query:";
fn completion_index() -> &'static CompletionIndex {
COMPLETION_INDEX.get_or_init(build_completion_index)
}
fn config_index() -> &'static CompletionConfigIndex {
CONFIG_INDEX.get_or_init(build_config_index)
}
fn add_layer_keys(keys: &mut BTreeSet<String>, layer: &config::ConfigLayer) {
keys.extend(layer.runtime.keys().cloned());
keys.extend(layer.startup.keys().cloned());
}
fn resolve_completion_paths_for_beads_dir(beads_dir: &Path) -> Option<config::ConfigPaths> {
config::resolve_paths(beads_dir, None).ok()
}
fn completion_paths() -> Option<config::ConfigPaths> {
let beads_dir = config::discover_beads_dir(None).ok()?;
resolve_completion_paths_for_beads_dir(&beads_dir)
}
fn saved_queries_from_db(db_path: &Path) -> BTreeSet<String> {
if !db_path.is_file() {
return BTreeSet::new();
}
let Ok(queries) = config::with_database_family_snapshot(db_path, |snapshot_db_path| {
let conn = Connection::open(snapshot_db_path.to_string_lossy().into_owned())?;
let _ = conn.execute("PRAGMA busy_timeout=0");
let rows = conn.query("SELECT key FROM config")?;
let mut queries = BTreeSet::new();
for row in &rows {
let Some(key) = row.get(0).and_then(SqliteValue::as_text) else {
continue;
};
if let Some(name) = key.strip_prefix(SAVED_QUERY_PREFIX)
&& !name.trim().is_empty()
{
queries.insert(name.to_string());
}
}
Ok(queries)
}) else {
return BTreeSet::new();
};
queries
}
fn build_completion_index() -> CompletionIndex {
let Some(paths) = completion_paths() else {
return CompletionIndex::default();
};
let Ok(file) = File::open(&paths.jsonl_path) else {
return CompletionIndex::default();
};
let reader = BufReader::new(file);
let mut issues = Vec::new();
let mut labels = BTreeSet::new();
let mut assignees = BTreeSet::new();
let mut owners = BTreeSet::new();
let mut types = BTreeSet::new();
for line_result in reader.lines() {
let Ok(line) = line_result else {
break;
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let issue: CompletionIssue = match serde_json::from_str(trimmed) {
Ok(issue) => issue,
Err(_) => continue,
};
for label in &issue.labels {
let label = label.trim();
if !label.is_empty() {
labels.insert(label.to_string());
}
}
if let Some(assignee) = issue.assignee.as_deref() {
let assignee = assignee.trim();
if !assignee.is_empty() {
assignees.insert(assignee.to_string());
}
}
if let Some(owner) = issue.owner.as_deref() {
let owner = owner.trim();
if !owner.is_empty() {
owners.insert(owner.to_string());
}
}
let issue_type = issue.issue_type.as_str().trim();
if !issue_type.is_empty() {
types.insert(issue_type.to_string());
}
issues.push(issue);
}
issues.sort_by(|a, b| a.id.cmp(&b.id));
CompletionIndex {
issues,
labels: labels.into_iter().collect(),
assignees: assignees.into_iter().collect(),
owners: owners.into_iter().collect(),
types: types.into_iter().collect(),
}
}
fn build_config_index() -> CompletionConfigIndex {
let mut keys = BTreeSet::new();
let mut saved_queries = BTreeSet::new();
add_layer_keys(&mut keys, &config::default_config_layer());
if let Ok(legacy_user) = config::load_legacy_user_config() {
add_layer_keys(&mut keys, &legacy_user);
}
if let Ok(user) = config::load_user_config() {
add_layer_keys(&mut keys, &user);
}
add_layer_keys(&mut keys, &config::ConfigLayer::from_env());
if let Some(paths) = completion_paths() {
if let Ok(project) = config::load_project_config(&paths.beads_dir) {
add_layer_keys(&mut keys, &project);
}
saved_queries.extend(saved_queries_from_db(&paths.db_path));
}
CompletionConfigIndex {
config_keys: keys.into_iter().collect(),
saved_queries: saved_queries.into_iter().collect(),
}
}
fn matches_prefix_case_insensitive(value: &str, prefix: &str) -> bool {
if prefix.is_empty() {
return true;
}
value
.to_ascii_lowercase()
.starts_with(&prefix.to_ascii_lowercase())
}
fn static_candidates(
prefix: &str,
values: &[(&'static str, &'static str)],
) -> Vec<CompletionCandidate> {
values
.iter()
.filter(|(value, _)| matches_prefix_case_insensitive(value, prefix))
.map(|(value, help)| CompletionCandidate::new(*value).help(Some(StyledStr::from(*help))))
.collect()
}
fn static_candidates_with_suffix(
prefix: &str,
values: &[(&'static str, &'static str)],
suffix: &str,
) -> Vec<CompletionCandidate> {
values
.iter()
.filter(|(value, _)| matches_prefix_case_insensitive(value, prefix))
.map(|(value, help)| {
CompletionCandidate::new(format!("{value}{suffix}")).help(Some(StyledStr::from(*help)))
})
.collect()
}
fn dynamic_candidates(prefix: &str, values: &[String]) -> Vec<CompletionCandidate> {
values
.iter()
.filter(|value| matches_prefix_case_insensitive(value, prefix))
.map(CompletionCandidate::new)
.collect()
}
fn split_delimited_prefix(current: &str, delimiter: char) -> (String, &str) {
current.rfind(delimiter).map_or_else(
|| (String::new(), current.trim_start()),
|idx| {
let (head, tail) = current.split_at(idx + delimiter.len_utf8());
let trimmed_tail = tail.trim_start();
let ws_len = tail.len().saturating_sub(trimmed_tail.len());
let mut prefix = String::with_capacity(head.len() + ws_len);
prefix.push_str(head);
prefix.push_str(&tail[..ws_len]);
(prefix, trimmed_tail)
},
)
}
fn split_key_prefix(current: &str, delimiter: char) -> Option<(String, &str)> {
let idx = current.find(delimiter)?;
let (head, tail) = current.split_at(idx + delimiter.len_utf8());
let trimmed_tail = tail.trim_start();
let ws_len = tail.len().saturating_sub(trimmed_tail.len());
let mut prefix = String::with_capacity(head.len() + ws_len);
prefix.push_str(head);
prefix.push_str(&tail[..ws_len]);
Some((prefix, trimmed_tail))
}
fn static_candidates_delimited(
current: &OsStr,
delimiter: char,
values: &[(&'static str, &'static str)],
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (prefix, needle) = split_delimited_prefix(current, delimiter);
static_candidates(needle, values)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect()
}
fn dynamic_candidates_delimited(
current: &OsStr,
delimiter: char,
values: &[String],
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (prefix, needle) = split_delimited_prefix(current, delimiter);
dynamic_candidates(needle, values)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect()
}
fn issue_id_completer(current: &OsStr) -> Vec<CompletionCandidate> {
issue_id_completer_with_filter(current, IssueCompletionFilter::Any)
}
fn open_issue_id_completer(current: &OsStr) -> Vec<CompletionCandidate> {
issue_id_completer_with_filter(current, IssueCompletionFilter::Open)
}
fn closed_issue_id_completer(current: &OsStr) -> Vec<CompletionCandidate> {
issue_id_completer_with_filter(current, IssueCompletionFilter::Closed)
}
fn issue_id_completer_with_filter(
current: &OsStr,
filter: IssueCompletionFilter,
) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
issue_id_candidates(prefix, filter)
}
fn issue_id_candidates(prefix: &str, filter: IssueCompletionFilter) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
for issue in &completion_index().issues {
if !prefix.is_empty() && !issue.id.starts_with(prefix) {
continue;
}
if filter.matches(&issue.status) {
let title = truncate_title(&issue.title, 60);
let help = format!("{} | {}", issue.status.as_str(), title);
candidates.push(CompletionCandidate::new(&issue.id).help(Some(StyledStr::from(help))));
}
}
candidates
}
fn status_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, STATUS_CANDIDATES)
}
fn status_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
static_candidates_delimited(current, ',', STATUS_CANDIDATES)
}
fn status_or_all_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, STATUS_WITH_ALL_CANDIDATES)
}
fn issue_type_is_standard(value: &str) -> bool {
ISSUE_TYPE_CANDIDATES
.iter()
.any(|(candidate, _)| candidate.eq_ignore_ascii_case(value))
}
fn issue_type_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
let mut candidates = static_candidates(prefix, ISSUE_TYPE_CANDIDATES);
for value in &completion_index().types {
if issue_type_is_standard(value) {
continue;
}
if matches_prefix_case_insensitive(value, prefix) {
candidates.push(CompletionCandidate::new(value));
}
}
candidates
}
fn issue_type_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (prefix, needle) = split_delimited_prefix(current, ',');
let mut candidates = static_candidates(needle, ISSUE_TYPE_CANDIDATES)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect::<Vec<_>>();
for value in &completion_index().types {
if issue_type_is_standard(value) {
continue;
}
if matches_prefix_case_insensitive(value, needle) {
candidates.push(CompletionCandidate::new(value).add_prefix(prefix.clone()));
}
}
candidates
}
fn issue_type_standard_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, ISSUE_TYPE_CANDIDATES)
}
fn priority_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, PRIORITY_CANDIDATES)
}
fn priority_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
static_candidates_delimited(current, ',', PRIORITY_CANDIDATES)
}
fn priority_numeric_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, PRIORITY_NUMERIC_CANDIDATES)
}
fn label_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &completion_index().labels)
}
fn label_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
dynamic_candidates_delimited(current, ',', &completion_index().labels)
}
fn assignee_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &completion_index().assignees)
}
fn owner_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &completion_index().owners)
}
fn dep_type_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, DEP_TYPE_CANDIDATES)
}
fn deps_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (outer_prefix, tail) = split_delimited_prefix(current, ',');
if let Some((type_prefix, id_prefix)) = split_key_prefix(tail, ':') {
let mut prefix = outer_prefix;
prefix.push_str(&type_prefix);
return issue_id_candidates(id_prefix, IssueCompletionFilter::Any)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect();
}
static_candidates_with_suffix(tail, DEP_TYPE_CANDIDATES, ":")
.into_iter()
.map(|candidate| candidate.add_prefix(outer_prefix.clone()))
.collect()
}
fn dep_tree_format_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, DEP_TREE_FORMAT_CANDIDATES)
}
fn saved_query_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &config_index().saved_queries)
}
fn config_key_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &config_index().config_keys)
}
fn config_key_assignment_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
if prefix.contains('=') {
return Vec::new();
}
let mut candidates = Vec::new();
for key in &config_index().config_keys {
if matches_prefix_case_insensitive(key, prefix) {
candidates.push(CompletionCandidate::new(key));
candidates.push(CompletionCandidate::new(format!("{key}=")));
}
}
candidates
}
fn export_error_policy_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, EXPORT_ERROR_POLICY_CANDIDATES)
}
fn orphan_mode_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, ORPHAN_MODE_CANDIDATES)
}
fn sort_key_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, SORT_KEY_CANDIDATES)
}
fn csv_fields_completer(current: &OsStr) -> Vec<CompletionCandidate> {
static_candidates_delimited(current, ',', CSV_FIELD_CANDIDATES)
}
#[derive(Parser, Debug)]
#[command(name = "br", author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true)]
pub db: Option<PathBuf>,
#[arg(long, global = true)]
pub actor: Option<String>,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub no_daemon: bool,
#[arg(long, global = true)]
pub no_auto_flush: bool,
#[arg(long, global = true)]
pub no_auto_import: bool,
#[arg(long, global = true)]
pub allow_stale: bool,
#[arg(long, global = true)]
pub lock_timeout: Option<u64>,
#[arg(long, global = true)]
pub no_db: bool,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Agents(AgentsArgs),
Audit {
#[command(subcommand)]
command: AuditCommands,
},
Blocked(BlockedArgs),
Changelog(ChangelogArgs),
Close(CloseArgs),
#[command(alias = "comment")]
Comments(CommentsArgs),
#[command(alias = "completion")]
Completions(CompletionsArgs),
Config {
#[command(subcommand)]
command: ConfigCommands,
},
Count(CountArgs),
Create(CreateArgs),
Defer(DeferArgs),
Delete(DeleteArgs),
Dep {
#[command(subcommand)]
command: DepCommands,
},
Doctor(DoctorArgs),
Epic {
#[command(subcommand)]
command: EpicCommands,
},
Graph(GraphArgs),
History(HistoryArgs),
Info(InfoArgs),
Init {
#[arg(long)]
prefix: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
backend: Option<String>,
},
Label {
#[command(subcommand)]
command: LabelCommands,
},
Lint(LintArgs),
List(ListArgs),
Orphans(OrphansArgs),
Q(QuickArgs),
Query {
#[command(subcommand)]
command: QueryCommands,
},
Ready(ReadyArgs),
Reopen(ReopenArgs),
Schema(SchemaArgs),
Search(SearchArgs),
Show(ShowArgs),
Stale(StaleArgs),
Stats(StatsArgs),
Status(StatsArgs),
#[command(long_about = "Sync database with JSONL file (export or import).
SAFETY GUARANTEES:
• br sync NEVER executes git commands or auto-commits
• br sync NEVER modifies files outside .beads/ (unless --allow-external-jsonl)
• All writes use atomic temp-file-then-rename pattern
• Safety guards prevent accidental data loss
MODES (one required unless --status):
--flush-only Export database to JSONL (safe by default)
--import-only Import JSONL into database (validates first)
--status Show sync status (read-only)
SAFETY GUARDS:
Export guards (bypassed with --force):
• Empty DB Guard: Refuses to export empty DB over non-empty JSONL
• Stale DB Guard: Refuses to export if JSONL has issues missing from DB
Import guards (cannot be bypassed):
• Conflict markers: Rejects files with git merge conflict markers
• Invalid JSON: Rejects malformed JSONL entries
VERBOSE LOGGING:
-v Show INFO-level safety guard decisions
-vv Show DEBUG-level file operations
EXAMPLES:
br sync --flush-only Export database to .beads/issues.jsonl
br sync --flush-only -v Export with safety logging
br sync --import-only Import from JSONL (validates first)
br sync --rebuild Import + remove DB entries not in JSONL
br sync --status Show current sync status")]
Sync(SyncArgs),
Undefer(UndeferArgs),
Update(UpdateArgs),
#[cfg(feature = "mcp")]
Serve(crate::mcp::ServeArgs),
#[cfg(feature = "self_update")]
Upgrade(UpgradeArgs),
Version(VersionArgs),
Where,
}
#[derive(Args, Debug, Clone)]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: ShellType,
#[arg(long, short = 'o')]
pub output: Option<std::path::PathBuf>,
}
#[derive(ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
pub enum ShellType {
Bash,
Zsh,
Fish,
#[value(name = "powershell")]
#[value(alias = "pwsh")]
PowerShell,
Elvish,
}
#[derive(Args, Debug, Default)]
pub struct CreateArgs {
pub title: Option<String>,
#[arg(long = "title")]
pub title_flag: Option<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Option<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Option<String>,
#[arg(long, short = 'd', visible_alias = "body")]
pub description: Option<String>,
#[arg(long, short = 'a', add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long, add = ArgValueCompleter::new(owner_completer))]
pub owner: Option<String>,
#[arg(long, short = 'l', value_delimiter = ',', add = ArgValueCompleter::new(label_completer_delimited))]
pub labels: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(deps_completer))]
pub deps: Vec<String>,
#[arg(long, short = 'e')]
pub estimate: Option<i32>,
#[arg(long)]
pub due: Option<String>,
#[arg(long)]
pub defer: Option<String>,
#[arg(long)]
pub external_ref: Option<String>,
#[arg(long)]
pub ephemeral: bool,
#[arg(long, short = 's', add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub silent: bool,
#[arg(long, short = 'f')]
pub file: Option<std::path::PathBuf>,
}
#[derive(Args, Debug)]
pub struct QuickArgs {
pub title: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Option<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Option<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub labels: Vec<String>,
#[arg(long, short = 'd', visible_alias = "body")]
pub description: Option<String>,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long, short = 'e')]
pub estimate: Option<i32>,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct UpdateArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long, visible_alias = "body")]
pub description: Option<String>,
#[arg(long)]
pub design: Option<String>,
#[arg(long, visible_alias = "acceptance")]
pub acceptance_criteria: Option<String>,
#[arg(long)]
pub notes: Option<String>,
#[arg(long, short = 's', add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Option<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Option<String>,
#[arg(long, add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long, add = ArgValueCompleter::new(owner_completer))]
pub owner: Option<String>,
#[arg(long)]
pub claim: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub due: Option<String>,
#[arg(long)]
pub defer: Option<String>,
#[arg(long)]
pub estimate: Option<i32>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub add_label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub remove_label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer_delimited))]
pub set_labels: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long)]
pub external_ref: Option<String>,
#[arg(long)]
pub session: Option<String>,
}
#[derive(Args, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct DeleteArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, default_value = "delete")]
pub reason: String,
#[arg(long)]
pub from_file: Option<PathBuf>,
#[arg(long)]
pub cascade: bool,
#[arg(long, conflicts_with = "cascade")]
pub force: bool,
#[arg(long)]
pub hard: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug, Default, Clone)]
pub struct InfoArgs {
#[arg(long)]
pub schema: bool,
#[arg(long = "whats-new", conflicts_with = "thanks")]
pub whats_new: bool,
#[arg(long, conflicts_with = "whats_new")]
pub thanks: bool,
}
#[derive(Args, Debug, Default, Clone)]
pub struct SchemaArgs {
#[arg(value_enum, default_value_t)]
pub target: SchemaTarget,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum SchemaTarget {
#[default]
All,
Issue,
IssueWithCounts,
IssueDetails,
ReadyIssue,
StaleIssue,
BlockedIssue,
TreeNode,
Statistics,
Error,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum OutputFormat {
#[default]
Text,
Json,
Csv,
Toon,
}
impl OutputFormat {
#[must_use]
pub fn from_env() -> Option<Self> {
if let Ok(value) = std::env::var("BR_OUTPUT_FORMAT")
&& let Some(format) = Self::parse_env_value(&value)
{
return Some(format);
}
if let Ok(value) = std::env::var("TOON_DEFAULT_FORMAT")
&& let Some(format) = Self::parse_env_value(&value)
{
return Some(format);
}
None
}
fn parse_env_value(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"text" | "plain" => Some(Self::Text),
"json" => Some(Self::Json),
"csv" => Some(Self::Csv),
"toon" => Some(Self::Toon),
_ => None,
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum OutputFormatBasic {
#[default]
Text,
Json,
Toon,
}
impl From<OutputFormatBasic> for OutputFormat {
fn from(format: OutputFormatBasic) -> Self {
match format {
OutputFormatBasic::Text => Self::Text,
OutputFormatBasic::Json => Self::Json,
OutputFormatBasic::Toon => Self::Toon,
}
}
}
#[must_use]
pub fn resolve_output_format(
requested: Option<OutputFormat>,
json: bool,
robot: bool,
) -> OutputFormat {
if json || robot {
OutputFormat::Json
} else if let Some(requested) = requested {
requested
} else {
OutputFormat::from_env().unwrap_or(OutputFormat::Text)
}
}
#[must_use]
pub const fn command_requests_robot_json(cmd: &Commands) -> bool {
match cmd {
Commands::Close(args) => args.robot,
Commands::Reopen(args) => args.robot,
Commands::Ready(args) => args.robot,
Commands::Blocked(args) => args.robot,
Commands::Stats(args) | Commands::Status(args) => args.robot,
Commands::Defer(args) => args.robot,
Commands::Undefer(args) => args.robot,
Commands::Orphans(args) => args.robot,
Commands::Changelog(args) => args.robot,
Commands::Sync(args) => args.robot,
_ => false,
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum InheritedOutputMode {
None,
Quiet,
Json,
Toon,
}
#[must_use]
pub fn resolve_output_format_with_outer_mode(
requested: Option<OutputFormat>,
inherited_mode: InheritedOutputMode,
robot: bool,
) -> OutputFormat {
if matches!(inherited_mode, InheritedOutputMode::Json) || robot {
OutputFormat::Json
} else if let Some(requested) = requested {
requested
} else if matches!(inherited_mode, InheritedOutputMode::Toon) {
OutputFormat::Toon
} else if matches!(inherited_mode, InheritedOutputMode::Quiet) {
OutputFormat::Text
} else {
OutputFormat::from_env().unwrap_or(OutputFormat::Text)
}
}
#[must_use]
pub fn resolve_output_format_basic(
requested: Option<OutputFormatBasic>,
json: bool,
robot: bool,
) -> OutputFormat {
let resolved = resolve_output_format(requested.map(Into::into), json, robot);
match resolved {
OutputFormat::Csv => OutputFormat::Text,
other => other,
}
}
#[must_use]
pub fn resolve_output_format_basic_with_outer_mode(
requested: Option<OutputFormatBasic>,
inherited_mode: InheritedOutputMode,
robot: bool,
) -> OutputFormat {
let resolved =
resolve_output_format_with_outer_mode(requested.map(Into::into), inherited_mode, robot);
match resolved {
OutputFormat::Csv => OutputFormat::Text,
other => other,
}
}
#[derive(Args, Debug, Default, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct ListArgs {
#[arg(long, short = 's', add = ArgValueCompleter::new(status_completer))]
pub status: Vec<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long)]
pub unassigned: bool,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub id: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub label_any: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(priority_numeric_completer))]
pub priority_min: Option<u8>,
#[arg(long, add = ArgValueCompleter::new(priority_numeric_completer))]
pub priority_max: Option<u8>,
#[arg(long)]
pub title_contains: Option<String>,
#[arg(long)]
pub desc_contains: Option<String>,
#[arg(long)]
pub notes_contains: Option<String>,
#[arg(long, short = 'a')]
pub all: bool,
#[arg(long, default_value = "50")]
pub limit: Option<usize>,
#[arg(long, default_value = "0")]
pub offset: Option<usize>,
#[arg(long, add = ArgValueCompleter::new(sort_key_completer))]
pub sort: Option<String>,
#[arg(long, short = 'r')]
pub reverse: bool,
#[arg(long)]
pub deferred: bool,
#[arg(long)]
pub overdue: bool,
#[arg(long)]
pub long: bool,
#[arg(long)]
pub pretty: bool,
#[arg(long)]
pub wrap: bool,
#[arg(long, value_enum)]
pub format: Option<OutputFormat>,
#[arg(long)]
pub stats: bool,
#[arg(long, value_name = "FIELDS", add = ArgValueCompleter::new(csv_fields_completer))]
pub fields: Option<String>,
}
#[derive(Args, Debug, Default)]
pub struct SearchArgs {
pub query: String,
#[command(flatten)]
pub filters: ListArgs,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ShowArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub wrap: bool,
#[arg(long)]
pub stats: bool,
}
#[derive(Subcommand, Debug)]
pub enum DepCommands {
Add(DepAddArgs),
#[command(visible_alias = "rm")]
Remove(DepRemoveArgs),
List(DepListArgs),
Tree(DepTreeArgs),
Cycles(DepCyclesArgs),
}
#[derive(Subcommand, Debug)]
pub enum EpicCommands {
Status(EpicStatusArgs),
#[command(name = "close-eligible")]
CloseEligible(EpicCloseEligibleArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub struct EpicStatusArgs {
#[arg(long)]
pub eligible_only: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct EpicCloseEligibleArgs {
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug, Default)]
pub struct DepAddArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub depends_on: String,
#[arg(long = "type", short = 't', default_value = "blocks", add = ArgValueCompleter::new(dep_type_completer))]
pub dep_type: String,
#[arg(long)]
pub metadata: Option<String>,
}
#[derive(Args, Debug)]
pub struct DepRemoveArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub depends_on: String,
}
#[derive(Args, Debug)]
pub struct DepListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(long, default_value = "down", value_enum)]
pub direction: DepDirection,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(dep_type_completer))]
pub dep_type: Option<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum DepDirection {
#[default]
Down,
Up,
Both,
}
#[derive(Args, Debug)]
pub struct DepTreeArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(long, short = 'd', default_value = "down", value_enum)]
pub direction: DepDirection,
#[arg(long, default_value_t = 10)]
pub max_depth: usize,
#[arg(long, default_value = "text", add = ArgValueCompleter::new(dep_tree_format_completer))]
pub format: String,
}
#[derive(Args, Debug)]
pub struct DepCyclesArgs {
#[arg(long)]
pub blocking_only: bool,
}
#[derive(Subcommand, Debug)]
pub enum LabelCommands {
Add(LabelAddArgs),
Remove(LabelRemoveArgs),
List(LabelListArgs),
#[command(name = "list-all")]
ListAll,
Rename(LabelRenameArgs),
}
#[derive(Args, Debug)]
pub struct LabelAddArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issues: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Option<String>,
}
#[derive(Args, Debug)]
pub struct LabelRemoveArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issues: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Option<String>,
}
#[derive(Args, Debug)]
pub struct LabelListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: Option<String>,
}
#[derive(Args, Debug)]
pub struct LabelRenameArgs {
#[arg(add = ArgValueCompleter::new(label_completer))]
pub old_name: String,
pub new_name: String,
}
#[derive(Args, Debug)]
pub struct CommentsArgs {
#[command(subcommand)]
pub command: Option<CommentCommands>,
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: Option<String>,
#[arg(long)]
pub wrap: bool,
}
#[derive(Subcommand, Debug)]
pub enum CommentCommands {
Add(CommentAddArgs),
List(CommentListArgs),
}
#[derive(Args, Debug)]
pub struct CommentAddArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
pub text: Vec<String>,
#[arg(short = 'f', long = "file")]
pub file: Option<PathBuf>,
#[arg(long)]
pub author: Option<String>,
#[arg(long = "message")]
pub message: Option<String>,
}
#[derive(Args, Debug)]
pub struct CommentListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long)]
pub wrap: bool,
}
#[derive(Subcommand, Debug)]
pub enum AuditCommands {
Record(AuditRecordArgs),
Label(AuditLabelArgs),
Log(AuditLogArgs),
Summary(AuditSummaryArgs),
}
#[derive(Args, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct AuditRecordArgs {
#[arg(long)]
pub kind: Option<String>,
#[arg(long = "issue-id", add = ArgValueCompleter::new(issue_id_completer))]
pub issue_id: Option<String>,
#[arg(long)]
pub model: Option<String>,
#[arg(long)]
pub prompt: Option<String>,
#[arg(long)]
pub response: Option<String>,
#[arg(long = "tool-name")]
pub tool_name: Option<String>,
#[arg(long = "exit-code")]
pub exit_code: Option<i32>,
#[arg(long)]
pub error: Option<String>,
#[arg(long)]
pub stdin: bool,
}
#[derive(Args, Debug, Clone)]
pub struct AuditLabelArgs {
pub entry_id: String,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub label: Option<String>,
#[arg(long)]
pub reason: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct AuditLogArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
}
#[derive(Args, Debug, Clone, Default)]
pub struct AuditSummaryArgs {
#[arg(long, default_value_t = 30)]
pub days: u32,
}
#[derive(Args, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct CountArgs {
#[arg(long, value_enum)]
pub by: Option<CountBy>,
#[arg(long)]
pub by_status: bool,
#[arg(long)]
pub by_priority: bool,
#[arg(long)]
pub by_type: bool,
#[arg(long)]
pub by_assignee: bool,
#[arg(long)]
pub by_label: bool,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(status_completer_delimited))]
pub status: Vec<String>,
#[arg(long = "type", value_delimiter = ',', add = ArgValueCompleter::new(issue_type_completer_delimited))]
pub types: Vec<String>,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(priority_completer_delimited))]
pub priority: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long)]
pub unassigned: bool,
#[arg(long)]
pub include_closed: bool,
#[arg(long)]
pub include_templates: bool,
#[arg(long)]
pub title_contains: Option<String>,
}
#[derive(ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
pub enum CountBy {
Status,
Priority,
Type,
Assignee,
Label,
}
#[derive(Args, Debug, Clone)]
pub struct StaleArgs {
#[arg(long, default_value_t = 30)]
pub days: i64,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(status_completer_delimited))]
pub status: Vec<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct LintArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, short = 't', add = ArgValueCompleter::new(issue_type_standard_completer))]
pub type_: Option<String>,
#[arg(long, short = 's', add = ArgValueCompleter::new(status_or_all_completer))]
pub status: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DeferArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long)]
pub until: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct UndeferArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct ReadyArgs {
#[arg(long, default_value_t = 20)]
pub limit: usize,
#[arg(
long,
num_args = 0..=1,
default_missing_value = "",
conflicts_with = "unassigned",
add = ArgValueCompleter::new(assignee_completer)
)]
pub assignee: Option<String>,
#[arg(long, conflicts_with = "assignee")]
pub unassigned: bool,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub label_any: Vec<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Vec<String>,
#[arg(long, default_value = "hybrid", value_enum)]
pub sort: SortPolicy,
#[arg(long)]
pub include_deferred: bool,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long, short = 'r')]
pub recursive: bool,
#[arg(long)]
pub wrap: bool,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Debug, Clone, Default)]
pub struct BlockedArgs {
#[arg(long, default_value_t = 50)]
pub limit: usize,
#[arg(long)]
pub detailed: bool,
#[arg(long)]
pub wrap: bool,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Vec<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct CloseArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, short = 'r')]
pub reason: Option<String>,
#[arg(long, short = 'f')]
pub force: bool,
#[arg(long)]
pub suggest_next: bool,
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ReopenArgs {
#[arg(add = ArgValueCompleter::new(closed_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, short = 'r')]
pub reason: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum SortPolicy {
#[default]
Hybrid,
Priority,
Oldest,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct SyncArgs {
#[arg(long, group = "sync_action")]
pub flush_only: bool,
#[arg(long)]
pub import_only: bool,
#[arg(long)]
pub merge: bool,
#[arg(long)]
pub status: bool,
#[arg(long, short = 'f')]
pub force: bool,
#[arg(long)]
pub allow_external_jsonl: bool,
#[arg(long)]
pub manifest: bool,
#[arg(long = "error-policy", add = ArgValueCompleter::new(export_error_policy_completer))]
pub error_policy: Option<String>,
#[arg(long, add = ArgValueCompleter::new(orphan_mode_completer))]
pub orphans: Option<String>,
#[arg(long)]
pub rename_prefix: bool,
#[arg(long)]
pub rebuild: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigCommands {
List {
#[arg(long, conflicts_with = "user")]
project: bool,
#[arg(long, conflicts_with = "project")]
user: bool,
},
Get {
#[arg(add = ArgValueCompleter::new(config_key_completer))]
key: String,
},
Set {
#[arg(
num_args = 1..=2,
value_name = "KV",
add = ArgValueCompleter::new(config_key_assignment_completer)
)]
args: Vec<String>,
},
#[command(visible_alias = "unset")]
Delete {
#[arg(add = ArgValueCompleter::new(config_key_completer))]
key: String,
},
Edit,
Path,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct StatsArgs {
#[arg(long)]
pub by_type: bool,
#[arg(long)]
pub by_priority: bool,
#[arg(long)]
pub by_assignee: bool,
#[arg(long)]
pub by_label: bool,
#[arg(long)]
pub activity: bool,
#[arg(long)]
pub no_activity: bool,
#[arg(long, default_value_t = 24)]
pub activity_hours: u32,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug)]
pub struct HistoryArgs {
#[command(subcommand)]
pub command: Option<HistoryCommands>,
}
#[derive(Subcommand, Debug)]
pub enum HistoryCommands {
List,
Diff {
file: String,
},
Restore {
file: String,
#[arg(long, short = 'f')]
force: bool,
},
Prune {
#[arg(long, default_value_t = 100)]
keep: usize,
#[arg(long)]
older_than: Option<u32>,
},
}
#[derive(Args, Debug, Clone, Default)]
pub struct VersionArgs {
#[arg(long, short = 'c')]
pub check: bool,
#[arg(long, short = 's')]
pub short: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorArgs {
#[arg(long)]
pub repair: bool,
}
#[cfg(feature = "self_update")]
#[derive(Args, Debug, Clone, Default)]
pub struct UpgradeArgs {
#[arg(long)]
pub check: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub version: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct OrphansArgs {
#[arg(long)]
pub details: bool,
#[arg(long)]
pub fix: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ChangelogArgs {
#[arg(long)]
pub since: Option<String>,
#[arg(long, conflicts_with = "since")]
pub since_tag: Option<String>,
#[arg(long, conflicts_with_all = ["since", "since_tag"])]
pub since_commit: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Subcommand, Debug)]
pub enum QueryCommands {
Save(QuerySaveArgs),
Run(QueryRunArgs),
List,
Delete(QueryDeleteArgs),
}
#[derive(Args, Debug, Clone)]
pub struct QuerySaveArgs {
pub name: String,
#[arg(long, short = 'd')]
pub description: Option<String>,
#[command(flatten)]
pub filters: ListArgs,
}
#[derive(Args, Debug, Clone)]
pub struct QueryRunArgs {
#[arg(add = ArgValueCompleter::new(saved_query_completer))]
pub name: String,
#[command(flatten)]
pub filters: ListArgs,
}
#[derive(Args, Debug, Clone)]
pub struct QueryDeleteArgs {
#[arg(add = ArgValueCompleter::new(saved_query_completer))]
pub name: String,
}
#[derive(Args, Debug, Clone, Default)]
pub struct GraphArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub issue: Option<String>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub compact: bool,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct AgentsArgs {
#[arg(long, conflicts_with_all = ["remove", "update", "check"])]
pub add: bool,
#[arg(long, conflicts_with_all = ["add", "update", "check"])]
pub remove: bool,
#[arg(long, conflicts_with_all = ["add", "remove", "check"])]
pub update: bool,
#[arg(long, conflicts_with_all = ["add", "remove", "update"])]
pub check: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, short = 'f')]
pub force: bool,
}
#[cfg(test)]
mod tests {
use super::{
Cli, Commands, InheritedOutputMode, OutputFormat, OutputFormatBasic,
resolve_output_format_basic_with_outer_mode, resolve_output_format_with_outer_mode,
};
use crate::storage::sqlite::SqliteStorage;
use clap::Parser;
use tempfile::TempDir;
#[test]
fn test_list_limit_defaults_to_50() {
let cli = Cli::parse_from(["br", "list"]);
match cli.command {
Commands::List(args) => assert_eq!(args.limit, Some(50)),
_ => panic!("expected list command"),
}
}
#[test]
fn test_list_limit_zero_parses_as_unlimited() {
let cli = Cli::parse_from(["br", "list", "--limit", "0"]);
match cli.command {
Commands::List(args) => assert_eq!(args.limit, Some(0)),
_ => panic!("expected list command"),
}
}
#[test]
fn test_ready_assignee_flag_accepts_missing_value() {
let cli = Cli::parse_from(["br", "ready", "--assignee"]);
match cli.command {
Commands::Ready(args) => assert_eq!(args.assignee.as_deref(), Some("")),
_ => panic!("expected ready command"),
}
}
#[test]
fn test_ready_assignee_conflicts_with_unassigned() {
let err = Cli::try_parse_from(["br", "ready", "--assignee", "alice", "--unassigned"])
.expect_err("ready filters should conflict");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_agents_add_conflicts_with_check() {
let err = Cli::try_parse_from(["br", "agents", "--add", "--check"])
.expect_err("agents actions should conflict");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_saved_queries_from_db_reads_saved_query_names_without_config_scan() {
let temp = TempDir::new().expect("tempdir");
let db_path = temp.path().join("beads.db");
let mut storage = SqliteStorage::open(&db_path).expect("open db");
storage
.set_config("saved_query:mine", r#"{"name":"mine"}"#)
.expect("save query");
storage
.set_config("saved_query:stale", r#"{"name":"stale"}"#)
.expect("save query");
storage
.set_config("ui.theme", "amber")
.expect("save regular config");
let saved_queries = super::saved_queries_from_db(&db_path);
assert_eq!(
saved_queries.into_iter().collect::<Vec<_>>(),
vec!["mine".to_string(), "stale".to_string()]
);
}
#[test]
fn test_resolve_output_format_with_outer_mode_inherits_toon() {
let resolved =
resolve_output_format_with_outer_mode(None, InheritedOutputMode::Toon, false);
assert_eq!(resolved, OutputFormat::Toon);
}
#[test]
fn test_resolve_output_format_with_outer_mode_keeps_quiet_over_env_defaults() {
let resolved =
resolve_output_format_with_outer_mode(None, InheritedOutputMode::Quiet, false);
assert_eq!(resolved, OutputFormat::Text);
}
#[test]
fn test_resolve_output_format_basic_with_outer_mode_honors_explicit_format() {
let resolved = resolve_output_format_basic_with_outer_mode(
Some(OutputFormatBasic::Json),
InheritedOutputMode::Toon,
false,
);
assert_eq!(resolved, OutputFormat::Json);
}
}