bookr-cli
Version:
A terminal-based CLI tool to book time in Jira using the Tempo plugin by parsing your current Git branch name
163 lines • 9.77 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Box, Text } from 'ink';
import { useEffect, useState } from 'react';
import { createClient } from '../api/jira-client.js';
import { createTempoClient } from '../api/tempo-client.js';
import { getCurrentBranch, getTicketFromBranch, isGitRepository } from '../utils/git.js';
import { formatTimeForDisplay, isValidTimeFormat, parseTimeToSeconds, } from '../utils/time-parser.js';
import { storeTempoWorklog } from '../utils/worklog-storage.js';
import { roundToNearest15Minutes } from '../utils/date.js';
import { ConfirmationPrompt } from './ConfirmationPrompt.js';
export const App = ({ input: _input, flags }) => {
const [currentBranch, setCurrentBranch] = useState('Loading...');
const [jiraIssue, setJiraIssue] = useState(null);
const [appState, setAppState] = useState('loading');
const [error, setError] = useState(null);
const [timeSpent, setTimeSpent] = useState('');
useEffect(() => {
async function initialize() {
try {
// Check if we're in a Git repository (only required if no ticket provided)
if (!flags.ticket && !isGitRepository()) {
setError('Not in a Git repository and no ticket provided. Please either run from a Git repository or specify a ticket: bookr PROJ-123 2h30m');
setAppState('error');
return;
}
// Get current Git branch (only if we need it)
if (!flags.ticket) {
const branch = getCurrentBranch();
setCurrentBranch(branch);
}
else {
setCurrentBranch('N/A (ticket provided)');
}
// Check if time was provided
if (!flags.time) {
setError('No time specified. Please provide time (e.g., "2h30m", "1h15m", "45m")');
setAppState('error');
return;
}
// Validate time format
if (!isValidTimeFormat(flags.time)) {
setError(`Invalid time format: "${flags.time}". Use formats like "2h30m", "1h15m", "45m", "2.5h"`);
setAppState('invalid-time');
return;
}
setTimeSpent(flags.time);
// Use provided ticket or extract from branch name
let issueKey = null;
if (flags.ticket) {
// Use explicitly provided ticket
issueKey = flags.ticket;
}
else {
// Try to extract JIRA issue key from branch name
const branch = getCurrentBranch();
issueKey = getTicketFromBranch(branch);
}
if (!issueKey) {
setAppState('no-issue');
return;
}
try {
const client = createClient();
const issue = await client.getIssue(issueKey);
setJiraIssue(issue);
setAppState('confirm');
}
catch (jiraError) {
setError(`Could not fetch JIRA issue ${issueKey}: ${jiraError instanceof Error ? jiraError.message : 'Unknown error'}`);
setAppState('error');
}
}
catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
setAppState('error');
}
}
initialize();
}, [flags.time, flags.ticket]);
const handleConfirm = async () => {
if (!jiraIssue || !timeSpent)
return;
setAppState('creating');
try {
const jiraClient = createClient();
const tempoClient = createTempoClient();
const timeSpentSeconds = parseTimeToSeconds(timeSpent);
const comment = flags.description || 'Work logged via Bookr CLI';
const user = await jiraClient.getCurrentUser();
// Calculate start time: current time minus time spent, rounded to nearest 15 minutes
const now = new Date();
const startTime = new Date(now.getTime() - (timeSpentSeconds * 1000));
const roundedStartTime = roundToNearest15Minutes(startTime);
// Format startDate as yyyy-MM-dd in UTC
const yyyyMMdd = roundedStartTime.toISOString().slice(0, 10);
// Format startTime as HH:mm:ss
const startTimeFormatted = roundedStartTime.toTimeString().slice(0, 8);
const createdWorklog = await tempoClient.addWorklog({
issueId: jiraIssue.id,
issueKey: jiraIssue.key,
timeSpentSeconds,
comment,
startDate: yyyyMMdd,
startTime: startTimeFormatted,
authorAccountId: user.accountId,
});
// Store the worklog locally for potential undo
storeTempoWorklog(jiraIssue, createdWorklog, comment);
setAppState('success');
}
catch (error) {
setError(`Failed to create worklog: ${error instanceof Error ? error.message : 'Unknown error'}`);
setAppState('error');
}
};
const handleCancel = () => {
setAppState('cancelled');
};
// Loading state
if (appState === 'loading') {
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { children: "\u23F3 Loading..." }) }));
}
// Error state
if (appState === 'error') {
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsxs(Text, { color: "red", children: ["\u274C Error: ", error] }) }));
}
// Invalid time format
if (appState === 'invalid-time') {
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["\u274C ", error] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Valid time formats:" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: " \u2022 2h30m (2 hours 30 minutes)" }), _jsx(Text, { children: " \u2022 1h15m (1 hour 15 minutes)" }), _jsx(Text, { children: " \u2022 45m (45 minutes)" }), _jsx(Text, { children: " \u2022 2.5h (2.5 hours)" }), _jsx(Text, { children: " \u2022 90m (90 minutes)" })] })] }));
}
// No JIRA issue found
if (appState === 'no-issue') {
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [flags.ticket ? (_jsx(Text, { color: "yellow", children: "\u26A0\uFE0F No JIRA issue key provided" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: ["\u26A0\uFE0F No JIRA issue key found in branch \"", currentBranch, "\""] }), _jsx(Text, { color: "gray", children: "Expected format: feature/PROJ-123, bugfix/PROJ-456, etc." })] })), _jsx(Text, { color: "gray", children: "Usage: bookr [TICKET] [TIME] -m \"description\"" }), _jsx(Text, { color: "gray", children: "Examples: bookr PROJ-123 2h30m -m \"Fixed bug\"" })] }) }));
}
// Confirmation state
if (appState === 'confirm' && jiraIssue) {
return (_jsx(ConfirmationPrompt, { issue: jiraIssue, timeSpent: timeSpent, description: flags.description || undefined, onConfirm: handleConfirm, onCancel: handleCancel }));
}
// Creating worklog state
if (appState === 'creating') {
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { children: "\u23F3 Creating worklog..." }) }));
}
// Success state
if (appState === 'success' && jiraIssue) {
// Construct JIRA issue URL
const client = createClient();
const issueUrl = `${client.getBaseUrl()}/browse/${jiraIssue.key}`;
// Calculate and format the start time for display (rounded to nearest 15 minutes)
const timeSpentSeconds = parseTimeToSeconds(timeSpent);
const now = new Date();
const startTime = new Date(now.getTime() - (timeSpentSeconds * 1000));
const roundedStartTime = roundToNearest15Minutes(startTime);
const startTimeFormatted = roundedStartTime.toLocaleTimeString();
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u2705 Worklog created!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "Issue:" }), " ", jiraIssue.key, " - ", jiraIssue.fields.summary] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "Time:" }), " ", formatTimeForDisplay(timeSpent), " (", timeSpent, ")"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "Start Time:" }), " ", startTimeFormatted] }), flags.description && (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "Description:" }), " ", flags.description] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "blue", underline: true, children: ["\uD83D\uDD17 ", _jsx(Text, { color: "cyan", children: issueUrl })] }) })] })] }));
}
// Cancelled state
if (appState === 'cancelled') {
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { color: "yellow", children: "\u274C Worklog creation cancelled" }) }));
}
// Fallback
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { children: "Unknown state" }) }));
};
//# sourceMappingURL=App.js.map