@geeveeh/atlassian-tools 0.2.0 → 0.4.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 (56) hide show
  1. package/README.md +156 -26
  2. package/dist/cli/confluence.d.ts.map +1 -1
  3. package/dist/cli/confluence.js +424 -9
  4. package/dist/cli/confluence.js.map +1 -1
  5. package/dist/cli/jira.d.ts.map +1 -1
  6. package/dist/cli/jira.js +518 -2
  7. package/dist/cli/jira.js.map +1 -1
  8. package/dist/confluence/client.d.ts +15 -1
  9. package/dist/confluence/client.d.ts.map +1 -1
  10. package/dist/confluence/client.js +114 -0
  11. package/dist/confluence/client.js.map +1 -1
  12. package/dist/confluence/index.d.ts +1 -0
  13. package/dist/confluence/index.d.ts.map +1 -1
  14. package/dist/confluence/index.js +1 -0
  15. package/dist/confluence/index.js.map +1 -1
  16. package/dist/confluence/markdown.d.ts +15 -0
  17. package/dist/confluence/markdown.d.ts.map +1 -0
  18. package/dist/confluence/markdown.js +83 -0
  19. package/dist/confluence/markdown.js.map +1 -0
  20. package/dist/confluence/types.d.ts +91 -0
  21. package/dist/confluence/types.d.ts.map +1 -1
  22. package/dist/core/auth.d.ts.map +1 -1
  23. package/dist/core/auth.js +54 -10
  24. package/dist/core/auth.js.map +1 -1
  25. package/dist/core/client.d.ts.map +1 -1
  26. package/dist/core/client.js +57 -18
  27. package/dist/core/client.js.map +1 -1
  28. package/dist/core/index.d.ts +1 -1
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/core/index.js +1 -1
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/mime.d.ts +2 -0
  33. package/dist/core/mime.d.ts.map +1 -0
  34. package/dist/core/mime.js +21 -0
  35. package/dist/core/mime.js.map +1 -0
  36. package/dist/jira/client.d.ts +20 -1
  37. package/dist/jira/client.d.ts.map +1 -1
  38. package/dist/jira/client.js +127 -0
  39. package/dist/jira/client.js.map +1 -1
  40. package/dist/jira/types.d.ts +115 -0
  41. package/dist/jira/types.d.ts.map +1 -1
  42. package/dist/mcp/confluence.d.ts +3 -0
  43. package/dist/mcp/confluence.d.ts.map +1 -0
  44. package/dist/mcp/confluence.js +405 -0
  45. package/dist/mcp/confluence.js.map +1 -0
  46. package/dist/mcp/helpers.d.ts +3 -0
  47. package/dist/mcp/helpers.d.ts.map +1 -0
  48. package/dist/mcp/helpers.js +15 -0
  49. package/dist/mcp/helpers.js.map +1 -0
  50. package/dist/mcp/index.js +8 -354
  51. package/dist/mcp/index.js.map +1 -1
  52. package/dist/mcp/jira.d.ts +3 -0
  53. package/dist/mcp/jira.d.ts.map +1 -0
  54. package/dist/mcp/jira.js +479 -0
  55. package/dist/mcp/jira.js.map +1 -0
  56. package/package.json +3 -3
package/dist/cli/jira.js CHANGED
@@ -40,10 +40,11 @@ export function registerJiraCommands(program) {
40
40
  .command("projects")
41
41
  .description("List available projects")
42
42
  .option("-l, --limit <n>", "Max projects to return", "25")
43
+ .option("--all", "Fetch up to 50 results (Jira default max per request)")
43
44
  .action(async (opts) => {
44
45
  try {
45
46
  const client = createClient();
46
- const projects = await client.listProjects(Number(opts.limit));
47
+ const projects = await client.listProjects(opts.all ? 50 : Number(opts.limit));
47
48
  for (const p of projects) {
48
49
  console.log(`${chalk.bold(p.name)} ${chalk.dim(`[${p.key}] id:${p.id}`)}`);
49
50
  }
@@ -52,6 +53,248 @@ export function registerJiraCommands(program) {
52
53
  handleError(err);
53
54
  }
54
55
  });
56
+ jira
57
+ .command("epic <epicKey>")
58
+ .description("List all issues belonging to an epic")
59
+ .option("-l, --limit <n>", "Max results", "50")
60
+ .option("--all", "Fetch up to 100 results per request")
61
+ .action(async (epicKey, opts) => {
62
+ try {
63
+ const issues = await createClient().listEpicIssues(epicKey, opts.all ? 100 : Number(opts.limit));
64
+ if (issues.length === 0) {
65
+ console.log(chalk.yellow("No issues found in this epic."));
66
+ return;
67
+ }
68
+ for (const i of issues) {
69
+ console.log(formatIssue(i));
70
+ }
71
+ }
72
+ catch (err) {
73
+ handleError(err);
74
+ }
75
+ });
76
+ jira
77
+ .command("boards")
78
+ .description("List Jira boards")
79
+ .option("-l, --limit <n>", "Max results", "25")
80
+ .action(async (opts) => {
81
+ try {
82
+ const boards = await createClient().listBoards(Number(opts.limit));
83
+ if (boards.length === 0) {
84
+ console.log(chalk.yellow("No boards found."));
85
+ return;
86
+ }
87
+ for (const b of boards) {
88
+ const proj = b.location ? chalk.dim(` — ${b.location.projectName} [${b.location.projectKey}]`) : "";
89
+ console.log(`${chalk.bold(b.name)}${proj} ${chalk.dim(`(id: ${b.id}, ${b.type})`)}`);
90
+ }
91
+ }
92
+ catch (err) {
93
+ handleError(err);
94
+ }
95
+ });
96
+ jira
97
+ .command("sprints <boardId>")
98
+ .description("List sprints on a board")
99
+ .option("--state <state>", "Filter by state: active, future, closed")
100
+ .action(async (boardId, opts) => {
101
+ try {
102
+ const state = opts.state;
103
+ const sprints = await createClient().listSprints(Number(boardId), state);
104
+ if (sprints.length === 0) {
105
+ console.log(chalk.yellow("No sprints found."));
106
+ return;
107
+ }
108
+ for (const s of sprints) {
109
+ const dates = s.startDate && s.endDate
110
+ ? chalk.dim(` (${new Date(s.startDate).toLocaleDateString()} – ${new Date(s.endDate).toLocaleDateString()})`)
111
+ : "";
112
+ const stateColour = s.state === "active" ? chalk.green(s.state) : chalk.dim(s.state);
113
+ console.log(`${chalk.bold(s.name)} — ${stateColour}${dates} ${chalk.dim(`(id: ${s.id})`)}`);
114
+ if (s.goal)
115
+ console.log(` ${chalk.dim(s.goal)}`);
116
+ }
117
+ }
118
+ catch (err) {
119
+ handleError(err);
120
+ }
121
+ });
122
+ jira
123
+ .command("move-to-sprint <sprintId> [issueKeys...]")
124
+ .description("Move one or more issues to a sprint")
125
+ .option("-y, --yes", "Skip confirmation prompt")
126
+ .option("--dry-run", "Print what would happen without making changes")
127
+ .action(async (sprintId, issueKeys, opts) => {
128
+ try {
129
+ if (issueKeys.length === 0) {
130
+ console.error(chalk.red("Specify at least one issue key."));
131
+ process.exit(1);
132
+ }
133
+ if (opts.dryRun) {
134
+ console.log(chalk.cyan("[dry run] Would move issues to sprint:"));
135
+ console.log(` Issues: ${issueKeys.join(", ")}`);
136
+ console.log(` Sprint: ${sprintId}`);
137
+ return;
138
+ }
139
+ if (!opts.yes) {
140
+ const ok = await confirm(`Move ${issueKeys.join(", ")} to sprint ${sprintId}?`);
141
+ if (!ok) {
142
+ console.log(chalk.dim("Cancelled."));
143
+ return;
144
+ }
145
+ }
146
+ await createClient().moveToSprint(Number(sprintId), issueKeys);
147
+ console.log(chalk.green(`✓ Moved ${issueKeys.join(", ")} to sprint ${sprintId}.`));
148
+ }
149
+ catch (err) {
150
+ handleError(err);
151
+ }
152
+ });
153
+ jira
154
+ .command("subtasks <issueKey>")
155
+ .description("List subtasks of an issue")
156
+ .option("-l, --limit <n>", "Max results", "50")
157
+ .action(async (issueKey, opts) => {
158
+ try {
159
+ const subtasks = await createClient().listSubtasks(issueKey, Number(opts.limit));
160
+ if (subtasks.length === 0) {
161
+ console.log(chalk.yellow("No subtasks found."));
162
+ return;
163
+ }
164
+ for (const i of subtasks) {
165
+ console.log(formatIssue(i));
166
+ }
167
+ }
168
+ catch (err) {
169
+ handleError(err);
170
+ }
171
+ });
172
+ jira
173
+ .command("create-sprint")
174
+ .description("Create a new sprint on a board")
175
+ .requiredOption("--board <id>", "Board ID")
176
+ .requiredOption("--name <name>", "Sprint name")
177
+ .option("--goal <text>", "Sprint goal")
178
+ .option("--start <date>", "Start date (ISO 8601)")
179
+ .option("--end <date>", "End date (ISO 8601)")
180
+ .option("-y, --yes", "Skip confirmation prompt")
181
+ .option("--dry-run", "Print what would happen without making changes")
182
+ .action(async (opts) => {
183
+ try {
184
+ if (opts.dryRun) {
185
+ console.log(chalk.cyan("[dry run] Would create sprint:"));
186
+ console.log(` Board: ${opts.board}`);
187
+ console.log(` Name: ${opts.name}`);
188
+ if (opts.goal)
189
+ console.log(` Goal: ${opts.goal}`);
190
+ return;
191
+ }
192
+ if (!opts.yes) {
193
+ const ok = await confirm(`Create sprint "${opts.name}" on board ${opts.board}?`);
194
+ if (!ok) {
195
+ console.log(chalk.dim("Cancelled."));
196
+ return;
197
+ }
198
+ }
199
+ const sprint = await createClient().createSprint({
200
+ boardId: Number(opts.board),
201
+ name: opts.name,
202
+ goal: opts.goal,
203
+ startDate: opts.start,
204
+ endDate: opts.end,
205
+ });
206
+ console.log(chalk.green(`✓ Created sprint: ${chalk.bold(sprint.name)} ${chalk.dim(`(id: ${sprint.id})`)}`));
207
+ }
208
+ catch (err) {
209
+ handleError(err);
210
+ }
211
+ });
212
+ jira
213
+ .command("update-sprint <sprintId>")
214
+ .description("Update a sprint's name, goal, or dates")
215
+ .option("--name <name>", "New sprint name")
216
+ .option("--goal <text>", "New sprint goal")
217
+ .option("--start <date>", "New start date (ISO 8601)")
218
+ .option("--end <date>", "New end date (ISO 8601)")
219
+ .option("-y, --yes", "Skip confirmation prompt")
220
+ .option("--dry-run", "Print what would happen without making changes")
221
+ .action(async (sprintId, opts) => {
222
+ try {
223
+ if (opts.dryRun) {
224
+ console.log(chalk.cyan("[dry run] Would update sprint:"));
225
+ console.log(` Sprint: ${sprintId}`);
226
+ if (opts.name)
227
+ console.log(` Name: ${opts.name}`);
228
+ if (opts.goal)
229
+ console.log(` Goal: ${opts.goal}`);
230
+ return;
231
+ }
232
+ if (!opts.yes) {
233
+ const ok = await confirm(`Update sprint ${sprintId}?`);
234
+ if (!ok) {
235
+ console.log(chalk.dim("Cancelled."));
236
+ return;
237
+ }
238
+ }
239
+ const sprint = await createClient().updateSprint(Number(sprintId), {
240
+ name: opts.name,
241
+ goal: opts.goal,
242
+ startDate: opts.start,
243
+ endDate: opts.end,
244
+ });
245
+ console.log(chalk.green(`✓ Updated sprint: ${chalk.bold(sprint.name)} ${chalk.dim(`(id: ${sprint.id})`)}`));
246
+ }
247
+ catch (err) {
248
+ handleError(err);
249
+ }
250
+ });
251
+ jira
252
+ .command("close-sprint <sprintId>")
253
+ .description("Close a sprint (moves remaining issues to backlog)")
254
+ .option("-y, --yes", "Skip confirmation prompt")
255
+ .option("--dry-run", "Print what would happen without making changes")
256
+ .action(async (sprintId, opts) => {
257
+ try {
258
+ if (opts.dryRun) {
259
+ console.log(chalk.cyan(`[dry run] Would close sprint ${sprintId}`));
260
+ return;
261
+ }
262
+ if (!opts.yes) {
263
+ const ok = await confirm(`Close sprint ${sprintId}? This cannot be undone.`);
264
+ if (!ok) {
265
+ console.log(chalk.dim("Cancelled."));
266
+ return;
267
+ }
268
+ }
269
+ const sprint = await createClient().closeSprint(Number(sprintId));
270
+ console.log(chalk.green(`✓ Closed sprint: ${chalk.bold(sprint.name)} ${chalk.dim(`(id: ${sprint.id})`)}`));
271
+ }
272
+ catch (err) {
273
+ handleError(err);
274
+ }
275
+ });
276
+ jira
277
+ .command("users <query>")
278
+ .description("Search for users by name or email (to find accountIds for assignee)")
279
+ .option("-l, --limit <n>", "Max results", "10")
280
+ .action(async (query, opts) => {
281
+ try {
282
+ const users = await createClient().searchUsers(query, Number(opts.limit));
283
+ if (users.length === 0) {
284
+ console.log(chalk.yellow("No users found."));
285
+ return;
286
+ }
287
+ for (const u of users) {
288
+ const email = u.emailAddress ? chalk.dim(` <${u.emailAddress}>`) : "";
289
+ const inactive = u.active ? "" : chalk.red(" (inactive)");
290
+ console.log(`${chalk.bold(u.displayName)}${email}${inactive}`);
291
+ console.log(` ${chalk.dim(`accountId: ${u.accountId}`)}`);
292
+ }
293
+ }
294
+ catch (err) {
295
+ handleError(err);
296
+ }
297
+ });
55
298
  jira
56
299
  .command("list")
57
300
  .description("Search for issues")
@@ -61,6 +304,7 @@ export function registerJiraCommands(program) {
61
304
  .option("--type <type>", "Filter by issue type")
62
305
  .option("--jql <query>", "Raw JQL query (overrides other filters)")
63
306
  .option("-l, --limit <n>", "Max results", "25")
307
+ .option("--all", "Fetch up to 100 results per request")
64
308
  .action(async (opts) => {
65
309
  try {
66
310
  const client = createClient();
@@ -70,7 +314,7 @@ export function registerJiraCommands(program) {
70
314
  status: opts.status,
71
315
  assignee: opts.assignee,
72
316
  type: opts.type,
73
- limit: Number(opts.limit),
317
+ limit: opts.all ? 100 : Number(opts.limit),
74
318
  });
75
319
  if (issues.length === 0) {
76
320
  console.log(chalk.yellow("No issues found."));
@@ -119,10 +363,23 @@ export function registerJiraCommands(program) {
119
363
  .option("--description <text>", "Issue description")
120
364
  .option("--priority <name>", "Priority (Highest, High, Medium, Low, Lowest)")
121
365
  .option("--labels <labels>", "Comma-separated labels")
366
+ .option("--parent <key>", "Parent issue key (creates a subtask)")
122
367
  .option("-y, --yes", "Skip confirmation prompt")
368
+ .option("--dry-run", "Print what would happen without making changes")
123
369
  .action(async (opts) => {
124
370
  try {
125
371
  const client = createClient();
372
+ if (opts.dryRun) {
373
+ console.log(chalk.cyan("[dry run] Would create issue:"));
374
+ console.log(` Project: ${opts.project}`);
375
+ console.log(` Type: ${opts.type}`);
376
+ console.log(` Summary: ${opts.summary}`);
377
+ if (opts.priority)
378
+ console.log(` Priority: ${opts.priority}`);
379
+ if (opts.parent)
380
+ console.log(` Parent: ${opts.parent}`);
381
+ return;
382
+ }
126
383
  if (!opts.yes) {
127
384
  console.log(chalk.yellow("⚠ About to create issue:"));
128
385
  console.log(` Project: ${opts.project}`);
@@ -130,6 +387,8 @@ export function registerJiraCommands(program) {
130
387
  console.log(` Summary: ${opts.summary}`);
131
388
  if (opts.priority)
132
389
  console.log(` Priority: ${opts.priority}`);
390
+ if (opts.parent)
391
+ console.log(` Parent: ${opts.parent}`);
133
392
  console.log();
134
393
  const ok = await confirm("Proceed?");
135
394
  if (!ok) {
@@ -144,6 +403,7 @@ export function registerJiraCommands(program) {
144
403
  description: opts.description,
145
404
  priority: opts.priority,
146
405
  labels: opts.labels?.split(",").map((l) => l.trim()),
406
+ parentKey: opts.parent,
147
407
  });
148
408
  console.log(chalk.green(`✓ Created: ${formatIssue(issue)}`));
149
409
  console.log(` ${chalk.cyan(issueUrl(issue.key))}`);
@@ -160,10 +420,20 @@ export function registerJiraCommands(program) {
160
420
  .option("--priority <name>", "New priority")
161
421
  .option("--labels <labels>", "Comma-separated labels")
162
422
  .option("-y, --yes", "Skip confirmation prompt")
423
+ .option("--dry-run", "Print what would happen without making changes")
163
424
  .action(async (issueKey, opts) => {
164
425
  try {
165
426
  const client = createClient();
166
427
  const current = await client.getIssue(issueKey);
428
+ if (opts.dryRun) {
429
+ console.log(chalk.cyan("[dry run] Would update issue:"));
430
+ console.log(` ${formatIssue(current)}`);
431
+ if (opts.summary)
432
+ console.log(` Summary: ${current.fields.summary} → ${opts.summary}`);
433
+ if (opts.priority)
434
+ console.log(` Priority: → ${opts.priority}`);
435
+ return;
436
+ }
167
437
  if (!opts.yes) {
168
438
  console.log(chalk.yellow("⚠ About to update issue:"));
169
439
  console.log(` ${formatIssue(current)}`);
@@ -198,6 +468,7 @@ export function registerJiraCommands(program) {
198
468
  .option("--to <status>", "Target status name")
199
469
  .option("--list", "List available transitions")
200
470
  .option("-y, --yes", "Skip confirmation prompt")
471
+ .option("--dry-run", "Print what would happen without making changes")
201
472
  .action(async (issueKey, opts) => {
202
473
  try {
203
474
  const client = createClient();
@@ -217,6 +488,12 @@ export function registerJiraCommands(program) {
217
488
  }
218
489
  process.exit(1);
219
490
  }
491
+ if (opts.dryRun) {
492
+ const issue = await client.getIssue(issueKey);
493
+ console.log(chalk.cyan(`[dry run] Would transition ${chalk.bold(issueKey)}:`));
494
+ console.log(` ${issue.fields.status.name} → ${match.to.name}`);
495
+ return;
496
+ }
220
497
  if (!opts.yes) {
221
498
  const issue = await client.getIssue(issueKey);
222
499
  console.log(chalk.yellow(`⚠ About to transition ${chalk.bold(issueKey)}:`));
@@ -235,14 +512,253 @@ export function registerJiraCommands(program) {
235
512
  handleError(err);
236
513
  }
237
514
  });
515
+ jira
516
+ .command("worklogs <issueKey>")
517
+ .description("List work log entries on an issue")
518
+ .action(async (issueKey) => {
519
+ try {
520
+ const worklogs = await createClient().listWorklogs(issueKey);
521
+ if (worklogs.length === 0) {
522
+ console.log(chalk.yellow("No work logged."));
523
+ return;
524
+ }
525
+ for (const w of worklogs) {
526
+ const author = w.author?.displayName ?? "Unknown";
527
+ const date = new Date(w.started).toLocaleDateString();
528
+ console.log(` ${chalk.bold(w.timeSpent)} ${chalk.dim(`· ${author} · ${date}`)}`);
529
+ }
530
+ }
531
+ catch (err) {
532
+ handleError(err);
533
+ }
534
+ });
535
+ jira
536
+ .command("log <issueKey>")
537
+ .description("Log time worked on an issue")
538
+ .requiredOption("--time <duration>", "Time spent, e.g. '2h', '30m', '1d 2h'")
539
+ .option("--comment <text>", "Work description")
540
+ .option("--started <datetime>", "When work started (ISO datetime, defaults to now)")
541
+ .option("-y, --yes", "Skip confirmation prompt")
542
+ .option("--dry-run", "Print what would happen without making changes")
543
+ .action(async (issueKey, opts) => {
544
+ try {
545
+ if (opts.dryRun) {
546
+ console.log(chalk.cyan(`[dry run] Would log time on ${issueKey}:`));
547
+ console.log(` Time: ${opts.time}`);
548
+ if (opts.comment)
549
+ console.log(` Comment: ${opts.comment}`);
550
+ if (opts.started)
551
+ console.log(` Started: ${opts.started}`);
552
+ return;
553
+ }
554
+ if (!opts.yes) {
555
+ const ok = await confirm(`Log ${opts.time} on ${issueKey}?`);
556
+ if (!ok) {
557
+ console.log(chalk.dim("Cancelled."));
558
+ return;
559
+ }
560
+ }
561
+ const log = await createClient().addWorklog({
562
+ issueKey,
563
+ timeSpent: opts.time,
564
+ started: opts.started,
565
+ comment: opts.comment,
566
+ });
567
+ console.log(chalk.green(`✓ Logged ${log.timeSpent} on ${issueKey}.`));
568
+ }
569
+ catch (err) {
570
+ handleError(err);
571
+ }
572
+ });
573
+ jira
574
+ .command("links <issueKey>")
575
+ .description("List links on an issue")
576
+ .action(async (issueKey) => {
577
+ try {
578
+ const links = await createClient().listIssueLinks(issueKey);
579
+ if (links.length === 0) {
580
+ console.log(chalk.yellow("No issue links."));
581
+ return;
582
+ }
583
+ for (const l of links) {
584
+ if (l.outwardIssue) {
585
+ console.log(` ${chalk.dim(l.type.outward)} ${chalk.bold(l.outwardIssue.key)} ${l.outwardIssue.fields.summary} ${chalk.dim(`[${l.outwardIssue.fields.status.name}]`)}`);
586
+ }
587
+ else if (l.inwardIssue) {
588
+ console.log(` ${chalk.dim(l.type.inward)} ${chalk.bold(l.inwardIssue.key)} ${l.inwardIssue.fields.summary} ${chalk.dim(`[${l.inwardIssue.fields.status.name}]`)}`);
589
+ }
590
+ }
591
+ }
592
+ catch (err) {
593
+ handleError(err);
594
+ }
595
+ });
596
+ jira
597
+ .command("link <issueKey>")
598
+ .description("Link an issue to another")
599
+ .requiredOption("--type <name>", "Link type (e.g. 'Blocks', 'Relates to')")
600
+ .requiredOption("--target <key>", "Target issue key")
601
+ .option("-y, --yes", "Skip confirmation prompt")
602
+ .option("--dry-run", "Print what would happen without making changes")
603
+ .action(async (issueKey, opts) => {
604
+ try {
605
+ if (opts.dryRun) {
606
+ console.log(chalk.cyan(`[dry run] Would link issue:`));
607
+ console.log(` ${issueKey} "${opts.type}" ${opts.target}`);
608
+ return;
609
+ }
610
+ if (!opts.yes) {
611
+ const ok = await confirm(`Link ${issueKey} "${opts.type}" ${opts.target}?`);
612
+ if (!ok) {
613
+ console.log(chalk.dim("Cancelled."));
614
+ return;
615
+ }
616
+ }
617
+ await createClient().linkIssues(issueKey, opts.type, opts.target);
618
+ console.log(chalk.green(`✓ Linked: ${issueKey} "${opts.type}" ${opts.target}`));
619
+ }
620
+ catch (err) {
621
+ handleError(err);
622
+ }
623
+ });
624
+ jira
625
+ .command("link-types")
626
+ .description("List available issue link types")
627
+ .action(async () => {
628
+ try {
629
+ const types = await createClient().listIssueLinkTypes();
630
+ for (const t of types) {
631
+ console.log(`${chalk.bold(t.name)} ${chalk.dim(`— outward: "${t.outward}", inward: "${t.inward}"`)}`);
632
+ }
633
+ }
634
+ catch (err) {
635
+ handleError(err);
636
+ }
637
+ });
638
+ jira
639
+ .command("comments <issueKey>")
640
+ .description("List comments on an issue")
641
+ .action(async (issueKey) => {
642
+ try {
643
+ const client = createClient();
644
+ const comments = await client.listComments(issueKey);
645
+ if (comments.length === 0) {
646
+ console.log(chalk.yellow("No comments."));
647
+ return;
648
+ }
649
+ for (const c of comments) {
650
+ const author = c.author?.displayName ?? "Unknown";
651
+ const date = new Date(c.created).toLocaleDateString();
652
+ console.log(chalk.dim("─".repeat(60)));
653
+ console.log(`${chalk.bold(author)} ${chalk.dim(`· ${date}`)}`);
654
+ console.log(client.descriptionToText({ fields: { description: c.body } }));
655
+ }
656
+ }
657
+ catch (err) {
658
+ handleError(err);
659
+ }
660
+ });
661
+ jira
662
+ .command("comment <issueKey>")
663
+ .description("Add a comment to an issue")
664
+ .requiredOption("-t, --text <text>", "Comment text")
665
+ .option("-y, --yes", "Skip confirmation prompt")
666
+ .option("--dry-run", "Print what would happen without making changes")
667
+ .action(async (issueKey, opts) => {
668
+ try {
669
+ if (opts.dryRun) {
670
+ console.log(chalk.cyan(`[dry run] Would add comment to ${issueKey}:`));
671
+ console.log(` Text: ${opts.text}`);
672
+ return;
673
+ }
674
+ if (!opts.yes) {
675
+ const ok = await confirm(`Add comment to ${issueKey}?`);
676
+ if (!ok) {
677
+ console.log(chalk.dim("Cancelled."));
678
+ return;
679
+ }
680
+ }
681
+ await createClient().addComment(issueKey, opts.text);
682
+ console.log(chalk.green(`✓ Comment added to ${issueKey}.`));
683
+ }
684
+ catch (err) {
685
+ handleError(err);
686
+ }
687
+ });
688
+ jira
689
+ .command("attach <issueKey> <file>")
690
+ .description("Upload a file as an attachment to an issue")
691
+ .option("-y, --yes", "Skip confirmation prompt")
692
+ .option("--dry-run", "Print what would happen without making changes")
693
+ .action(async (issueKey, file, opts) => {
694
+ try {
695
+ const client = createClient();
696
+ const issue = await client.getIssue(issueKey);
697
+ if (opts.dryRun) {
698
+ console.log(chalk.cyan("[dry run] Would attach file to issue:"));
699
+ console.log(` Issue: ${chalk.bold(issue.key)} ${issue.fields.summary}`);
700
+ console.log(` File: ${file}`);
701
+ return;
702
+ }
703
+ if (!opts.yes) {
704
+ console.log(chalk.yellow("⚠ About to attach file to issue:"));
705
+ console.log(` Issue: ${chalk.bold(issue.key)} ${issue.fields.summary}`);
706
+ console.log(` File: ${file}`);
707
+ console.log();
708
+ const ok = await confirm("Proceed?");
709
+ if (!ok) {
710
+ console.log(chalk.dim("Cancelled."));
711
+ return;
712
+ }
713
+ }
714
+ const att = await client.uploadAttachment({ issueKey, filePath: file });
715
+ console.log(chalk.green(`✓ Uploaded: ${chalk.bold(att.filename)} ${chalk.dim(`(id: ${att.id}, ${att.mimeType}, ${att.size} bytes)`)}`));
716
+ if (att.content) {
717
+ console.log(` ${chalk.cyan(att.content)}`);
718
+ }
719
+ }
720
+ catch (err) {
721
+ handleError(err);
722
+ }
723
+ });
724
+ jira
725
+ .command("attachments <issueKey>")
726
+ .description("List attachments on an issue")
727
+ .action(async (issueKey) => {
728
+ try {
729
+ const client = createClient();
730
+ const issue = await client.getIssue(issueKey);
731
+ const attachments = await client.listAttachments(issueKey);
732
+ console.log(`Attachments on ${chalk.bold(issue.key)} ${issue.fields.summary}`);
733
+ if (attachments.length === 0) {
734
+ console.log(chalk.yellow(" No attachments."));
735
+ return;
736
+ }
737
+ for (const att of attachments) {
738
+ console.log(` ${chalk.bold(att.filename)} ${chalk.dim(`(id: ${att.id}, ${att.mimeType}, ${att.size} bytes)`)}`);
739
+ if (att.content) {
740
+ console.log(` ${chalk.cyan(att.content)}`);
741
+ }
742
+ }
743
+ }
744
+ catch (err) {
745
+ handleError(err);
746
+ }
747
+ });
238
748
  jira
239
749
  .command("delete <issueKey>")
240
750
  .description("Delete an issue")
241
751
  .option("-y, --yes", "Skip confirmation prompt")
752
+ .option("--dry-run", "Print what would happen without making changes")
242
753
  .action(async (issueKey, opts) => {
243
754
  try {
244
755
  const client = createClient();
245
756
  const issue = await client.getIssue(issueKey);
757
+ if (opts.dryRun) {
758
+ console.log(chalk.cyan("[dry run] Would DELETE issue:"));
759
+ console.log(` ${formatIssue(issue)}`);
760
+ return;
761
+ }
246
762
  if (!opts.yes) {
247
763
  console.log(chalk.red("⚠ About to DELETE issue:"));
248
764
  console.log(` ${formatIssue(issue)}`);