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

348 lines โ€ข 16.5 kB
import { createClient } from '../api/jira-client.js'; import { createTempoClient } from '../api/tempo-client.js'; import { secondsToJiraFormat } from '../utils/time-parser.js'; import { removeStoredWorklog, getLastWorklog, getStoredWorklogById, } from '../utils/worklog-storage.js'; import { parallelMapLimit } from '../utils/concurrency.js'; import inquirer from 'inquirer'; import chalk from 'chalk'; /** * Show interactive selection of recent worklogs to undo (Tempo-first) */ export async function showUndoSelection() { try { const client = createClient(); const tempoClient = createTempoClient(); const user = await client.getCurrentUser(); const todayISO = new Date().toISOString().slice(0, 10); const tempoWorklogsResponse = await tempoClient.getWorklogsForUser(user.accountId, todayISO, todayISO); const tempoWorklogs = (tempoWorklogsResponse && typeof tempoWorklogsResponse === 'object' && Array.isArray(tempoWorklogsResponse.results)) ? tempoWorklogsResponse.results : Array.isArray(tempoWorklogsResponse) ? tempoWorklogsResponse : []; // Optionally, you could also fetch Jira worklogs if you want to support undo for both sources // For now, we focus on Tempo worklogs if (!tempoWorklogs.length) { console.log('๐Ÿ“ญ No worklogs found for today.'); console.log('๐Ÿ’ก You can undo any worklog by its ID: bookr undo <WORKLOG_ID>'); return; } // Enrich with Jira issue info const uniqueIssueIds = Array.from(new Set(tempoWorklogs.map((w) => w.issue?.id).filter(Boolean))); const issueMap = new Map(); await parallelMapLimit(uniqueIssueIds, 5, async (id) => { try { const issue = await client.getIssue(String(id)); issueMap.set(id, { key: issue.key, summary: issue.fields.summary }); } catch { // If not found, skip } }); // Prepare worklog choices for interactive selection const worklogChoices = tempoWorklogs.map((worklog) => { const timeSpentSeconds = typeof worklog.timeSpentSeconds === 'number' ? worklog.timeSpentSeconds : 0; const timeDisplay = secondsToJiraFormat(timeSpentSeconds); const issueId = worklog.issue?.id || ''; const issueInfo = issueMap.get(issueId) || {}; const issueKey = issueInfo.key || ''; const summary = (issueInfo.summary || worklog.description || '').slice(0, 50); const worklogId = String(worklog.tempoWorklogId || worklog.id || ''); const source = worklog.tempoWorklogId ? 'tempo' : 'jira'; return { name: `${worklogId.padEnd(17)} | ${issueKey.padEnd(12)} | ${timeDisplay.padEnd(8)} | ${summary.padEnd(50)} | ${source}`, value: worklogId, short: worklogId, }; }); // Add a cancel option worklogChoices.push({ name: 'โŒ Cancel', value: 'cancel', short: 'Cancel', }); // Show interactive selection const { selectedWorklog } = await inquirer.prompt([ { type: 'list', name: 'selectedWorklog', message: '๐Ÿ—‘๏ธ Select a worklog to delete:', choices: worklogChoices, pageSize: 10, }, ]); if (selectedWorklog === 'cancel') { console.log('โŒ Cancelled.'); return; } // Proceed with deletion await undoWorklogById(selectedWorklog); } catch (error) { console.error('โŒ Error:', error instanceof Error ? error.message : error); } } /** * Find worklog details by searching Tempo worklogs */ async function findWorklogDetails(client, worklogId) { try { const tempoClient = createTempoClient(); const user = await client.getCurrentUser(); // Search today's Tempo worklogs const todayISO = new Date().toISOString().slice(0, 10); const tempoWorklogsResponse = await tempoClient.getWorklogsForUser(user.accountId, todayISO, todayISO); const tempoWorklogs = (tempoWorklogsResponse && typeof tempoWorklogsResponse === 'object' && Array.isArray(tempoWorklogsResponse.results)) ? tempoWorklogsResponse.results : Array.isArray(tempoWorklogsResponse) ? tempoWorklogsResponse : []; for (const tempoWorklog of tempoWorklogs) { const tempoWorklogId = String(tempoWorklog.tempoWorklogId || tempoWorklog.id || ''); if (tempoWorklogId === worklogId) { // Get issue details const issueId = tempoWorklog.issue?.id; if (issueId) { try { const issue = await client.getIssue(String(issueId)); return { issueKey: issue.key, worklog: { id: worklogId, timeSpentSeconds: tempoWorklog.timeSpentSeconds || 0, timeSpent: tempoWorklog.timeSpent || '', comment: tempoWorklog.description || '', started: tempoWorklog.started || new Date().toISOString(), }, issue: { key: issue.key, fields: { summary: issue.fields.summary }, }, isTempoWorklog: true, }; } catch { // If we can't get issue details, still return the worklog info return { issueKey: tempoWorklog.issue?.key || 'UNKNOWN', worklog: { id: worklogId, timeSpentSeconds: tempoWorklog.timeSpentSeconds || 0, timeSpent: tempoWorklog.timeSpent || '', comment: tempoWorklog.description || '', started: tempoWorklog.started || new Date().toISOString(), }, issue: { key: tempoWorklog.issue?.key || 'UNKNOWN', fields: { summary: 'Unknown issue' }, }, isTempoWorklog: true, }; } } } } // If not found in today's Tempo worklogs, search recent Tempo worklogs (last 7 days) const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - 7); const fromISO = startDate.toISOString().slice(0, 10); const toISO = endDate.toISOString().slice(0, 10); const recentTempoWorklogsResponse = await tempoClient.getWorklogsForUser(user.accountId, fromISO, toISO); const recentTempoWorklogs = (recentTempoWorklogsResponse && typeof recentTempoWorklogsResponse === 'object' && Array.isArray(recentTempoWorklogsResponse.results)) ? recentTempoWorklogsResponse.results : Array.isArray(recentTempoWorklogsResponse) ? recentTempoWorklogsResponse : []; for (const tempoWorklog of recentTempoWorklogs) { const tempoWorklogId = String(tempoWorklog.tempoWorklogId || tempoWorklog.id || ''); if (tempoWorklogId === worklogId) { // Get issue details const issueId = tempoWorklog.issue?.id; if (issueId) { try { const issue = await client.getIssue(String(issueId)); return { issueKey: issue.key, worklog: { id: worklogId, timeSpentSeconds: tempoWorklog.timeSpentSeconds || 0, timeSpent: tempoWorklog.timeSpent || '', comment: tempoWorklog.description || '', started: tempoWorklog.started || new Date().toISOString(), }, issue: { key: issue.key, fields: { summary: issue.fields.summary }, }, isTempoWorklog: true, }; } catch { // If we can't get issue details, still return the worklog info return { issueKey: tempoWorklog.issue?.key || 'UNKNOWN', worklog: { id: worklogId, timeSpentSeconds: tempoWorklog.timeSpentSeconds || 0, timeSpent: tempoWorklog.timeSpent || '', comment: tempoWorklog.description || '', started: tempoWorklog.started || new Date().toISOString(), }, issue: { key: tempoWorklog.issue?.key || 'UNKNOWN', fields: { summary: 'Unknown issue' }, }, isTempoWorklog: true, }; } } } } return null; } catch { return null; } } /** * Undo a specific worklog by ID */ export async function undoWorklogById(worklogId) { try { const client = createClient(); // Test connection const isConnected = await client.testConnection(); if (!isConnected) { console.log('โŒ Failed to connect to JIRA'); return; } console.log(`๐Ÿ”„ Looking for worklog ${worklogId}...`); // Try to find worklog details const worklogDetails = await findWorklogDetails(client, worklogId); if (worklogDetails) { // Show styled, multi-line summary for confirmation const { issue, worklog } = worklogDetails; const time = secondsToJiraFormat(worklog.timeSpentSeconds || 0); const comment = typeof worklog.comment === 'string' ? worklog.comment : ''; const date = new Date(worklog.started || '').toLocaleString(); let project; let status; if (issue.fields && typeof issue.fields === 'object') { const f = issue.fields; const proj = f['project']; if (proj && typeof proj === 'object' && 'name' in proj && typeof proj['name'] === 'string') { project = proj.name; } const stat = f['status']; if (stat && typeof stat === 'object' && 'name' in stat && typeof stat['name'] === 'string') { status = stat.name; } } console.log(); console.log(chalk.bold.cyan('๐Ÿ—‘๏ธ Confirm Worklog Deletion')); console.log(); console.log(`${chalk.bold('Issue:')} ${chalk.yellow(issue.key)}${issue.fields.summary ? ` โ€“ ${issue.fields.summary}` : ''}`); if (project) console.log(`${chalk.bold('Project:')} ${project}`); if (status) console.log(`${chalk.bold('Status:')} ${status}`); console.log(`${chalk.bold('Time:')} ${time}`); if (comment) console.log(`${chalk.bold('Comment:')} ${comment}`); console.log(`${chalk.bold('Date:')} ${date}`); console.log(); // List-style confirmation prompt const { confirmDelete } = await inquirer.prompt([ { type: 'list', name: 'confirmDelete', message: chalk.red('Are you sure you want to delete this worklog?'), choices: [ { name: chalk.green('โœ… Confirm and delete worklog'), value: true, short: 'Confirm', }, { name: chalk.red('โŒ Cancel'), value: false, short: 'Cancel', }, ], }, ]); if (!confirmDelete) { console.log('โŒ Deletion cancelled.'); return; } console.log('โณ Deleting worklog from Tempo...'); try { // Delete from Tempo const tempoClient = createTempoClient(); await tempoClient.deleteWorklog(worklogId); // Remove from local storage if it exists there removeStoredWorklog(worklogId); console.log('โœ… Worklog successfully deleted!'); console.log(`๐Ÿ—‘๏ธ Removed ${secondsToJiraFormat(worklogDetails.worklog.timeSpentSeconds || 0)} from ${worklogDetails.issueKey}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : error; console.error('โŒ Failed to delete worklog from Tempo:', errorMessage); // Check if the error indicates the worklog was already deleted if (typeof errorMessage === 'string' && (errorMessage.includes('404') || errorMessage.includes('Not Found') || errorMessage.includes('already deleted'))) { console.log('๐Ÿ’ก The worklog appears to have already been deleted.'); // Clean up the local cache anyway removeStoredWorklog(worklogId); console.log('๐Ÿงน Cleaned up local cache.'); } else { console.log('๐Ÿ’ก The worklog may not exist or you may not have permission to delete it.'); } return; } } else { // Worklog not found in Tempo searches console.log(`โš ๏ธ Could not find Tempo worklog ${worklogId} in recent worklogs.`); console.log('๐Ÿ’ก The worklog might be:'); console.log(' - Older than 7 days'); console.log(' - From a different user'); console.log(' - Already deleted'); console.log(' - In a project you no longer have access to'); // Check if this worklog exists in our local cache const storedWorklog = getStoredWorklogById(worklogId); if (storedWorklog) { console.log('๐Ÿ’ก Found worklog in local cache - it may have been deleted from Tempo.'); console.log('๐Ÿงน Cleaning up local cache...'); removeStoredWorklog(worklogId); console.log('โœ… Local cache cleaned up.'); } return; } } catch (error) { console.error('โŒ Error:', error instanceof Error ? error.message : error); } } /** * Main undo function - handles both cases */ export async function handleUndo(worklogId) { if (worklogId) { await undoWorklogById(worklogId); } else { // Try to undo the last worklog if no ID provided const lastWorklog = getLastWorklog(); if (lastWorklog) { console.log(`๐Ÿ”„ No worklog ID provided. Attempting to undo the last worklog: ${lastWorklog.id}`); await undoWorklogById(lastWorklog.id); } else { console.log('๐Ÿ“ญ No recent worklog found to undo.'); console.log('๐Ÿ’ก You can undo any worklog by its ID: bookr undo <WORKLOG_ID>'); console.log('๐Ÿ’ก Or run "bookr undo" to see recent worklogs'); await showUndoSelection(); } } } // Default export for CLI usage export default handleUndo; //# sourceMappingURL=undo.js.map