jira-pilot 1.0.3 → 1.1.0
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/README.md +16 -0
- package/bin/jira.js +9 -2
- package/package.json +1 -1
- package/src/commands/ai.js +158 -14
- package/src/commands/config.js +1 -1
- package/src/commands/issue.js +544 -1
- package/src/commands/project.js +5 -1
- package/src/commands/sprint.js +5 -1
- package/src/server/mcp-server.js +237 -17
- package/src/services/ai-service.js +65 -14
- package/src/utils/text-to-adf.js +34 -0
package/src/commands/issue.js
CHANGED
|
@@ -3,11 +3,20 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { table } from 'table';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import enquirer from 'enquirer';
|
|
6
7
|
import { parseADF } from '../utils/adf-parser.js';
|
|
8
|
+
import { textToADF } from '../utils/text-to-adf.js';
|
|
7
9
|
|
|
8
10
|
export function registerIssueCommand(program) {
|
|
9
11
|
const issueCmd = new Command('issue')
|
|
10
|
-
.description('Manage Jira issues')
|
|
12
|
+
.description('Manage Jira issues')
|
|
13
|
+
.addHelpText('after', `
|
|
14
|
+
Common Actions:
|
|
15
|
+
$ jira issue list # List assigned issues
|
|
16
|
+
$ jira issue view <KEY> # View issue details
|
|
17
|
+
$ jira issue create # Create new issue (interactive)
|
|
18
|
+
$ jira issue transition <KEY> # Move issue status
|
|
19
|
+
`);
|
|
11
20
|
|
|
12
21
|
issueCmd
|
|
13
22
|
.command('list')
|
|
@@ -18,6 +27,13 @@ export function registerIssueCommand(program) {
|
|
|
18
27
|
.option('-a, --assignee <id>', 'Filter by assignee (use "currentUser" for self)')
|
|
19
28
|
.option('-s, --status <status>', 'Filter by status')
|
|
20
29
|
.option('-e, --export <format>', 'Export output (json, md)')
|
|
30
|
+
.addHelpText('after', `
|
|
31
|
+
Examples:
|
|
32
|
+
$ jira issue list --project PROJ --status "In Progress"
|
|
33
|
+
$ jira issue list --assignee currentUser --limit 10
|
|
34
|
+
$ jira issue list --jql "created >= -7d"
|
|
35
|
+
$ jira issue list --export json
|
|
36
|
+
`)
|
|
21
37
|
.action(async (options) => {
|
|
22
38
|
const spinner = ora('Fetching issues...').start();
|
|
23
39
|
try {
|
|
@@ -115,6 +131,10 @@ export function registerIssueCommand(program) {
|
|
|
115
131
|
.command('view')
|
|
116
132
|
.description('View issue details')
|
|
117
133
|
.argument('<issueKey>', 'Issue Key')
|
|
134
|
+
.addHelpText('after', `
|
|
135
|
+
Examples:
|
|
136
|
+
$ jira issue view PROJ-123
|
|
137
|
+
`)
|
|
118
138
|
.action(async (issueKey) => {
|
|
119
139
|
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
120
140
|
try {
|
|
@@ -151,5 +171,528 @@ export function registerIssueCommand(program) {
|
|
|
151
171
|
}
|
|
152
172
|
});
|
|
153
173
|
|
|
174
|
+
// ── CREATE ────────────────────────────────────────────────────────
|
|
175
|
+
issueCmd
|
|
176
|
+
.command('create')
|
|
177
|
+
.description('Create a new Jira issue')
|
|
178
|
+
.option('-p, --project <key>', 'Project key')
|
|
179
|
+
.option('-t, --type <type>', 'Issue type (e.g., Bug, Story, Task)')
|
|
180
|
+
.option('-s, --summary <text>', 'Issue summary')
|
|
181
|
+
.option('-d, --description <text>', 'Issue description')
|
|
182
|
+
.option('--priority <name>', 'Priority name (e.g., High, Medium, Low)')
|
|
183
|
+
.option('-a, --assignee <id>', 'Assignee account ID (use "me" for self)')
|
|
184
|
+
.addHelpText('after', `
|
|
185
|
+
Examples:
|
|
186
|
+
$ jira issue create # Interactive wizard
|
|
187
|
+
$ jira issue create -p PROJ -s "Fix login bug" # Quick create
|
|
188
|
+
$ jira issue create -p PROJ -t Bug -s "Crash on save" --priority High
|
|
189
|
+
$ jira issue create -p PROJ -s "New feature" -a me
|
|
190
|
+
`)
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
try {
|
|
193
|
+
// ── Step 1: Select Project ──────────────────────────
|
|
194
|
+
let projectKey = options.project;
|
|
195
|
+
if (!projectKey) {
|
|
196
|
+
const spinner = ora('Fetching projects...').start();
|
|
197
|
+
const projectData = await api.get('/project/search');
|
|
198
|
+
spinner.stop();
|
|
199
|
+
|
|
200
|
+
if (!projectData.values || projectData.values.length === 0) {
|
|
201
|
+
console.error(chalk.red('No projects found. Check your permissions.'));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const projectChoices = projectData.values.map(p => ({
|
|
206
|
+
name: p.key,
|
|
207
|
+
message: `${p.key} — ${p.name}`
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
const { selectedProject } = await enquirer.prompt({
|
|
211
|
+
type: 'select',
|
|
212
|
+
name: 'selectedProject',
|
|
213
|
+
message: 'Select Project:',
|
|
214
|
+
choices: projectChoices
|
|
215
|
+
});
|
|
216
|
+
projectKey = selectedProject;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Step 2: Select Issue Type ───────────────────────
|
|
220
|
+
let issueTypeName = options.type;
|
|
221
|
+
if (!issueTypeName) {
|
|
222
|
+
const spinner = ora('Fetching issue types...').start();
|
|
223
|
+
let issueTypes = [];
|
|
224
|
+
try {
|
|
225
|
+
// Jira Cloud v3 - createmeta endpoint
|
|
226
|
+
const metaData = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
|
|
227
|
+
issueTypes = metaData.issueTypes || metaData.values || [];
|
|
228
|
+
} catch (metaErr) {
|
|
229
|
+
// Fallback: use project-level issue types
|
|
230
|
+
try {
|
|
231
|
+
const projectInfo = await api.get(`/project/${projectKey}`);
|
|
232
|
+
issueTypes = projectInfo.issueTypes || [];
|
|
233
|
+
} catch {
|
|
234
|
+
issueTypes = [
|
|
235
|
+
{ name: 'Task' }, { name: 'Bug' },
|
|
236
|
+
{ name: 'Story' }, { name: 'Epic' }
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
spinner.stop();
|
|
241
|
+
|
|
242
|
+
if (issueTypes.length === 0) {
|
|
243
|
+
issueTypes = [
|
|
244
|
+
{ name: 'Task' }, { name: 'Bug' },
|
|
245
|
+
{ name: 'Story' }, { name: 'Epic' }
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Filter out sub-tasks if present
|
|
250
|
+
const filteredTypes = issueTypes.filter(t => !t.subtask);
|
|
251
|
+
const typeChoices = (filteredTypes.length > 0 ? filteredTypes : issueTypes)
|
|
252
|
+
.map(t => ({ name: t.name, message: t.name }));
|
|
253
|
+
|
|
254
|
+
const { selectedType } = await enquirer.prompt({
|
|
255
|
+
type: 'select',
|
|
256
|
+
name: 'selectedType',
|
|
257
|
+
message: 'Select Issue Type:',
|
|
258
|
+
choices: typeChoices
|
|
259
|
+
});
|
|
260
|
+
issueTypeName = selectedType;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Step 3: Summary (required) ──────────────────────
|
|
264
|
+
let summary = options.summary;
|
|
265
|
+
if (!summary) {
|
|
266
|
+
const { inputSummary } = await enquirer.prompt({
|
|
267
|
+
type: 'input',
|
|
268
|
+
name: 'inputSummary',
|
|
269
|
+
message: 'Summary (required):',
|
|
270
|
+
validate: (val) => val.trim().length > 0 || 'Summary cannot be empty'
|
|
271
|
+
});
|
|
272
|
+
summary = inputSummary;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Step 4: Description (optional) ──────────────────
|
|
276
|
+
let description = options.description;
|
|
277
|
+
if (description === undefined) {
|
|
278
|
+
const { inputDescription } = await enquirer.prompt({
|
|
279
|
+
type: 'input',
|
|
280
|
+
name: 'inputDescription',
|
|
281
|
+
message: 'Description (optional, press Enter to skip):'
|
|
282
|
+
});
|
|
283
|
+
description = inputDescription || null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Step 5: Priority ────────────────────────────────
|
|
287
|
+
let priorityName = options.priority;
|
|
288
|
+
if (!priorityName) {
|
|
289
|
+
const spinner = ora('Fetching priorities...').start();
|
|
290
|
+
try {
|
|
291
|
+
const priorities = await api.get('/priority');
|
|
292
|
+
spinner.stop();
|
|
293
|
+
|
|
294
|
+
if (Array.isArray(priorities) && priorities.length > 0) {
|
|
295
|
+
const priorityChoices = priorities.map(p => ({
|
|
296
|
+
name: p.name,
|
|
297
|
+
message: p.name
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
const { selectedPriority } = await enquirer.prompt({
|
|
301
|
+
type: 'select',
|
|
302
|
+
name: 'selectedPriority',
|
|
303
|
+
message: 'Select Priority:',
|
|
304
|
+
choices: priorityChoices
|
|
305
|
+
});
|
|
306
|
+
priorityName = selectedPriority;
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
spinner.stop();
|
|
310
|
+
// Priority endpoint may not be available; skip
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Step 6: Assignee ────────────────────────────────
|
|
315
|
+
let assigneeId = options.assignee;
|
|
316
|
+
if (!assigneeId) {
|
|
317
|
+
const { assigneeChoice } = await enquirer.prompt({
|
|
318
|
+
type: 'select',
|
|
319
|
+
name: 'assigneeChoice',
|
|
320
|
+
message: 'Assign to:',
|
|
321
|
+
choices: [
|
|
322
|
+
{ name: 'me', message: 'Myself' },
|
|
323
|
+
{ name: 'unassigned', message: 'Leave Unassigned' },
|
|
324
|
+
{ name: 'search', message: 'Search for a user...' }
|
|
325
|
+
]
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (assigneeChoice === 'me') {
|
|
329
|
+
const spinner = ora('Fetching your account...').start();
|
|
330
|
+
try {
|
|
331
|
+
const myself = await api.get('/myself');
|
|
332
|
+
assigneeId = myself.accountId;
|
|
333
|
+
spinner.stop();
|
|
334
|
+
} catch {
|
|
335
|
+
spinner.fail('Could not fetch your account. Leaving unassigned.');
|
|
336
|
+
assigneeId = null;
|
|
337
|
+
}
|
|
338
|
+
} else if (assigneeChoice === 'search') {
|
|
339
|
+
const { searchQuery } = await enquirer.prompt({
|
|
340
|
+
type: 'input',
|
|
341
|
+
name: 'searchQuery',
|
|
342
|
+
message: 'Search user by name or email:'
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (searchQuery.trim()) {
|
|
346
|
+
const spinner = ora('Searching users...').start();
|
|
347
|
+
try {
|
|
348
|
+
const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
|
|
349
|
+
spinner.stop();
|
|
350
|
+
|
|
351
|
+
if (Array.isArray(users) && users.length > 0) {
|
|
352
|
+
const userChoices = users.map(u => ({
|
|
353
|
+
name: u.accountId,
|
|
354
|
+
message: `${u.displayName} (${u.emailAddress || u.accountId})`
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
const { selectedUser } = await enquirer.prompt({
|
|
358
|
+
type: 'select',
|
|
359
|
+
name: 'selectedUser',
|
|
360
|
+
message: 'Select User:',
|
|
361
|
+
choices: userChoices
|
|
362
|
+
});
|
|
363
|
+
assigneeId = selectedUser;
|
|
364
|
+
} else {
|
|
365
|
+
console.log(chalk.yellow('No users found. Leaving unassigned.'));
|
|
366
|
+
assigneeId = null;
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
spinner.fail('User search failed. Leaving unassigned.');
|
|
370
|
+
assigneeId = null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
assigneeId = null;
|
|
375
|
+
}
|
|
376
|
+
} else if (assigneeId === 'me') {
|
|
377
|
+
// --assignee me flag: resolve to account ID
|
|
378
|
+
const spinner = ora('Fetching your account...').start();
|
|
379
|
+
try {
|
|
380
|
+
const myself = await api.get('/myself');
|
|
381
|
+
assigneeId = myself.accountId;
|
|
382
|
+
spinner.stop();
|
|
383
|
+
} catch {
|
|
384
|
+
spinner.fail('Could not fetch your account. Leaving unassigned.');
|
|
385
|
+
assigneeId = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── Confirmation ────────────────────────────────────
|
|
390
|
+
console.log(chalk.blue('\n── Issue Summary ──────────────────'));
|
|
391
|
+
console.log(` Project: ${chalk.cyan(projectKey)}`);
|
|
392
|
+
console.log(` Type: ${issueTypeName}`);
|
|
393
|
+
console.log(` Summary: ${summary}`);
|
|
394
|
+
console.log(` Description: ${description || chalk.grey('(none)')}`);
|
|
395
|
+
console.log(` Priority: ${priorityName || chalk.grey('(default)')}`);
|
|
396
|
+
console.log(` Assignee: ${assigneeId || chalk.grey('Unassigned')}`);
|
|
397
|
+
console.log(chalk.blue('──────────────────────────────────\n'));
|
|
398
|
+
|
|
399
|
+
const { confirmed } = await enquirer.prompt({
|
|
400
|
+
type: 'confirm',
|
|
401
|
+
name: 'confirmed',
|
|
402
|
+
message: 'Create this issue?',
|
|
403
|
+
initial: true
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
if (!confirmed) {
|
|
407
|
+
console.log(chalk.yellow('Issue creation cancelled.'));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Build Request Body ──────────────────────────────
|
|
412
|
+
const issueBody = {
|
|
413
|
+
fields: {
|
|
414
|
+
project: { key: projectKey },
|
|
415
|
+
issuetype: { name: issueTypeName },
|
|
416
|
+
summary: summary
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
if (description) {
|
|
421
|
+
issueBody.fields.description = textToADF(description);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (priorityName) {
|
|
425
|
+
issueBody.fields.priority = { name: priorityName };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (assigneeId) {
|
|
429
|
+
issueBody.fields.assignee = { accountId: assigneeId };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Create Issue ────────────────────────────────────
|
|
433
|
+
const spinner = ora('Creating issue...').start();
|
|
434
|
+
const result = await api.post('/issue', issueBody);
|
|
435
|
+
spinner.succeed(chalk.green(`Issue created: ${chalk.bold(result.key)}`));
|
|
436
|
+
|
|
437
|
+
console.log(chalk.grey(`View it: jira issue view ${result.key}`));
|
|
438
|
+
|
|
439
|
+
} catch (e) {
|
|
440
|
+
if (e === '' || e.message === '') {
|
|
441
|
+
// User cancelled prompt (Ctrl+C)
|
|
442
|
+
console.log(chalk.yellow('\nCancelled.'));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
console.error(chalk.red('\nFailed to create issue:'));
|
|
446
|
+
if (e.response) {
|
|
447
|
+
console.error(chalk.red(`Error ${e.response.status}: `), JSON.stringify(e.response.data, null, 2));
|
|
448
|
+
} else {
|
|
449
|
+
console.error(chalk.red(e.message));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ── TRANSITION ────────────────────────────────────────────────────
|
|
455
|
+
issueCmd
|
|
456
|
+
.command('transition')
|
|
457
|
+
.description('Transition an issue to a new status')
|
|
458
|
+
.argument('<issueKey>', 'Issue Key (e.g., PROJ-123)')
|
|
459
|
+
.option('-s, --status <name>', 'Target status name (skips interactive selection)')
|
|
460
|
+
.addHelpText('after', `
|
|
461
|
+
Examples:
|
|
462
|
+
$ jira issue transition PROJ-123 # Interactive
|
|
463
|
+
$ jira issue transition PROJ-123 --status "In Progress"
|
|
464
|
+
$ jira issue transition PROJ-123 -s Done
|
|
465
|
+
`)
|
|
466
|
+
.action(async (issueKey, options) => {
|
|
467
|
+
const spinner = ora(`Fetching transitions for ${issueKey}...`).start();
|
|
468
|
+
try {
|
|
469
|
+
// Fetch current issue to show context
|
|
470
|
+
const issue = await api.get(`/issue/${issueKey}?fields=summary,status`);
|
|
471
|
+
const currentStatus = issue.fields.status.name;
|
|
472
|
+
|
|
473
|
+
// Fetch available transitions
|
|
474
|
+
const transData = await api.get(`/issue/${issueKey}/transitions`);
|
|
475
|
+
spinner.stop();
|
|
476
|
+
|
|
477
|
+
if (!transData.transitions || transData.transitions.length === 0) {
|
|
478
|
+
console.log(chalk.yellow(`No transitions available for ${issueKey} (current status: ${currentStatus}).`));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
|
|
483
|
+
console.log(chalk.grey(`Current Status: ${currentStatus}\n`));
|
|
484
|
+
|
|
485
|
+
let targetTransition;
|
|
486
|
+
|
|
487
|
+
if (options.status) {
|
|
488
|
+
// Non-interactive: find matching transition
|
|
489
|
+
targetTransition = transData.transitions.find(
|
|
490
|
+
t => t.name.toLowerCase() === options.status.toLowerCase() ||
|
|
491
|
+
t.to.name.toLowerCase() === options.status.toLowerCase()
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
if (!targetTransition) {
|
|
495
|
+
console.error(chalk.red(`Status "${options.status}" is not a valid transition from "${currentStatus}".`));
|
|
496
|
+
console.log(chalk.grey('Available transitions:'));
|
|
497
|
+
transData.transitions.forEach(t => {
|
|
498
|
+
console.log(chalk.grey(` • ${t.name} → ${t.to.name}`));
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
// Interactive: show selection
|
|
504
|
+
const transitionChoices = transData.transitions.map(t => ({
|
|
505
|
+
name: t.id,
|
|
506
|
+
message: `${t.name} → ${chalk.cyan(t.to.name)}`
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
const { selectedTransition } = await enquirer.prompt({
|
|
510
|
+
type: 'select',
|
|
511
|
+
name: 'selectedTransition',
|
|
512
|
+
message: 'Select transition:',
|
|
513
|
+
choices: transitionChoices
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
targetTransition = transData.transitions.find(t => t.id === selectedTransition);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Execute transition
|
|
520
|
+
const execSpinner = ora(`Transitioning to "${targetTransition.to.name}"...`).start();
|
|
521
|
+
await api.post(`/issue/${issueKey}/transitions`, {
|
|
522
|
+
transition: { id: targetTransition.id }
|
|
523
|
+
});
|
|
524
|
+
execSpinner.succeed(chalk.green(`${issueKey} transitioned: ${currentStatus} → ${chalk.bold(targetTransition.to.name)}`));
|
|
525
|
+
|
|
526
|
+
} catch (e) {
|
|
527
|
+
spinner.stop();
|
|
528
|
+
if (e === '' || e.message === '') {
|
|
529
|
+
console.log(chalk.yellow('\nCancelled.'));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
console.error(chalk.red('\nFailed to transition issue:'));
|
|
533
|
+
if (e.response) {
|
|
534
|
+
if (e.response.status === 404) {
|
|
535
|
+
console.error(chalk.red(`Issue "${issueKey}" not found.`));
|
|
536
|
+
} else {
|
|
537
|
+
console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
console.error(chalk.red(e.message));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
// ── ASSIGN ────────────────────────────────────────────────────────
|
|
545
|
+
issueCmd
|
|
546
|
+
.command('assign')
|
|
547
|
+
.description('Assign or reassign an issue')
|
|
548
|
+
.argument('<issueKey>', 'Issue Key (e.g., PROJ-123)')
|
|
549
|
+
.option('-a, --assignee <id>', 'Assignee account ID (use "me" for self, "none" to unassign)')
|
|
550
|
+
.addHelpText('after', `
|
|
551
|
+
Examples:
|
|
552
|
+
$ jira issue assign PROJ-123 # Interactive
|
|
553
|
+
$ jira issue assign PROJ-123 -a me # Assign to yourself
|
|
554
|
+
$ jira issue assign PROJ-123 -a none # Unassign
|
|
555
|
+
`)
|
|
556
|
+
.action(async (issueKey, options) => {
|
|
557
|
+
try {
|
|
558
|
+
let assigneeId = options.assignee;
|
|
559
|
+
|
|
560
|
+
if (!assigneeId) {
|
|
561
|
+
// Interactive selection
|
|
562
|
+
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
563
|
+
const issue = await api.get(`/issue/${issueKey}?fields=summary,assignee`);
|
|
564
|
+
spinner.stop();
|
|
565
|
+
|
|
566
|
+
const currentAssignee = issue.fields.assignee?.displayName || 'Unassigned';
|
|
567
|
+
console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
|
|
568
|
+
console.log(chalk.grey(`Current Assignee: ${currentAssignee}\n`));
|
|
569
|
+
|
|
570
|
+
const { assignChoice } = await enquirer.prompt({
|
|
571
|
+
type: 'select',
|
|
572
|
+
name: 'assignChoice',
|
|
573
|
+
message: 'Assign to:',
|
|
574
|
+
choices: [
|
|
575
|
+
{ name: 'me', message: 'Myself' },
|
|
576
|
+
{ name: 'none', message: 'Unassign' },
|
|
577
|
+
{ name: 'search', message: 'Search for a user...' }
|
|
578
|
+
]
|
|
579
|
+
});
|
|
580
|
+
assigneeId = assignChoice;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (assigneeId === 'me') {
|
|
584
|
+
const spinner = ora('Fetching your account...').start();
|
|
585
|
+
const myself = await api.get('/myself');
|
|
586
|
+
assigneeId = myself.accountId;
|
|
587
|
+
spinner.stop();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (assigneeId === 'search') {
|
|
591
|
+
const { searchQuery } = await enquirer.prompt({
|
|
592
|
+
type: 'input',
|
|
593
|
+
name: 'searchQuery',
|
|
594
|
+
message: 'Search user by name or email:'
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const spinner = ora('Searching users...').start();
|
|
598
|
+
const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
|
|
599
|
+
spinner.stop();
|
|
600
|
+
|
|
601
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
602
|
+
console.log(chalk.yellow('No users found.'));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const { selectedUser } = await enquirer.prompt({
|
|
607
|
+
type: 'select',
|
|
608
|
+
name: 'selectedUser',
|
|
609
|
+
message: 'Select User:',
|
|
610
|
+
choices: users.map(u => ({
|
|
611
|
+
name: u.accountId,
|
|
612
|
+
message: `${u.displayName} (${u.emailAddress || u.accountId})`
|
|
613
|
+
}))
|
|
614
|
+
});
|
|
615
|
+
assigneeId = selectedUser;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const spinner = ora('Updating assignee...').start();
|
|
619
|
+
const body = assigneeId === 'none'
|
|
620
|
+
? { accountId: null }
|
|
621
|
+
: { accountId: assigneeId };
|
|
622
|
+
|
|
623
|
+
await api.put(`/issue/${issueKey}/assignee`, body);
|
|
624
|
+
spinner.succeed(chalk.green(`${issueKey} ${assigneeId === 'none' ? 'unassigned' : 'assigned'} successfully.`));
|
|
625
|
+
|
|
626
|
+
} catch (e) {
|
|
627
|
+
if (e === '' || e.message === '') {
|
|
628
|
+
console.log(chalk.yellow('\nCancelled.'));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
console.error(chalk.red('\nFailed to assign issue:'));
|
|
632
|
+
if (e.response) {
|
|
633
|
+
console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
|
|
634
|
+
} else {
|
|
635
|
+
console.error(chalk.red(e.message));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// ── COMMENT ───────────────────────────────────────────────────────
|
|
641
|
+
issueCmd
|
|
642
|
+
.command('comment')
|
|
643
|
+
.description('Add a comment to an issue')
|
|
644
|
+
.argument('<issueKey>', 'Issue Key (e.g., PROJ-123)')
|
|
645
|
+
.option('-m, --message <text>', 'Comment text (skips interactive prompt)')
|
|
646
|
+
.addHelpText('after', `
|
|
647
|
+
Examples:
|
|
648
|
+
$ jira issue comment PROJ-123 # Interactive
|
|
649
|
+
$ jira issue comment PROJ-123 -m "Fixed in latest build"
|
|
650
|
+
`)
|
|
651
|
+
.action(async (issueKey, options) => {
|
|
652
|
+
try {
|
|
653
|
+
let commentText = options.message;
|
|
654
|
+
|
|
655
|
+
if (!commentText) {
|
|
656
|
+
// Show issue context first
|
|
657
|
+
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
658
|
+
const issue = await api.get(`/issue/${issueKey}?fields=summary,status`);
|
|
659
|
+
spinner.stop();
|
|
660
|
+
|
|
661
|
+
console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
|
|
662
|
+
console.log(chalk.grey(`Status: ${issue.fields.status.name}\n`));
|
|
663
|
+
|
|
664
|
+
const { inputComment } = await enquirer.prompt({
|
|
665
|
+
type: 'input',
|
|
666
|
+
name: 'inputComment',
|
|
667
|
+
message: 'Enter your comment:',
|
|
668
|
+
validate: (val) => val.trim().length > 0 || 'Comment cannot be empty'
|
|
669
|
+
});
|
|
670
|
+
commentText = inputComment;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const spinner = ora('Adding comment...').start();
|
|
674
|
+
await api.post(`/issue/${issueKey}/comment`, {
|
|
675
|
+
body: textToADF(commentText)
|
|
676
|
+
});
|
|
677
|
+
spinner.succeed(chalk.green(`Comment added to ${issueKey}.`));
|
|
678
|
+
|
|
679
|
+
} catch (e) {
|
|
680
|
+
if (e === '' || e.message === '') {
|
|
681
|
+
console.log(chalk.yellow('\nCancelled.'));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
console.error(chalk.red('\nFailed to add comment:'));
|
|
685
|
+
if (e.response) {
|
|
686
|
+
if (e.response.status === 404) {
|
|
687
|
+
console.error(chalk.red(`Issue "${issueKey}" not found.`));
|
|
688
|
+
} else {
|
|
689
|
+
console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
console.error(chalk.red(e.message));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
154
697
|
program.addCommand(issueCmd);
|
|
155
698
|
}
|
package/src/commands/project.js
CHANGED
|
@@ -6,7 +6,11 @@ import ora from 'ora';
|
|
|
6
6
|
|
|
7
7
|
export function registerProjectCommand(program) {
|
|
8
8
|
const projectCmd = new Command('project')
|
|
9
|
-
.description('Manage Jira projects')
|
|
9
|
+
.description('Manage Jira projects')
|
|
10
|
+
.addHelpText('after', `
|
|
11
|
+
Common Actions:
|
|
12
|
+
$ jira project list # List all projects
|
|
13
|
+
`);
|
|
10
14
|
|
|
11
15
|
projectCmd
|
|
12
16
|
.command('list')
|
package/src/commands/sprint.js
CHANGED
|
@@ -6,7 +6,11 @@ import ora from 'ora';
|
|
|
6
6
|
|
|
7
7
|
export function registerSprintCommand(program) {
|
|
8
8
|
const sprintCmd = new Command('sprint')
|
|
9
|
-
.description('Manage Sprints')
|
|
9
|
+
.description('Manage Sprints')
|
|
10
|
+
.addHelpText('after', `
|
|
11
|
+
Common Actions:
|
|
12
|
+
$ jira sprint list --board <ID|Name> # List sprints for a board
|
|
13
|
+
`);
|
|
10
14
|
|
|
11
15
|
sprintCmd
|
|
12
16
|
.command('list')
|