|
1 | | -//! Linear integration commands - DISABLED |
2 | | -//! |
3 | | -//! This module is disabled because cortex_engine::linear is not available in cortex-cli. |
4 | | -//! The linear module exists in cortex-app but was not migrated to cortex-cli. |
5 | | -//! |
6 | | -//! To re-enable this feature, the linear module would need to be copied from cortex-app |
7 | | -//! and all its dependencies resolved in cortex-cli's cortex-engine. |
| 1 | +//! Linear integration commands. |
| 2 | +
|
| 3 | +use cortex_engine::linear::client::{Issue, IssueDetails, LinearClient, Team}; |
| 4 | +use keyring::Entry; |
| 5 | +use serde::Deserialize; |
| 6 | +use std::env; |
| 7 | +use tauri::State; |
8 | 8 |
|
9 | 9 | use crate::error::CommandError; |
| 10 | +use crate::state::AppState; |
| 11 | + |
| 12 | +const SERVICE_NAME: &str = "cortex-linear"; |
| 13 | +const USER_NAME: &str = "auth-token"; |
| 14 | + |
| 15 | +/// Get Linear token from keychain or environment. |
| 16 | +fn get_linear_token() -> Result<String, CommandError> { |
| 17 | + if let Ok(token) = env::var("LINEAR_API_KEY") { |
| 18 | + return Ok(token); |
| 19 | + } |
| 20 | + |
| 21 | + let entry = Entry::new(SERVICE_NAME, USER_NAME) |
| 22 | + .map_err(|e| CommandError::EngineError(format!("Failed to access keychain: {}", e)))?; |
10 | 23 |
|
11 | | -/// Placeholder for linear_set_token - feature disabled |
| 24 | + match entry.get_password() { |
| 25 | + Ok(token) => Ok(token), |
| 26 | + Err(keyring::Error::NoEntry) => Err(CommandError::ValidationError( |
| 27 | + "Linear token not found. Please sign in.".to_string(), |
| 28 | + )), |
| 29 | + Err(e) => Err(CommandError::EngineError(format!( |
| 30 | + "Failed to retrieve token: {}", |
| 31 | + e |
| 32 | + ))), |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +/// Store Linear token in keychain. |
12 | 37 | #[tauri::command] |
13 | | -pub async fn linear_set_token(_token: String) -> Result<(), CommandError> { |
14 | | - Err(CommandError::EngineError( |
15 | | - "Linear integration is not available in this build".to_string(), |
16 | | - )) |
| 38 | +pub async fn linear_set_token(token: String) -> Result<(), CommandError> { |
| 39 | + let entry = Entry::new(SERVICE_NAME, USER_NAME) |
| 40 | + .map_err(|e| CommandError::EngineError(format!("Failed to access keychain: {}", e)))?; |
| 41 | + |
| 42 | + entry |
| 43 | + .set_password(&token) |
| 44 | + .map_err(|e| CommandError::EngineError(format!("Failed to store token: {}", e)))?; |
| 45 | + |
| 46 | + Ok(()) |
17 | 47 | } |
18 | 48 |
|
19 | | -/// Placeholder for linear_logout - feature disabled |
| 49 | +/// Remove Linear token from keychain. |
20 | 50 | #[tauri::command] |
21 | 51 | pub async fn linear_logout() -> Result<(), CommandError> { |
22 | | - Err(CommandError::EngineError( |
23 | | - "Linear integration is not available in this build".to_string(), |
24 | | - )) |
| 52 | + let entry = Entry::new(SERVICE_NAME, USER_NAME) |
| 53 | + .map_err(|e| CommandError::EngineError(format!("Failed to access keychain: {}", e)))?; |
| 54 | + |
| 55 | + match entry.delete_credential() { |
| 56 | + Ok(_) => Ok(()), |
| 57 | + Err(keyring::Error::NoEntry) => Ok(()), |
| 58 | + Err(e) => Err(CommandError::EngineError(format!( |
| 59 | + "Failed to delete token: {}", |
| 60 | + e |
| 61 | + ))), |
| 62 | + } |
25 | 63 | } |
26 | 64 |
|
27 | | -/// Placeholder for linear_check_auth - feature disabled |
| 65 | +/// Check if user is authenticated with Linear. |
28 | 66 | #[tauri::command] |
29 | 67 | pub async fn linear_check_auth() -> Result<bool, CommandError> { |
30 | | - Err(CommandError::EngineError( |
31 | | - "Linear integration is not available in this build".to_string(), |
32 | | - )) |
| 68 | + match get_linear_token() { |
| 69 | + Ok(_) => Ok(true), |
| 70 | + Err(_) => Ok(false), |
| 71 | + } |
33 | 72 | } |
34 | 73 |
|
35 | | -/// Placeholder for linear_auth_url - feature disabled |
| 74 | +/// Get Linear OAuth URL. |
36 | 75 | #[tauri::command] |
37 | 76 | pub async fn linear_auth_url( |
38 | | - _client_id: String, |
39 | | - _redirect_uri: String, |
40 | | - _state: String, |
| 77 | + client_id: String, |
| 78 | + redirect_uri: String, |
| 79 | + state: String, |
41 | 80 | ) -> Result<String, CommandError> { |
42 | | - Err(CommandError::EngineError( |
43 | | - "Linear integration is not available in this build".to_string(), |
| 81 | + Ok(format!( |
| 82 | + "{}?client_id={}&redirect_uri={}&response_type=code&state={}&scope=read,write", |
| 83 | + cortex_engine::linear::client::LINEAR_OAUTH_AUTHORIZE, |
| 84 | + client_id, |
| 85 | + redirect_uri, |
| 86 | + state |
44 | 87 | )) |
45 | 88 | } |
46 | 89 |
|
47 | | -/// Placeholder for linear_exchange_token - feature disabled |
| 90 | +/// Exchange OAuth code for token. |
48 | 91 | #[tauri::command] |
49 | 92 | pub async fn linear_exchange_token( |
50 | | - _code: String, |
51 | | - _client_id: String, |
52 | | - _client_secret: String, |
53 | | - _redirect_uri: String, |
| 93 | + code: String, |
| 94 | + client_id: String, |
| 95 | + client_secret: String, |
| 96 | + redirect_uri: String, |
54 | 97 | ) -> Result<String, CommandError> { |
55 | | - Err(CommandError::EngineError( |
56 | | - "Linear integration is not available in this build".to_string(), |
57 | | - )) |
| 98 | + let client = reqwest::Client::new(); |
| 99 | + |
| 100 | + let params = [ |
| 101 | + ("code", code), |
| 102 | + ("client_id", client_id), |
| 103 | + ("client_secret", client_secret), |
| 104 | + ("redirect_uri", redirect_uri), |
| 105 | + ("grant_type", "authorization_code".to_string()), |
| 106 | + ]; |
| 107 | + |
| 108 | + let response = client |
| 109 | + .post(cortex_engine::linear::client::LINEAR_OAUTH_TOKEN) |
| 110 | + .form(¶ms) |
| 111 | + .send() |
| 112 | + .await |
| 113 | + .map_err(|e| CommandError::EngineError(format!("Failed to send token request: {}", e)))?; |
| 114 | + |
| 115 | + if !response.status().is_success() { |
| 116 | + let body = response.text().await.unwrap_or_default(); |
| 117 | + return Err(CommandError::EngineError(format!( |
| 118 | + "Token exchange failed: {}", |
| 119 | + body |
| 120 | + ))); |
| 121 | + } |
| 122 | + |
| 123 | + #[derive(Deserialize)] |
| 124 | + struct TokenResponse { |
| 125 | + access_token: String, |
| 126 | + } |
| 127 | + |
| 128 | + let token_res: TokenResponse = response |
| 129 | + .json() |
| 130 | + .await |
| 131 | + .map_err(|e| CommandError::EngineError(format!("Failed to parse token response: {}", e)))?; |
| 132 | + |
| 133 | + linear_set_token(token_res.access_token.clone()).await?; |
| 134 | + |
| 135 | + Ok(token_res.access_token) |
58 | 136 | } |
59 | 137 |
|
60 | | -/// Placeholder for linear_get_teams - feature disabled |
| 138 | +/// Get all teams. |
61 | 139 | #[tauri::command] |
62 | | -pub async fn linear_get_teams() -> Result<Vec<String>, CommandError> { |
63 | | - Err(CommandError::EngineError( |
64 | | - "Linear integration is not available in this build".to_string(), |
65 | | - )) |
| 140 | +pub async fn linear_get_teams(_state: State<'_, AppState>) -> Result<Vec<Team>, CommandError> { |
| 141 | + let token = get_linear_token()?; |
| 142 | + let client = LinearClient::new(&token) |
| 143 | + .map_err(|e| CommandError::EngineError(format!("Failed to create Linear client: {}", e)))?; |
| 144 | + |
| 145 | + client |
| 146 | + .get_teams() |
| 147 | + .await |
| 148 | + .map_err(|e| CommandError::EngineError(format!("Failed to get teams: {}", e))) |
66 | 149 | } |
67 | 150 |
|
68 | | -/// Placeholder for linear_get_issues - feature disabled |
| 151 | +/// Get issues for a team. |
69 | 152 | #[tauri::command] |
70 | | -pub async fn linear_get_issues(_team_id: String) -> Result<Vec<String>, CommandError> { |
71 | | - Err(CommandError::EngineError( |
72 | | - "Linear integration is not available in this build".to_string(), |
73 | | - )) |
| 153 | +pub async fn linear_get_issues( |
| 154 | + _state: State<'_, AppState>, |
| 155 | + team_id: String, |
| 156 | +) -> Result<Vec<Issue>, CommandError> { |
| 157 | + let token = get_linear_token()?; |
| 158 | + let client = LinearClient::new(&token) |
| 159 | + .map_err(|e| CommandError::EngineError(format!("Failed to create Linear client: {}", e)))?; |
| 160 | + |
| 161 | + client |
| 162 | + .get_issues(&team_id) |
| 163 | + .await |
| 164 | + .map_err(|e| CommandError::EngineError(format!("Failed to get issues: {}", e))) |
74 | 165 | } |
75 | 166 |
|
76 | | -/// Placeholder for linear_get_issue_details - feature disabled |
| 167 | +/// Get issue details. |
77 | 168 | #[tauri::command] |
78 | | -pub async fn linear_get_issue_details(_issue_id: String) -> Result<String, CommandError> { |
79 | | - Err(CommandError::EngineError( |
80 | | - "Linear integration is not available in this build".to_string(), |
81 | | - )) |
| 169 | +pub async fn linear_get_issue_details( |
| 170 | + _state: State<'_, AppState>, |
| 171 | + issue_id: String, |
| 172 | +) -> Result<IssueDetails, CommandError> { |
| 173 | + let token = get_linear_token()?; |
| 174 | + let client = LinearClient::new(&token) |
| 175 | + .map_err(|e| CommandError::EngineError(format!("Failed to create Linear client: {}", e)))?; |
| 176 | + |
| 177 | + client |
| 178 | + .get_issue_details(&issue_id) |
| 179 | + .await |
| 180 | + .map_err(|e| CommandError::EngineError(format!("Failed to get issue details: {}", e))) |
82 | 181 | } |
83 | 182 |
|
84 | | -/// Placeholder for linear_create_session_from_issue - feature disabled |
| 183 | +/// Create a session from a Linear issue. |
85 | 184 | #[tauri::command] |
86 | | -pub async fn linear_create_session_from_issue(_issue_id: String) -> Result<String, CommandError> { |
87 | | - Err(CommandError::EngineError( |
88 | | - "Linear integration is not available in this build".to_string(), |
89 | | - )) |
| 185 | +pub async fn linear_create_session_from_issue( |
| 186 | + _state: State<'_, AppState>, |
| 187 | + issue_id: String, |
| 188 | +) -> Result<String, CommandError> { |
| 189 | + let token = get_linear_token()?; |
| 190 | + let client = LinearClient::new(&token) |
| 191 | + .map_err(|e| CommandError::EngineError(format!("Failed to create Linear client: {}", e)))?; |
| 192 | + |
| 193 | + let issue = client |
| 194 | + .get_issue_details(&issue_id) |
| 195 | + .await |
| 196 | + .map_err(|e| CommandError::EngineError(format!("Failed to get issue details: {}", e)))?; |
| 197 | + |
| 198 | + let prompt = format!( |
| 199 | + "I'm working on Linear issue {}: {}\n\nDescription:\n{}\n\nPlease help me implement this.", |
| 200 | + issue.identifier, |
| 201 | + issue.title, |
| 202 | + issue.description.unwrap_or_default() |
| 203 | + ); |
| 204 | + |
| 205 | + Ok(prompt) |
90 | 206 | } |
0 commit comments