jira-pilot 2.0.4 → 2.1.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.
- package/README.md +216 -173
- package/bin/{jira.js → jira.ts} +10 -1
- package/dist/bin/jira.js +64 -0
- package/package.json +21 -15
- package/src/commands/ai-actions/{plan.js → plan.ts} +9 -9
- package/src/commands/ai-actions/{review.js → review.ts} +2 -2
- package/src/commands/ai-actions/{standup.js → standup.ts} +4 -4
- package/src/commands/{ai.js → ai.ts} +11 -11
- package/src/commands/{board.js → board.ts} +11 -11
- package/src/commands/bulk.ts +230 -0
- package/src/commands/{config.js → config.ts} +57 -8
- package/src/commands/dashboard.ts +222 -0
- package/src/commands/filter.ts +84 -0
- package/src/commands/{git.js → git.ts} +4 -4
- package/src/commands/issue-attach.ts +44 -0
- package/src/commands/issue-pr.ts +87 -0
- package/src/commands/issue-worklog.ts +90 -0
- package/src/commands/{issue.js → issue.ts} +359 -68
- package/src/commands/{mcp.js → mcp.ts} +2 -2
- package/src/commands/{project.js → project.ts} +11 -11
- package/src/commands/sprint.ts +269 -0
- package/src/server/{mcp-server.js → mcp-server.ts} +235 -8
- package/src/services/{ai-service.js → ai-service.ts} +16 -16
- package/src/services/{api-service.js → api-service.ts} +33 -9
- package/src/services/config-service.ts +21 -0
- package/src/types.ts +68 -0
- package/src/utils/{adf-parser.js → adf-parser.ts} +12 -12
- package/src/utils/config-store.ts +109 -0
- package/src/utils/{config.js → config.ts} +14 -41
- package/src/utils/{error-handler.js → error-handler.ts} +2 -1
- package/src/utils/{text-to-adf.js → text-to-adf.ts} +1 -1
- package/src/utils/{validators.js → validators.ts} +4 -4
- package/src/commands/bulk.js +0 -108
- package/src/commands/dashboard.js +0 -89
- package/src/commands/sprint.js +0 -153
package/package.json
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jira-pilot",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "AI powered Jira CLI and MCP server for humans and agents — manage issues, sprints, boards with interactive wizards, multi-provider AI (OpenAI/Gemini/Anthropic), and an 8-tool MCP server for AI assistants",
|
|
5
|
-
"main": "src/index.js",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"jira": "./bin/jira.js",
|
|
9
|
-
"jira-pilot": "./bin/jira.js"
|
|
8
|
+
"jira": "./dist/bin/jira.js",
|
|
9
|
+
"jira-pilot": "./dist/bin/jira.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/bin/jira.js",
|
|
14
|
+
"start:dev": "tsx bin/jira.ts",
|
|
15
|
+
"mcp": "node dist/bin/jira.js mcp",
|
|
16
|
+
"mcp:dev": "tsx bin/jira.ts mcp",
|
|
14
17
|
"test": "vitest run",
|
|
15
18
|
"test:watch": "vitest",
|
|
16
19
|
"test:coverage": "vitest run --coverage",
|
|
17
|
-
"link": "npm link"
|
|
20
|
+
"link": "npm link",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
18
22
|
},
|
|
19
23
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
24
|
+
"node": ">=20.0.0"
|
|
21
25
|
},
|
|
22
26
|
"files": [
|
|
23
27
|
"bin/",
|
|
@@ -74,18 +78,20 @@
|
|
|
74
78
|
"homepage": "https://github.com/Aarul5/jira-pilot#readme",
|
|
75
79
|
"license": "ISC",
|
|
76
80
|
"dependencies": {
|
|
77
|
-
"@modelcontextprotocol/sdk": "^
|
|
78
|
-
"axios": "^1.
|
|
81
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
82
|
+
"axios": "^1.13.5",
|
|
79
83
|
"chalk": "^5.3.0",
|
|
80
|
-
"
|
|
81
|
-
"
|
|
84
|
+
"cli-table3": "^0.6.5",
|
|
85
|
+
"commander": "^14.0.3",
|
|
82
86
|
"enquirer": "^2.4.1",
|
|
83
|
-
"open": "^
|
|
84
|
-
"ora": "^
|
|
85
|
-
"table": "^6.8.0"
|
|
87
|
+
"open": "^11.0.0",
|
|
88
|
+
"ora": "^9.3.0"
|
|
86
89
|
},
|
|
87
90
|
"devDependencies": {
|
|
91
|
+
"@types/node": "^25.2.3",
|
|
88
92
|
"@vitest/coverage-v8": "^4.0.18",
|
|
93
|
+
"tsx": "^4.21.0",
|
|
94
|
+
"typescript": "^5.9.3",
|
|
89
95
|
"vitest": "^4.0.18"
|
|
90
96
|
}
|
|
91
97
|
}
|
|
@@ -7,7 +7,7 @@ import { validateIssueKey } from '../../utils/validators.js';
|
|
|
7
7
|
import { parseADF } from '../../utils/adf-parser.js';
|
|
8
8
|
import { handleCommandError } from '../../utils/error-handler.js';
|
|
9
9
|
|
|
10
|
-
export async function planAction(epicKey, options) {
|
|
10
|
+
export async function planAction(epicKey: string, options: any) {
|
|
11
11
|
const check = validateIssueKey(epicKey);
|
|
12
12
|
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
13
13
|
|
|
@@ -33,7 +33,7 @@ export async function planAction(epicKey, options) {
|
|
|
33
33
|
console.log(chalk.cyan(`\nProposed Breakdown for ${epicKey} (${summary}):\n`));
|
|
34
34
|
|
|
35
35
|
// Let user select items to create
|
|
36
|
-
const choices = plan.map((item, index) => ({
|
|
36
|
+
const choices = plan.map((item: any, index: number) => ({
|
|
37
37
|
name: `${item.type}: ${item.summary}`,
|
|
38
38
|
value: index, // store index to retrieve item
|
|
39
39
|
checked: true
|
|
@@ -43,16 +43,16 @@ export async function planAction(epicKey, options) {
|
|
|
43
43
|
type: 'multiselect',
|
|
44
44
|
name: 'selectedIndices',
|
|
45
45
|
message: 'Select issues to create:',
|
|
46
|
-
choices: choices.map((c, i) => ({ ...c, value: i })), // ensure value is index
|
|
47
|
-
result(names) {
|
|
46
|
+
choices: choices.map((c: any, i: number) => ({ ...c, value: i })), // ensure value is index
|
|
47
|
+
result(names: any) {
|
|
48
48
|
// map names back to indices
|
|
49
|
-
return names.map(name => this.map(name)); // 'this.map' returns value (index)
|
|
49
|
+
return names.map((name: any) => (this as any).map(name)); // 'this.map' returns value (index)
|
|
50
50
|
}
|
|
51
|
-
});
|
|
51
|
+
}) as any;
|
|
52
52
|
|
|
53
53
|
// Loop through selected and create
|
|
54
54
|
// Convert map result (object/array) to array of indices
|
|
55
|
-
const indicesToCreate = Object.values(selectedIndices);
|
|
55
|
+
const indicesToCreate = Object.values(selectedIndices) as number[];
|
|
56
56
|
|
|
57
57
|
if (indicesToCreate.length === 0) {
|
|
58
58
|
console.log(chalk.yellow('No items selected.'));
|
|
@@ -71,7 +71,7 @@ export async function planAction(epicKey, options) {
|
|
|
71
71
|
fields: {
|
|
72
72
|
project: { key: projectKey },
|
|
73
73
|
summary: item.summary,
|
|
74
|
-
description: item.description, // Simple string, Jira converts to ADF or accepts text depending on config.
|
|
74
|
+
// description: item.description, // Simple string, Jira converts to ADF or accepts text depending on config.
|
|
75
75
|
// Note: v3 API often needs ADF. If description is simple text, it might fail.
|
|
76
76
|
// We should construct a basic paragraph ADF document.
|
|
77
77
|
description: {
|
|
@@ -113,7 +113,7 @@ export async function planAction(epicKey, options) {
|
|
|
113
113
|
|
|
114
114
|
console.log(chalk.green(`\nDone! Created ${results.length} issues linked to ${epicKey}.`));
|
|
115
115
|
|
|
116
|
-
} catch (e) {
|
|
116
|
+
} catch (e: any) {
|
|
117
117
|
handleCommandError(spinner, e, `Failed to plan ${epicKey}`);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -9,7 +9,7 @@ import { getCredentials } from '../../utils/config.js';
|
|
|
9
9
|
import { parseADF } from '../../utils/adf-parser.js';
|
|
10
10
|
import { handleCommandError } from '../../utils/error-handler.js';
|
|
11
11
|
|
|
12
|
-
export async function reviewAction(issueKey, options) {
|
|
12
|
+
export async function reviewAction(issueKey: string, options: any) {
|
|
13
13
|
const check = validateIssueKey(issueKey);
|
|
14
14
|
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
15
15
|
|
|
@@ -96,7 +96,7 @@ export async function reviewAction(issueKey, options) {
|
|
|
96
96
|
console.log(review);
|
|
97
97
|
console.log(chalk.dim(`\nPR Link: ${pr.html_url}`));
|
|
98
98
|
|
|
99
|
-
} catch (e) {
|
|
99
|
+
} catch (e: any) {
|
|
100
100
|
handleCommandError(spinner, e, `Failed to review ${issueKey}`);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -5,7 +5,7 @@ import { aiService } from '../../services/ai-service.js';
|
|
|
5
5
|
import { parseADF } from '../../utils/adf-parser.js';
|
|
6
6
|
import { handleCommandError } from '../../utils/error-handler.js';
|
|
7
7
|
|
|
8
|
-
export async function standupAction(options) {
|
|
8
|
+
export async function standupAction(options: any) {
|
|
9
9
|
const spinner = ora('Analyzing your recent activity...').start();
|
|
10
10
|
|
|
11
11
|
try {
|
|
@@ -20,12 +20,12 @@ export async function standupAction(options) {
|
|
|
20
20
|
const yesterdayJql = `assignee = currentUser() AND updated >= -1d ORDER BY updated DESC`;
|
|
21
21
|
|
|
22
22
|
const yesterdayIssuesRes = await api.get(`/search?jql=${encodeURIComponent(yesterdayJql)}&fields=summary,status,comment,worklog`);
|
|
23
|
-
const yesterdayIssues = yesterdayIssuesRes.issues.map(i => `- ${i.key} ${i.fields.summary} (${i.fields.status.name})`).join('\n');
|
|
23
|
+
const yesterdayIssues = yesterdayIssuesRes.issues.map((i: any) => `- ${i.key} ${i.fields.summary} (${i.fields.status.name})`).join('\n');
|
|
24
24
|
|
|
25
25
|
// 3. Fetch Issues Assigned for Today (In Progress or To Do)
|
|
26
26
|
const todayJql = `assignee = currentUser() AND statusCategory IN ("To Do", "In Progress") ORDER BY priority DESC`;
|
|
27
27
|
const todayIssuesRes = await api.get(`/search?jql=${encodeURIComponent(todayJql)}&fields=summary,status,priority`);
|
|
28
|
-
const todayIssues = todayIssuesRes.issues.map(i => `- ${i.key} ${i.fields.summary} [${i.fields.priority.name}]`).join('\n');
|
|
28
|
+
const todayIssues = todayIssuesRes.issues.map((i: any) => `- ${i.key} ${i.fields.summary} [${i.fields.priority.name}]`).join('\n');
|
|
29
29
|
|
|
30
30
|
spinner.text = 'Generating standup report...';
|
|
31
31
|
|
|
@@ -36,7 +36,7 @@ export async function standupAction(options) {
|
|
|
36
36
|
console.log(chalk.green(`\n📢 Standup Report for ${displayName}:\n`));
|
|
37
37
|
console.log(report);
|
|
38
38
|
|
|
39
|
-
} catch (e) {
|
|
39
|
+
} catch (e: any) {
|
|
40
40
|
handleCommandError(spinner, e, 'Failed to generate standup');
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -11,7 +11,7 @@ import { reviewAction } from './ai-actions/review.js';
|
|
|
11
11
|
import { planAction } from './ai-actions/plan.js';
|
|
12
12
|
import { standupAction } from './ai-actions/standup.js';
|
|
13
13
|
|
|
14
|
-
export function registerAiCommand(program) {
|
|
14
|
+
export function registerAiCommand(program: Command) {
|
|
15
15
|
const aiCmd = new Command('ai')
|
|
16
16
|
.description('AI Helper commands')
|
|
17
17
|
.addHelpText('after', `
|
|
@@ -26,7 +26,7 @@ Common Actions:
|
|
|
26
26
|
.command('summarize')
|
|
27
27
|
.description('Summarize an issue using AI')
|
|
28
28
|
.argument('<issueKey>', 'Jira Issue Key')
|
|
29
|
-
.action(async (issueKey) => {
|
|
29
|
+
.action(async (issueKey: string) => {
|
|
30
30
|
const check = validateIssueKey(issueKey);
|
|
31
31
|
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
32
32
|
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
@@ -39,7 +39,7 @@ Common Actions:
|
|
|
39
39
|
? parseADF(issue.fields.description)
|
|
40
40
|
: 'No description';
|
|
41
41
|
const comments = (issue.fields.comment?.comments || [])
|
|
42
|
-
.map(c => `${c.author.displayName}: ${typeof c.body === 'object' ? parseADF(c.body) : c.body}`)
|
|
42
|
+
.map((c: any) => `${c.author.displayName}: ${typeof c.body === 'object' ? parseADF(c.body) : c.body}`)
|
|
43
43
|
.join('\n');
|
|
44
44
|
|
|
45
45
|
const prompt = `
|
|
@@ -60,7 +60,7 @@ Provide a concise summary of the current status, key discussion points, and next
|
|
|
60
60
|
console.log(chalk.green(`\n🤖 AI Summary for ${issueKey}:\n`));
|
|
61
61
|
console.log(aiResponse);
|
|
62
62
|
|
|
63
|
-
} catch (e) {
|
|
63
|
+
} catch (e: any) {
|
|
64
64
|
handleCommandError(spinner, e, `Failed to summarize ${issueKey}`);
|
|
65
65
|
}
|
|
66
66
|
});
|
|
@@ -77,7 +77,7 @@ Examples:
|
|
|
77
77
|
$ jira ai draft -i "login fails, returns 500, only on mobile"
|
|
78
78
|
$ jira ai draft -i "add dark mode toggle" -t story
|
|
79
79
|
`)
|
|
80
|
-
.action(async (options) => {
|
|
80
|
+
.action(async (options: any) => {
|
|
81
81
|
try {
|
|
82
82
|
let bulletPoints = options.input;
|
|
83
83
|
|
|
@@ -86,8 +86,8 @@ Examples:
|
|
|
86
86
|
type: 'input',
|
|
87
87
|
name: 'inputNotes',
|
|
88
88
|
message: 'Enter your bullet points or rough notes:',
|
|
89
|
-
validate: (val) => val.trim().length > 0 || 'Input cannot be empty'
|
|
90
|
-
});
|
|
89
|
+
validate: (val: any) => val.trim().length > 0 || 'Input cannot be empty'
|
|
90
|
+
}) as any;
|
|
91
91
|
bulletPoints = inputNotes;
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -122,7 +122,7 @@ Keep it professional and concise. Output in plain text (not markdown headers, us
|
|
|
122
122
|
console.log(aiResponse);
|
|
123
123
|
console.log(chalk.grey('\nTip: Copy this into "jira issue create" or use it as a starting point.'));
|
|
124
124
|
|
|
125
|
-
} catch (e) {
|
|
125
|
+
} catch (e: any) {
|
|
126
126
|
handleCommandError(null, e, 'Failed to generate draft');
|
|
127
127
|
}
|
|
128
128
|
});
|
|
@@ -132,7 +132,7 @@ Keep it professional and concise. Output in plain text (not markdown headers, us
|
|
|
132
132
|
.command('suggest')
|
|
133
133
|
.description('Suggest next actions for an issue based on its context')
|
|
134
134
|
.argument('<issueKey>', 'Jira Issue Key')
|
|
135
|
-
.action(async (issueKey) => {
|
|
135
|
+
.action(async (issueKey: string) => {
|
|
136
136
|
const check = validateIssueKey(issueKey);
|
|
137
137
|
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
138
138
|
const spinner = ora(`Analyzing issue ${issueKey}...`).start();
|
|
@@ -149,7 +149,7 @@ Keep it professional and concise. Output in plain text (not markdown headers, us
|
|
|
149
149
|
const assignee = issue.fields.assignee?.displayName || 'Unassigned';
|
|
150
150
|
const comments = (issue.fields.comment?.comments || [])
|
|
151
151
|
.slice(-5) // Last 5 comments for context
|
|
152
|
-
.map(c => `${c.author.displayName}: ${typeof c.body === 'object' ? parseADF(c.body) : c.body}`)
|
|
152
|
+
.map((c: any) => `${c.author.displayName}: ${typeof c.body === 'object' ? parseADF(c.body) : c.body}`)
|
|
153
153
|
.join('\n');
|
|
154
154
|
|
|
155
155
|
spinner.text = 'Generating suggestions...';
|
|
@@ -183,7 +183,7 @@ Keep suggestions actionable and concise.
|
|
|
183
183
|
console.log(chalk.green(`\n💡 AI Suggestions for ${issueKey}:\n`));
|
|
184
184
|
console.log(aiResponse);
|
|
185
185
|
|
|
186
|
-
} catch (e) {
|
|
186
|
+
} catch (e: any) {
|
|
187
187
|
handleCommandError(spinner, e, `Failed to suggest for ${issueKey}`);
|
|
188
188
|
}
|
|
189
189
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { handleCommandError } from '../utils/error-handler.js';
|
|
7
7
|
|
|
8
|
-
export function registerBoardCommand(program) {
|
|
8
|
+
export function registerBoardCommand(program: Command) {
|
|
9
9
|
const boardCmd = new Command('board')
|
|
10
10
|
.description('Manage Jira boards')
|
|
11
11
|
.addHelpText('after', `
|
|
@@ -21,7 +21,7 @@ Common Actions:
|
|
|
21
21
|
.option('-t, --type <type>', 'Filter by board type (scrum, kanban, simple)')
|
|
22
22
|
.option('-l, --limit <n>', 'Max results', '50')
|
|
23
23
|
.option('-o, --output <format>', 'Output format (json)')
|
|
24
|
-
.action(async (options) => {
|
|
24
|
+
.action(async (options: any) => {
|
|
25
25
|
const spinner = ora('Fetching boards...').start();
|
|
26
26
|
try {
|
|
27
27
|
const params = new URLSearchParams();
|
|
@@ -43,19 +43,19 @@ Common Actions:
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (options.output === 'json') {
|
|
46
|
-
console.log(JSON.stringify(data.values.map(b => ({
|
|
46
|
+
console.log(JSON.stringify(data.values.map((b: any) => ({
|
|
47
47
|
id: b.id, name: b.name,
|
|
48
48
|
type: b.type, project: b.location?.projectKey || null
|
|
49
49
|
})), null, 2));
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const
|
|
54
|
-
[chalk.bold('ID'), chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Project')]
|
|
55
|
-
|
|
53
|
+
const table = new Table({
|
|
54
|
+
head: [chalk.bold('ID'), chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Project')]
|
|
55
|
+
});
|
|
56
56
|
|
|
57
|
-
data.values.forEach(b => {
|
|
58
|
-
|
|
57
|
+
data.values.forEach((b: any) => {
|
|
58
|
+
table.push([
|
|
59
59
|
b.id,
|
|
60
60
|
b.name,
|
|
61
61
|
b.type,
|
|
@@ -63,10 +63,10 @@ Common Actions:
|
|
|
63
63
|
]);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
console.log(table(
|
|
66
|
+
console.log(table.toString());
|
|
67
67
|
console.log(chalk.grey(`Showing ${data.values.length} board(s)`));
|
|
68
68
|
|
|
69
|
-
} catch (e) {
|
|
69
|
+
} catch (e: any) {
|
|
70
70
|
handleCommandError(spinner, e, 'Failed to list boards');
|
|
71
71
|
}
|
|
72
72
|
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import enquirer from 'enquirer';
|
|
5
|
+
import { api } from '../services/api-service.js';
|
|
6
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
7
|
+
|
|
8
|
+
export function registerBulkCommand(program: Command) {
|
|
9
|
+
const bulkCmd = new Command('bulk')
|
|
10
|
+
.description('Bulk operations on Jira issues')
|
|
11
|
+
.addHelpText('after', `
|
|
12
|
+
Common Actions:
|
|
13
|
+
$ jira bulk transition -j "project = PROJ AND status = 'To Do'" -s "In Progress"
|
|
14
|
+
`);
|
|
15
|
+
|
|
16
|
+
// ── BULK TRANSITION ──────────────────────────────────────────────
|
|
17
|
+
bulkCmd
|
|
18
|
+
.command('transition')
|
|
19
|
+
.description('Transition multiple issues matching a JQL filter')
|
|
20
|
+
.requiredOption('-j, --jql <query>', 'JQL query to select issues')
|
|
21
|
+
.option('-s, --status <name>', 'Target status name')
|
|
22
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
23
|
+
.option('-l, --limit <n>', 'Max issues to process', '50')
|
|
24
|
+
.addHelpText('after', `
|
|
25
|
+
Examples:
|
|
26
|
+
$ jira bulk transition -j "project = PROJ AND status = 'To Do'" -s "In Progress"
|
|
27
|
+
$ jira bulk transition -j "assignee = currentUser() AND status = Review" -s Done -y
|
|
28
|
+
`)
|
|
29
|
+
.action(async (options: any) => {
|
|
30
|
+
const spinner = ora('Finding matching issues...').start();
|
|
31
|
+
try {
|
|
32
|
+
const data = await api.post('/search/jql', {
|
|
33
|
+
jql: options.jql,
|
|
34
|
+
maxResults: parseInt(options.limit),
|
|
35
|
+
fields: ['summary', 'status']
|
|
36
|
+
});
|
|
37
|
+
spinner.stop();
|
|
38
|
+
|
|
39
|
+
if (!data.issues || data.issues.length === 0) {
|
|
40
|
+
console.log(chalk.yellow('No issues match the query.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.bold(`\nFound ${data.issues.length} issue(s):\n`));
|
|
45
|
+
data.issues.forEach((i: any) => {
|
|
46
|
+
console.log(` ${chalk.cyan(i.key)} ${i.fields.summary} [${i.fields.status.name}]`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let targetStatus = options.status;
|
|
50
|
+
|
|
51
|
+
if (!targetStatus) {
|
|
52
|
+
// Get transitions from the first issue to show available statuses
|
|
53
|
+
const transData = await api.get(`/issue/${data.issues[0].key}/transitions`);
|
|
54
|
+
const { Select } = enquirer as any;
|
|
55
|
+
const statusSelect = new Select({
|
|
56
|
+
name: 'status',
|
|
57
|
+
message: 'Target status',
|
|
58
|
+
choices: transData.transitions.map((t: any) => ({ name: t.name, message: t.name }))
|
|
59
|
+
});
|
|
60
|
+
targetStatus = await statusSelect.run();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!options.yes) {
|
|
64
|
+
const { Confirm } = enquirer as any;
|
|
65
|
+
const confirm = new Confirm({
|
|
66
|
+
name: 'proceed',
|
|
67
|
+
message: `Transition ${data.issues.length} issue(s) to "${targetStatus}"?`
|
|
68
|
+
});
|
|
69
|
+
if (!await confirm.run()) {
|
|
70
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const transSpinner = ora(`Transitioning ${data.issues.length} issue(s)...`).start();
|
|
76
|
+
let success = 0;
|
|
77
|
+
let failed = 0;
|
|
78
|
+
|
|
79
|
+
for (const issue of data.issues) {
|
|
80
|
+
try {
|
|
81
|
+
const transData = await api.get(`/issue/${issue.key}/transitions`);
|
|
82
|
+
const transition = transData.transitions.find(
|
|
83
|
+
(t: any) => t.name.toLowerCase() === targetStatus.toLowerCase()
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (transition) {
|
|
87
|
+
await api.post(`/issue/${issue.key}/transitions`, {
|
|
88
|
+
transition: { id: transition.id }
|
|
89
|
+
});
|
|
90
|
+
success++;
|
|
91
|
+
} else {
|
|
92
|
+
failed++;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
failed++;
|
|
96
|
+
}
|
|
97
|
+
transSpinner.text = `Transitioning... (${success + failed}/${data.issues.length})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
transSpinner.succeed(`Done: ${chalk.green(`${success} succeeded`)}, ${failed > 0 ? chalk.red(`${failed} failed`) : '0 failed'}`);
|
|
101
|
+
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
handleCommandError(spinner, e, 'Bulk transition failed');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── BULK ASSIGN ──────────────────────────────────────────────────
|
|
108
|
+
bulkCmd
|
|
109
|
+
.command('assign')
|
|
110
|
+
.description('Assign multiple issues')
|
|
111
|
+
.requiredOption('-j, --jql <query>', 'JQL query')
|
|
112
|
+
.option('-a, --assignee <id>', 'AccountId or "me"')
|
|
113
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
114
|
+
.action(async (options: any) => {
|
|
115
|
+
const spinner = ora('Finding issues...').start();
|
|
116
|
+
try {
|
|
117
|
+
const data = await api.post('/search/jql', {
|
|
118
|
+
jql: options.jql,
|
|
119
|
+
maxResults: 50,
|
|
120
|
+
fields: ['summary', 'assignee']
|
|
121
|
+
});
|
|
122
|
+
spinner.stop();
|
|
123
|
+
|
|
124
|
+
if (!data.issues?.length) {
|
|
125
|
+
console.log(chalk.yellow('No issues found.'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(chalk.bold(`Found ${data.issues.length} issue(s):`));
|
|
130
|
+
data.issues.forEach((i: any) => console.log(` ${i.key}: ${i.fields.summary} (${i.fields.assignee?.displayName || 'Unassigned'})`));
|
|
131
|
+
|
|
132
|
+
let assigneeId = options.assignee;
|
|
133
|
+
if (!assigneeId) {
|
|
134
|
+
const { userId } = await enquirer.prompt({
|
|
135
|
+
type: 'input',
|
|
136
|
+
name: 'userId',
|
|
137
|
+
message: 'Enter Account ID (or "me"):',
|
|
138
|
+
validate: (val: string) => val.length > 0
|
|
139
|
+
}) as any;
|
|
140
|
+
assigneeId = userId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (assigneeId === 'me') {
|
|
144
|
+
const me = await api.get('/myself');
|
|
145
|
+
assigneeId = me.accountId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!options.yes) {
|
|
149
|
+
const { confirm } = await enquirer.prompt({
|
|
150
|
+
type: 'confirm',
|
|
151
|
+
name: 'confirm',
|
|
152
|
+
message: `Assign ${data.issues.length} issues to ${assigneeId}?`
|
|
153
|
+
}) as any;
|
|
154
|
+
if (!confirm) return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const processSpinner = ora('Assigning...').start();
|
|
158
|
+
for (const issue of data.issues) {
|
|
159
|
+
await api.put(`/issue/${issue.key}/assignee`, { accountId: assigneeId });
|
|
160
|
+
}
|
|
161
|
+
processSpinner.succeed('Bulk assign complete.');
|
|
162
|
+
|
|
163
|
+
} catch (e: any) {
|
|
164
|
+
handleCommandError(spinner, e, 'Bulk assign failed');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── BULK LABEL ───────────────────────────────────────────────────
|
|
169
|
+
bulkCmd
|
|
170
|
+
.command('label')
|
|
171
|
+
.description('Add or remove labels from multiple issues')
|
|
172
|
+
.requiredOption('-j, --jql <query>', 'JQL query')
|
|
173
|
+
.option('--add <labels>', 'Comma-separated labels to add')
|
|
174
|
+
.option('--remove <labels>', 'Comma-separated labels to remove')
|
|
175
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
176
|
+
.action(async (options: any) => {
|
|
177
|
+
if (!options.add && !options.remove) {
|
|
178
|
+
console.log(chalk.red('Must specify --add or --remove'));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const spinner = ora('Finding issues...').start();
|
|
183
|
+
try {
|
|
184
|
+
const data = await api.post('/search/jql', {
|
|
185
|
+
jql: options.jql,
|
|
186
|
+
maxResults: 50,
|
|
187
|
+
fields: ['summary', 'labels']
|
|
188
|
+
});
|
|
189
|
+
spinner.stop();
|
|
190
|
+
|
|
191
|
+
if (!data.issues?.length) {
|
|
192
|
+
console.log(chalk.yellow('No issues found.'));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(chalk.bold(`Found ${data.issues.length} issue(s).`));
|
|
197
|
+
|
|
198
|
+
if (!options.yes) {
|
|
199
|
+
const { confirm } = await enquirer.prompt({
|
|
200
|
+
type: 'confirm',
|
|
201
|
+
name: 'confirm',
|
|
202
|
+
message: `Update labels for ${data.issues.length} issues?`
|
|
203
|
+
}) as any;
|
|
204
|
+
if (!confirm) return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const processSpinner = ora('Updating labels...').start();
|
|
208
|
+
const addList = options.add ? options.add.split(',').map((l: string) => l.trim()) : [];
|
|
209
|
+
const removeList = options.remove ? options.remove.split(',').map((l: string) => l.trim()) : [];
|
|
210
|
+
|
|
211
|
+
for (const issue of data.issues) {
|
|
212
|
+
const currentLabels = issue.fields.labels || [];
|
|
213
|
+
let newLabels = new Set(currentLabels);
|
|
214
|
+
|
|
215
|
+
addList.forEach((l: string) => newLabels.add(l));
|
|
216
|
+
removeList.forEach((l: string) => newLabels.delete(l));
|
|
217
|
+
|
|
218
|
+
await api.put(`/issue/${issue.key}`, {
|
|
219
|
+
fields: { labels: Array.from(newLabels) }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
processSpinner.succeed('Bulk labels updated.');
|
|
223
|
+
|
|
224
|
+
} catch (e: any) {
|
|
225
|
+
handleCommandError(spinner, e, 'Bulk label failed');
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
program.addCommand(bulkCmd);
|
|
230
|
+
}
|