UNPKG

bookr-cli

Version:

A terminal-based CLI tool to book time in Jira using the Tempo plugin by parsing your current Git branch name

225 lines 8.95 kB
import { loadConfigFromFile } from '../utils/config.js'; export class JiraClient { constructor(auth) { this.auth = auth; this.baseHeaders = { Authorization: `Basic ${Buffer.from(`${auth.email}:${auth.apiToken}`).toString('base64')}`, Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'bookr-cli/1.0', }; } /** * Get a JIRA issue by key (e.g., "PROJ-123") */ async getIssue(issueKey) { const url = `${this.auth.baseUrl}/rest/api/3/issue/${issueKey}`; try { const response = await fetch(url, { method: 'GET', headers: this.baseHeaders, }); if (!response.ok) { const errorData = (await response.json()); throw new Error(`Failed to get issue: ${errorData.errorMessages?.join(', ') || response.statusText}`); } return (await response.json()); } catch (error) { throw new Error(`Error fetching issue ${issueKey}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get current user information */ async getCurrentUser() { const url = `${this.auth.baseUrl}/rest/api/3/myself`; try { const response = await fetch(url, { method: 'GET', headers: this.baseHeaders, }); if (!response.ok) { // Clone the response before reading it to avoid consuming the stream const responseClone = response.clone(); let errorMessage = response.statusText; try { const errorData = (await response.json()); errorMessage = errorData.errorMessages?.join(', ') || response.statusText; } catch { // If JSON parsing fails, try to get the response as text from the cloned response try { const textResponse = await responseClone.text(); errorMessage = textResponse || response.statusText; } catch { errorMessage = response.statusText; } } if (response.status === 401 || response.status === 403) { throw new Error('Authentication failed. Please check your JIRA credentials and run `bookr init` to update them.'); } throw new Error(`Failed to get current user: ${errorMessage}`); } return (await response.json()); } catch (error) { throw new Error(`Error fetching current user: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get the base URL for constructing JIRA issue links */ getBaseUrl() { return this.auth.baseUrl; } /** * Test the connection and authentication */ async testConnection() { try { await this.getCurrentUser(); return true; } catch (_error) { return false; } } /** * Search for issues using JQL (JIRA Query Language) */ async searchIssues(jql, maxResults = 10) { const url = `${this.auth.baseUrl}/rest/api/3/search`; try { const response = await fetch(url, { method: 'POST', headers: this.baseHeaders, body: JSON.stringify({ jql, maxResults, fields: ['summary', 'status', 'project'], }), }); if (!response.ok) { const errorData = (await response.json()); throw new Error(`Failed to search issues: ${errorData.errorMessages?.join(', ') || response.statusText}`); } const data = (await response.json()); return data; } catch (error) { throw new Error(`Error searching issues: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get recent issues assigned to current user */ async getMyRecentIssues(maxResults = 5) { const jql = 'assignee = currentUser() ORDER BY updated DESC'; const result = await this.searchIssues(jql, maxResults); return result.issues; } /** * Get current active sprint from the first board */ async getCurrentSprint() { const boards = await this.getBoards(); if (!boards.length) { throw new Error('No boards found'); } // Use the first board (you might want to make this configurable) const boardId = boards[0]?.id; if (!boardId) { throw new Error('Invalid board configuration'); } const sprintUrl = `${this.auth.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active`; const sprintResponse = await fetch(sprintUrl, { method: 'GET', headers: this.baseHeaders, }); if (!sprintResponse.ok) { const errorData = (await sprintResponse.json()); throw new Error(`Failed to get current sprint: ${errorData.errorMessages?.join(', ') || sprintResponse.statusText}`); } const sprints = (await sprintResponse.json()); if (!sprints.values.length) { throw new Error('No active sprint found'); } const sprint = sprints.values[0]; if (!sprint) { throw new Error('No active sprint found'); } return sprint; } /** * Get all boards */ async getBoards() { const url = `${this.auth.baseUrl}/rest/agile/1.0/board`; const response = await fetch(url, { method: 'GET', headers: this.baseHeaders, }); if (!response.ok) { const errorData = (await response.json()); throw new Error(`Failed to get boards: ${errorData.errorMessages?.join(', ') || response.statusText}`); } const boards = (await response.json()); return boards.values; } /** * Get all sprints for a board (active, future, closed) */ async getSprintsForBoard(boardId) { const url = `${this.auth.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active,future,closed&maxResults=100&orderBy=startDate DESC`; const response = await fetch(url, { method: 'GET', headers: this.baseHeaders, }); if (!response.ok) { const errorData = (await response.json()); throw new Error(`Failed to get sprints: ${errorData.errorMessages?.join(', ') || response.statusText}`); } const sprints = (await response.json()); // If there are more sprints, fetch them let allSprints = [...sprints.values]; let startAt = sprints.startAt + sprints.maxResults; while (!sprints.isLast && allSprints.length < sprints.total) { const nextUrl = `${this.auth.baseUrl}/rest/agile/1.0/board/${boardId}/sprint?state=active,future,closed&maxResults=100&startAt=${startAt}&orderBy=startDate DESC`; const nextResponse = await fetch(nextUrl, { method: 'GET', headers: this.baseHeaders, }); if (!nextResponse.ok) { const errorData = (await nextResponse.json()); throw new Error(`Failed to get sprints: ${errorData.errorMessages?.join(', ') || nextResponse.statusText}`); } const nextSprints = (await nextResponse.json()); allSprints = [...allSprints, ...nextSprints.values]; startAt += nextSprints.maxResults; if (nextSprints.isLast) break; } return allSprints; } } /** * Create a JIRA client from environment variables */ export function createClient() { // Try config file first const fileConfig = loadConfigFromFile(); const baseUrl = fileConfig?.baseUrl || process.env['JIRA_BASE_URL']; const email = fileConfig?.email || process.env['JIRA_EMAIL']; const apiToken = fileConfig?.apiToken || process.env['JIRA_API_TOKEN']; if (!baseUrl || !email || !apiToken) { throw new Error('Missing JIRA configuration. Please run `bookr init` or set the following environment variables:\n' + '- JIRA_BASE_URL (e.g., https://your-domain.atlassian.net)\n' + '- JIRA_EMAIL (your JIRA email)\n' + '- JIRA_API_TOKEN (your JIRA API token)\n\n' + 'You can set these in ~/.config/bookr/config.json or as environment variables.'); } return new JiraClient({ baseUrl, email, apiToken }); } //# sourceMappingURL=jira-client.js.map