use std::path::{Path, PathBuf};
use clap::builder::NonEmptyStringValueParser;
use clap::{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)]
links: Option<String>,
#[arg(long)]
files: Option<String>,
#[arg(long)]
estimate: Option<String>,
},
Link {
a: String,
b: String,
},
Unlink {
a: String,
b: String,
},
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)]
links: Option<String>,
#[arg(long)]
files: Option<String>,
#[arg(long)]
estimate: Option<String>,
#[arg(long)]
commit: Option<String>,
},
Move {
issue: String,
column: String,
#[arg(long, value_name = "HASH")]
commit: Option<String>,
},
#[command(visible_alias = "show")]
View {
issue: String,
},
List {
#[arg(long)]
status: Option<String>,
},
Status,
Delete {
issue: String,
#[arg(long)]
yes: bool,
},
Export {
#[arg(long, value_name = "gitlab|linear")]
target: String,
#[arg(long)]
dry_run: bool,
#[arg(long)]
project: Option<String>,
},
Import {
#[arg(long, value_name = "gitlab|linear")]
target: String,
},
Search {
#[arg(value_name = "STRING", value_parser = NonEmptyStringValueParser::new())]
string: String,
},
Lint,
Mcp,
}
pub fn run(cli: Cli) -> u8 {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match &cli.command {
Some(Command::Lint) => run_lint(&cwd),
Some(Command::Mcp) => match run_mcp(&cwd) {
Ok(()) => ExitCode::Success as u8,
Err(e) => {
eprintln!("tix: {e}");
exit_code_for(&e) as u8
}
},
_ => {
let result = match &cli.command {
None => run_tui(&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!("tix: {e}");
exit_code_for(&e) as u8
}
}
}
}
}
fn run_lint(cwd: &Path) -> u8 {
let (config, store) = match resolve(cwd) {
Ok(pair) => pair,
Err(e) => {
eprintln!("tix: {e}");
return 2;
}
};
match commands::lint::run(&config, &store) {
Ok(true) => 1,
Ok(false) => 0,
Err(e) => {
eprintln!("tix: {e}");
2
}
}
}
fn run_tui(cwd: &Path) -> Result<()> {
let config = repo_config(cwd)?;
crate::app::run(&config)
}
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()))?;
Config::load(&root)
}
fn run_mcp(cwd: &Path) -> Result<()> {
let config = repo_config(cwd)?;
let store = Store::open(&config)?;
crate::mcp::serve(&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 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 (config, store) = resolve(cwd)?;
commands::dispatch(cmd, &config, &store)
}