jira-ai 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +6 -0
- package/dist/commands/get-issue-statistics.js +29 -0
- package/dist/lib/formatters.js +38 -1
- package/dist/lib/jira-client.js +23 -1
- package/dist/lib/utils.js +61 -0
- package/package.json +1 -1
- package/settings.yaml +2 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { addCommentCommand } from './commands/add-comment.js';
|
|
|
14
14
|
import { addLabelCommand } from './commands/add-label.js';
|
|
15
15
|
import { deleteLabelCommand } from './commands/delete-label.js';
|
|
16
16
|
import { createTaskCommand } from './commands/create-task.js';
|
|
17
|
+
import { getIssueStatisticsCommand } from './commands/get-issue-statistics.js';
|
|
17
18
|
import { aboutCommand } from './commands/about.js';
|
|
18
19
|
import { authCommand } from './commands/auth.js';
|
|
19
20
|
import { isCommandAllowed, getAllowedCommands } from './lib/settings.js';
|
|
@@ -161,6 +162,11 @@ program
|
|
|
161
162
|
.requiredOption('--issue-type <type>', 'Issue type (e.g., Task, Epic, Subtask)')
|
|
162
163
|
.option('--parent <key>', 'Parent issue key (required for subtasks)')
|
|
163
164
|
.action(withPermission('create-task', createTaskCommand, { schema: CreateTaskSchema }));
|
|
165
|
+
// Get issue statistics command
|
|
166
|
+
program
|
|
167
|
+
.command('get-issue-statistics <task-ids>')
|
|
168
|
+
.description('Show time metrics and lifecycle of issues (comma-separated keys)')
|
|
169
|
+
.action(withPermission('get-issue-statistics', getIssueStatisticsCommand));
|
|
164
170
|
// About command (always allowed)
|
|
165
171
|
program
|
|
166
172
|
.command('about')
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getIssueStatistics } from '../lib/jira-client.js';
|
|
3
|
+
import { formatIssueStatistics } from '../lib/formatters.js';
|
|
4
|
+
import { ui } from '../lib/ui.js';
|
|
5
|
+
export async function getIssueStatisticsCommand(taskIds) {
|
|
6
|
+
const ids = taskIds.split(',').map(id => id.trim()).filter(id => id !== '');
|
|
7
|
+
if (ids.length === 0) {
|
|
8
|
+
console.error(chalk.red('Please provide at least one issue ID.'));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
ui.startSpinner(`Fetching statistics for ${ids.length} issue(s)...`);
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const id of ids) {
|
|
14
|
+
try {
|
|
15
|
+
const stats = await getIssueStatistics(id);
|
|
16
|
+
results.push(stats);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(chalk.red(`\nFailed to fetch statistics for ${id}: ${error.message}`));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (results.length > 0) {
|
|
23
|
+
ui.succeedSpinner(chalk.green('Statistics retrieved'));
|
|
24
|
+
console.log(formatIssueStatistics(results));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
ui.failSpinner('Failed to retrieve statistics');
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/lib/formatters.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import Table from 'cli-table3';
|
|
3
|
-
import { formatTimestamp, truncate } from './utils.js';
|
|
3
|
+
import { formatTimestamp, truncate, formatDuration } from './utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Create a styled table
|
|
6
6
|
*/
|
|
@@ -231,3 +231,40 @@ export function formatProjectIssueTypes(projectKey, issueTypes) {
|
|
|
231
231
|
}
|
|
232
232
|
return output;
|
|
233
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Format issue statistics
|
|
236
|
+
*/
|
|
237
|
+
export function formatIssueStatistics(statsList) {
|
|
238
|
+
if (statsList.length === 0) {
|
|
239
|
+
return chalk.yellow('No statistics to display.');
|
|
240
|
+
}
|
|
241
|
+
const table = createTable([
|
|
242
|
+
'Key',
|
|
243
|
+
'Summary',
|
|
244
|
+
'Time Logged',
|
|
245
|
+
'Estimate',
|
|
246
|
+
'Status Breakdown'
|
|
247
|
+
], [12, 30, 15, 15, 40]);
|
|
248
|
+
statsList.forEach((stats) => {
|
|
249
|
+
// Breakdown of time spent in each unique status
|
|
250
|
+
const breakdown = Object.entries(stats.statusDurations)
|
|
251
|
+
.map(([status, seconds]) => `${status}: ${formatDuration(seconds, 24)}`)
|
|
252
|
+
.join('\n');
|
|
253
|
+
const timeSpentStr = formatDuration(stats.timeSpentSeconds, 8);
|
|
254
|
+
const estimateStr = formatDuration(stats.originalEstimateSeconds, 8);
|
|
255
|
+
// Highlight if time spent exceeds estimate
|
|
256
|
+
const timeSpentFormatted = stats.timeSpentSeconds > stats.originalEstimateSeconds && stats.originalEstimateSeconds > 0
|
|
257
|
+
? chalk.red(timeSpentStr)
|
|
258
|
+
: chalk.green(timeSpentStr);
|
|
259
|
+
table.push([
|
|
260
|
+
chalk.cyan(stats.key),
|
|
261
|
+
truncate(stats.summary, 30),
|
|
262
|
+
timeSpentFormatted,
|
|
263
|
+
estimateStr,
|
|
264
|
+
breakdown
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
let output = '\n' + chalk.bold('Issue Statistics') + '\n\n';
|
|
268
|
+
output += table.toString() + '\n';
|
|
269
|
+
return output;
|
|
270
|
+
}
|
package/dist/lib/jira-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Version3Client } from 'jira.js';
|
|
2
|
-
import { convertADFToMarkdown } from './utils.js';
|
|
2
|
+
import { calculateStatusStatistics, convertADFToMarkdown } from './utils.js';
|
|
3
3
|
import { loadCredentials } from './auth-storage.js';
|
|
4
4
|
let jiraClient = null;
|
|
5
5
|
/**
|
|
@@ -324,3 +324,25 @@ export async function removeIssueLabels(taskId, labels) {
|
|
|
324
324
|
},
|
|
325
325
|
});
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Get issue statistics including status transitions and time tracking
|
|
329
|
+
*/
|
|
330
|
+
export async function getIssueStatistics(issueIdOrKey) {
|
|
331
|
+
const client = getJiraClient();
|
|
332
|
+
const issue = await client.issues.getIssue({
|
|
333
|
+
issueIdOrKey,
|
|
334
|
+
expand: 'changelog',
|
|
335
|
+
fields: ['summary', 'status', 'timetracking', 'created'],
|
|
336
|
+
});
|
|
337
|
+
const histories = issue.changelog?.histories || [];
|
|
338
|
+
const statusName = issue.fields.status?.name || 'Unknown';
|
|
339
|
+
const statusDurations = calculateStatusStatistics(issue.fields.created, histories, statusName);
|
|
340
|
+
return {
|
|
341
|
+
key: issue.key || '',
|
|
342
|
+
summary: issue.fields.summary || '',
|
|
343
|
+
timeSpentSeconds: issue.fields.timetracking?.timeSpentSeconds || 0,
|
|
344
|
+
originalEstimateSeconds: issue.fields.timetracking?.originalEstimateSeconds || 0,
|
|
345
|
+
statusDurations,
|
|
346
|
+
currentStatus: statusName,
|
|
347
|
+
};
|
|
348
|
+
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -67,3 +67,64 @@ export function convertADFToMarkdown(content) {
|
|
|
67
67
|
return JSON.stringify(content, null, 2);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Calculate time spent in each status
|
|
72
|
+
*/
|
|
73
|
+
export function calculateStatusStatistics(created, histories, currentStatus, now = Date.now()) {
|
|
74
|
+
const stats = {};
|
|
75
|
+
// Sort histories by date
|
|
76
|
+
const sortedHistories = [...histories].sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
|
|
77
|
+
let lastTransitionTime = new Date(created).getTime();
|
|
78
|
+
let lastStatus = '';
|
|
79
|
+
const statusHistories = sortedHistories.filter(h => h.items.some((item) => item.field === 'status'));
|
|
80
|
+
if (statusHistories.length > 0) {
|
|
81
|
+
const firstStatusItem = statusHistories[0].items.find((item) => item.field === 'status');
|
|
82
|
+
lastStatus = firstStatusItem.fromString;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lastStatus = currentStatus;
|
|
86
|
+
}
|
|
87
|
+
for (const history of statusHistories) {
|
|
88
|
+
const statusItem = history.items.find((item) => item.field === 'status');
|
|
89
|
+
if (!statusItem)
|
|
90
|
+
continue;
|
|
91
|
+
const transitionTime = new Date(history.created).getTime();
|
|
92
|
+
const durationSeconds = Math.max(0, Math.floor((transitionTime - lastTransitionTime) / 1000));
|
|
93
|
+
stats[lastStatus] = (stats[lastStatus] || 0) + durationSeconds;
|
|
94
|
+
lastStatus = statusItem.toString;
|
|
95
|
+
lastTransitionTime = transitionTime;
|
|
96
|
+
}
|
|
97
|
+
// Add time for the current status
|
|
98
|
+
const finalDurationSeconds = Math.max(0, Math.floor((now - lastTransitionTime) / 1000));
|
|
99
|
+
stats[lastStatus] = (stats[lastStatus] || 0) + finalDurationSeconds;
|
|
100
|
+
return stats;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Format duration in seconds to Jira human-readable format
|
|
104
|
+
* @param seconds - Duration in seconds
|
|
105
|
+
* @param hoursPerDay - Hours in a working day (default 24 for brutto time)
|
|
106
|
+
*/
|
|
107
|
+
export function formatDuration(seconds, hoursPerDay = 24) {
|
|
108
|
+
if (seconds <= 0)
|
|
109
|
+
return '0m';
|
|
110
|
+
const daysPerWeek = hoursPerDay === 24 ? 7 : 5;
|
|
111
|
+
const secondsInDay = hoursPerDay * 3600;
|
|
112
|
+
const secondsInWeek = daysPerWeek * secondsInDay;
|
|
113
|
+
const w = Math.floor(seconds / secondsInWeek);
|
|
114
|
+
seconds %= secondsInWeek;
|
|
115
|
+
const d = Math.floor(seconds / secondsInDay);
|
|
116
|
+
seconds %= secondsInDay;
|
|
117
|
+
const h = Math.floor(seconds / 3600);
|
|
118
|
+
seconds %= 3600;
|
|
119
|
+
const m = Math.floor(seconds / 60);
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (w > 0)
|
|
122
|
+
parts.push(`${w}w`);
|
|
123
|
+
if (d > 0)
|
|
124
|
+
parts.push(`${d}d`);
|
|
125
|
+
if (h > 0)
|
|
126
|
+
parts.push(`${h}h`);
|
|
127
|
+
if (m > 0)
|
|
128
|
+
parts.push(`${m}m`);
|
|
129
|
+
return parts.length > 0 ? parts.join(' ') : '0m';
|
|
130
|
+
}
|
package/package.json
CHANGED
package/settings.yaml
CHANGED
|
@@ -11,9 +11,10 @@ projects:
|
|
|
11
11
|
# - all
|
|
12
12
|
|
|
13
13
|
# Commands: List of allowed commands (use "all" to allow all commands)
|
|
14
|
-
# Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, create-task, about
|
|
14
|
+
# Available commands: me, projects, task-with-details, project-statuses, list-issue-types, run-jql, update-description, add-comment, create-task, get-issue-statistics, about
|
|
15
15
|
commands:
|
|
16
16
|
- me
|
|
17
|
+
- get-issue-statistics
|
|
17
18
|
# Uncomment below to allow all commands
|
|
18
19
|
# - all
|
|
19
20
|
# - task-with-details
|