bookr-cli
Version:
A terminal-based CLI tool to book time in Jira using the Tempo plugin by parsing your current Git branch name
152 lines • 6.23 kB
JavaScript
import inquirer from 'inquirer';
import { createClient } from '../api/jira-client.js';
import { createTempoClient } from '../api/tempo-client.js';
import { formatDateRange } from '../utils/date.js';
import { secondsToJiraFormat } from '../utils/time-parser.js';
import { parallelMapLimit } from '../utils/concurrency.js';
function parseArgs() {
return {
history: process.argv.includes('--history'),
};
}
async function selectSprint(client) {
const boards = await client.getBoards();
if (!boards.length) {
console.log('❌ No boards found.');
return null;
}
let boardId;
if (boards.length === 1) {
const board = boards[0];
if (!board) {
console.log('❌ No boards found.');
return null;
}
boardId = board.id;
console.log(`📋 Using board: ${board.name}`);
}
else {
const boardChoices = boards.map((board) => ({
name: board.name,
value: board.id,
short: board.name,
}));
const { selectedBoardId } = await inquirer.prompt([
{
type: 'list',
name: 'selectedBoardId',
message: 'Select a board:',
choices: boardChoices,
pageSize: 10,
},
]);
boardId = selectedBoardId;
}
const sprints = await client.getSprintsForBoard(boardId);
if (!sprints.length) {
console.log('❌ No sprints found for the selected board.');
return null;
}
console.log(`📊 Found ${sprints.length} sprints for this board.`);
const choices = sprints.map((sprint) => ({
name: `${sprint.name} [${sprint.state}]${sprint.startDate && sprint.endDate ? ` (${formatDateRange(new Date(sprint.startDate), new Date(sprint.endDate))})` : ''}`,
value: sprint.id,
short: sprint.name,
}));
const { sprintId } = await inquirer.prompt([
{
type: 'list',
name: 'sprintId',
message: 'Select a sprint:',
choices,
pageSize: 15,
},
]);
return sprints.find((s) => s.id === sprintId) || null;
}
export async function showSprintWorklogs() {
try {
const client = createClient();
const tempoClient = createTempoClient();
const { history } = parseArgs();
let sprint;
if (history) {
sprint = await selectSprint(client);
if (!sprint)
return;
if (!sprint.startDate || !sprint.endDate) {
console.log('❌ Selected sprint does not have valid start or end dates.');
return;
}
}
else {
sprint = await client.getCurrentSprint();
if (!sprint.startDate || !sprint.endDate) {
console.log('❌ Current sprint does not have valid start or end dates.');
return;
}
}
const startDate = new Date(sprint.startDate);
const endDate = new Date(sprint.endDate);
const fromISO = startDate.toISOString().slice(0, 10);
const toISO = endDate.toISOString().slice(0, 10);
const user = await client.getCurrentUser();
console.log(`${sprint.name}`);
console.log(`${fromISO} to ${toISO}`);
const tempoWorklogsResponse = await tempoClient.getWorklogsForUser(user.accountId, fromISO, toISO);
const tempoWorklogs = (tempoWorklogsResponse && typeof tempoWorklogsResponse === 'object' && Array.isArray(tempoWorklogsResponse.results))
? tempoWorklogsResponse.results
: Array.isArray(tempoWorklogsResponse) ? tempoWorklogsResponse : [];
if (!tempoWorklogs.length) {
console.log('📭 No worklogs found for this sprint period.');
return;
}
// Collect unique issue IDs
const uniqueIssueIds = Array.from(new Set(tempoWorklogs.map((w) => w.issue?.id).filter(Boolean)));
// Fetch all issues in parallel (limit concurrency to 5)
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
}
});
// Table header
console.log('─'.repeat(120));
console.log(`${'ID'.padEnd(12)} | ${'Issue'.padEnd(16)} | ${'Time'.padEnd(6)} | Summary`);
console.log('─'.repeat(120));
let totalSeconds = 0;
const uniqueIssues = new Set();
for (const worklog of tempoWorklogs) {
const timeSpentSeconds = worklog.timeSpentSeconds || 0;
totalSeconds += timeSpentSeconds;
const timeDisplay = secondsToJiraFormat(timeSpentSeconds);
const issueId = worklog.issue?.id || '';
const issueInfo = issueMap.get(issueId) || {};
const issueKey = issueInfo.key || '';
uniqueIssues.add(String(issueKey));
const summary = issueInfo.summary || worklog.description || '';
const worklogId = worklog.tempoWorklogId ? String(worklog.tempoWorklogId) : String(worklog.id || '');
console.log(`${worklogId.padEnd(12)} | ${issueKey.padEnd(16)} | ${timeDisplay.padEnd(6)} | ${summary}`);
}
console.log('─'.repeat(120));
const totalTime = secondsToJiraFormat(totalSeconds);
const totalHours = (totalSeconds / 3600).toFixed(2);
console.log(`📊 Sprint Total: ${totalTime} (${totalHours} hours)`);
console.log(`📝 Worklog entries: ${tempoWorklogs.length}`);
console.log(`🎯 Issues worked on: ${uniqueIssues.size}`);
}
catch (error) {
console.error('❌ Error:', error instanceof Error ? error.message : error);
}
}
// Default export for CLI usage
export default showSprintWorklogs;
// For backward compatibility when running directly
if (import.meta.url === `file://${process.argv[1]}`) {
showSprintWorklogs().catch(console.error);
}
//# sourceMappingURL=sprint.js.map