use std::path::{Path, PathBuf};
use clap::builder::NonEmptyStringValueParser;
use clap::{Args, Parser, Subcommand};
use crate::commands;
use crate::config::Config;
use crate::error::{Result, TixError};
use crate::storage::Store;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitCode {
Success = 0,
UsageConfig = 1,
NotFoundAmbiguous = 2,
ExternalTool = 3,
}
pub fn exit_code_for(err: &TixError) -> ExitCode {
match err {
TixError::NotFound(_) => ExitCode::NotFoundAmbiguous,
TixError::InvalidInput(s) if s.starts_with(commands::AMBIGUITY_PREFIX) => {
ExitCode::NotFoundAmbiguous
}
TixError::External(_) => ExitCode::ExternalTool,
_ => ExitCode::UsageConfig,
}
}
#[derive(Parser)]
#[command(
name = "tix",
version,
about = "Terminal issue tracker: markdown issues with a kanban TUI and CLI"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand)]
pub enum Command {
Init,
InitAgents,
Create {
#[arg(long)]
title: Option<String>,
#[arg(long)]
body: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
assignee: Option<String>,
#[arg(long)]
labels: Option<String>,
#[arg(long)]
files: Option<String>,
#[arg(long)]
estimate: Option<String>,
#[arg(long)]
cycles: Option<String>,
#[arg(long)]
milestones: Option<String>,
#[arg(long, value_name = "ID")]
projects: Option<String>,
#[arg(long, value_name = "ID")]
epics: Option<String>,
#[arg(long)]
reviewer: Option<String>,
#[arg(long)]
start_date: Option<String>,
#[arg(long)]
due_date: Option<String>,
#[arg(long, value_name = "SELECTOR")]
parent: Option<String>,
#[arg(long)]
related_issues: Option<String>,
#[arg(long)]
r#type: Option<String>,
#[arg(long, value_name = "STABLE-ID")]
template: Option<String>,
#[arg(long, value_name = "SCOPE", requires = "template")]
scope: Option<String>,
},
Link {
#[command(subcommand)]
action: Option<LinkAction>,
from: Option<String>,
to: Option<String>,
#[arg(long, value_name = "KIND")]
r#type: Option<String>,
},
Bulk {
#[command(subcommand)]
action: BulkAction,
},
Unlink {
from: String,
to: String,
#[arg(long, value_name = "KIND")]
r#type: Option<String>,
},
Chain {
issue: String,
},
Parent {
#[command(subcommand)]
action: ParentAction,
},
Edit {
issue: String,
#[arg(long)]
title: Option<String>,
#[arg(long)]
body: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
assignee: Option<String>,
#[arg(long)]
labels: Option<String>,
#[arg(long)]
files: Option<String>,
#[arg(long)]
estimate: Option<String>,
#[arg(long)]
cycles: Option<String>,
#[arg(long)]
milestones: Option<String>,
#[arg(long, value_name = "ID")]
add_project: Vec<String>,
#[arg(long, value_name = "ID")]
remove_project: Vec<String>,
#[arg(long, value_name = "IDS")]
projects: Option<String>,
#[arg(long = "add-epic", value_name = "ID")]
add_epic: Vec<String>,
#[arg(long = "remove-epic", value_name = "ID")]
remove_epic: Vec<String>,
#[arg(long, value_name = "IDS")]
epics: Option<String>,
#[arg(long)]
reviewer: Option<String>,
#[arg(long)]
start_date: Option<String>,
#[arg(long)]
due_date: Option<String>,
#[arg(long)]
commit: Option<String>,
#[arg(long, value_name = "SELECTOR")]
parent: Option<String>,
#[arg(long, value_name = "TEXT")]
reason: Option<String>,
#[arg(long)]
related_issues: Option<String>,
#[arg(long)]
r#type: Option<String>,
},
Move {
issue: String,
column: String,
#[arg(long, value_name = "HASH")]
commit: Option<String>,
#[arg(long, value_name = "TEXT")]
reason: Option<String>,
#[arg(long)]
yes: bool,
#[arg(long)]
r#override: bool,
},
Wip {
#[command(subcommand)]
action: WipAction,
},
#[command(visible_alias = "show")]
View {
#[command(subcommand)]
action: Option<ViewAction>,
issue: Option<String>,
#[arg(long, value_name = "relationships")]
show: Option<String>,
},
Recurring {
#[command(subcommand)]
action: RecurringAction,
},
List {
#[arg(long)]
status: Option<String>,
#[arg(long)]
filter: Option<String>,
#[arg(long)]
project: Option<String>,
#[arg(long)]
epic: Option<String>,
#[arg(long)]
with_archived: bool,
#[arg(long)]
sort: Option<String>,
#[arg(long)]
group: Option<String>,
#[arg(long, value_name = "COLUMNS")]
columns: Option<String>,
#[arg(long, value_name = "SHOW")]
show: Option<String>,
},
Workflow {
#[command(subcommand)]
action: WorkflowAction,
},
Status {
#[arg(long)]
filter: Option<String>,
#[arg(long)]
sort: Option<String>,
#[arg(long)]
group: Option<String>,
#[arg(long)]
health: bool,
},
Health {
#[command(subcommand)]
action: HealthAction,
},
Mentions {
#[command(subcommand)]
action: MentionsAction,
},
Triage {
#[command(subcommand)]
action: TriageAction,
},
Delete {
issue: String,
#[arg(long)]
yes: bool,
},
Archive {
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
Restore {
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
Cycle {
#[command(subcommand)]
command: CycleCommand,
},
Capacity {
#[command(subcommand)]
command: CapacityCommand,
},
Time {
#[command(subcommand)]
command: TimeCommand,
},
Milestone {
#[command(subcommand)]
command: MilestoneCommand,
},
Project {
#[command(subcommand)]
command: ProjectCommand,
},
Epic {
#[command(subcommand)]
command: EpicCommand,
},
Initiative {
#[command(subcommand)]
command: InitiativeCommand,
},
Roadmap {
#[command(subcommand)]
command: RoadmapCommand,
},
Analytics {
#[command(subcommand)]
command: AnalyticsCommand,
},
Export {
#[arg(long, value_name = "gitlab|linear|ics")]
target: String,
#[arg(long)]
dry_run: bool,
#[arg(long)]
project: Option<String>,
#[arg(long)]
cycle: Option<String>,
#[arg(long)]
output: Option<String>,
},
Import {
#[arg(long, value_name = "gitlab|linear|speckit")]
target: String,
#[arg(long, value_name = "entry|remote")]
triage_policy: Option<String>,
},
Rollover {
#[arg(long)]
cycle: Option<String>,
#[arg(long)]
yes: bool,
},
Search {
#[arg(value_name = "STRING", value_parser = NonEmptyStringValueParser::new())]
string: String,
#[arg(long)]
with_archived: bool,
},
Render,
Clean {
#[arg(short, long)]
force: bool,
#[arg(long, default_value_t = 10)]
keep: usize,
},
New,
Lint {
#[arg(long)]
no_foreign: bool,
},
Layout {
#[command(subcommand)]
action: LayoutAction,
},
Mcp,
Template {
#[command(subcommand)]
action: TemplateAction,
},
}
#[derive(Subcommand)]
pub enum TemplateAction {
List,
Show {
reference: String,
},
Create {
#[arg(long, value_name = "ID")]
id: String,
#[arg(long, value_name = "NAME")]
name: Option<String>,
#[arg(long, value_name = "SCOPE")]
scope: Option<String>,
#[arg(long = "default", value_name = "FIELD=VALUE")]
defaults: Vec<String>,
#[arg(long = "prompt", value_name = "KEY,ATTR=VALUE...")]
prompts: Vec<String>,
#[arg(long = "reference", value_name = "KIND=VALUE,ATTR=VALUE...")]
references: Vec<String>,
},
Update {
reference: String,
#[arg(long, value_name = "NAME")]
name: Option<String>,
#[arg(long, value_name = "SCOPE")]
scope: Option<String>,
#[arg(long = "default", value_name = "FIELD=VALUE")]
defaults: Vec<String>,
#[arg(long = "prompt", value_name = "KEY,ATTR=VALUE...")]
prompts: Vec<String>,
#[arg(long = "reference", value_name = "KIND=VALUE,ATTR=VALUE...")]
references: Vec<String>,
},
Duplicate {
reference: String,
#[arg(long, value_name = "ID")]
id: Option<String>,
#[arg(long, value_name = "NAME")]
name: Option<String>,
},
Delete {
reference: String,
#[arg(long)]
yes: bool,
},
}
#[derive(Subcommand)]
pub enum HealthAction {
Set {
issue: String,
#[arg(long)]
state: String,
#[arg(long)]
actor: Option<String>,
#[arg(long)]
date: Option<String>,
#[arg(long)]
rationale: Option<String>,
},
Show {
issue: String,
},
Rollup {
#[arg(long)]
scope: String,
#[arg(long)]
id: String,
},
Override {
#[arg(long)]
scope: String,
#[arg(long)]
id: String,
#[arg(long)]
state: String,
#[arg(long)]
actor: Option<String>,
#[arg(long)]
date: Option<String>,
#[arg(long)]
rationale: Option<String>,
#[arg(long)]
confirm: bool,
},
History {
scope: String,
},
Board,
}
#[derive(Subcommand)]
pub enum TriageAction {
List {
#[arg(long)]
age: bool,
#[arg(long)]
count: bool,
},
Status,
Accept {
issue: String,
#[arg(long)]
reason: Option<String>,
#[arg(long, value_name = "COLUMN")]
to: Option<String>,
#[arg(long)]
actor: Option<String>,
},
Decline {
issue: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
actor: Option<String>,
},
Duplicate {
issue: String,
#[arg(long, value_name = "ISSUE")]
of: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
actor: Option<String>,
},
Defer {
issue: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
actor: Option<String>,
},
Batch {
#[arg(long, value_name = "LIST")]
select: Option<String>,
#[arg(long, value_name = "QUERY")]
filter: Option<String>,
#[arg(long, value_name = "KIND")]
decision: String,
#[arg(long)]
reason: Option<String>,
#[arg(long)]
actor: Option<String>,
#[arg(long, value_name = "ISSUE")]
of: Option<String>,
#[arg(long, value_name = "COLUMN")]
to: Option<String>,
#[arg(long)]
apply: bool,
},
}
#[derive(Subcommand)]
pub enum CycleCommand {
Create {
#[arg(long)]
name: Option<String>,
#[arg(long)]
description: Option<String>,
#[arg(long = "start-date")]
start_date: Option<String>,
#[arg(long = "due-date")]
due_date: Option<String>,
},
List,
Edit {
#[arg(long)]
name: String,
#[arg(long = "new-name")]
new_name: Option<String>,
#[arg(long)]
description: Option<String>,
#[arg(long = "start-date")]
start_date: Option<String>,
#[arg(long = "due-date")]
due_date: Option<String>,
},
Delete {
#[arg(long)]
name: String,
#[arg(long)]
yes: bool,
},
Schedule {
#[command(subcommand)]
command: CycleScheduleCommand,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum CycleScheduleCommand {
List,
Show {
#[arg(value_name = "SCHEDULE")]
schedule: String,
},
Create(CycleScheduleCreateArgs),
Edit {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleEditArgs,
#[arg(long)]
confirm: bool,
},
Delete {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[arg(long)]
confirm: bool,
},
Detect {
#[arg(long, value_name = "YYYY-MM-DD")]
date: Option<String>,
},
Generate {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[arg(long, value_name = "YYYY-MM-DD")]
through: Option<String>,
#[arg(long)]
confirm: bool,
},
Close {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleReasonArgs,
},
Reopen {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleReasonArgs,
},
Extend {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleExtendArgs,
},
Skip {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleReasonArgs,
},
Pause {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleLifecycleArgs,
},
Resume {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleLifecycleArgs,
},
AdjustFuture {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleReasonArgs,
},
Ignore {
#[arg(value_name = "SCHEDULE")]
schedule: String,
#[command(flatten)]
args: CycleScheduleLifecycleArgs,
},
}
#[derive(Debug, Clone, Args)]
pub struct CycleScheduleCreateArgs {
#[arg(long, value_name = "ID")]
pub id: String,
#[arg(long, value_name = "NAME")]
pub name: String,
#[arg(long, value_name = "KIND")]
pub cadence: String,
#[arg(long, value_name = "N")]
pub interval: Option<u32>,
#[arg(long, value_name = "UNIT")]
pub unit: Option<String>,
#[arg(long, value_name = "N")]
pub duration: u32,
#[arg(long = "duration-unit", value_name = "UNIT")]
pub duration_unit: String,
#[arg(long = "start-date", value_name = "YYYY-MM-DD")]
pub start_date: String,
#[arg(long, value_name = "TZ", allow_hyphen_values = true)]
pub timezone: String,
#[arg(long = "look-ahead", value_name = "N")]
pub look_ahead: Option<u32>,
#[arg(long)]
pub confirm: bool,
}
#[derive(Debug, Clone, Args)]
pub struct CycleScheduleEditArgs {
#[arg(long, value_name = "NAME")]
pub name: Option<String>,
#[arg(long, value_name = "KIND")]
pub cadence: Option<String>,
#[arg(long, value_name = "N")]
pub interval: Option<u32>,
#[arg(long, value_name = "UNIT")]
pub unit: Option<String>,
#[arg(long, value_name = "N")]
pub duration: Option<u32>,
#[arg(long = "duration-unit", value_name = "UNIT")]
pub duration_unit: Option<String>,
#[arg(long = "start-date", value_name = "YYYY-MM-DD")]
pub start_date: Option<String>,
#[arg(long, value_name = "TZ", allow_hyphen_values = true)]
pub timezone: Option<String>,
#[arg(long = "look-ahead", value_name = "N")]
pub look_ahead: Option<u32>,
}
#[derive(Debug, Clone, Args)]
pub struct CycleScheduleLifecycleArgs {
#[arg(long, value_name = "CYCLE")]
pub cycle: Option<String>,
#[arg(long, value_name = "YYYY-MM-DD")]
pub from: Option<String>,
#[arg(long)]
pub confirm: bool,
}
#[derive(Debug, Clone, Args)]
pub struct CycleScheduleReasonArgs {
#[command(flatten)]
pub lifecycle: CycleScheduleLifecycleArgs,
#[arg(long, value_name = "TEXT")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct CycleScheduleExtendArgs {
#[command(flatten)]
pub reason: CycleScheduleReasonArgs,
#[arg(long, value_name = "N")]
pub by: Option<u32>,
}
#[derive(Debug, Clone, Subcommand)]
pub enum CapacityCommand {
List,
Show {
#[arg(value_name = "REFERENCE")]
reference: String,
#[arg(long, value_name = "SCOPE")]
scope: Option<String>,
},
Set {
#[arg(long, value_name = "SCOPE")]
scope: String,
#[arg(long, value_name = "ID")]
id: String,
#[arg(long, value_name = "N", allow_hyphen_values = true)]
value: i64,
#[arg(long, value_name = "UNIT")]
unit: String,
#[arg(long)]
disabled: bool,
},
Delete {
#[arg(long, value_name = "SCOPE")]
scope: String,
#[arg(long, value_name = "ID")]
id: String,
},
Calculate {
#[arg(long, value_name = "SCOPE")]
scope: String,
#[arg(long, value_name = "ID")]
id: String,
#[arg(long)]
warnings: bool,
},
Snapshot {
#[arg(value_name = "CYCLE")]
cycle: String,
#[arg(long)]
warnings: bool,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum TimeCommand {
Add {
#[arg(value_name = "ISSUE")]
issue: String,
#[arg(long, value_name = "N", allow_hyphen_values = true)]
minutes: i64,
#[arg(long, value_name = "YYYY-MM-DD")]
date: Option<String>,
#[arg(long, value_name = "IDENTITY")]
author: String,
#[arg(long, value_name = "TEXT")]
note: Option<String>,
},
Edit {
#[arg(value_name = "ISSUE")]
issue: String,
#[arg(long, value_name = "ID")]
entry: String,
#[command(flatten)]
change: TimeEditArgs,
},
Delete {
#[arg(value_name = "ISSUE")]
issue: String,
#[arg(long, value_name = "ID")]
entry: String,
#[arg(long, value_name = "TEXT")]
reason: Option<String>,
#[arg(long)]
confirm: bool,
},
Show {
#[arg(value_name = "ISSUE")]
issue: String,
},
History {
#[arg(value_name = "ISSUE")]
issue: String,
},
Total {
#[arg(value_name = "ISSUE")]
selector: Option<String>,
#[command(flatten)]
filter: TimeTotalArgs,
},
}
#[derive(Debug, Clone, Args)]
pub struct TimeEditArgs {
#[arg(long, value_name = "N", allow_hyphen_values = true)]
pub minutes: Option<i64>,
#[arg(long, value_name = "YYYY-MM-DD", conflicts_with = "clear_date")]
pub date: Option<String>,
#[arg(long)]
pub clear_date: bool,
#[arg(long, value_name = "TEXT", conflicts_with = "clear_note")]
pub note: Option<String>,
#[arg(long)]
pub clear_note: bool,
#[arg(long, value_name = "IDENTITY")]
pub author: Option<String>,
#[arg(long, value_name = "TEXT")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct TimeTotalArgs {
#[arg(long, value_name = "N")]
pub issue: Option<u32>,
#[arg(long, value_name = "IDENTITY")]
pub assignee: Option<String>,
#[arg(long, value_name = "YYYY-MM-DD")]
pub from: Option<String>,
#[arg(long, value_name = "YYYY-MM-DD")]
pub to: Option<String>,
#[arg(long)]
pub audit: bool,
#[arg(long, value_name = "DIMENSION")]
pub by: Option<String>,
}
#[derive(Subcommand)]
pub enum MilestoneCommand {
Create {
#[arg(long)]
name: String,
#[arg(long = "target-date")]
target_date: Option<String>,
#[arg(long)]
description: Option<String>,
},
List,
Edit {
#[arg(long)]
name: String,
#[arg(long = "new-name")]
new_name: Option<String>,
#[arg(long = "target-date")]
target_date: Option<String>,
#[arg(long)]
description: Option<String>,
},
Delete {
#[arg(long)]
name: String,
#[arg(long)]
yes: bool,
},
}
#[derive(Subcommand)]
pub enum ProjectCommand {
Create {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
lead: Option<String>,
#[arg(long = "start-date")]
start_date: Option<String>,
#[arg(long = "due-date")]
due_date: Option<String>,
},
Edit {
#[arg(value_name = "ID")]
reference: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
description: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
lead: Option<String>,
#[arg(long = "start-date")]
start_date: Option<String>,
#[arg(long = "due-date")]
due_date: Option<String>,
},
List,
Show {
#[arg(value_name = "ID")]
reference: String,
},
Delete {
#[arg(value_name = "ID")]
reference: String,
#[arg(long)]
yes: bool,
},
Add {
#[arg(value_name = "ID")]
reference: String,
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
Remove {
#[arg(value_name = "ID")]
reference: String,
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
BulkAdd {
#[arg(value_name = "ID")]
reference: String,
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
}
#[derive(Subcommand)]
pub enum EpicCommand {
Create {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
owner: Option<String>,
#[arg(long = "start-date")]
start_date: Option<String>,
#[arg(long = "due-date")]
due_date: Option<String>,
#[arg(long, allow_hyphen_values = true)]
weight: Option<String>,
#[arg(long, value_name = "ID")]
projects: Option<String>,
#[arg(long)]
milestone: Option<String>,
#[arg(long)]
cycle: Option<String>,
},
Edit {
#[arg(value_name = "ID")]
reference: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
description: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
owner: Option<String>,
#[arg(long = "start-date")]
start_date: Option<String>,
#[arg(long = "due-date")]
due_date: Option<String>,
#[arg(long, allow_hyphen_values = true)]
weight: Option<String>,
#[arg(long, value_name = "ID")]
parent: Option<String>,
#[arg(long, value_name = "ID")]
projects: Option<String>,
#[arg(long)]
milestone: Option<String>,
#[arg(long)]
cycle: Option<String>,
},
List,
Show {
#[arg(value_name = "ID")]
reference: String,
},
Navigate {
#[arg(value_name = "ID")]
reference: String,
},
Delete {
#[arg(value_name = "ID")]
reference: String,
#[arg(long)]
yes: bool,
},
Add {
#[arg(value_name = "ID")]
reference: String,
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
Remove {
#[arg(value_name = "ID")]
reference: String,
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
BulkAdd {
#[arg(value_name = "ID")]
reference: String,
#[arg(required = true, value_name = "ISSUE")]
issues: Vec<String>,
},
}
#[derive(Subcommand)]
pub enum InitiativeCommand {
Create {
#[arg(value_name = "NAME")]
name: String,
#[arg(long)]
outcome: Option<String>,
#[arg(long)]
owner: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
labels: Option<String>,
#[arg(long = "target-date")]
target_date: Option<String>,
#[arg(long, value_name = "ID")]
projects: Option<String>,
#[arg(long, value_name = "ID")]
epics: Option<String>,
#[arg(long, value_name = "NAME")]
milestones: Option<String>,
},
Edit {
#[arg(value_name = "ID")]
reference: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
outcome: Option<String>,
#[arg(long)]
owner: Option<String>,
#[arg(long)]
status: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
labels: Option<String>,
#[arg(long = "target-date")]
target_date: Option<String>,
#[arg(long, value_name = "ID")]
projects: Option<String>,
#[arg(long, value_name = "ID")]
epics: Option<String>,
#[arg(long, value_name = "NAME")]
milestones: Option<String>,
},
List,
Show {
#[arg(value_name = "ID")]
reference: String,
},
Delete {
#[arg(value_name = "ID")]
reference: String,
#[arg(long)]
yes: bool,
},
Associate {
#[arg(value_name = "ID")]
reference: String,
#[arg(long, value_name = "ID")]
project: Option<String>,
#[arg(long, value_name = "ID")]
epic: Option<String>,
#[arg(long, value_name = "NAME")]
milestone: Option<String>,
},
Dereference {
#[arg(value_name = "ID")]
reference: String,
#[arg(long, value_name = "ID")]
project: Option<String>,
#[arg(long, value_name = "ID")]
epic: Option<String>,
#[arg(long, value_name = "NAME")]
milestone: Option<String>,
},
}
#[derive(Subcommand)]
pub enum RoadmapCommand {
List {
#[arg(long, value_name = "DATE")]
from: Option<String>,
#[arg(long, value_name = "DATE")]
to: Option<String>,
#[arg(long, value_name = "ID")]
owner: Option<String>,
#[arg(long, value_name = "STATE")]
status: Option<String>,
#[arg(long = "label", value_name = "LABEL")]
labels: Vec<String>,
#[arg(long, value_name = "TEXT")]
query: Option<String>,
},
Show {
#[arg(value_name = "ID")]
reference: String,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum ParentAction {
Show {
issue: String,
},
Children {
issue: String,
},
List,
}
#[derive(Subcommand)]
pub enum WorkflowAction {
Categories,
Show {
board: Option<String>,
},
Transitions,
}
#[derive(Subcommand)]
pub enum LayoutAction {
Show,
Migrate {
#[arg(long)]
apply: bool,
},
}
#[derive(Subcommand)]
pub enum LinkAction {
Edit {
id: String,
#[arg(long)]
to: Option<String>,
#[arg(long, value_name = "KIND")]
r#type: Option<String>,
},
Remove {
id: String,
},
List {
issue: Option<String>,
#[arg(long, value_name = "KIND")]
kind: Option<String>,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum RecurringAction {
List,
Show {
series: String,
},
Create(RecurringCreateArgs),
Edit {
series: String,
#[command(flatten)]
args: RecurringEditArgs,
},
Detect {
#[arg(long, value_name = "YYYY-MM-DD")]
date: Option<String>,
#[arg(long, value_name = "TZ")]
timezone: Option<String>,
},
Generate {
series: String,
#[arg(long, value_name = "YYYY-MM-DD")]
date: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
Pause {
series: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
Resume {
series: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
End {
series: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
Skip {
series: String,
#[arg(long, value_name = "YYYY-MM-DD")]
date: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
Defer {
series: String,
#[arg(long, value_name = "YYYY-MM-DD")]
date: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
Ignore {
series: String,
#[arg(long, value_name = "YYYY-MM-DD")]
date: String,
#[command(flatten)]
confirmation: RecurringLifecycleArgs,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum WipAction {
Show {
#[arg(long, value_name = "BOARD")]
board: Option<String>,
},
Set {
column: String,
#[arg(long, value_name = "N")]
max_items: Option<u32>,
#[arg(long, value_name = "N")]
max_estimate: Option<u32>,
#[arg(long, value_name = "UNIT")]
unit: Option<String>,
#[arg(long, value_name = "warn|block")]
policy: Option<String>,
#[arg(long)]
disabled: bool,
#[arg(long, value_name = "BOARD")]
board: Option<String>,
},
Unset {
column: String,
#[arg(long, value_name = "BOARD")]
board: Option<String>,
},
Unit {
name: String,
factor: Option<u32>,
#[arg(long)]
remove: bool,
#[arg(long, value_name = "BOARD")]
board: Option<String>,
},
}
#[derive(Debug, Clone, Args)]
pub struct RecurringRuleArgs {
#[arg(long, value_name = "KIND")]
pub rule: Option<String>,
#[arg(long, value_name = "YYYY-MM-DD")]
pub anchor: Option<String>,
#[arg(long, value_name = "N")]
pub interval: Option<u32>,
#[arg(long, value_name = "UNIT")]
pub unit: Option<String>,
#[arg(long, value_name = "EXPR")]
pub expression: Option<String>,
#[arg(long, value_name = "TZ")]
pub timezone: Option<String>,
}
impl RecurringRuleArgs {
pub const fn is_any(&self) -> bool {
self.rule.is_some()
|| self.anchor.is_some()
|| self.interval.is_some()
|| self.unit.is_some()
|| self.expression.is_some()
|| self.timezone.is_some()
}
}
#[derive(Debug, Clone, Args)]
pub struct RecurringLifecycleArgs {
#[arg(long)]
pub yes: bool,
#[arg(long, value_name = "TEXT")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct RecurringCreateArgs {
#[arg(long, value_name = "TITLE")]
pub title: String,
#[arg(long, value_name = "BODY")]
pub body: Option<String>,
#[arg(long, value_name = "STATUS")]
pub status: Option<String>,
#[arg(long, value_name = "PRIORITY")]
pub priority: Option<String>,
#[arg(long, value_name = "LABELS")]
pub labels: Option<String>,
#[arg(long, value_name = "ASSIGNEE")]
pub assignee: Option<String>,
#[arg(long, value_name = "NAME")]
pub name: Option<String>,
#[arg(long, value_name = "ID")]
pub id: Option<String>,
#[command(flatten)]
pub rule: RecurringRuleArgs,
}
#[derive(Debug, Clone, Args)]
pub struct RecurringEditArgs {
#[arg(long, value_name = "NAME")]
pub name: Option<String>,
#[command(flatten)]
pub rule: RecurringRuleArgs,
}
#[derive(Subcommand)]
pub enum ViewAction {
List {
#[arg(long, value_name = "repository|project|user")]
scope: Option<String>,
},
Show {
reference: String,
},
Create {
#[arg(long, value_name = "NAME")]
name: String,
#[command(flatten)]
scope: ViewScopeArgs,
#[command(flatten)]
plan: ViewPlanArgs,
},
Update {
reference: String,
#[arg(long, value_name = "NAME")]
name: Option<String>,
#[command(flatten)]
plan: ViewPlanArgs,
},
Duplicate {
reference: String,
#[arg(long, value_name = "NAME")]
name: Option<String>,
},
Delete {
reference: String,
#[arg(long)]
confirm: bool,
},
Apply {
reference: String,
},
Status {
reference: Option<String>,
#[command(flatten)]
live: ViewPlanArgs,
},
Share {
reference: String,
#[command(flatten)]
scope: ViewScopeArgs,
},
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct ViewScopeArgs {
#[arg(long, value_name = "repository|project|user")]
pub scope: Option<String>,
#[arg(long, value_name = "PROJECT")]
pub project: Option<String>,
#[arg(long, value_name = "IDENTITY")]
pub owner: Option<String>,
#[arg(long, value_name = "LIST")]
pub shared_with: Option<String>,
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct ViewPlanArgs {
#[arg(long, value_name = "QUERY")]
pub filter: Option<String>,
#[arg(long, value_name = "KEY")]
pub sort: Option<String>,
#[arg(long, value_name = "KEY")]
pub group: Option<String>,
#[arg(long, value_name = "LIST")]
pub hide_columns: Option<String>,
#[arg(long, value_name = "true|false", num_args = 1, action = clap::ArgAction::Set)]
pub show_mismatch: Option<bool>,
}
impl ViewPlanArgs {
#[must_use]
pub const fn is_any(&self) -> bool {
self.filter.is_some()
|| self.sort.is_some()
|| self.group.is_some()
|| self.hide_columns.is_some()
|| self.show_mismatch.is_some()
}
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct BulkSelection {
#[arg(long, value_name = "LIST")]
pub select: Option<String>,
#[arg(long, value_name = "QUERY")]
pub filter: Option<String>,
#[arg(long, value_name = "QUERY")]
pub search: Option<String>,
#[arg(long, value_name = "SCOPE")]
pub scope: Option<String>,
#[arg(long)]
pub apply: bool,
}
impl BulkAction {
pub fn selection(&self) -> &BulkSelection {
match self {
Self::Edit { selection, .. }
| Self::Link { selection, .. }
| Self::Unlink { selection, .. }
| Self::Archive { selection } => selection,
}
}
pub fn is_apply(&self) -> bool {
self.selection().apply
}
}
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum BulkAction {
Edit {
#[command(flatten)]
selection: BulkSelection,
#[arg(long, value_name = "COLUMN")]
status: Option<String>,
#[arg(long, value_name = "PRIORITY")]
priority: Option<String>,
#[arg(long, value_name = "ASSIGNEE")]
assignee: Option<String>,
#[arg(long, value_name = "LABELS")]
labels: Option<String>,
#[arg(long, value_name = "DATE")]
start_date: Option<String>,
#[arg(long, value_name = "DATE")]
due_date: Option<String>,
#[arg(long, value_name = "ESTIMATE")]
estimate: Option<String>,
#[arg(long, value_name = "CYCLES")]
cycles: Option<String>,
#[arg(long, value_name = "MILESTONES")]
milestones: Option<String>,
#[arg(long, value_name = "FIELD")]
clear: Option<String>,
},
Link {
#[command(flatten)]
selection: BulkSelection,
#[arg(long, value_name = "SELECTOR")]
target: String,
#[arg(long, value_name = "KIND")]
r#type: Option<String>,
},
Unlink {
#[command(flatten)]
selection: BulkSelection,
#[arg(long, value_name = "SELECTOR")]
target: String,
#[arg(long, value_name = "KIND")]
r#type: Option<String>,
},
Archive {
#[command(flatten)]
selection: BulkSelection,
},
}
#[derive(Subcommand)]
pub enum MentionsAction {
List,
Show {
id: String,
},
Read {
id: String,
},
Ignore {
id: String,
},
Jump {
id: String,
},
}
#[derive(Debug, Clone, Subcommand)]
pub enum AnalyticsCommand {
Burndown {
#[command(flatten)]
args: AnalyticsScopeArgs,
},
Projection {
#[command(flatten)]
args: AnalyticsScopeArgs,
#[arg(long, value_name = "YYYY-MM-DD")]
as_of: String,
},
Export {
#[command(flatten)]
args: AnalyticsScopeArgs,
#[arg(long, value_name = "YYYY-MM-DD")]
as_of: Option<String>,
#[arg(long, value_name = "PATH")]
output: Option<String>,
},
}
#[derive(Debug, Clone, Args)]
pub struct AnalyticsScopeArgs {
#[arg(long, value_name = "cycle|milestone")]
pub scope: String,
#[arg(long, value_name = "ID")]
pub id: String,
#[arg(long, value_name = "YYYY-MM-DD")]
pub from: Option<String>,
#[arg(long, value_name = "YYYY-MM-DD")]
pub to: Option<String>,
#[arg(long, value_name = "count|estimate")]
pub mode: Option<String>,
#[arg(long, value_name = "UNIT")]
pub unit: Option<String>,
}
pub fn run(cli: Cli) -> u8 {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match &cli.command {
Some(Command::Lint { no_foreign }) => run_lint(&cwd, *no_foreign),
Some(Command::Mcp) => match run_mcp(&cwd) {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
},
Some(Command::Render) => match run_render(&cwd) {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
},
Some(Command::Bulk { action }) => {
let result = resolve(&cwd).and_then(|(_, store)| {
commands::bulk::run(
&store,
action,
action.selection(),
if action.is_apply() {
commands::bulk::Mode::Apply
} else {
commands::bulk::Mode::Preview
},
)
});
match result {
Ok(code) => code as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Triage { action }) => {
let result = resolve(&cwd).and_then(|(_, store)| commands::triage::run(&store, action));
match result {
Ok(code) => code as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Archive { issues }) => {
route_archive(&cwd, commands::archive::Action::Archive, issues)
}
Some(Command::Restore { issues }) => {
route_archive(&cwd, commands::archive::Action::Restore, issues)
}
Some(Command::Project { command }) => {
let result = resolve(&cwd).and_then(|(mut config, store)| {
commands::project::run(&mut config, &store, command)
});
match result {
Ok(code) => code as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Epic { command }) => {
let result = resolve(&cwd)
.and_then(|(mut config, store)| commands::epic::run(&mut config, &store, command));
match result {
Ok(code) => code as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Initiative { command }) => {
let result = resolve(&cwd).and_then(|(mut config, store)| {
commands::roadmap::run_initiative(&mut config, &store, command)
});
match result {
Ok(code) => code as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Cycle {
command: CycleCommand::Schedule { command },
}) => {
let result = resolve(&cwd).and_then(|(mut config, store)| {
commands::cycle::run_schedule(&mut config, &store, command)
});
match result {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Capacity { command }) => {
let result = resolve(&cwd).and_then(|(mut config, store)| {
commands::capacity::run(&mut config, &store, command)
});
match result {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
Some(Command::Template { action }) => match commands::templates::run(&cwd, action) {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
},
_ => {
let result = match &cli.command {
None => run_tui(&cwd),
Some(Command::New) => run_tui_new(&cwd),
Some(Command::Init) => commands::init::run(&cwd),
Some(Command::InitAgents) => {
let root = Config::project_root(&cwd).unwrap_or(cwd.clone());
commands::agents::run(&root).map(|_| ())
}
Some(cmd) => run_subcommand(&cwd, cmd),
};
match result {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
}
}
fn route_archive(cwd: &Path, action: commands::archive::Action, issues: &[String]) -> u8 {
match resolve(cwd).and_then(|(_, store)| commands::archive::run(&store, action, issues)) {
Ok(code) => code as u8,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
exit_code_for(&e) as u8
}
}
}
fn run_lint(cwd: &Path, no_foreign: bool) -> u8 {
let (config, store) = match resolve(cwd) {
Ok(pair) => pair,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
return 2;
}
};
match commands::lint::run_with(&config, &store, !no_foreign) {
Ok(true) => 1,
Ok(false) => 0,
Err(e) => {
eprintln!("{}", crate::error::render_error(&e));
2
}
}
}
fn run_tui(cwd: &Path) -> Result<()> {
let config = tui_config(cwd)?;
crate::app::run(&config)
}
fn run_tui_new(cwd: &Path) -> Result<()> {
let config = tui_config(cwd)?;
crate::app::run_new_issue(&config)
}
fn tui_config(cwd: &Path) -> Result<Config> {
if !offer_first_run_init(cwd)? {
return Err(TixError::Config(
"no tix.toml found — run \"tix init\" first".into(),
));
}
repo_config(cwd)
}
fn offer_first_run_init(cwd: &Path) -> Result<bool> {
use std::io::IsTerminal;
if Config::project_root(cwd).is_some() {
return Ok(true);
}
if !std::io::stdin().is_terminal() {
return Ok(false);
}
eprint!("tix: no tix.toml found — create one with default settings? [Y/n] ");
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(TixError::Io)?;
let answer = line.trim();
if answer.is_empty() || answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes") {
commands::init::run(cwd)?;
return Ok(true);
}
eprintln!("tix: aborted — run \"tix init\" when you are ready");
Ok(false)
}
fn repo_config(cwd: &Path) -> Result<Config> {
let root = Config::project_root(cwd)
.ok_or_else(|| TixError::Config("no tix.toml found — run \"tix init\" first".into()))?;
let mut config = Config::load(&root)?;
config.resolve_repo_paths(&root);
Ok(config)
}
fn run_mcp(cwd: &Path) -> Result<()> {
let config = repo_config(cwd)?;
let store = Store::open(&config)?;
let mut config = config;
if config.milestones_dir().is_relative() {
let root = Config::project_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
let configured = config.milestones_dir();
config.milestones_dir = Some(root.join(configured));
}
crate::mcp::serve(&config, &store)
}
fn run_render(cwd: &Path) -> Result<()> {
let config = repo_config(cwd)?;
let store = Store::open(&config)?;
commands::dispatch(&Command::Render, &config, &store)
}
fn resolve(cwd: &Path) -> Result<(Config, Store)> {
let root = Config::project_root(cwd);
let mut config = match &root {
Some(r) => Config::load(r)?,
None => Config::default(),
};
if config.issues_dir.is_relative() {
let base = root.as_deref().unwrap_or(cwd);
config.issues_dir = base.join(&config.issues_dir);
}
if config.milestones_dir().is_relative() {
let base = root.as_deref().unwrap_or(cwd);
let configured = config.milestones_dir();
config.milestones_dir = Some(base.join(configured));
}
config.resolve_repo_paths(root.as_deref().unwrap_or(cwd));
if root.is_none() {
eprintln!("tix: no tix.toml found (run 'tix init' to create one)");
}
let store = Store::open(&config)?;
Ok((config, store))
}
fn run_subcommand(cwd: &Path, cmd: &Command) -> Result<()> {
let (mut config, store) = resolve(cwd)?;
if let Command::View {
action: Some(action),
..
} = cmd
{
return commands::views::run(&mut config, &store, action);
}
if let Command::Wip { action } = cmd {
return commands::wip::run(&mut config, &store, action);
}
if !matches!(cmd, Command::Rollover { .. }) {
let planner = crate::planning::Planner::from_config(config.clone(), cwd.to_path_buf());
let plan = planner.rollover_plan(&store, &crate::model::today());
if let Some(first) = plan.first() {
eprintln!(
"hint: rollover of {} issue(s) from \"{}\" to \"{}\" is suggested; run 'tix rollover' to apply",
first.issues.len(),
first.from_cycle,
first.to_cycle
);
}
}
commands::dispatch(cmd, &config, &store)
}