jira-pilot 2.1.2 → 2.2.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 (114) hide show
  1. package/README.md +54 -0
  2. package/bin/jira.ts +2 -0
  3. package/dist/bin/jira.js +2 -0
  4. package/dist/bin/jira.js.map +1 -1
  5. package/dist/src/commands/ai-actions/plan.js +1 -1
  6. package/dist/src/commands/ai-actions/plan.js.map +1 -1
  7. package/dist/src/commands/ai-actions/review.js +5 -4
  8. package/dist/src/commands/ai-actions/review.js.map +1 -1
  9. package/dist/src/commands/ai-actions/standup.js +1 -1
  10. package/dist/src/commands/ai-actions/standup.js.map +1 -1
  11. package/dist/src/commands/ai.js +1 -1
  12. package/dist/src/commands/ai.js.map +1 -1
  13. package/dist/src/commands/board.js +10 -5
  14. package/dist/src/commands/board.js.map +1 -1
  15. package/dist/src/commands/bulk.js +11 -10
  16. package/dist/src/commands/bulk.js.map +1 -1
  17. package/dist/src/commands/config.js +1 -1
  18. package/dist/src/commands/config.js.map +1 -1
  19. package/dist/src/commands/dashboard.js +19 -12
  20. package/dist/src/commands/dashboard.js.map +1 -1
  21. package/dist/src/commands/filter.js +7 -4
  22. package/dist/src/commands/filter.js.map +1 -1
  23. package/dist/src/commands/git.js +1 -1
  24. package/dist/src/commands/git.js.map +1 -1
  25. package/dist/src/commands/issue-attach.js +1 -1
  26. package/dist/src/commands/issue-attach.js.map +1 -1
  27. package/dist/src/commands/issue-pr.js +1 -1
  28. package/dist/src/commands/issue-pr.js.map +1 -1
  29. package/dist/src/commands/issue-worklog.js +10 -5
  30. package/dist/src/commands/issue-worklog.js.map +1 -1
  31. package/dist/src/commands/issue.js +173 -122
  32. package/dist/src/commands/issue.js.map +1 -1
  33. package/dist/src/commands/project.js +10 -5
  34. package/dist/src/commands/project.js.map +1 -1
  35. package/dist/src/commands/sprint.js +19 -8
  36. package/dist/src/commands/sprint.js.map +1 -1
  37. package/dist/src/commands/tui.d.ts +2 -0
  38. package/dist/src/commands/tui.js +10 -0
  39. package/dist/src/commands/tui.js.map +1 -0
  40. package/dist/src/server/mcp-server.js +209 -27
  41. package/dist/src/server/mcp-server.js.map +1 -1
  42. package/dist/src/services/ai-service.js +7 -4
  43. package/dist/src/services/ai-service.js.map +1 -1
  44. package/dist/src/services/api-service.d.ts +2 -0
  45. package/dist/src/services/api-service.js +32 -20
  46. package/dist/src/services/api-service.js.map +1 -1
  47. package/dist/src/tui/App.d.ts +1 -0
  48. package/dist/src/tui/App.js +26 -0
  49. package/dist/src/tui/App.js.map +1 -0
  50. package/dist/src/tui/index.d.ts +1 -0
  51. package/dist/src/tui/index.js +8 -0
  52. package/dist/src/tui/index.js.map +1 -0
  53. package/dist/src/tui/screens/BoardList.d.ts +1 -0
  54. package/dist/src/tui/screens/BoardList.js +71 -0
  55. package/dist/src/tui/screens/BoardList.js.map +1 -0
  56. package/dist/src/tui/screens/Dashboard.d.ts +1 -0
  57. package/dist/src/tui/screens/Dashboard.js +41 -0
  58. package/dist/src/tui/screens/Dashboard.js.map +1 -0
  59. package/dist/src/tui/screens/IssueDetail.d.ts +6 -0
  60. package/dist/src/tui/screens/IssueDetail.js +40 -0
  61. package/dist/src/tui/screens/IssueDetail.js.map +1 -0
  62. package/dist/src/tui/screens/IssueList.d.ts +1 -0
  63. package/dist/src/tui/screens/IssueList.js +72 -0
  64. package/dist/src/tui/screens/IssueList.js.map +1 -0
  65. package/dist/src/tui/screens/KanbanBoard.d.ts +6 -0
  66. package/dist/src/tui/screens/KanbanBoard.js +86 -0
  67. package/dist/src/tui/screens/KanbanBoard.js.map +1 -0
  68. package/dist/src/tui/utils/adf-render.d.ts +1 -0
  69. package/dist/src/tui/utils/adf-render.js +29 -0
  70. package/dist/src/tui/utils/adf-render.js.map +1 -0
  71. package/dist/src/utils/api-paths.d.ts +31 -0
  72. package/dist/src/utils/api-paths.js +32 -0
  73. package/dist/src/utils/api-paths.js.map +1 -0
  74. package/dist/src/utils/error-handler.d.ts +2 -2
  75. package/dist/src/utils/error-handler.js.map +1 -1
  76. package/dist/src/utils/http.d.ts +27 -0
  77. package/dist/src/utils/http.js +95 -0
  78. package/dist/src/utils/http.js.map +1 -0
  79. package/dist/src/utils/spinner.d.ts +21 -0
  80. package/dist/src/utils/spinner.js +79 -0
  81. package/dist/src/utils/spinner.js.map +1 -0
  82. package/package.json +10 -5
  83. package/src/commands/ai-actions/plan.ts +1 -1
  84. package/src/commands/ai-actions/review.ts +5 -4
  85. package/src/commands/ai-actions/standup.ts +1 -1
  86. package/src/commands/ai.ts +1 -1
  87. package/src/commands/board.ts +10 -5
  88. package/src/commands/bulk.ts +11 -10
  89. package/src/commands/config.ts +1 -1
  90. package/src/commands/dashboard.ts +20 -12
  91. package/src/commands/filter.ts +8 -5
  92. package/src/commands/git.ts +1 -1
  93. package/src/commands/issue-attach.ts +1 -1
  94. package/src/commands/issue-pr.ts +1 -1
  95. package/src/commands/issue-worklog.ts +10 -5
  96. package/src/commands/issue.ts +181 -124
  97. package/src/commands/project.ts +10 -5
  98. package/src/commands/sprint.ts +19 -8
  99. package/src/commands/tui.ts +11 -0
  100. package/src/server/mcp-server.ts +234 -27
  101. package/src/services/ai-service.ts +7 -4
  102. package/src/services/api-service.ts +34 -21
  103. package/src/tui/App.tsx +61 -0
  104. package/src/tui/index.tsx +8 -0
  105. package/src/tui/screens/BoardList.tsx +102 -0
  106. package/src/tui/screens/Dashboard.tsx +75 -0
  107. package/src/tui/screens/IssueDetail.tsx +93 -0
  108. package/src/tui/screens/IssueList.tsx +116 -0
  109. package/src/tui/screens/KanbanBoard.tsx +133 -0
  110. package/src/tui/utils/adf-render.ts +30 -0
  111. package/src/utils/api-paths.ts +32 -0
  112. package/src/utils/error-handler.ts +2 -2
  113. package/src/utils/http.ts +128 -0
  114. package/src/utils/spinner.ts +87 -0
@@ -1,9 +1,9 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import Table from 'cli-table3';
3
+ import { Table } from 'cmd-table';
4
4
  import { api } from '../services/api-service.js';
5
5
  import { aiService } from '../services/ai-service.js';
6
- import ora from 'ora';
6
+ import ora from '../utils/spinner.js';
7
7
  import enquirer from 'enquirer';
8
8
  import { parseADF } from '../utils/adf-parser.js';
9
9
  import { textToADF } from '../utils/text-to-adf.js';
@@ -14,6 +14,7 @@ import { registerWorklogCommand } from './issue-worklog.js';
14
14
  import { registerPrCommand } from './issue-pr.js';
15
15
  import { registerAttachCommand } from './issue-attach.js';
16
16
  import { ConfigService } from '../services/config-service.js';
17
+ import { API } from '../utils/api-paths.js';
17
18
 
18
19
  export function registerIssueCommand(program: Command) {
19
20
  const issueCmd = new Command('issue')
@@ -68,23 +69,19 @@ Examples:
68
69
  if (options.jql) jqlParts.push(options.jql);
69
70
 
70
71
  // Order by updated desc by default if no JQL
71
- if (!options.jql && jqlParts.length === 0) {
72
- jqlParts.push('order by updated DESC');
73
- } else if (jqlParts.length > 0 && !options.jql) {
74
- // Add order if not custom jql
75
- // jqlParts.push('order by updated DESC');
76
- }
77
-
78
72
  const jql = jqlParts.join(' AND ');
79
73
 
80
- const searchApi = '/search/jql';
74
+ // Default to last 30 days if no filter provided to satisfy "unbounded" check
75
+ const defaultJql = 'updated >= -30d ORDER BY updated DESC';
76
+ const finalJql = jql || defaultJql;
77
+
81
78
  const body = {
82
- jql: jql || 'created is not empty',
79
+ jql: finalJql,
83
80
  maxResults: parseInt(options.limit),
84
- fields: ['summary', 'status', 'assignee', 'created', 'updated', 'description']
81
+ fields: ['summary', 'status', 'assignee', 'created', 'updated', 'description', 'priority', 'issuetype', 'project', 'reporter']
85
82
  };
86
83
 
87
- const data = await api.post(searchApi, body);
84
+ const data = await api.post(API.SEARCH.JQL, body);
88
85
  spinner.stop();
89
86
 
90
87
  if (!data.issues || data.issues.length === 0) {
@@ -134,11 +131,18 @@ Examples:
134
131
  }
135
132
 
136
133
  const table = new Table({
137
- head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Created'), chalk.bold('Updated')]
134
+ columns: [
135
+ { name: chalk.bold('Key') },
136
+ { name: chalk.bold('Summary') },
137
+ { name: chalk.bold('Status') },
138
+ { name: chalk.bold('Assignee') },
139
+ { name: chalk.bold('Created') },
140
+ { name: chalk.bold('Updated') }
141
+ ]
138
142
  });
139
143
 
140
144
  data.issues.forEach((i: any) => {
141
- table.push([
145
+ table.addRow([
142
146
  chalk.cyan(i.key),
143
147
  i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
144
148
  i.fields.status ? i.fields.status.name : '',
@@ -148,7 +152,7 @@ Examples:
148
152
  ]);
149
153
  });
150
154
 
151
- console.log(table.toString());
155
+ console.log(table.render());
152
156
 
153
157
  } catch (e: any) {
154
158
  handleCommandError(spinner, e, 'Failed to list issues');
@@ -170,7 +174,7 @@ Examples:
170
174
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
171
175
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
172
176
  try {
173
- const issue = await api.get(`/issue/${issueKey}`);
177
+ const issue = await api.get(API.ISSUE.GET(issueKey));
174
178
  spinner.stop();
175
179
 
176
180
  if (options.output === 'json') {
@@ -213,7 +217,7 @@ Examples:
213
217
  if (issue.fields.comment && issue.fields.comment.comments.length > 0) {
214
218
  console.log(chalk.bold('\nComments:'));
215
219
  issue.fields.comment.comments.forEach((c: any) => {
216
- console.log(chalk.cyan(c.author.displayName) + ': ' + c.body);
220
+ console.log(chalk.cyan(c.author.displayName) + ': ' + (parseADF(c.body) || ''));
217
221
  });
218
222
  }
219
223
  console.log('');
@@ -232,6 +236,11 @@ Examples:
232
236
  .option('-d, --description <text>', 'Issue description')
233
237
  .option('--priority <name>', 'Priority name (e.g., High, Medium, Low)')
234
238
  .option('-a, --assignee <id>', 'Assignee account ID (use "me" for self)')
239
+ .option('-l, --labels <list>', 'Labels (comma separated)')
240
+ .option('-c, --components <list>', 'Component IDs (comma separated)', (v: string, l: string[]) => l.concat([v]), [])
241
+ .option('--fix-versions <list>', 'Fix Version IDs (comma separated)', (v: string, l: string[]) => l.concat([v]), [])
242
+ .option('--due-date <date>', 'Due Date (YYYY-MM-DD)')
243
+ .option('--no-input', 'Disable interactive prompts for optional fields')
235
244
  .option('--custom <key=value>', 'Custom fields (key=value, repeatable)', (v: string, l: string[]) => l.concat([v]), [])
236
245
  .addHelpText('after', `
237
246
  Examples:
@@ -248,7 +257,7 @@ Examples:
248
257
  let projectKey = options.project;
249
258
  if (!projectKey) {
250
259
  const spinner = ora('Fetching projects...').start();
251
- const projectData = await api.get('/project/search');
260
+ const projectData = await api.get(API.PROJECT.SEARCH);
252
261
  spinner.stop();
253
262
 
254
263
  if (!projectData.values || projectData.values.length === 0) {
@@ -277,12 +286,12 @@ Examples:
277
286
  let issueTypes = [];
278
287
  try {
279
288
  // Jira Cloud v3 - createmeta endpoint
280
- const metaData = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
289
+ const metaData = await api.get(API.ISSUE.CREATEMETA(projectKey));
281
290
  issueTypes = metaData.issueTypes || metaData.values || [];
282
291
  } catch (metaErr) {
283
292
  // Fallback: use project-level issue types
284
293
  try {
285
- const projectInfo = await api.get(`/project/${projectKey}`);
294
+ const projectInfo = await api.get(API.PROJECT.GET(projectKey));
286
295
  issueTypes = projectInfo.issueTypes || [];
287
296
  } catch {
288
297
  issueTypes = [
@@ -339,10 +348,10 @@ Examples:
339
348
 
340
349
  // ── Step 5: Priority ────────────────────────────────
341
350
  let priorityName = options.priority;
342
- if (!priorityName) {
351
+ if (!priorityName && !options.noInput) {
343
352
  const spinner = ora('Fetching priorities...').start();
344
353
  try {
345
- const priorities = await api.get('/priority');
354
+ const priorities = await api.get(API.PRIORITY.ALL);
346
355
  spinner.stop();
347
356
 
348
357
  if (Array.isArray(priorities) && priorities.length > 0) {
@@ -366,73 +375,87 @@ Examples:
366
375
  }
367
376
 
368
377
  // ── 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;
378
+ let componentIds: string[] = options.components || [];
379
+ // Interactive only if components not provided and input allowed
380
+ if (componentIds.length === 0 && !options.noInput) {
381
+ const compSpinner = ora('Fetching components...').start();
382
+ try {
383
+ const components = await api.get(API.PROJECT.COMPONENTS(projectKey));
384
+ compSpinner.stop();
385
+
386
+ if (Array.isArray(components) && components.length > 0) {
387
+ const { selectedComponents } = await enquirer.prompt({
388
+ type: 'multiselect',
389
+ name: 'selectedComponents',
390
+ message: 'Select Components (Space to select, Enter to confirm):',
391
+ choices: components.map((c: any) => ({ name: c.id, message: c.name }))
392
+ }) as any;
393
+ componentIds = selectedComponents;
394
+ }
395
+ } catch {
396
+ compSpinner.stop();
384
397
  }
385
- } catch {
386
- compSpinner.stop();
387
398
  }
388
399
 
389
400
  // ── Step 5.6: Labels ────────────────────────────────
390
401
  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);
402
+ if (options.labels) {
403
+ labels = options.labels.split(',').map((l: string) => l.trim()).filter((l: string) => l.length > 0);
404
+ }
405
+
406
+ if (labels.length === 0 && !options.noInput) {
407
+ const { inputLabels } = await enquirer.prompt({
408
+ type: 'input',
409
+ name: 'inputLabels',
410
+ message: 'Labels (comma-separated, optional):'
411
+ }) as any;
412
+
413
+ if (inputLabels && inputLabels.trim().length > 0) {
414
+ labels = inputLabels.split(',').map((l: string) => l.trim()).filter((l: string) => l.length > 0);
415
+ }
399
416
  }
400
417
 
401
418
  // ── 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
+ let fixVersionIds: string[] = options.fixVersions || [];
420
+
421
+ if (fixVersionIds.length === 0 && !options.noInput) {
422
+ const verSpinner = ora('Fetching versions...').start();
423
+ try {
424
+ const versions = await api.get(API.PROJECT.VERSIONS(projectKey));
425
+ verSpinner.stop();
426
+
427
+ // Filter unreleased versions usually
428
+ const unreleased = versions.filter((v: any) => !v.released);
429
+
430
+ if (Array.isArray(unreleased) && unreleased.length > 0) {
431
+ const { selectedVersions } = await enquirer.prompt({
432
+ type: 'multiselect',
433
+ name: 'selectedVersions',
434
+ message: 'Fix Versions:',
435
+ choices: unreleased.map((v: any) => ({ name: v.id, message: v.name }))
436
+ }) as any;
437
+ fixVersionIds = selectedVersions;
438
+ }
439
+ } catch {
440
+ verSpinner.stop();
419
441
  }
420
- } catch {
421
- verSpinner.stop();
422
442
  }
423
443
 
424
444
  // ── 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;
445
+ let duedate: string | null = options.dueDate || null;
446
+
447
+ if (!duedate && !options.noInput) {
448
+ const { inputDueDate } = await enquirer.prompt({
449
+ type: 'input',
450
+ name: 'inputDueDate',
451
+ message: 'Due Date (YYYY-MM-DD, optional):',
452
+ validate: (val: string) => {
453
+ if (!val) return true;
454
+ return /^\d{4}-\d{2}-\d{2}$/.test(val) || 'Format must be YYYY-MM-DD';
455
+ }
456
+ }) as any;
457
+ if (inputDueDate) duedate = inputDueDate;
458
+ }
436
459
 
437
460
  // ── Step 6: Assignee ────────────────────────────────
438
461
  let assigneeId = options.assignee;
@@ -451,7 +474,7 @@ Examples:
451
474
  if (assigneeChoice === 'me') {
452
475
  const spinner = ora('Fetching your account...').start();
453
476
  try {
454
- const myself = await api.get('/myself');
477
+ const myself = await api.get(API.USER.MYSELF);
455
478
  assigneeId = myself.accountId;
456
479
  spinner.stop();
457
480
  } catch {
@@ -468,7 +491,7 @@ Examples:
468
491
  if (searchQuery.trim()) {
469
492
  const spinner = ora('Searching users...').start();
470
493
  try {
471
- const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
494
+ const users = await api.get(`${API.USER.SEARCH}?query=${encodeURIComponent(searchQuery)}`);
472
495
  spinner.stop();
473
496
 
474
497
  if (Array.isArray(users) && users.length > 0) {
@@ -500,7 +523,7 @@ Examples:
500
523
  // --assignee me flag: resolve to account ID
501
524
  const spinner = ora('Fetching your account...').start();
502
525
  try {
503
- const myself = await api.get('/myself');
526
+ const myself = await api.get(API.USER.MYSELF);
504
527
  assigneeId = myself.accountId;
505
528
  spinner.stop();
506
529
  } catch {
@@ -510,25 +533,27 @@ Examples:
510
533
  }
511
534
 
512
535
  // ── Confirmation ────────────────────────────────────
513
- console.log(chalk.blue('\n── Issue Summary ──────────────────'));
514
- console.log(` Project: ${chalk.cyan(projectKey)}`);
515
- console.log(` Type: ${issueTypeName}`);
516
- console.log(` Summary: ${summary}`);
517
- console.log(` Description: ${description || chalk.grey('(none)')}`);
518
- console.log(` Priority: ${priorityName || chalk.grey('(default)')}`);
519
- console.log(` Assignee: ${assigneeId || chalk.grey('Unassigned')}`);
520
- console.log(chalk.blue('──────────────────────────────────\n'));
521
-
522
- const { confirmed } = await enquirer.prompt({
523
- type: 'confirm',
524
- name: 'confirmed',
525
- message: 'Create this issue?',
526
- initial: true
527
- }) as any;
528
-
529
- if (!confirmed) {
530
- console.log(chalk.yellow('Issue creation cancelled.'));
531
- return;
536
+ if (!options.noInput) {
537
+ console.log(chalk.blue('\n── Issue Summary ──────────────────'));
538
+ console.log(` Project: ${chalk.cyan(projectKey)}`);
539
+ console.log(` Type: ${issueTypeName}`);
540
+ console.log(` Summary: ${summary}`);
541
+ console.log(` Description: ${description || chalk.grey('(none)')}`);
542
+ console.log(` Priority: ${priorityName || chalk.grey('(default)')}`);
543
+ console.log(` Assignee: ${assigneeId || chalk.grey('Unassigned')}`);
544
+ console.log(chalk.blue('──────────────────────────────────\n'));
545
+
546
+ const { confirmed } = await enquirer.prompt({
547
+ type: 'confirm',
548
+ name: 'confirmed',
549
+ message: 'Create this issue?',
550
+ initial: true
551
+ }) as any;
552
+
553
+ if (!confirmed) {
554
+ console.log(chalk.yellow('Issue creation cancelled.'));
555
+ return;
556
+ }
532
557
  }
533
558
 
534
559
  // ── Build Request Body ──────────────────────────────
@@ -583,7 +608,7 @@ Examples:
583
608
 
584
609
  // ── Create Issue ────────────────────────────────────
585
610
  const spinner = ora('Creating issue...').start();
586
- const result = await api.post('/issue', issueBody);
611
+ const result = await api.post(API.ISSUE.BASE, issueBody);
587
612
  spinner.succeed(chalk.green(`Issue created: ${chalk.bold(result.key)}`));
588
613
 
589
614
  console.log(chalk.grey(`View it: jira issue view ${result.key}`));
@@ -611,11 +636,11 @@ Examples:
611
636
  const spinner = ora(`Fetching transitions for ${issueKey}...`).start();
612
637
  try {
613
638
  // Fetch current issue to show context
614
- const issue = await api.get(`/issue/${issueKey}?fields=summary,status`);
639
+ const issue = await api.get(`${API.ISSUE.GET(issueKey)}?fields=summary,status`);
615
640
  const currentStatus = issue.fields.status.name;
616
641
 
617
642
  // Fetch available transitions
618
- const transData = await api.get(`/issue/${issueKey}/transitions`);
643
+ const transData = await api.get(API.ISSUE.TRANSITIONS(issueKey));
619
644
  spinner.stop();
620
645
 
621
646
  if (!transData.transitions || transData.transitions.length === 0) {
@@ -662,7 +687,7 @@ Examples:
662
687
 
663
688
  // Execute transition
664
689
  const execSpinner = ora(`Transitioning to "${targetTransition.to.name}"...`).start();
665
- await api.post(`/issue/${issueKey}/transitions`, {
690
+ await api.post(API.ISSUE.TRANSITIONS(issueKey), {
666
691
  transition: { id: targetTransition.id }
667
692
  });
668
693
  execSpinner.succeed(chalk.green(`${issueKey} transitioned: ${currentStatus} → ${chalk.bold(targetTransition.to.name)}`));
@@ -693,7 +718,7 @@ Examples:
693
718
  if (!assigneeId) {
694
719
  // Interactive selection
695
720
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
696
- const issue = await api.get(`/issue/${issueKey}?fields=summary,assignee`);
721
+ const issue = await api.get(`${API.ISSUE.GET(issueKey)}?fields=summary,assignee`);
697
722
  spinner.stop();
698
723
 
699
724
  const currentAssignee = issue.fields.assignee?.displayName || 'Unassigned';
@@ -715,7 +740,7 @@ Examples:
715
740
 
716
741
  if (assigneeId === 'me') {
717
742
  const spinner = ora('Fetching your account...').start();
718
- const myself = await api.get('/myself');
743
+ const myself = await api.get(API.USER.MYSELF);
719
744
  assigneeId = myself.accountId;
720
745
  spinner.stop();
721
746
  }
@@ -728,7 +753,7 @@ Examples:
728
753
  }) as any;
729
754
 
730
755
  const spinner = ora('Searching users...').start();
731
- const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
756
+ const users = await api.get(`${API.USER.SEARCH}?query=${encodeURIComponent(searchQuery)}`);
732
757
  spinner.stop();
733
758
 
734
759
  if (!Array.isArray(users) || users.length === 0) {
@@ -753,7 +778,7 @@ Examples:
753
778
  ? { accountId: null }
754
779
  : { accountId: assigneeId };
755
780
 
756
- await api.put(`/issue/${issueKey}/assignee`, body);
781
+ await api.put(API.ISSUE.ASSIGNEE(issueKey), body);
757
782
  spinner.succeed(chalk.green(`${issueKey} ${assigneeId === 'none' ? 'unassigned' : 'assigned'} successfully.`));
758
783
 
759
784
  } catch (e: any) {
@@ -798,7 +823,7 @@ Examples:
798
823
  }
799
824
 
800
825
  const spinner = ora('Adding comment...').start();
801
- await api.post(`/issue/${issueKey}/comment`, {
826
+ await api.post(API.ISSUE.COMMENT(issueKey), {
802
827
  body: textToADF(commentText)
803
828
  });
804
829
  spinner.succeed(chalk.green(`Comment added to ${issueKey}.`));
@@ -828,7 +853,7 @@ Examples:
828
853
  if (!check.valid) { console.error(chalk.red(check.message)); return; }
829
854
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
830
855
  try {
831
- const issue = await api.get(`/issue/${issueKey}?fields=summary,description,priority`);
856
+ const issue = await api.get(`${API.ISSUE.GET(issueKey)}?fields=summary,description,priority`);
832
857
  spinner.stop();
833
858
 
834
859
  const updateBody: any = { fields: {} };
@@ -886,7 +911,7 @@ Examples:
886
911
  if (desc) updateBody.fields.description = textToADF(desc);
887
912
  }
888
913
  if (field === 'priority') {
889
- const priorities = await api.get('/priority');
914
+ const priorities = await api.get(API.PRIORITY.ALL);
890
915
  const prioSelect = new Select({
891
916
  name: 'priority',
892
917
  message: 'Select priority',
@@ -895,7 +920,7 @@ Examples:
895
920
  updateBody.fields.priority = { name: await prioSelect.run() };
896
921
  }
897
922
  if (field === 'components') {
898
- const components = await api.get(`/project/${issue.fields.project.key}/components`);
923
+ const components = await api.get(API.PROJECT.COMPONENTS(issue.fields.project.key));
899
924
  if (components.length > 0) {
900
925
  const compSelect = new Select({ // Using Enquirer directly via 'any' above, but actually Select is single select?
901
926
  // Wait, fieldSelect was initialized from enquirer as any.
@@ -931,7 +956,7 @@ Examples:
931
956
  updateBody.fields.labels = labelStr.split(',').map((l: string) => l.trim()).filter((l: string) => l.length > 0);
932
957
  }
933
958
  if (field === 'fixVersions') {
934
- const versions = await api.get(`/project/${issue.fields.project.key}/versions`);
959
+ const versions = await api.get(API.PROJECT.VERSIONS(issue.fields.project.key));
935
960
  const unreleased = versions.filter((v: any) => !v.released);
936
961
  if (unreleased.length > 0) {
937
962
  const { selectedVersions } = await enquirer.prompt({
@@ -961,7 +986,7 @@ Examples:
961
986
  }
962
987
 
963
988
  const updateSpinner = ora('Updating issue...').start();
964
- await api.put(`/issue/${issueKey}`, updateBody);
989
+ await api.put(API.ISSUE.GET(issueKey), updateBody);
965
990
  updateSpinner.succeed(`${chalk.cyan(issueKey)} updated successfully`);
966
991
 
967
992
  } catch (e: any) {
@@ -990,7 +1015,7 @@ Examples:
990
1015
  if (options.project) jqlParts.push(`project = "${options.project}"`);
991
1016
  const jql = jqlParts.join(' AND ') + ' ORDER BY updated DESC';
992
1017
 
993
- const data = await api.post('/search/jql', {
1018
+ const data = await api.post(API.SEARCH.JQL, {
994
1019
  jql,
995
1020
  maxResults: parseInt(options.limit),
996
1021
  fields: ['summary', 'status', 'assignee', 'updated']
@@ -1012,17 +1037,22 @@ Examples:
1012
1037
  }
1013
1038
 
1014
1039
  const table = new Table({
1015
- head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
1040
+ columns: [
1041
+ { name: chalk.bold('Key') },
1042
+ { name: chalk.bold('Summary') },
1043
+ { name: chalk.bold('Status') },
1044
+ { name: chalk.bold('Assignee') }
1045
+ ]
1016
1046
  });
1017
1047
  data.issues.forEach((i: any) => {
1018
- table.push([
1048
+ table.addRow([
1019
1049
  chalk.cyan(i.key),
1020
1050
  i.fields.summary ? (i.fields.summary.length > 55 ? i.fields.summary.substring(0, 52) + '...' : i.fields.summary) : '',
1021
1051
  i.fields.status?.name || '',
1022
1052
  i.fields.assignee?.displayName || 'Unassigned'
1023
1053
  ]);
1024
1054
  });
1025
- console.log(table.toString());
1055
+ console.log(table.render());
1026
1056
  console.log(chalk.grey(`Found ${data.issues.length} result(s)`));
1027
1057
 
1028
1058
  } catch (e) {
@@ -1136,14 +1166,39 @@ Examples:
1136
1166
 
1137
1167
  const spinner = ora(`Fetching parent ${parentKey}...`).start();
1138
1168
  try {
1139
- const parent = await api.get(`/issue/${parentKey}?fields=project,summary`);
1169
+ const parent = await api.get(`/issue/${parentKey}?fields=project,summary,issuetype,id`);
1140
1170
  const projectKey = parent.fields.project.key;
1171
+
1172
+ if (parent.fields.issuetype.subtask) {
1173
+ spinner.fail(chalk.red(`Issue ${parentKey} is already a subtask. Cannot create a subtask of a subtask.`));
1174
+ return;
1175
+ }
1176
+
1177
+ if (parent.fields.issuetype.name === 'Epic') {
1178
+ spinner.fail(chalk.red(`Issue ${parentKey} is an Epic. Epics cannot have sub-tasks.`));
1179
+ console.log(chalk.yellow('Tip: To add work to an Epic, create a standard issue (Story, Task) and link it to the Epic.'));
1180
+ return;
1181
+ }
1182
+
1141
1183
  spinner.text = 'Fetching subtask types...';
1142
1184
 
1143
1185
  // 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);
1186
+ let subtaskTypes: any[] = [];
1187
+ try {
1188
+ // Correct V3 endpoint for creation metadata
1189
+ const meta = await api.get(`/issue/createmeta?projectKeys=${projectKey}`);
1190
+ if (meta.projects && meta.projects.length > 0) {
1191
+ subtaskTypes = meta.projects[0].issuetypes.filter((t: any) => t.subtask);
1192
+ }
1193
+ } catch (err) {
1194
+ // Fallback to project fetch
1195
+ try {
1196
+ const proj = await api.get(API.PROJECT.GET(projectKey));
1197
+ subtaskTypes = (proj.issueTypes || []).filter((t: any) => t.subtask);
1198
+ } catch (e) {
1199
+ console.error(chalk.red('Failed to fetch project issue types.'));
1200
+ }
1201
+ }
1147
1202
  spinner.stop();
1148
1203
 
1149
1204
  if (subtaskTypes.length === 0) {
@@ -1183,13 +1238,15 @@ Examples:
1183
1238
  const issueBody: any = {
1184
1239
  fields: {
1185
1240
  project: { key: projectKey },
1186
- parent: { key: parentKey },
1241
+ parent: { id: parent.id }, // Use ID instead of Key
1187
1242
  issuetype: { id: subtaskTypeId },
1188
1243
  summary: summary
1189
1244
  }
1190
1245
  };
1191
1246
 
1192
1247
  if (priorityName) issueBody.fields.priority = { name: priorityName };
1248
+ // ... rest of assignee logic ...
1249
+
1193
1250
  if (assigneeId === 'me') {
1194
1251
  const me = await api.get('/myself');
1195
1252
  issueBody.fields.assignee = { accountId: me.accountId };
@@ -1198,7 +1255,7 @@ Examples:
1198
1255
  }
1199
1256
 
1200
1257
  const createSpinner = ora('Creating subtask...').start();
1201
- const result = await api.post('/issue', issueBody);
1258
+ const result = await api.post(API.ISSUE.BASE, issueBody);
1202
1259
  createSpinner.succeed(chalk.green(`Subtask created: ${chalk.bold(result.key)}`));
1203
1260
 
1204
1261
  } catch (e: any) {
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import Table from 'cli-table3';
3
+ import { Table } from 'cmd-table';
4
4
  import { api } from '../services/api-service.js';
5
- import ora from 'ora';
5
+ import ora from '../utils/spinner.js';
6
6
  import { handleCommandError } from '../utils/error-handler.js';
7
7
 
8
8
  export function registerProjectCommand(program: Command) {
@@ -37,11 +37,16 @@ Common Actions:
37
37
  }
38
38
 
39
39
  const table = new Table({
40
- head: [chalk.bold('Key'), chalk.bold('Name'), chalk.bold('Leader'), chalk.bold('Style')]
40
+ columns: [
41
+ { name: chalk.bold('Key') },
42
+ { name: chalk.bold('Name') },
43
+ { name: chalk.bold('Leader') },
44
+ { name: chalk.bold('Style') }
45
+ ]
41
46
  });
42
47
 
43
48
  data.values.forEach((p: any) => {
44
- table.push([
49
+ table.addRow([
45
50
  chalk.cyan(p.key),
46
51
  p.name,
47
52
  p.lead ? p.lead.displayName : 'N/A',
@@ -49,7 +54,7 @@ Common Actions:
49
54
  ]);
50
55
  });
51
56
 
52
- console.log(table.toString());
57
+ console.log(table.render());
53
58
  } catch (e: any) {
54
59
  handleCommandError(spinner, e, 'Failed to list projects');
55
60
  }