use std::io::{self, Write};
use clap::{Parser, Subcommand};
use crate::{
env,
task_context::TaskContext,
utils::{build_jira_payload, print_line_separator},
JirunResult,
};
use crate::{config::JiraConfig, jira::RealJiraApi};
#[derive(Parser)]
#[command(name = "jirun")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(
about = concat!("✨ generates JIRA sub-task(s) with template (v.", env!("CARGO_PKG_VERSION"), ")"),
long_about = None,
after_help = "\
📘 Examples:
1. jirun help init
Help menu on initializing jirun's configuration files.
2. jirun init --global
Create config files in the global directory.
3. jirun template --parent PROJ-123
Use [sub_tasks.template_tasks] to create sub-tasks under PROJ-123
4. jirun new --parent PROJ-123 --assignee alice
Use [sub_tasks.new_tasks], overriding assignee with 'alice'
5. jirun new --parent PROJ-123 --simple \"Fix login bug\"
Create a single sub-task with the given title (bypasses .jirun.toml)
6. jirun template -p PROJ-123 --dry-run
Show request payloads without sending to JIRA"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(long = "global")]
global: bool,
},
New {
#[arg(short = 'p', long = "parent")]
parent: String,
#[arg(short = 'a', long = "assignee")]
assignee: Option<String>,
#[arg(short = 's', long = "simple")]
simple: Option<String>,
#[arg(short = 'd', long = "dry-run")]
dry_run: bool,
},
Template {
#[arg(short = 'p', long = "parent")]
parent: String,
#[arg(short = 'a', long = "assignee")]
assignee: Option<String>,
#[arg(short = 'd', long = "dry-run")]
dry_run: bool,
},
}
pub fn run() -> JirunResult<()> {
env::try_load_dotenv();
let cli = crate::commands::Cli::parse();
match cli.command {
Commands::Init { global } => handle_init(global),
Commands::Template {
parent,
assignee,
dry_run,
} => handle_template_command(parent, assignee.as_deref(), dry_run)?,
crate::commands::Commands::New {
parent,
assignee,
simple,
dry_run,
} => handle_new_command(parent, assignee.as_deref(), simple, dry_run)?,
}
Ok(())
}
fn handle_init(global: bool) {
if global {
JiraConfig::init_global()
} else {
JiraConfig::init_local()
}
}
fn handle_template_command(
parent: String,
assignee: Option<&str>,
dry_run: bool,
) -> JirunResult<()> {
handle_subtask_command(
parent,
assignee,
None,
dry_run,
JiraConfig::template_task_list,
)
}
fn handle_new_command(
parent: String,
assignee: Option<&str>,
simple: Option<String>,
dry_run: bool,
) -> JirunResult<()> {
handle_subtask_command(parent, assignee, simple, dry_run, JiraConfig::new_task_list)
}
fn handle_subtask_command<F>(
parent: String,
assignee: Option<&str>,
simple: Option<String>,
dry_run: bool,
select_tasks: F,
) -> JirunResult<()>
where
F: FnOnce(&JiraConfig) -> Vec<String>,
{
let token = dotenvy::var("JIRA_TOKEN")?;
let config = JiraConfig::load()?;
let tasks = if let Some(simple_title) = simple {
vec![simple_title]
} else {
select_tasks(&config)
};
let api = RealJiraApi::new(config.api_url(), token);
let ctx = TaskContext::new(config, Box::new(api), &parent, assignee.map(str::to_string))?;
let (to_create, duplicates) = ctx.filter_new_tasks(&tasks);
ctx.print_task_summary(&tasks, &duplicates)?;
if dry_run {
ctx.print_dry_run_summary(&to_create)?;
return Ok(());
}
if to_create.is_empty() {
print_line_separator();
println!("⚠️ No new tasks to create. Terminating...");
return Ok(());
}
if !prompt_confirm(to_create.len())? {
println!("❌ Aborted.");
return Ok(());
}
for summary in &to_create {
let payload = build_jira_payload(
&ctx.config,
&ctx.parent_key,
summary,
ctx.assignee.as_deref(),
);
let key = ctx.api.create_subtask(&payload)?;
println!("✅ Created sub-task: {}", ctx.issue_link(&key));
}
Ok(())
}
fn prompt_confirm(size: usize) -> JirunResult<bool> {
print!("\n✅ {} sub-task(s) to create, proceed? [y/N]: ", size);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
Ok(matches!(answer.as_str(), "y" | "yes"))
}