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 +44 -0
- package/dist/cli.js +24 -2
- package/dist/commands/transition.js +93 -3
- package/dist/lib/jira-client.js +5 -1
- package/package.json +1 -1
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
|
-
.
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/lib/jira-client.js
CHANGED
|
@@ -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
|
/**
|