xbp_cli/commands/linear_cmd/
mod.rs1mod bulk;
4mod comments;
5mod interactive;
6mod issues;
7mod meta;
8mod tui;
9
10use crate::cli::commands::{LinearCmd, LinearSubCommand};
11use crate::commands::cli_session::fetch_linear_api_key_from_dashboard;
12use crate::commands::cloudflare_config::{is_interactive_terminal, require_interactive_terminal};
13use crate::commands::config_cmd::run_config_secret_set;
14use crate::commands::ssh_helpers::prompt_for_password;
15use crate::config::resolve_linear_api_key;
16use crate::utils::open_with_default_handler;
17use colored::Colorize;
18use dialoguer::{theme::ColorfulTheme, Confirm};
19
20pub use comments::{create_comment, list_comments, LinearComment};
21pub use issues::{
22 archive_issue, attachment_link_url, create_issue, default_team_hint, get_issue,
23 issues_for_attachment_url, list_issues, mark_duplicate_of, prompt_and_save_default_team,
24 sort_issues, update_issue, CreateIssueInput, IssueSortField, LinearIssue, ListIssuesFilter,
25 UpdateIssueInput,
26};
27pub use meta::{
28 ensure_label_ids, list_labels, list_projects, list_teams, list_users, list_workflow_states,
29 resolve_assignee_id, resolve_project_id, resolve_team_id, resolve_team_id_auto, LinearLabel,
30 LinearProject, LinearTeam, LinearUser, LinearWorkflowState,
31};
32
33pub const LINEAR_API_KEYS_URL: &str = "https://linear.app/settings/account/security/api-keys/new";
35
36pub async fn run_linear(cmd: LinearCmd) -> Result<(), String> {
37 let api_key = ensure_linear_api_key().await?;
38 match cmd.command {
39 None => {
40 let filter = issues::build_list_filter_from_hub_cmd(&api_key, &cmd).await?;
41 tui::run_linear_tui(&api_key, filter).await
42 }
43 Some(LinearSubCommand::List(args)) => issues::run_list(&api_key, args).await,
44 Some(LinearSubCommand::Search(args)) => issues::run_search(&api_key, args).await,
45 Some(LinearSubCommand::Show(args)) => issues::run_show(&api_key, args).await,
46 Some(LinearSubCommand::Create(args)) => issues::run_create(&api_key, args).await,
47 Some(LinearSubCommand::Edit(args)) => issues::run_edit(&api_key, args).await,
48 Some(LinearSubCommand::Status(args)) => issues::run_status(&api_key, args).await,
49 Some(LinearSubCommand::Priority(args)) => issues::run_priority(&api_key, args).await,
50 Some(LinearSubCommand::Labels(args)) => issues::run_labels(&api_key, args).await,
51 Some(LinearSubCommand::Assign(args)) => issues::run_assign(&api_key, args).await,
52 Some(LinearSubCommand::Bulk(args)) => bulk::run_bulk(&api_key, args).await,
53 Some(LinearSubCommand::Comment(args)) => comments::run_comment(&api_key, args).await,
54 Some(LinearSubCommand::Comments(args)) => comments::run_comments(&api_key, args).await,
55 }
56}
57
58pub(crate) async fn run_linear_tui_with_filter(
59 api_key: &str,
60 filter: ListIssuesFilter,
61) -> Result<(), String> {
62 tui::run_linear_tui(api_key, filter).await
63}
64
65pub async fn ensure_linear_api_key() -> Result<String, String> {
70 if let Some(key) = resolve_linear_api_key() {
71 return Ok(key);
72 }
73 match fetch_linear_api_key_from_dashboard().await {
74 Ok(Some(key)) => return Ok(key),
75 Ok(None) => {}
76 Err(err) => {
77 eprintln!(
79 "{} dashboard Linear key lookup: {err}",
80 "note".bright_black()
81 );
82 }
83 }
84
85 if !is_interactive_terminal() {
86 return Err(non_interactive_linear_key_message());
87 }
88
89 ensure_linear_api_key_interactive().await
90}
91
92async fn ensure_linear_api_key_interactive() -> Result<String, String> {
93 println!();
94 println!("{}", "Linear API key required".bright_cyan().bold());
95 println!(
96 "{}",
97 "No local or dashboard Linear key found. Set one now to continue.".bright_black()
98 );
99 println!();
100
101 let open = Confirm::with_theme(&ColorfulTheme::default())
102 .with_prompt("Open Linear API key page in your browser?")
103 .default(true)
104 .interact()
105 .map_err(|e| format!("Failed to read choice: {e}"))?;
106 if open {
107 let _ = open_with_default_handler(LINEAR_API_KEYS_URL);
108 println!("{} {}", "Opened:".dimmed(), LINEAR_API_KEYS_URL);
109 println!(
110 "{}",
111 "Create a Personal API key, then paste it below (input is hidden).".bright_black()
112 );
113 } else {
114 println!("{} {}", "Create a key at:".dimmed(), LINEAR_API_KEYS_URL);
115 }
116 println!();
117
118 let key = prompt_for_password("Linear API key: ")?;
119 let key = key.trim().to_string();
120 if key.is_empty() {
121 return Err(
122 "No Linear API key entered. Run `xbp config linear set-key` or retry the command."
123 .to_string(),
124 );
125 }
126
127 run_config_secret_set("linear", Some(key.clone())).await?;
128
129 if default_team_hint().is_none() {
131 let pick = Confirm::with_theme(&ColorfulTheme::default())
132 .with_prompt("Also pick a default Linear team for this repo now?")
133 .default(true)
134 .interact()
135 .map_err(|e| format!("Failed to read choice: {e}"))?;
136 if pick {
137 if let Err(err) = issues::prompt_and_save_default_team(&key).await {
138 eprintln!(
139 "{} could not save default team: {err}",
140 "note".bright_black()
141 );
142 eprintln!(
143 "{}",
144 "You can set it later with `linear.default_team_key` in `.xbp/xbp.yaml`."
145 .bright_black()
146 );
147 }
148 }
149 }
150
151 Ok(key)
152}
153
154fn non_interactive_linear_key_message() -> String {
155 format!(
156 "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}"
157 )
158}
159
160pub(crate) fn require_interactive(context: &str) -> Result<(), String> {
161 require_interactive_terminal(context)
162}
163
164pub(crate) fn priority_label(priority: i32) -> &'static str {
165 match priority {
166 1 => "Urgent",
167 2 => "High",
168 3 => "Medium",
169 4 => "Low",
170 _ => "None",
171 }
172}
173
174pub(crate) fn parse_priority_arg(raw: &str) -> Result<i32, String> {
175 let trimmed = raw.trim();
176 if let Ok(n) = trimmed.parse::<i32>() {
177 if (0..=4).contains(&n) {
178 return Ok(n);
179 }
180 return Err("Priority must be 0–4 (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low).".into());
181 }
182 match trimmed.to_ascii_lowercase().as_str() {
183 "none" | "no" => Ok(0),
184 "urgent" | "u" => Ok(1),
185 "high" | "h" => Ok(2),
186 "medium" | "med" | "m" => Ok(3),
187 "low" | "l" => Ok(4),
188 _ => Err(format!(
189 "Unknown priority `{raw}`. Use 0–4 or none|urgent|high|medium|low."
190 )),
191 }
192}