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';
@@ -13,6 +13,7 @@ import { registerWorklogCommand } from './issue-worklog.js';
13
13
  import { registerPrCommand } from './issue-pr.js';
14
14
  import { registerAttachCommand } from './issue-attach.js';
15
15
  import { ConfigService } from '../services/config-service.js';
16
+ import { API } from '../utils/api-paths.js';
16
17
  export function registerIssueCommand(program) {
17
18
  const issueCmd = new Command('issue')
18
19
  .description('Manage Jira issues')
@@ -68,21 +69,16 @@ Examples:
68
69
  if (options.jql)
69
70
  jqlParts.push(options.jql);
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
- }
74
- else if (jqlParts.length > 0 && !options.jql) {
75
- // Add order if not custom jql
76
- // jqlParts.push('order by updated DESC');
77
- }
78
72
  const jql = jqlParts.join(' AND ');
79
- const searchApi = '/search/jql';
73
+ // Default to last 30 days if no filter provided to satisfy "unbounded" check
74
+ const defaultJql = 'updated >= -30d ORDER BY updated DESC';
75
+ const finalJql = jql || defaultJql;
80
76
  const body = {
81
- jql: jql || 'created is not empty',
77
+ jql: finalJql,
82
78
  maxResults: parseInt(options.limit),
83
- fields: ['summary', 'status', 'assignee', 'created', 'updated', 'description']
79
+ fields: ['summary', 'status', 'assignee', 'created', 'updated', 'description', 'priority', 'issuetype', 'project', 'reporter']
84
80
  };
85
- const data = await api.post(searchApi, body);
81
+ const data = await api.post(API.SEARCH.JQL, body);
86
82
  spinner.stop();
87
83
  if (!data.issues || data.issues.length === 0) {
88
84
  console.log(chalk.yellow('No issues found.'));
@@ -124,10 +120,17 @@ Examples:
124
120
  return;
125
121
  }
126
122
  const table = new Table({
127
- head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Created'), chalk.bold('Updated')]
123
+ columns: [
124
+ { name: chalk.bold('Key') },
125
+ { name: chalk.bold('Summary') },
126
+ { name: chalk.bold('Status') },
127
+ { name: chalk.bold('Assignee') },
128
+ { name: chalk.bold('Created') },
129
+ { name: chalk.bold('Updated') }
130
+ ]
128
131
  });
129
132
  data.issues.forEach((i) => {
130
- table.push([
133
+ table.addRow([
131
134
  chalk.cyan(i.key),
132
135
  i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
133
136
  i.fields.status ? i.fields.status.name : '',
@@ -136,7 +139,7 @@ Examples:
136
139
  i.fields.updated ? i.fields.updated.split('T')[0] : ''
137
140
  ]);
138
141
  });
139
- console.log(table.toString());
142
+ console.log(table.render());
140
143
  }
141
144
  catch (e) {
142
145
  handleCommandError(spinner, e, 'Failed to list issues');
@@ -160,7 +163,7 @@ Examples:
160
163
  }
161
164
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
162
165
  try {
163
- const issue = await api.get(`/issue/${issueKey}`);
166
+ const issue = await api.get(API.ISSUE.GET(issueKey));
164
167
  spinner.stop();
165
168
  if (options.output === 'json') {
166
169
  console.log(JSON.stringify({
@@ -195,7 +198,7 @@ Examples:
195
198
  if (issue.fields.comment && issue.fields.comment.comments.length > 0) {
196
199
  console.log(chalk.bold('\nComments:'));
197
200
  issue.fields.comment.comments.forEach((c) => {
198
- console.log(chalk.cyan(c.author.displayName) + ': ' + c.body);
201
+ console.log(chalk.cyan(c.author.displayName) + ': ' + (parseADF(c.body) || ''));
199
202
  });
200
203
  }
201
204
  console.log('');
@@ -214,6 +217,11 @@ Examples:
214
217
  .option('-d, --description <text>', 'Issue description')
215
218
  .option('--priority <name>', 'Priority name (e.g., High, Medium, Low)')
216
219
  .option('-a, --assignee <id>', 'Assignee account ID (use "me" for self)')
220
+ .option('-l, --labels <list>', 'Labels (comma separated)')
221
+ .option('-c, --components <list>', 'Component IDs (comma separated)', (v, l) => l.concat([v]), [])
222
+ .option('--fix-versions <list>', 'Fix Version IDs (comma separated)', (v, l) => l.concat([v]), [])
223
+ .option('--due-date <date>', 'Due Date (YYYY-MM-DD)')
224
+ .option('--no-input', 'Disable interactive prompts for optional fields')
217
225
  .option('--custom <key=value>', 'Custom fields (key=value, repeatable)', (v, l) => l.concat([v]), [])
218
226
  .addHelpText('after', `
219
227
  Examples:
@@ -230,7 +238,7 @@ Examples:
230
238
  let projectKey = options.project;
231
239
  if (!projectKey) {
232
240
  const spinner = ora('Fetching projects...').start();
233
- const projectData = await api.get('/project/search');
241
+ const projectData = await api.get(API.PROJECT.SEARCH);
234
242
  spinner.stop();
235
243
  if (!projectData.values || projectData.values.length === 0) {
236
244
  console.error(chalk.red('No projects found. Check your permissions.'));
@@ -255,13 +263,13 @@ Examples:
255
263
  let issueTypes = [];
256
264
  try {
257
265
  // Jira Cloud v3 - createmeta endpoint
258
- const metaData = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
266
+ const metaData = await api.get(API.ISSUE.CREATEMETA(projectKey));
259
267
  issueTypes = metaData.issueTypes || metaData.values || [];
260
268
  }
261
269
  catch (metaErr) {
262
270
  // Fallback: use project-level issue types
263
271
  try {
264
- const projectInfo = await api.get(`/project/${projectKey}`);
272
+ const projectInfo = await api.get(API.PROJECT.GET(projectKey));
265
273
  issueTypes = projectInfo.issueTypes || [];
266
274
  }
267
275
  catch {
@@ -313,10 +321,10 @@ Examples:
313
321
  }
314
322
  // ── Step 5: Priority ────────────────────────────────
315
323
  let priorityName = options.priority;
316
- if (!priorityName) {
324
+ if (!priorityName && !options.noInput) {
317
325
  const spinner = ora('Fetching priorities...').start();
318
326
  try {
319
- const priorities = await api.get('/priority');
327
+ const priorities = await api.get(API.PRIORITY.ALL);
320
328
  spinner.stop();
321
329
  if (Array.isArray(priorities) && priorities.length > 0) {
322
330
  const priorityChoices = priorities.map((p) => ({
@@ -338,70 +346,81 @@ Examples:
338
346
  }
339
347
  }
340
348
  // ── Step 5.5: Components ────────────────────────────
341
- let componentIds = [];
342
- // Interactive only for now (TODO: add flags)
343
- const compSpinner = ora('Fetching components...').start();
344
- try {
345
- const components = await api.get(`/project/${projectKey}/components`);
346
- compSpinner.stop();
347
- if (Array.isArray(components) && components.length > 0) {
348
- const { selectedComponents } = await enquirer.prompt({
349
- type: 'multiselect',
350
- name: 'selectedComponents',
351
- message: 'Select Components (Space to select, Enter to confirm):',
352
- choices: components.map((c) => ({ name: c.id, message: c.name }))
353
- });
354
- componentIds = selectedComponents;
349
+ let componentIds = options.components || [];
350
+ // Interactive only if components not provided and input allowed
351
+ if (componentIds.length === 0 && !options.noInput) {
352
+ const compSpinner = ora('Fetching components...').start();
353
+ try {
354
+ const components = await api.get(API.PROJECT.COMPONENTS(projectKey));
355
+ compSpinner.stop();
356
+ if (Array.isArray(components) && components.length > 0) {
357
+ const { selectedComponents } = await enquirer.prompt({
358
+ type: 'multiselect',
359
+ name: 'selectedComponents',
360
+ message: 'Select Components (Space to select, Enter to confirm):',
361
+ choices: components.map((c) => ({ name: c.id, message: c.name }))
362
+ });
363
+ componentIds = selectedComponents;
364
+ }
365
+ }
366
+ catch {
367
+ compSpinner.stop();
355
368
  }
356
- }
357
- catch {
358
- compSpinner.stop();
359
369
  }
360
370
  // ── Step 5.6: Labels ────────────────────────────────
361
371
  let labels = [];
362
- const { inputLabels } = await enquirer.prompt({
363
- type: 'input',
364
- name: 'inputLabels',
365
- message: 'Labels (comma-separated, optional):'
366
- });
367
- if (inputLabels && inputLabels.trim().length > 0) {
368
- labels = inputLabels.split(',').map((l) => l.trim()).filter((l) => l.length > 0);
372
+ if (options.labels) {
373
+ labels = options.labels.split(',').map((l) => l.trim()).filter((l) => l.length > 0);
369
374
  }
370
- // ── Step 5.7: Fix Versions ──────────────────────────
371
- let fixVersionIds = [];
372
- const verSpinner = ora('Fetching versions...').start();
373
- try {
374
- const versions = await api.get(`/project/${projectKey}/versions`);
375
- verSpinner.stop();
376
- // Filter unreleased versions usually
377
- const unreleased = versions.filter((v) => !v.released);
378
- if (Array.isArray(unreleased) && unreleased.length > 0) {
379
- const { selectedVersions } = await enquirer.prompt({
380
- type: 'multiselect',
381
- name: 'selectedVersions',
382
- message: 'Fix Versions:',
383
- choices: unreleased.map((v) => ({ name: v.id, message: v.name }))
384
- });
385
- fixVersionIds = selectedVersions;
375
+ if (labels.length === 0 && !options.noInput) {
376
+ const { inputLabels } = await enquirer.prompt({
377
+ type: 'input',
378
+ name: 'inputLabels',
379
+ message: 'Labels (comma-separated, optional):'
380
+ });
381
+ if (inputLabels && inputLabels.trim().length > 0) {
382
+ labels = inputLabels.split(',').map((l) => l.trim()).filter((l) => l.length > 0);
386
383
  }
387
384
  }
388
- catch {
389
- verSpinner.stop();
385
+ // ── Step 5.7: Fix Versions ──────────────────────────
386
+ let fixVersionIds = options.fixVersions || [];
387
+ if (fixVersionIds.length === 0 && !options.noInput) {
388
+ const verSpinner = ora('Fetching versions...').start();
389
+ try {
390
+ const versions = await api.get(API.PROJECT.VERSIONS(projectKey));
391
+ verSpinner.stop();
392
+ // Filter unreleased versions usually
393
+ const unreleased = versions.filter((v) => !v.released);
394
+ if (Array.isArray(unreleased) && unreleased.length > 0) {
395
+ const { selectedVersions } = await enquirer.prompt({
396
+ type: 'multiselect',
397
+ name: 'selectedVersions',
398
+ message: 'Fix Versions:',
399
+ choices: unreleased.map((v) => ({ name: v.id, message: v.name }))
400
+ });
401
+ fixVersionIds = selectedVersions;
402
+ }
403
+ }
404
+ catch {
405
+ verSpinner.stop();
406
+ }
390
407
  }
391
408
  // ── Step 5.8: Due Date ──────────────────────────────
392
- let duedate = null;
393
- const { inputDueDate } = await enquirer.prompt({
394
- type: 'input',
395
- name: 'inputDueDate',
396
- message: 'Due Date (YYYY-MM-DD, optional):',
397
- validate: (val) => {
398
- if (!val)
399
- return true;
400
- return /^\d{4}-\d{2}-\d{2}$/.test(val) || 'Format must be YYYY-MM-DD';
401
- }
402
- });
403
- if (inputDueDate)
404
- duedate = inputDueDate;
409
+ let duedate = options.dueDate || null;
410
+ if (!duedate && !options.noInput) {
411
+ const { inputDueDate } = await enquirer.prompt({
412
+ type: 'input',
413
+ name: 'inputDueDate',
414
+ message: 'Due Date (YYYY-MM-DD, optional):',
415
+ validate: (val) => {
416
+ if (!val)
417
+ return true;
418
+ return /^\d{4}-\d{2}-\d{2}$/.test(val) || 'Format must be YYYY-MM-DD';
419
+ }
420
+ });
421
+ if (inputDueDate)
422
+ duedate = inputDueDate;
423
+ }
405
424
  // ── Step 6: Assignee ────────────────────────────────
406
425
  let assigneeId = options.assignee;
407
426
  if (!assigneeId) {
@@ -418,7 +437,7 @@ Examples:
418
437
  if (assigneeChoice === 'me') {
419
438
  const spinner = ora('Fetching your account...').start();
420
439
  try {
421
- const myself = await api.get('/myself');
440
+ const myself = await api.get(API.USER.MYSELF);
422
441
  assigneeId = myself.accountId;
423
442
  spinner.stop();
424
443
  }
@@ -436,7 +455,7 @@ Examples:
436
455
  if (searchQuery.trim()) {
437
456
  const spinner = ora('Searching users...').start();
438
457
  try {
439
- const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
458
+ const users = await api.get(`${API.USER.SEARCH}?query=${encodeURIComponent(searchQuery)}`);
440
459
  spinner.stop();
441
460
  if (Array.isArray(users) && users.length > 0) {
442
461
  const userChoices = users.map((u) => ({
@@ -470,7 +489,7 @@ Examples:
470
489
  // --assignee me flag: resolve to account ID
471
490
  const spinner = ora('Fetching your account...').start();
472
491
  try {
473
- const myself = await api.get('/myself');
492
+ const myself = await api.get(API.USER.MYSELF);
474
493
  assigneeId = myself.accountId;
475
494
  spinner.stop();
476
495
  }
@@ -480,23 +499,25 @@ Examples:
480
499
  }
481
500
  }
482
501
  // ── Confirmation ────────────────────────────────────
483
- console.log(chalk.blue('\n── Issue Summary ──────────────────'));
484
- console.log(` Project: ${chalk.cyan(projectKey)}`);
485
- console.log(` Type: ${issueTypeName}`);
486
- console.log(` Summary: ${summary}`);
487
- console.log(` Description: ${description || chalk.grey('(none)')}`);
488
- console.log(` Priority: ${priorityName || chalk.grey('(default)')}`);
489
- console.log(` Assignee: ${assigneeId || chalk.grey('Unassigned')}`);
490
- console.log(chalk.blue('──────────────────────────────────\n'));
491
- const { confirmed } = await enquirer.prompt({
492
- type: 'confirm',
493
- name: 'confirmed',
494
- message: 'Create this issue?',
495
- initial: true
496
- });
497
- if (!confirmed) {
498
- console.log(chalk.yellow('Issue creation cancelled.'));
499
- return;
502
+ if (!options.noInput) {
503
+ console.log(chalk.blue('\n── Issue Summary ──────────────────'));
504
+ console.log(` Project: ${chalk.cyan(projectKey)}`);
505
+ console.log(` Type: ${issueTypeName}`);
506
+ console.log(` Summary: ${summary}`);
507
+ console.log(` Description: ${description || chalk.grey('(none)')}`);
508
+ console.log(` Priority: ${priorityName || chalk.grey('(default)')}`);
509
+ console.log(` Assignee: ${assigneeId || chalk.grey('Unassigned')}`);
510
+ console.log(chalk.blue('──────────────────────────────────\n'));
511
+ const { confirmed } = await enquirer.prompt({
512
+ type: 'confirm',
513
+ name: 'confirmed',
514
+ message: 'Create this issue?',
515
+ initial: true
516
+ });
517
+ if (!confirmed) {
518
+ console.log(chalk.yellow('Issue creation cancelled.'));
519
+ return;
520
+ }
500
521
  }
501
522
  // ── Build Request Body ──────────────────────────────
502
523
  const issueBody = {
@@ -541,7 +562,7 @@ Examples:
541
562
  }
542
563
  // ── Create Issue ────────────────────────────────────
543
564
  const spinner = ora('Creating issue...').start();
544
- const result = await api.post('/issue', issueBody);
565
+ const result = await api.post(API.ISSUE.BASE, issueBody);
545
566
  spinner.succeed(chalk.green(`Issue created: ${chalk.bold(result.key)}`));
546
567
  console.log(chalk.grey(`View it: jira issue view ${result.key}`));
547
568
  }
@@ -570,10 +591,10 @@ Examples:
570
591
  const spinner = ora(`Fetching transitions for ${issueKey}...`).start();
571
592
  try {
572
593
  // Fetch current issue to show context
573
- const issue = await api.get(`/issue/${issueKey}?fields=summary,status`);
594
+ const issue = await api.get(`${API.ISSUE.GET(issueKey)}?fields=summary,status`);
574
595
  const currentStatus = issue.fields.status.name;
575
596
  // Fetch available transitions
576
- const transData = await api.get(`/issue/${issueKey}/transitions`);
597
+ const transData = await api.get(API.ISSUE.TRANSITIONS(issueKey));
577
598
  spinner.stop();
578
599
  if (!transData.transitions || transData.transitions.length === 0) {
579
600
  console.log(chalk.yellow(`No transitions available for ${issueKey} (current status: ${currentStatus}).`));
@@ -611,7 +632,7 @@ Examples:
611
632
  }
612
633
  // Execute transition
613
634
  const execSpinner = ora(`Transitioning to "${targetTransition.to.name}"...`).start();
614
- await api.post(`/issue/${issueKey}/transitions`, {
635
+ await api.post(API.ISSUE.TRANSITIONS(issueKey), {
615
636
  transition: { id: targetTransition.id }
616
637
  });
617
638
  execSpinner.succeed(chalk.green(`${issueKey} transitioned: ${currentStatus} → ${chalk.bold(targetTransition.to.name)}`));
@@ -644,7 +665,7 @@ Examples:
644
665
  if (!assigneeId) {
645
666
  // Interactive selection
646
667
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
647
- const issue = await api.get(`/issue/${issueKey}?fields=summary,assignee`);
668
+ const issue = await api.get(`${API.ISSUE.GET(issueKey)}?fields=summary,assignee`);
648
669
  spinner.stop();
649
670
  const currentAssignee = issue.fields.assignee?.displayName || 'Unassigned';
650
671
  console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
@@ -663,7 +684,7 @@ Examples:
663
684
  }
664
685
  if (assigneeId === 'me') {
665
686
  const spinner = ora('Fetching your account...').start();
666
- const myself = await api.get('/myself');
687
+ const myself = await api.get(API.USER.MYSELF);
667
688
  assigneeId = myself.accountId;
668
689
  spinner.stop();
669
690
  }
@@ -674,7 +695,7 @@ Examples:
674
695
  message: 'Search user by name or email:'
675
696
  });
676
697
  const spinner = ora('Searching users...').start();
677
- const users = await api.get(`/user/search?query=${encodeURIComponent(searchQuery)}`);
698
+ const users = await api.get(`${API.USER.SEARCH}?query=${encodeURIComponent(searchQuery)}`);
678
699
  spinner.stop();
679
700
  if (!Array.isArray(users) || users.length === 0) {
680
701
  console.log(chalk.yellow('No users found.'));
@@ -695,7 +716,7 @@ Examples:
695
716
  const body = assigneeId === 'none'
696
717
  ? { accountId: null }
697
718
  : { accountId: assigneeId };
698
- await api.put(`/issue/${issueKey}/assignee`, body);
719
+ await api.put(API.ISSUE.ASSIGNEE(issueKey), body);
699
720
  spinner.succeed(chalk.green(`${issueKey} ${assigneeId === 'none' ? 'unassigned' : 'assigned'} successfully.`));
700
721
  }
701
722
  catch (e) {
@@ -738,7 +759,7 @@ Examples:
738
759
  commentText = inputComment;
739
760
  }
740
761
  const spinner = ora('Adding comment...').start();
741
- await api.post(`/issue/${issueKey}/comment`, {
762
+ await api.post(API.ISSUE.COMMENT(issueKey), {
742
763
  body: textToADF(commentText)
743
764
  });
744
765
  spinner.succeed(chalk.green(`Comment added to ${issueKey}.`));
@@ -770,7 +791,7 @@ Examples:
770
791
  }
771
792
  const spinner = ora(`Fetching issue ${issueKey}...`).start();
772
793
  try {
773
- const issue = await api.get(`/issue/${issueKey}?fields=summary,description,priority`);
794
+ const issue = await api.get(`${API.ISSUE.GET(issueKey)}?fields=summary,description,priority`);
774
795
  spinner.stop();
775
796
  const updateBody = { fields: {} };
776
797
  const hasFlags = options.summary || options.description || options.priority || (options.custom && options.custom.length > 0);
@@ -826,7 +847,7 @@ Examples:
826
847
  updateBody.fields.description = textToADF(desc);
827
848
  }
828
849
  if (field === 'priority') {
829
- const priorities = await api.get('/priority');
850
+ const priorities = await api.get(API.PRIORITY.ALL);
830
851
  const prioSelect = new Select({
831
852
  name: 'priority',
832
853
  message: 'Select priority',
@@ -835,7 +856,7 @@ Examples:
835
856
  updateBody.fields.priority = { name: await prioSelect.run() };
836
857
  }
837
858
  if (field === 'components') {
838
- const components = await api.get(`/project/${issue.fields.project.key}/components`);
859
+ const components = await api.get(API.PROJECT.COMPONENTS(issue.fields.project.key));
839
860
  if (components.length > 0) {
840
861
  const compSelect = new Select({
841
862
  // Wait, fieldSelect was initialized from enquirer as any.
@@ -871,7 +892,7 @@ Examples:
871
892
  updateBody.fields.labels = labelStr.split(',').map((l) => l.trim()).filter((l) => l.length > 0);
872
893
  }
873
894
  if (field === 'fixVersions') {
874
- const versions = await api.get(`/project/${issue.fields.project.key}/versions`);
895
+ const versions = await api.get(API.PROJECT.VERSIONS(issue.fields.project.key));
875
896
  const unreleased = versions.filter((v) => !v.released);
876
897
  if (unreleased.length > 0) {
877
898
  const { selectedVersions } = await enquirer.prompt({
@@ -899,7 +920,7 @@ Examples:
899
920
  return;
900
921
  }
901
922
  const updateSpinner = ora('Updating issue...').start();
902
- await api.put(`/issue/${issueKey}`, updateBody);
923
+ await api.put(API.ISSUE.GET(issueKey), updateBody);
903
924
  updateSpinner.succeed(`${chalk.cyan(issueKey)} updated successfully`);
904
925
  }
905
926
  catch (e) {
@@ -927,7 +948,7 @@ Examples:
927
948
  if (options.project)
928
949
  jqlParts.push(`project = "${options.project}"`);
929
950
  const jql = jqlParts.join(' AND ') + ' ORDER BY updated DESC';
930
- const data = await api.post('/search/jql', {
951
+ const data = await api.post(API.SEARCH.JQL, {
931
952
  jql,
932
953
  maxResults: parseInt(options.limit),
933
954
  fields: ['summary', 'status', 'assignee', 'updated']
@@ -946,17 +967,22 @@ Examples:
946
967
  return;
947
968
  }
948
969
  const table = new Table({
949
- head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
970
+ columns: [
971
+ { name: chalk.bold('Key') },
972
+ { name: chalk.bold('Summary') },
973
+ { name: chalk.bold('Status') },
974
+ { name: chalk.bold('Assignee') }
975
+ ]
950
976
  });
951
977
  data.issues.forEach((i) => {
952
- table.push([
978
+ table.addRow([
953
979
  chalk.cyan(i.key),
954
980
  i.fields.summary ? (i.fields.summary.length > 55 ? i.fields.summary.substring(0, 52) + '...' : i.fields.summary) : '',
955
981
  i.fields.status?.name || '',
956
982
  i.fields.assignee?.displayName || 'Unassigned'
957
983
  ]);
958
984
  });
959
- console.log(table.toString());
985
+ console.log(table.render());
960
986
  console.log(chalk.grey(`Found ${data.issues.length} result(s)`));
961
987
  }
962
988
  catch (e) {
@@ -1078,13 +1104,37 @@ Examples:
1078
1104
  }
1079
1105
  const spinner = ora(`Fetching parent ${parentKey}...`).start();
1080
1106
  try {
1081
- const parent = await api.get(`/issue/${parentKey}?fields=project,summary`);
1107
+ const parent = await api.get(`/issue/${parentKey}?fields=project,summary,issuetype,id`);
1082
1108
  const projectKey = parent.fields.project.key;
1109
+ if (parent.fields.issuetype.subtask) {
1110
+ spinner.fail(chalk.red(`Issue ${parentKey} is already a subtask. Cannot create a subtask of a subtask.`));
1111
+ return;
1112
+ }
1113
+ if (parent.fields.issuetype.name === 'Epic') {
1114
+ spinner.fail(chalk.red(`Issue ${parentKey} is an Epic. Epics cannot have sub-tasks.`));
1115
+ console.log(chalk.yellow('Tip: To add work to an Epic, create a standard issue (Story, Task) and link it to the Epic.'));
1116
+ return;
1117
+ }
1083
1118
  spinner.text = 'Fetching subtask types...';
1084
1119
  // Get valid subtask types for project
1085
- const meta = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
1086
- const allTypes = meta.issueTypes || meta.values || [];
1087
- const subtaskTypes = allTypes.filter((t) => t.subtask);
1120
+ let subtaskTypes = [];
1121
+ try {
1122
+ // Correct V3 endpoint for creation metadata
1123
+ const meta = await api.get(`/issue/createmeta?projectKeys=${projectKey}`);
1124
+ if (meta.projects && meta.projects.length > 0) {
1125
+ subtaskTypes = meta.projects[0].issuetypes.filter((t) => t.subtask);
1126
+ }
1127
+ }
1128
+ catch (err) {
1129
+ // Fallback to project fetch
1130
+ try {
1131
+ const proj = await api.get(API.PROJECT.GET(projectKey));
1132
+ subtaskTypes = (proj.issueTypes || []).filter((t) => t.subtask);
1133
+ }
1134
+ catch (e) {
1135
+ console.error(chalk.red('Failed to fetch project issue types.'));
1136
+ }
1137
+ }
1088
1138
  spinner.stop();
1089
1139
  if (subtaskTypes.length === 0) {
1090
1140
  console.error(chalk.red(`No subtask types found in project ${projectKey}.`));
@@ -1118,13 +1168,14 @@ Examples:
1118
1168
  const issueBody = {
1119
1169
  fields: {
1120
1170
  project: { key: projectKey },
1121
- parent: { key: parentKey },
1171
+ parent: { id: parent.id }, // Use ID instead of Key
1122
1172
  issuetype: { id: subtaskTypeId },
1123
1173
  summary: summary
1124
1174
  }
1125
1175
  };
1126
1176
  if (priorityName)
1127
1177
  issueBody.fields.priority = { name: priorityName };
1178
+ // ... rest of assignee logic ...
1128
1179
  if (assigneeId === 'me') {
1129
1180
  const me = await api.get('/myself');
1130
1181
  issueBody.fields.assignee = { accountId: me.accountId };
@@ -1133,7 +1184,7 @@ Examples:
1133
1184
  issueBody.fields.assignee = { accountId: assigneeId };
1134
1185
  }
1135
1186
  const createSpinner = ora('Creating subtask...').start();
1136
- const result = await api.post('/issue', issueBody);
1187
+ const result = await api.post(API.ISSUE.BASE, issueBody);
1137
1188
  createSpinner.succeed(chalk.green(`Subtask created: ${chalk.bold(result.key)}`));
1138
1189
  }
1139
1190
  catch (e) {