jira-ai 0.9.97 → 0.9.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,6 +112,50 @@ Discover available fields for a project (including custom fields):
112
112
  jira-ai project fields PROJ --type Task
113
113
  ```
114
114
 
115
+ ### Transition Issues
116
+
117
+ Change the status of an issue:
118
+ ```bash
119
+ jira-ai issue transition PROJ-123 "In Progress"
120
+ ```
121
+
122
+ Add a comment and resolution during transition:
123
+ ```bash
124
+ jira-ai issue transition PROJ-123 Done --resolution Done --comment "Completed the feature."
125
+ ```
126
+
127
+ Change assignee and fix version during transition:
128
+ ```bash
129
+ jira-ai issue transition PROJ-123 "In Review" --assignee "Jane Smith" --fix-version "v2.0"
130
+ ```
131
+
132
+ Set a custom field during transition:
133
+ ```bash
134
+ jira-ai issue transition PROJ-123 Done --custom-field "Story Points=5"
135
+ ```
136
+
137
+ Pass a comment from a file (useful for long comments):
138
+ ```bash
139
+ jira-ai issue transition PROJ-123 Done --comment-file ./release-notes.md
140
+ ```
141
+
142
+ Discover which transitions are available and what fields they require:
143
+ ```bash
144
+ jira-ai issue transitions PROJ-123
145
+ ```
146
+
147
+ Only show transitions that have required fields:
148
+ ```bash
149
+ jira-ai issue transitions PROJ-123 --required-only
150
+ ```
151
+
152
+ Get structured output for scripting:
153
+ ```bash
154
+ jira-ai --json issue transitions PROJ-123
155
+ ```
156
+
157
+ When a transition fails due to missing required fields, the error message lists what is needed and suggests running `issue transitions <key>` to discover them.
158
+
115
159
  ## Service Account Authentication
116
160
 
117
161
  Atlassian service accounts use scoped API tokens that must authenticate through the `api.atlassian.com` gateway rather than direct site URLs.
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import { createIssueLinkCommand } from './commands/create-issue-link.js';
19
19
  import { deleteIssueLinkCommand } from './commands/delete-issue-link.js';
20
20
  import { listLinkTypesCommand } from './commands/list-link-types.js';
21
21
  import { createTaskCommand } from './commands/create-task.js';
22
- import { transitionCommand } from './commands/transition.js';
22
+ import { transitionCommand, listTransitionsCommand } from './commands/transition.js';
23
23
  import { issueAssignCommand } from './commands/issue.js';
24
24
  import { getIssueStatisticsCommand } from './commands/get-issue-statistics.js';
25
25
  import { getPersonWorklogCommand } from './commands/get-person-worklog.js';
@@ -144,7 +144,29 @@ issue
144
144
  issue
145
145
  .command('transition <issue-id> <to-status>')
146
146
  .description('Change the status of a Jira issue. The <to-status> can be either the status name or ID.')
147
- .action(withPermission('issue.transition', transitionCommand, {
147
+ .option('--resolution <name>', 'Resolution name (e.g., "Done", "Won\'t Do")')
148
+ .option('--comment <text>', 'Add a comment (markdown) during transition')
149
+ .option('--comment-file <path>', 'Read comment from a markdown file (mutually exclusive with --comment)')
150
+ .option('--assignee <email-or-name>', 'Assignee (accountid:<id> or display name)')
151
+ .option('--fix-version <name>', 'Fix version name')
152
+ .option('--custom-field <entry>', 'Custom field as "Field Name=value" (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
153
+ .action(withPermission('issue.transition', (taskId, toStatus, opts) => transitionCommand(taskId, toStatus, {
154
+ resolution: opts.resolution,
155
+ comment: opts.comment,
156
+ commentFile: opts.commentFile,
157
+ assignee: opts.assignee,
158
+ fixVersion: opts.fixVersion,
159
+ customFields: opts.customField,
160
+ }), {
161
+ validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
162
+ }));
163
+ issue
164
+ .command('transitions <issue-id>')
165
+ .description('List available transitions for a Jira issue, including required fields.')
166
+ .option('--required-only', 'Only show transitions that have required fields')
167
+ .action(withPermission('issue.transition', (issueId, opts) => listTransitionsCommand(issueId, {
168
+ requiredOnly: opts.requiredOnly,
169
+ }), {
148
170
  validateArgs: (args) => validateOptions(IssueKeySchema, args[0])
149
171
  }));
150
172
  issue
@@ -1,9 +1,17 @@
1
1
  import chalk from 'chalk';
2
- import { getIssueTransitions, transitionIssue, validateIssuePermissions } from '../lib/jira-client.js';
2
+ import * as fs from 'fs';
3
+ import { getIssueTransitions, transitionIssue, validateIssuePermissions, resolveUserByName } from '../lib/jira-client.js';
3
4
  import { CommandError } from '../lib/errors.js';
4
5
  import { ui } from '../lib/ui.js';
5
6
  import { outputResult } from '../lib/json-mode.js';
6
- export async function transitionCommand(taskId, toStatus) {
7
+ import { markdownToAdf } from 'marklassian';
8
+ import { FieldResolver } from '../lib/field-resolver.js';
9
+ import { processMentionsInADF } from '../lib/adf-mentions.js';
10
+ export async function transitionCommand(taskId, toStatus, options) {
11
+ // Validate mutual exclusivity of --comment and --comment-file
12
+ if (options?.comment && options?.commentFile) {
13
+ throw new CommandError('Cannot use both --comment and --comment-file flags simultaneously.');
14
+ }
7
15
  // Check permissions and filters
8
16
  ui.startSpinner(`Validating permissions for ${taskId}...`);
9
17
  await validateIssuePermissions(taskId, 'transition');
@@ -31,8 +39,66 @@ export async function transitionCommand(taskId, toStatus) {
31
39
  });
32
40
  }
33
41
  const transition = matchingTransitions[0];
42
+ // Build optional payload if any options were provided
43
+ let payload;
44
+ if (options && Object.keys(options).some(k => options[k] !== undefined)) {
45
+ const fields = {};
46
+ const update = {};
47
+ if (options.resolution) {
48
+ fields['resolution'] = { name: options.resolution };
49
+ }
50
+ if (options.comment) {
51
+ let adf = markdownToAdf(options.comment);
52
+ adf = await processMentionsInADF(adf, resolveUserByName);
53
+ update['comment'] = [{ add: { body: adf } }];
54
+ }
55
+ else if (options.commentFile) {
56
+ const content = fs.readFileSync(options.commentFile, 'utf-8');
57
+ let adf = markdownToAdf(content);
58
+ adf = await processMentionsInADF(adf, resolveUserByName);
59
+ update['comment'] = [{ add: { body: adf } }];
60
+ }
61
+ if (options.assignee) {
62
+ if (options.assignee.startsWith('accountid:')) {
63
+ fields['assignee'] = { accountId: options.assignee.slice('accountid:'.length) };
64
+ }
65
+ else {
66
+ const accountId = await resolveUserByName(options.assignee);
67
+ if (!accountId) {
68
+ throw new CommandError(`Could not resolve user: "${options.assignee}". Check the display name and try again.`);
69
+ }
70
+ fields['assignee'] = { accountId };
71
+ }
72
+ }
73
+ if (options.fixVersion) {
74
+ fields['fixVersions'] = options.fixVersion.split(',').map((v) => ({ name: v.trim() }));
75
+ }
76
+ if (options.customFields && options.customFields.length > 0) {
77
+ const resolver = new FieldResolver();
78
+ for (const entry of options.customFields) {
79
+ const eqIdx = entry.indexOf('=');
80
+ if (eqIdx === -1)
81
+ continue;
82
+ const fieldId = entry.slice(0, eqIdx).trim();
83
+ const value = entry.slice(eqIdx + 1).trim();
84
+ try {
85
+ fields[fieldId] = await resolver.coerceValue(fieldId, value);
86
+ }
87
+ catch {
88
+ // Fall back to raw string if field schema is not accessible
89
+ fields[fieldId] = value;
90
+ }
91
+ }
92
+ }
93
+ payload = {
94
+ ...(Object.keys(fields).length > 0 && { fields }),
95
+ ...(Object.keys(update).length > 0 && { update }),
96
+ };
97
+ if (Object.keys(payload).length === 0)
98
+ payload = undefined;
99
+ }
34
100
  ui.startSpinner(`Transitioning ${taskId} to ${transition.to.name}...`);
35
- await transitionIssue(taskId, transition.id);
101
+ await transitionIssue(taskId, transition.id, payload);
36
102
  ui.succeedSpinner(chalk.green(`Issue ${taskId} successfully transitioned to ${transition.to.name}.`));
37
103
  outputResult({ success: true, issueKey: taskId, status: transition.to.name }, (data) => chalk.green(`Issue ${data.issueKey} successfully transitioned to ${data.status}.`));
38
104
  }
@@ -55,3 +121,27 @@ export async function transitionCommand(taskId, toStatus) {
55
121
  throw new CommandError(`Failed to transition issue: ${error.message}`, { hints });
56
122
  }
57
123
  }
124
+ export async function listTransitionsCommand(issueKey, options) {
125
+ await validateIssuePermissions(issueKey, 'transition');
126
+ const transitions = await getIssueTransitions(issueKey);
127
+ let rows = transitions.map((t) => {
128
+ const requiredFieldNames = t.fields
129
+ ? Object.entries(t.fields)
130
+ .filter(([, f]) => f.required)
131
+ .map(([key]) => key)
132
+ : [];
133
+ return {
134
+ id: t.id,
135
+ name: t.name,
136
+ to: t.to.name,
137
+ requiredFields: requiredFieldNames.length > 0 ? requiredFieldNames.join(', ') : '(none)',
138
+ };
139
+ });
140
+ if (options?.requiredOnly) {
141
+ rows = rows.filter((r) => r.requiredFields !== '(none)');
142
+ }
143
+ outputResult(rows, (data) => {
144
+ const lines = data.map((r) => ` ${r.id.padEnd(6)} ${r.name.padEnd(30)} → ${r.to.padEnd(20)} [required: ${r.requiredFields}]`);
145
+ return lines.join('\n');
146
+ });
147
+ }
@@ -463,6 +463,7 @@ export async function getIssueTransitions(issueIdOrKey) {
463
463
  const client = getJiraClient();
464
464
  const response = await client.issues.getTransitions({
465
465
  issueIdOrKey,
466
+ expand: "transitions.fields",
466
467
  });
467
468
  return (response.transitions || []).map((t) => ({
468
469
  id: t.id || '',
@@ -471,18 +472,21 @@ export async function getIssueTransitions(issueIdOrKey) {
471
472
  id: t.to?.id || '',
472
473
  name: t.to?.name || '',
473
474
  },
475
+ fields: t.fields,
474
476
  }));
475
477
  }
476
478
  /**
477
479
  * Perform a transition on an issue
478
480
  */
479
- export async function transitionIssue(issueIdOrKey, transitionId) {
481
+ export async function transitionIssue(issueIdOrKey, transitionId, payload) {
480
482
  const client = getJiraClient();
481
483
  await client.issues.doTransition({
482
484
  issueIdOrKey,
483
485
  transition: {
484
486
  id: transitionId,
485
487
  },
488
+ ...(payload?.fields && { fields: payload.fields }),
489
+ ...(payload?.update && { update: payload.update }),
486
490
  });
487
491
  }
488
492
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.9.97",
3
+ "version": "0.9.98",
4
4
  "description": "AI friendly Jira CLI to save context",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",