mod bulk;
mod comments;
mod interactive;
mod issues;
mod meta;
mod tui;
use crate::cli::commands::{LinearCmd, LinearSubCommand};
use crate::commands::cli_session::fetch_linear_api_key_from_dashboard;
use crate::commands::cloudflare_config::{is_interactive_terminal, require_interactive_terminal};
use crate::commands::config_cmd::run_config_secret_set;
use crate::commands::ssh_helpers::prompt_for_password;
use crate::config::resolve_linear_api_key;
use crate::utils::open_with_default_handler;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Confirm};
pub use comments::{create_comment, list_comments, LinearComment};
pub use issues::{
archive_issue, attachment_link_url, create_issue, default_team_hint, get_issue,
issues_for_attachment_url, list_issues, mark_duplicate_of, prompt_and_save_default_team,
sort_issues, update_issue, CreateIssueInput, IssueSortField, LinearIssue, ListIssuesFilter,
UpdateIssueInput,
};
pub use meta::{
ensure_label_ids, list_labels, list_projects, list_teams, list_users, list_workflow_states,
resolve_assignee_id, resolve_project_id, resolve_team_id, resolve_team_id_auto, LinearLabel,
LinearProject, LinearTeam, LinearUser, LinearWorkflowState,
};
pub const LINEAR_API_KEYS_URL: &str = "https://linear.app/settings/account/security/api-keys/new";
pub async fn run_linear(cmd: LinearCmd) -> Result<(), String> {
let api_key = ensure_linear_api_key().await?;
match cmd.command {
None => {
let filter = issues::build_list_filter_from_hub_cmd(&api_key, &cmd).await?;
tui::run_linear_tui(&api_key, filter).await
}
Some(LinearSubCommand::List(args)) => issues::run_list(&api_key, args).await,
Some(LinearSubCommand::Search(args)) => issues::run_search(&api_key, args).await,
Some(LinearSubCommand::Show(args)) => issues::run_show(&api_key, args).await,
Some(LinearSubCommand::Create(args)) => issues::run_create(&api_key, args).await,
Some(LinearSubCommand::Edit(args)) => issues::run_edit(&api_key, args).await,
Some(LinearSubCommand::Status(args)) => issues::run_status(&api_key, args).await,
Some(LinearSubCommand::Priority(args)) => issues::run_priority(&api_key, args).await,
Some(LinearSubCommand::Labels(args)) => issues::run_labels(&api_key, args).await,
Some(LinearSubCommand::Assign(args)) => issues::run_assign(&api_key, args).await,
Some(LinearSubCommand::Bulk(args)) => bulk::run_bulk(&api_key, args).await,
Some(LinearSubCommand::Comment(args)) => comments::run_comment(&api_key, args).await,
Some(LinearSubCommand::Comments(args)) => comments::run_comments(&api_key, args).await,
}
}
pub(crate) async fn run_linear_tui_with_filter(
api_key: &str,
filter: ListIssuesFilter,
) -> Result<(), String> {
tui::run_linear_tui(api_key, filter).await
}
pub async fn ensure_linear_api_key() -> Result<String, String> {
if let Some(key) = resolve_linear_api_key() {
return Ok(key);
}
match fetch_linear_api_key_from_dashboard().await {
Ok(Some(key)) => return Ok(key),
Ok(None) => {}
Err(err) => {
eprintln!(
"{} dashboard Linear key lookup: {err}",
"note".bright_black()
);
}
}
if !is_interactive_terminal() {
return Err(non_interactive_linear_key_message());
}
ensure_linear_api_key_interactive().await
}
async fn ensure_linear_api_key_interactive() -> Result<String, String> {
println!();
println!("{}", "Linear API key required".bright_cyan().bold());
println!(
"{}",
"No local or dashboard Linear key found. Set one now to continue.".bright_black()
);
println!();
let open = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Open Linear API key page in your browser?")
.default(true)
.interact()
.map_err(|e| format!("Failed to read choice: {e}"))?;
if open {
let _ = open_with_default_handler(LINEAR_API_KEYS_URL);
println!("{} {}", "Opened:".dimmed(), LINEAR_API_KEYS_URL);
println!(
"{}",
"Create a Personal API key, then paste it below (input is hidden).".bright_black()
);
} else {
println!("{} {}", "Create a key at:".dimmed(), LINEAR_API_KEYS_URL);
}
println!();
let key = prompt_for_password("Linear API key: ")?;
let key = key.trim().to_string();
if key.is_empty() {
return Err(
"No Linear API key entered. Run `xbp config linear set-key` or retry the command."
.to_string(),
);
}
run_config_secret_set("linear", Some(key.clone())).await?;
if default_team_hint().is_none() {
let pick = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Also pick a default Linear team for this repo now?")
.default(true)
.interact()
.map_err(|e| format!("Failed to read choice: {e}"))?;
if pick {
if let Err(err) = issues::prompt_and_save_default_team(&key).await {
eprintln!(
"{} could not save default team: {err}",
"note".bright_black()
);
eprintln!(
"{}",
"You can set it later with `linear.default_team_key` in `.xbp/xbp.yaml`."
.bright_black()
);
}
}
}
Ok(key)
}
fn non_interactive_linear_key_message() -> String {
format!(
"No Linear API key found. Run `xbp config linear set-key` in an interactive terminal, or save it in the dashboard settings. API keys: {LINEAR_API_KEYS_URL}"
)
}
pub(crate) fn require_interactive(context: &str) -> Result<(), String> {
require_interactive_terminal(context)
}
pub(crate) fn priority_label(priority: i32) -> &'static str {
match priority {
1 => "Urgent",
2 => "High",
3 => "Medium",
4 => "Low",
_ => "None",
}
}
pub(crate) fn parse_priority_arg(raw: &str) -> Result<i32, String> {
let trimmed = raw.trim();
if let Ok(n) = trimmed.parse::<i32>() {
if (0..=4).contains(&n) {
return Ok(n);
}
return Err("Priority must be 0–4 (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low).".into());
}
match trimmed.to_ascii_lowercase().as_str() {
"none" | "no" => Ok(0),
"urgent" | "u" => Ok(1),
"high" | "h" => Ok(2),
"medium" | "med" | "m" => Ok(3),
"low" | "l" => Ok(4),
_ => Err(format!(
"Unknown priority `{raw}`. Use 0–4 or none|urgent|high|medium|low."
)),
}
}