jira-pilot 2.0.4 → 2.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.
Files changed (35) hide show
  1. package/README.md +216 -173
  2. package/bin/{jira.js → jira.ts} +10 -1
  3. package/dist/bin/jira.js +64 -0
  4. package/package.json +21 -15
  5. package/src/commands/ai-actions/{plan.js → plan.ts} +9 -9
  6. package/src/commands/ai-actions/{review.js → review.ts} +2 -2
  7. package/src/commands/ai-actions/{standup.js → standup.ts} +4 -4
  8. package/src/commands/{ai.js → ai.ts} +11 -11
  9. package/src/commands/{board.js → board.ts} +11 -11
  10. package/src/commands/bulk.ts +230 -0
  11. package/src/commands/{config.js → config.ts} +57 -8
  12. package/src/commands/dashboard.ts +222 -0
  13. package/src/commands/filter.ts +84 -0
  14. package/src/commands/{git.js → git.ts} +4 -4
  15. package/src/commands/issue-attach.ts +44 -0
  16. package/src/commands/issue-pr.ts +87 -0
  17. package/src/commands/issue-worklog.ts +90 -0
  18. package/src/commands/{issue.js → issue.ts} +359 -68
  19. package/src/commands/{mcp.js → mcp.ts} +2 -2
  20. package/src/commands/{project.js → project.ts} +11 -11
  21. package/src/commands/sprint.ts +269 -0
  22. package/src/server/{mcp-server.js → mcp-server.ts} +235 -8
  23. package/src/services/{ai-service.js → ai-service.ts} +16 -16
  24. package/src/services/{api-service.js → api-service.ts} +33 -9
  25. package/src/services/config-service.ts +21 -0
  26. package/src/types.ts +68 -0
  27. package/src/utils/{adf-parser.js → adf-parser.ts} +12 -12
  28. package/src/utils/config-store.ts +109 -0
  29. package/src/utils/{config.js → config.ts} +14 -41
  30. package/src/utils/{error-handler.js → error-handler.ts} +2 -1
  31. package/src/utils/{text-to-adf.js → text-to-adf.ts} +1 -1
  32. package/src/utils/{validators.js → validators.ts} +4 -4
  33. package/src/commands/bulk.js +0 -108
  34. package/src/commands/dashboard.js +0 -89
  35. package/src/commands/sprint.js +0 -153
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { table } from 'table';
3
+ import Table from 'cli-table3';
4
4
  import { api } from '../services/api-service.js';
5
5
  import { aiService } from '../services/ai-service.js';
6
6
  import ora from 'ora';
@@ -8,9 +8,14 @@ import enquirer from 'enquirer';
8
8
  import { parseADF } from '../utils/adf-parser.js';
9
9
  import { textToADF } from '../utils/text-to-adf.js';
10
10
  import { validateIssueKey } from '../utils/validators.js';
11
+
11
12
  import { handleCommandError } from '../utils/error-handler.js';
13
+ import { registerWorklogCommand } from './issue-worklog.js';
14
+ import { registerPrCommand } from './issue-pr.js';
15
+ import { registerAttachCommand } from './issue-attach.js';
16
+ import { ConfigService } from '../services/config-service.js';
12
17
 
13
- export function registerIssueCommand(program) {
18
+ export function registerIssueCommand(program: Command) {
14
19
  const issueCmd = new Command('issue')
15
20
  .description('Manage Jira issues')
16
21
  .addHelpText('after', `
@@ -39,7 +44,7 @@ Examples:
39
44
  $ jira issue list --jql "created >= -7d"
40
45
  $ jira issue list --export json
41
46
  `)
42
- .action(async (options) => {
47
+ .action(async (options: any) => {
43
48
  const spinner = ora('Fetching issues...').start();
44
49
  try {
45
50
  // Natural Language JQL
@@ -49,7 +54,7 @@ Examples:
49
54
  const generatedJql = await aiService.generateJql(options.ask);
50
55
  aiSpinner.succeed(`JQL: ${chalk.cyan(generatedJql)}`);
51
56
  options.jql = generatedJql; // Override/Set JQL
52
- } catch (e) {
57
+ } catch (e: any) {
53
58
  aiSpinner.fail('Failed to translate query.');
54
59
  console.error(chalk.red(e.message));
55
60
  return;
@@ -105,7 +110,7 @@ Examples:
105
110
  mdContent += `| Key | Summary | Status | Assignee |\n`;
106
111
  mdContent += `|---|---|---|---|\n`;
107
112
 
108
- data.issues.forEach(i => {
113
+ data.issues.forEach((i: any) => {
109
114
  const key = i.key;
110
115
  const summary = i.fields.summary || '';
111
116
  const status = i.fields.status?.name || '';
@@ -120,7 +125,7 @@ Examples:
120
125
  }
121
126
 
122
127
  if (options.output === 'json') {
123
- console.log(JSON.stringify(data.issues.map(i => ({
128
+ console.log(JSON.stringify(data.issues.map((i: any) => ({
124
129
  key: i.key, summary: i.fields.summary,
125
130
  status: i.fields.status?.name, assignee: i.fields.assignee?.displayName || null,
126
131
  created: i.fields.created, updated: i.fields.updated
@@ -128,12 +133,12 @@ Examples:
128
133
  return;
129
134
  }
130
135
 
131
- const tableData = [
132
- [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Created'), chalk.bold('Updated')]
133
- ];
136
+ const table = new Table({
137
+ head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Created'), chalk.bold('Updated')]
138
+ });
134
139
 
135
- data.issues.forEach(i => {
136
- tableData.push([
140
+ data.issues.forEach((i: any) => {
141
+ table.push([
137
142
  chalk.cyan(i.key),
138
143
  i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
139
144
  i.fields.status ? i.fields.status.name : '',
@@ -143,9 +148,9 @@ Examples:
143
148
  ]);
144
149
  });
145
150
 
146
- console.log(table(tableData));
151
+ console.log(table.toString());
147
152
 
148
- } catch (e) {
153
+ } catch (e: any) {
149
154
  handleCommandError(spinner, e, 'Failed to list issues');
150
155
  }
151
156
  });
@@ -160,7 +165,7 @@ Examples:
160
165
  $ jira issue view PROJ-123
161
166
  $ jira issue view PROJ-123 --output json
162
167
  `)
163
- .action(async (issueKey, options) => {
168
+ .action(async (issueKey: string, options: any) => {
164
169
  const check = validateIssueKey(issueKey);
165
170
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
166
171
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
@@ -189,14 +194,30 @@ Examples:
189
194
  console.log(chalk.bold('\nAssignee: ') + issue.fields.assignee.displayName);
190
195
  }
191
196
 
197
+ if (issue.fields.components && issue.fields.components.length > 0) {
198
+ console.log(chalk.bold('Components: ') + issue.fields.components.map((c: any) => c.name).join(', '));
199
+ }
200
+
201
+ if (issue.fields.labels && issue.fields.labels.length > 0) {
202
+ console.log(chalk.bold('Labels: ') + issue.fields.labels.join(', '));
203
+ }
204
+
205
+ if (issue.fields.duedate) {
206
+ console.log(chalk.bold('Due Date: ') + issue.fields.duedate);
207
+ }
208
+
209
+ if (issue.fields.fixVersions && issue.fields.fixVersions.length > 0) {
210
+ console.log(chalk.bold('Fix Versions: ') + issue.fields.fixVersions.map((v: any) => v.name).join(', '));
211
+ }
212
+
192
213
  if (issue.fields.comment && issue.fields.comment.comments.length > 0) {
193
214
  console.log(chalk.bold('\nComments:'));
194
- issue.fields.comment.comments.forEach(c => {
215
+ issue.fields.comment.comments.forEach((c: any) => {
195
216
  console.log(chalk.cyan(c.author.displayName) + ': ' + c.body);
196
217
  });
197
218
  }
198
219
  console.log('');
199
- } catch (e) {
220
+ } catch (e: any) {
200
221
  handleCommandError(spinner, e, 'Failed to fetch issue');
201
222
  }
202
223
  });
@@ -211,14 +232,17 @@ Examples:
211
232
  .option('-d, --description <text>', 'Issue description')
212
233
  .option('--priority <name>', 'Priority name (e.g., High, Medium, Low)')
213
234
  .option('-a, --assignee <id>', 'Assignee account ID (use "me" for self)')
235
+ .option('--custom <key=value>', 'Custom fields (key=value, repeatable)', (v: string, l: string[]) => l.concat([v]), [])
214
236
  .addHelpText('after', `
215
237
  Examples:
216
238
  $ jira issue create # Interactive wizard
217
239
  $ jira issue create -p PROJ -s "Fix login bug" # Quick create
218
240
  $ jira issue create -p PROJ -t Bug -s "Crash on save" --priority High
219
241
  $ jira issue create -p PROJ -s "New feature" -a me
242
+ $ jira issue create -p PROJ -s "Story" --custom "storyPoints=5"
220
243
  `)
221
- .action(async (options) => {
244
+ .action(async (options: any) => {
245
+ let spinner: any = null;
222
246
  try {
223
247
  // ── Step 1: Select Project ──────────────────────────
224
248
  let projectKey = options.project;
@@ -232,7 +256,7 @@ Examples:
232
256
  return;
233
257
  }
234
258
 
235
- const projectChoices = projectData.values.map(p => ({
259
+ const projectChoices = projectData.values.map((p: any) => ({
236
260
  name: p.key,
237
261
  message: `${p.key} — ${p.name}`
238
262
  }));
@@ -242,7 +266,7 @@ Examples:
242
266
  name: 'selectedProject',
243
267
  message: 'Select Project:',
244
268
  choices: projectChoices
245
- });
269
+ }) as any;
246
270
  projectKey = selectedProject;
247
271
  }
248
272
 
@@ -277,16 +301,16 @@ Examples:
277
301
  }
278
302
 
279
303
  // Filter out sub-tasks if present
280
- const filteredTypes = issueTypes.filter(t => !t.subtask);
304
+ const filteredTypes = issueTypes.filter((t: any) => !t.subtask);
281
305
  const typeChoices = (filteredTypes.length > 0 ? filteredTypes : issueTypes)
282
- .map(t => ({ name: t.name, message: t.name }));
306
+ .map((t: any) => ({ name: t.name, message: t.name }));
283
307
 
284
308
  const { selectedType } = await enquirer.prompt({
285
309
  type: 'select',
286
310
  name: 'selectedType',
287
311
  message: 'Select Issue Type:',
288
312
  choices: typeChoices
289
- });
313
+ }) as any;
290
314
  issueTypeName = selectedType;
291
315
  }
292
316
 
@@ -297,8 +321,8 @@ Examples:
297
321
  type: 'input',
298
322
  name: 'inputSummary',
299
323
  message: 'Summary (required):',
300
- validate: (val) => val.trim().length > 0 || 'Summary cannot be empty'
301
- });
324
+ validate: (val: any) => val.trim().length > 0 || 'Summary cannot be empty'
325
+ }) as any;
302
326
  summary = inputSummary;
303
327
  }
304
328
 
@@ -309,7 +333,7 @@ Examples:
309
333
  type: 'input',
310
334
  name: 'inputDescription',
311
335
  message: 'Description (optional, press Enter to skip):'
312
- });
336
+ }) as any;
313
337
  description = inputDescription || null;
314
338
  }
315
339
 
@@ -322,7 +346,7 @@ Examples:
322
346
  spinner.stop();
323
347
 
324
348
  if (Array.isArray(priorities) && priorities.length > 0) {
325
- const priorityChoices = priorities.map(p => ({
349
+ const priorityChoices = priorities.map((p: any) => ({
326
350
  name: p.name,
327
351
  message: p.name
328
352
  }));
@@ -332,7 +356,7 @@ Examples:
332
356
  name: 'selectedPriority',
333
357
  message: 'Select Priority:',
334
358
  choices: priorityChoices
335
- });
359
+ }) as any;
336
360
  priorityName = selectedPriority;
337
361
  }
338
362
  } catch {
@@ -341,6 +365,75 @@ Examples:
341
365
  }
342
366
  }
343
367
 
368
+ // ── Step 5.5: Components ────────────────────────────
369
+ let componentIds: string[] = [];
370
+ // Interactive only for now (TODO: add flags)
371
+ const compSpinner = ora('Fetching components...').start();
372
+ try {
373
+ const components = await api.get(`/project/${projectKey}/components`);
374
+ compSpinner.stop();
375
+
376
+ if (Array.isArray(components) && components.length > 0) {
377
+ const { selectedComponents } = await enquirer.prompt({
378
+ type: 'multiselect',
379
+ name: 'selectedComponents',
380
+ message: 'Select Components (Space to select, Enter to confirm):',
381
+ choices: components.map((c: any) => ({ name: c.id, message: c.name }))
382
+ }) as any;
383
+ componentIds = selectedComponents;
384
+ }
385
+ } catch {
386
+ compSpinner.stop();
387
+ }
388
+
389
+ // ── Step 5.6: Labels ────────────────────────────────
390
+ let labels: string[] = [];
391
+ const { inputLabels } = await enquirer.prompt({
392
+ type: 'input',
393
+ name: 'inputLabels',
394
+ message: 'Labels (comma-separated, optional):'
395
+ }) as any;
396
+
397
+ if (inputLabels && inputLabels.trim().length > 0) {
398
+ labels = inputLabels.split(',').map((l: string) => l.trim()).filter((l: string) => l.length > 0);
399
+ }
400
+
401
+ // ── Step 5.7: Fix Versions ──────────────────────────
402
+ let fixVersionIds: string[] = [];
403
+ const verSpinner = ora('Fetching versions...').start();
404
+ try {
405
+ const versions = await api.get(`/project/${projectKey}/versions`);
406
+ verSpinner.stop();
407
+
408
+ // Filter unreleased versions usually
409
+ const unreleased = versions.filter((v: any) => !v.released);
410
+
411
+ if (Array.isArray(unreleased) && unreleased.length > 0) {
412
+ const { selectedVersions } = await enquirer.prompt({
413
+ type: 'multiselect',
414
+ name: 'selectedVersions',
415
+ message: 'Fix Versions:',
416
+ choices: unreleased.map((v: any) => ({ name: v.id, message: v.name }))
417
+ }) as any;
418
+ fixVersionIds = selectedVersions;
419
+ }
420
+ } catch {
421
+ verSpinner.stop();
422
+ }
423
+
424
+ // ── Step 5.8: Due Date ──────────────────────────────
425
+ let duedate: string | null = null;
426
+ const { inputDueDate } = await enquirer.prompt({
427
+ type: 'input',
428
+ name: 'inputDueDate',
429
+ message: 'Due Date (YYYY-MM-DD, optional):',
430
+ validate: (val: string) => {
431
+ if (!val) return true;
432
+ return /^\d{4}-\d{2}-\d{2}$/.test(val) || 'Format must be YYYY-MM-DD';
433
+ }
434
+ }) as any;
435
+ if (inputDueDate) duedate = inputDueDate;
436
+
344
437
  // ── Step 6: Assignee ────────────────────────────────
345
438
  let assigneeId = options.assignee;
346
439
  if (!assigneeId) {
@@ -353,7 +446,7 @@ Examples:
353
446
  { name: 'unassigned', message: 'Leave Unassigned' },
354
447
  { name: 'search', message: 'Search for a user...' }
355
448
  ]
356
- });
449
+ }) as any;
357
450
 
358
451
  if (assigneeChoice === 'me') {
359
452
  const spinner = ora('Fetching your account...').start();
@@ -370,7 +463,7 @@ Examples:
370
463
  type: 'input',
371
464
  name: 'searchQuery',
372
465
  message: 'Search user by name or email:'
373
- });
466
+ }) as any;
374
467
 
375
468
  if (searchQuery.trim()) {
376
469
  const spinner = ora('Searching users...').start();
@@ -379,7 +472,7 @@ Examples:
379
472
  spinner.stop();
380
473
 
381
474
  if (Array.isArray(users) && users.length > 0) {
382
- const userChoices = users.map(u => ({
475
+ const userChoices = users.map((u: any) => ({
383
476
  name: u.accountId,
384
477
  message: `${u.displayName} (${u.emailAddress || u.accountId})`
385
478
  }));
@@ -389,7 +482,7 @@ Examples:
389
482
  name: 'selectedUser',
390
483
  message: 'Select User:',
391
484
  choices: userChoices
392
- });
485
+ }) as any;
393
486
  assigneeId = selectedUser;
394
487
  } else {
395
488
  console.log(chalk.yellow('No users found. Leaving unassigned.'));
@@ -431,7 +524,7 @@ Examples:
431
524
  name: 'confirmed',
432
525
  message: 'Create this issue?',
433
526
  initial: true
434
- });
527
+ }) as any;
435
528
 
436
529
  if (!confirmed) {
437
530
  console.log(chalk.yellow('Issue creation cancelled.'));
@@ -439,7 +532,7 @@ Examples:
439
532
  }
440
533
 
441
534
  // ── Build Request Body ──────────────────────────────
442
- const issueBody = {
535
+ const issueBody: any = {
443
536
  fields: {
444
537
  project: { key: projectKey },
445
538
  issuetype: { name: issueTypeName },
@@ -459,6 +552,35 @@ Examples:
459
552
  issueBody.fields.assignee = { accountId: assigneeId };
460
553
  }
461
554
 
555
+ if (componentIds.length > 0) {
556
+ issueBody.fields.components = componentIds.map(id => ({ id }));
557
+ }
558
+
559
+ if (labels.length > 0) {
560
+ issueBody.fields.labels = labels;
561
+ }
562
+
563
+ if (fixVersionIds.length > 0) {
564
+ issueBody.fields.fixVersions = fixVersionIds.map(id => ({ id }));
565
+ }
566
+
567
+ if (duedate) {
568
+ issueBody.fields.duedate = duedate;
569
+ }
570
+
571
+ // ── Step 5.9: Custom Fields ─────────────────────────
572
+ if (options.custom && options.custom.length > 0) {
573
+ options.custom.forEach((cf: string) => {
574
+ const [key, ...rest] = cf.split('=');
575
+ const value = rest.join('=');
576
+ if (!key || !value) return;
577
+
578
+ const fieldId = ConfigService.get(`customFields.${key}`) || key;
579
+ const parsedValue = isNaN(Number(value)) ? value : Number(value);
580
+ issueBody.fields[fieldId] = parsedValue;
581
+ });
582
+ }
583
+
462
584
  // ── Create Issue ────────────────────────────────────
463
585
  const spinner = ora('Creating issue...').start();
464
586
  const result = await api.post('/issue', issueBody);
@@ -466,7 +588,7 @@ Examples:
466
588
 
467
589
  console.log(chalk.grey(`View it: jira issue view ${result.key}`));
468
590
 
469
- } catch (e) {
591
+ } catch (e: any) {
470
592
  handleCommandError(spinner, e, 'Failed to create issue');
471
593
  }
472
594
  });
@@ -483,7 +605,7 @@ Examples:
483
605
  $ jira issue transition PROJ-123 --status "In Progress"
484
606
  $ jira issue transition PROJ-123 -s Done
485
607
  `)
486
- .action(async (issueKey, options) => {
608
+ .action(async (issueKey: string, options: any) => {
487
609
  const check = validateIssueKey(issueKey);
488
610
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
489
611
  const spinner = ora(`Fetching transitions for ${issueKey}...`).start();
@@ -509,21 +631,21 @@ Examples:
509
631
  if (options.status) {
510
632
  // Non-interactive: find matching transition
511
633
  targetTransition = transData.transitions.find(
512
- t => t.name.toLowerCase() === options.status.toLowerCase() ||
634
+ (t: any) => t.name.toLowerCase() === options.status.toLowerCase() ||
513
635
  t.to.name.toLowerCase() === options.status.toLowerCase()
514
636
  );
515
637
 
516
638
  if (!targetTransition) {
517
639
  console.error(chalk.red(`Status "${options.status}" is not a valid transition from "${currentStatus}".`));
518
640
  console.log(chalk.grey('Available transitions:'));
519
- transData.transitions.forEach(t => {
641
+ transData.transitions.forEach((t: any) => {
520
642
  console.log(chalk.grey(` • ${t.name} → ${t.to.name}`));
521
643
  });
522
644
  return;
523
645
  }
524
646
  } else {
525
647
  // Interactive: show selection
526
- const transitionChoices = transData.transitions.map(t => ({
648
+ const transitionChoices = transData.transitions.map((t: any) => ({
527
649
  name: t.id,
528
650
  message: `${t.name} → ${chalk.cyan(t.to.name)}`
529
651
  }));
@@ -533,9 +655,9 @@ Examples:
533
655
  name: 'selectedTransition',
534
656
  message: 'Select transition:',
535
657
  choices: transitionChoices
536
- });
658
+ }) as any;
537
659
 
538
- targetTransition = transData.transitions.find(t => t.id === selectedTransition);
660
+ targetTransition = transData.transitions.find((t: any) => t.id === selectedTransition);
539
661
  }
540
662
 
541
663
  // Execute transition
@@ -545,7 +667,7 @@ Examples:
545
667
  });
546
668
  execSpinner.succeed(chalk.green(`${issueKey} transitioned: ${currentStatus} → ${chalk.bold(targetTransition.to.name)}`));
547
669
 
548
- } catch (e) {
670
+ } catch (e: any) {
549
671
  handleCommandError(spinner, e, 'Failed to transition issue');
550
672
  }
551
673
  });
@@ -561,9 +683,10 @@ Examples:
561
683
  $ jira issue assign PROJ-123 -a me # Assign to yourself
562
684
  $ jira issue assign PROJ-123 -a none # Unassign
563
685
  `)
564
- .action(async (issueKey, options) => {
686
+ .action(async (issueKey: string, options: any) => {
565
687
  const check = validateIssueKey(issueKey);
566
688
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
689
+ let spinner: any = null;
567
690
  try {
568
691
  let assigneeId = options.assignee;
569
692
 
@@ -586,7 +709,7 @@ Examples:
586
709
  { name: 'none', message: 'Unassign' },
587
710
  { name: 'search', message: 'Search for a user...' }
588
711
  ]
589
- });
712
+ }) as any;
590
713
  assigneeId = assignChoice;
591
714
  }
592
715
 
@@ -602,7 +725,7 @@ Examples:
602
725
  type: 'input',
603
726
  name: 'searchQuery',
604
727
  message: 'Search user by name or email:'
605
- });
728
+ }) as any;
606
729
 
607
730
  const spinner = ora('Searching users...').start();
608
731
  const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
@@ -617,11 +740,11 @@ Examples:
617
740
  type: 'select',
618
741
  name: 'selectedUser',
619
742
  message: 'Select User:',
620
- choices: users.map(u => ({
743
+ choices: users.map((u: any) => ({
621
744
  name: u.accountId,
622
745
  message: `${u.displayName} (${u.emailAddress || u.accountId})`
623
746
  }))
624
- });
747
+ }) as any;
625
748
  assigneeId = selectedUser;
626
749
  }
627
750
 
@@ -633,7 +756,7 @@ Examples:
633
756
  await api.put(`/issue/${issueKey}/assignee`, body);
634
757
  spinner.succeed(chalk.green(`${issueKey} ${assigneeId === 'none' ? 'unassigned' : 'assigned'} successfully.`));
635
758
 
636
- } catch (e) {
759
+ } catch (e: any) {
637
760
  handleCommandError(spinner, e, 'Failed to assign issue');
638
761
  }
639
762
  });
@@ -649,9 +772,10 @@ Examples:
649
772
  $ jira issue comment PROJ-123 # Interactive
650
773
  $ jira issue comment PROJ-123 -m "Fixed in latest build"
651
774
  `)
652
- .action(async (issueKey, options) => {
775
+ .action(async (issueKey: string, options: any) => {
653
776
  const check = validateIssueKey(issueKey);
654
777
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
778
+ let spinner: any = null;
655
779
  try {
656
780
  let commentText = options.message;
657
781
 
@@ -668,8 +792,8 @@ Examples:
668
792
  type: 'input',
669
793
  name: 'inputComment',
670
794
  message: 'Enter your comment:',
671
- validate: (val) => val.trim().length > 0 || 'Comment cannot be empty'
672
- });
795
+ validate: (val: any) => val.trim().length > 0 || 'Comment cannot be empty'
796
+ }) as any;
673
797
  commentText = inputComment;
674
798
  }
675
799
 
@@ -679,7 +803,7 @@ Examples:
679
803
  });
680
804
  spinner.succeed(chalk.green(`Comment added to ${issueKey}.`));
681
805
 
682
- } catch (e) {
806
+ } catch (e: any) {
683
807
  handleCommandError(spinner, e, 'Failed to add comment');
684
808
  }
685
809
  });
@@ -699,7 +823,7 @@ Examples:
699
823
  $ jira issue edit PROJ-123 --priority High
700
824
  $ jira issue edit PROJ-123 -d "New description"
701
825
  `)
702
- .action(async (issueKey, options) => {
826
+ .action(async (issueKey: string, options: any) => {
703
827
  const check = validateIssueKey(issueKey);
704
828
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
705
829
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
@@ -707,18 +831,30 @@ Examples:
707
831
  const issue = await api.get(`/issue/${issueKey}?fields=summary,description,priority`);
708
832
  spinner.stop();
709
833
 
710
- const updateBody = { fields: {} };
711
- const hasFlags = options.summary || options.description || options.priority;
834
+ const updateBody: any = { fields: {} };
835
+ const hasFlags = options.summary || options.description || options.priority || (options.custom && options.custom.length > 0);
712
836
 
713
837
  if (hasFlags) {
714
838
  if (options.summary) updateBody.fields.summary = options.summary;
715
839
  if (options.description) updateBody.fields.description = textToADF(options.description);
716
840
  if (options.priority) updateBody.fields.priority = { name: options.priority };
841
+
842
+ if (options.custom && options.custom.length > 0) {
843
+ options.custom.forEach((cf: string) => {
844
+ const [key, ...rest] = cf.split('=');
845
+ const value = rest.join('=');
846
+ if (!key || !value) return;
847
+
848
+ const fieldId = ConfigService.get(`customFields.${key}`) || key;
849
+ const parsedValue = isNaN(Number(value)) ? value : Number(value);
850
+ updateBody.fields[fieldId] = parsedValue;
851
+ });
852
+ }
717
853
  } else {
718
854
  // Interactive: pick which fields to edit
719
855
  console.log(chalk.bold(`\nEditing ${chalk.cyan(issueKey)}: ${issue.fields.summary}\n`));
720
856
 
721
- const { Select, Input } = enquirer;
857
+ const { Select, Input } = enquirer as any;
722
858
 
723
859
  const fieldSelect = new Select({
724
860
  name: 'fields',
@@ -726,7 +862,9 @@ Examples:
726
862
  choices: [
727
863
  { name: 'summary', message: `Summary: ${issue.fields.summary}` },
728
864
  { name: 'description', message: 'Description' },
729
- { name: 'priority', message: `Priority: ${issue.fields.priority?.name || 'None'}` }
865
+ { name: 'priority', message: `Priority: ${issue.fields.priority?.name || 'None'}` },
866
+ { name: 'components', message: `Components: ${(issue.fields.components || []).map((c: any) => c.name).join(', ')}` },
867
+ { name: 'labels', message: `Labels: ${(issue.fields.labels || []).join(', ')}` }
730
868
  ],
731
869
  multiple: true
732
870
  });
@@ -752,10 +890,68 @@ Examples:
752
890
  const prioSelect = new Select({
753
891
  name: 'priority',
754
892
  message: 'Select priority',
755
- choices: priorities.map(p => ({ name: p.name, message: p.name }))
893
+ choices: priorities.map((p: any) => ({ name: p.name, message: p.name }))
756
894
  });
757
895
  updateBody.fields.priority = { name: await prioSelect.run() };
758
896
  }
897
+ if (field === 'components') {
898
+ const components = await api.get(`/project/${issue.fields.project.key}/components`);
899
+ if (components.length > 0) {
900
+ const compSelect = new Select({ // Using Enquirer directly via 'any' above, but actually Select is single select?
901
+ // Wait, fieldSelect was initialized from enquirer as any.
902
+ // Multiselect is needed here.
903
+ name: 'components',
904
+ message: 'Select components',
905
+ multiple: true,
906
+ choices: components.map((c: any) => ({ name: c.id, message: c.name, enabled: (issue.fields.components || []).some((ic: any) => ic.id === c.id) }))
907
+ });
908
+ // Enquirer 'Select' with 'multiple: true' is actually 'MultiSelect'? No, standard Enquirer has 'MultiSelect'.
909
+ // We cast enquirer to any so we can check if MultiSelect exists or use Select with multiple: true (which might not work in all versions).
910
+ // Let's try to use 'MultiSelect' if available, or 'Select' with multiple.
911
+ // Actually, in step 5.5 I used type: 'multiselect'. Here I am instantiating classes.
912
+ // Let's use the prompt method for consistency.
913
+ const { selectedComps } = await enquirer.prompt({
914
+ type: 'multiselect',
915
+ name: 'selectedComps',
916
+ message: 'Select Components:',
917
+ choices: components.map((c: any) => ({
918
+ name: c.id,
919
+ message: c.name,
920
+ initial: (issue.fields.components || []).some((ic: any) => ic.id === c.id) // Enquirer uses 'initial' or 'enabled'? Checks docs... usually 'initial' for multiselect is index or name list?
921
+ // Simple approach: Pre-select not easy without specific logic.
922
+ // Let's just show the list.
923
+ }))
924
+ }) as any;
925
+ updateBody.fields.components = selectedComps.map((id: string) => ({ id }));
926
+ }
927
+ }
928
+ if (field === 'labels') {
929
+ const prompt = new Input({ message: 'New labels (comma separated)', initial: (issue.fields.labels || []).join(', ') });
930
+ const labelStr = await prompt.run();
931
+ updateBody.fields.labels = labelStr.split(',').map((l: string) => l.trim()).filter((l: string) => l.length > 0);
932
+ }
933
+ if (field === 'fixVersions') {
934
+ const versions = await api.get(`/project/${issue.fields.project.key}/versions`);
935
+ const unreleased = versions.filter((v: any) => !v.released);
936
+ if (unreleased.length > 0) {
937
+ const { selectedVersions } = await enquirer.prompt({
938
+ type: 'multiselect',
939
+ name: 'selectedVersions',
940
+ message: 'Select Fix Versions:',
941
+ choices: unreleased.map((v: any) => ({ name: v.id, message: v.name }))
942
+ }) as any;
943
+ updateBody.fields.fixVersions = selectedVersions.map((id: string) => ({ id }));
944
+ }
945
+ }
946
+ if (field === 'duedate') {
947
+ const prompt = new Input({
948
+ message: 'Due Date (YYYY-MM-DD)',
949
+ initial: issue.fields.duedate,
950
+ validate: (val: string) => !val || /^\d{4}-\d{2}-\d{2}$/.test(val) || 'Format must be YYYY-MM-DD'
951
+ });
952
+ const date = await prompt.run();
953
+ updateBody.fields.duedate = date || null;
954
+ }
759
955
  }
760
956
  }
761
957
 
@@ -768,7 +964,7 @@ Examples:
768
964
  await api.put(`/issue/${issueKey}`, updateBody);
769
965
  updateSpinner.succeed(`${chalk.cyan(issueKey)} updated successfully`);
770
966
 
771
- } catch (e) {
967
+ } catch (e: any) {
772
968
  handleCommandError(spinner, e, `Failed to edit ${issueKey}`);
773
969
  }
774
970
  });
@@ -787,7 +983,7 @@ Examples:
787
983
  $ jira issue search "payment" -p PROJ
788
984
  $ jira issue search "crash" --output json
789
985
  `)
790
- .action(async (query, options) => {
986
+ .action(async (query: string, options: any) => {
791
987
  const spinner = ora(`Searching for "${query}"...`).start();
792
988
  try {
793
989
  const jqlParts = [`text ~ "${query.replace(/"/g, '\\"')}"`];
@@ -807,7 +1003,7 @@ Examples:
807
1003
  }
808
1004
 
809
1005
  if (options.output === 'json') {
810
- console.log(JSON.stringify(data.issues.map(i => ({
1006
+ console.log(JSON.stringify(data.issues.map((i: any) => ({
811
1007
  key: i.key, summary: i.fields.summary,
812
1008
  status: i.fields.status?.name, assignee: i.fields.assignee?.displayName || null,
813
1009
  updated: i.fields.updated
@@ -815,18 +1011,18 @@ Examples:
815
1011
  return;
816
1012
  }
817
1013
 
818
- const tableData = [
819
- [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
820
- ];
821
- data.issues.forEach(i => {
822
- tableData.push([
1014
+ const table = new Table({
1015
+ head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
1016
+ });
1017
+ data.issues.forEach((i: any) => {
1018
+ table.push([
823
1019
  chalk.cyan(i.key),
824
1020
  i.fields.summary ? (i.fields.summary.length > 55 ? i.fields.summary.substring(0, 52) + '...' : i.fields.summary) : '',
825
1021
  i.fields.status?.name || '',
826
1022
  i.fields.assignee?.displayName || 'Unassigned'
827
1023
  ]);
828
1024
  });
829
- console.log(table(tableData));
1025
+ console.log(table.toString());
830
1026
  console.log(chalk.grey(`Found ${data.issues.length} result(s)`));
831
1027
 
832
1028
  } catch (e) {
@@ -861,11 +1057,11 @@ Examples:
861
1057
  const linkTypes = await api.get('/issueLinkType');
862
1058
  spinner.stop();
863
1059
 
864
- const { Select } = enquirer;
1060
+ const { Select } = enquirer as any;
865
1061
  const typeSelect = new Select({
866
1062
  name: 'linkType',
867
1063
  message: `Link type: ${chalk.cyan(sourceKey)} → ${chalk.cyan(targetKey)}`,
868
- choices: linkTypes.issueLinkTypes.map(lt => ({
1064
+ choices: linkTypes.issueLinkTypes.map((lt: any) => ({
869
1065
  name: lt.name,
870
1066
  message: `${lt.name} (${lt.inward} / ${lt.outward})`
871
1067
  }))
@@ -921,5 +1117,100 @@ Examples:
921
1117
  }
922
1118
  });
923
1119
 
1120
+ // ── SUBTASK ───────────────────────────────────────────────────────
1121
+ issueCmd
1122
+ .command('subtask')
1123
+ .description('Create a subtask for an existing issue')
1124
+ .argument('<parentKey>', 'Parent Issue Key')
1125
+ .option('-s, --summary <text>', 'Subtask summary')
1126
+ .option('--priority <name>', 'Priority')
1127
+ .option('-a, --assignee <id>', 'Assignee')
1128
+ .addHelpText('after', `
1129
+ Examples:
1130
+ $ jira issue subtask PROJ-123 # Interactive
1131
+ $ jira issue subtask PROJ-123 -s "Dev task"
1132
+ `)
1133
+ .action(async (parentKey: string, options: any) => {
1134
+ const check = validateIssueKey(parentKey);
1135
+ if (!check.valid) { console.error(chalk.red(check.message)); return; }
1136
+
1137
+ const spinner = ora(`Fetching parent ${parentKey}...`).start();
1138
+ try {
1139
+ const parent = await api.get(`/issue/${parentKey}?fields=project,summary`);
1140
+ const projectKey = parent.fields.project.key;
1141
+ spinner.text = 'Fetching subtask types...';
1142
+
1143
+ // Get valid subtask types for project
1144
+ const meta = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
1145
+ const allTypes = meta.issueTypes || meta.values || [];
1146
+ const subtaskTypes = allTypes.filter((t: any) => t.subtask);
1147
+ spinner.stop();
1148
+
1149
+ if (subtaskTypes.length === 0) {
1150
+ console.error(chalk.red(`No subtask types found in project ${projectKey}.`));
1151
+ return;
1152
+ }
1153
+
1154
+ console.log(chalk.bold(`\nParent: ${chalk.cyan(parentKey)} ${parent.fields.summary}`));
1155
+
1156
+ let subtaskTypeId = subtaskTypes[0].id;
1157
+ if (subtaskTypes.length > 1) {
1158
+ const { selectedType } = await enquirer.prompt({
1159
+ type: 'select',
1160
+ name: 'selectedType',
1161
+ message: 'Select Subtask Type:',
1162
+ choices: subtaskTypes.map((t: any) => ({ name: t.id, message: t.name }))
1163
+ }) as any;
1164
+ subtaskTypeId = selectedType;
1165
+ }
1166
+
1167
+ let summary = options.summary;
1168
+ if (!summary) {
1169
+ const { inputSummary } = await enquirer.prompt({
1170
+ type: 'input',
1171
+ name: 'inputSummary',
1172
+ message: 'Subtask Summary:',
1173
+ validate: (val: string) => val.trim().length > 0 || 'Summary required'
1174
+ }) as any;
1175
+ summary = inputSummary;
1176
+ }
1177
+
1178
+ // Optional: Priority
1179
+ let priorityName = options.priority;
1180
+ // Optional: Assignee
1181
+ let assigneeId = options.assignee;
1182
+
1183
+ const issueBody: any = {
1184
+ fields: {
1185
+ project: { key: projectKey },
1186
+ parent: { key: parentKey },
1187
+ issuetype: { id: subtaskTypeId },
1188
+ summary: summary
1189
+ }
1190
+ };
1191
+
1192
+ if (priorityName) issueBody.fields.priority = { name: priorityName };
1193
+ if (assigneeId === 'me') {
1194
+ const me = await api.get('/myself');
1195
+ issueBody.fields.assignee = { accountId: me.accountId };
1196
+ } else if (assigneeId) {
1197
+ issueBody.fields.assignee = { accountId: assigneeId };
1198
+ }
1199
+
1200
+ const createSpinner = ora('Creating subtask...').start();
1201
+ const result = await api.post('/issue', issueBody);
1202
+ createSpinner.succeed(chalk.green(`Subtask created: ${chalk.bold(result.key)}`));
1203
+
1204
+ } catch (e: any) {
1205
+ handleCommandError(spinner, e, 'Failed to create subtask');
1206
+ }
1207
+ });
1208
+
1209
+
1210
+
1211
+ registerWorklogCommand(issueCmd);
1212
+ registerPrCommand(issueCmd);
1213
+ registerAttachCommand(issueCmd);
1214
+
924
1215
  program.addCommand(issueCmd);
925
1216
  }