jira-pilot 2.0.5 → 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 +207 -158
- package/bin/{jira.js → jira.ts} +10 -1
- package/dist/bin/jira.js +64 -0
- package/package.json +14 -7
- 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} +5 -5
- 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} +348 -57
- package/src/commands/{mcp.js → mcp.ts} +2 -2
- package/src/commands/{project.js → project.ts} +5 -5
- package/src/commands/{sprint.js → sprint.ts} +125 -9
- 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.js → config-store.ts} +18 -5
- package/src/utils/{config.js → config.ts} +12 -8
- 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 -90
|
@@ -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
|
});
|
|
@@ -5,7 +5,7 @@ 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,7 +43,7 @@ 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));
|
|
@@ -54,7 +54,7 @@ Common Actions:
|
|
|
54
54
|
head: [chalk.bold('ID'), chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Project')]
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
data.values.forEach(b => {
|
|
57
|
+
data.values.forEach((b: any) => {
|
|
58
58
|
table.push([
|
|
59
59
|
b.id,
|
|
60
60
|
b.name,
|
|
@@ -66,7 +66,7 @@ Common Actions:
|
|
|
66
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
|
+
}
|
|
@@ -2,10 +2,11 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import enquirer from 'enquirer';
|
|
4
4
|
import { setCredentials, getCredentials, clearCredentials, saveProfile, loadProfile, deleteProfile, listProfiles, getActiveProfile } from '../utils/config.js';
|
|
5
|
+
import ConfigStore from '../utils/config-store.js';
|
|
5
6
|
import ora from 'ora';
|
|
6
7
|
import { api } from '../services/api-service.js';
|
|
7
8
|
|
|
8
|
-
export function registerConfigCommand(program) {
|
|
9
|
+
export function registerConfigCommand(program: Command) {
|
|
9
10
|
const configCmd = new Command('config')
|
|
10
11
|
.description('Configure Jira credentials');
|
|
11
12
|
|
|
@@ -65,7 +66,7 @@ export function registerConfigCommand(program) {
|
|
|
65
66
|
initial: current.githubToken ? '*****' : undefined,
|
|
66
67
|
skip: function () { return !this.state.answers.aiEnabled; }
|
|
67
68
|
}
|
|
68
|
-
]);
|
|
69
|
+
] as any) as any;
|
|
69
70
|
|
|
70
71
|
// Keep existing token if user didn't change it (and entered ***** which is not real)
|
|
71
72
|
// Actually prompt returns text. If they leave it blank?
|
|
@@ -82,12 +83,12 @@ export function registerConfigCommand(program) {
|
|
|
82
83
|
try {
|
|
83
84
|
await api.get('/myself');
|
|
84
85
|
spinner.succeed(chalk.green('Credentials verified and saved!'));
|
|
85
|
-
} catch (e) {
|
|
86
|
+
} catch (e: any) {
|
|
86
87
|
spinner.fail(chalk.red('Verification failed! Credentials saved but might be incorrect.'));
|
|
87
88
|
console.error(e.message);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
} catch (e) {
|
|
91
|
+
} catch (e: any) {
|
|
91
92
|
console.error(chalk.red('Setup cancelled or failed'), e);
|
|
92
93
|
}
|
|
93
94
|
});
|
|
@@ -130,7 +131,7 @@ export function registerConfigCommand(program) {
|
|
|
130
131
|
type: 'password',
|
|
131
132
|
name: 'aiKey',
|
|
132
133
|
message: 'Enter AI API Key:'
|
|
133
|
-
});
|
|
134
|
+
}) as any;
|
|
134
135
|
key = response.aiKey;
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -162,7 +163,7 @@ export function registerConfigCommand(program) {
|
|
|
162
163
|
.command('save')
|
|
163
164
|
.description('Save current config as a named profile')
|
|
164
165
|
.argument('<name>', 'Profile name')
|
|
165
|
-
.action((name) => {
|
|
166
|
+
.action((name: string) => {
|
|
166
167
|
saveProfile(name);
|
|
167
168
|
console.log(chalk.green(`Profile "${name}" saved and set as active.`));
|
|
168
169
|
});
|
|
@@ -171,7 +172,7 @@ export function registerConfigCommand(program) {
|
|
|
171
172
|
.command('use')
|
|
172
173
|
.description('Switch to a saved profile')
|
|
173
174
|
.argument('<name>', 'Profile name')
|
|
174
|
-
.action((name) => {
|
|
175
|
+
.action((name: string) => {
|
|
175
176
|
if (loadProfile(name)) {
|
|
176
177
|
console.log(chalk.green(`Switched to profile "${name}".`));
|
|
177
178
|
const creds = getCredentials();
|
|
@@ -210,7 +211,7 @@ export function registerConfigCommand(program) {
|
|
|
210
211
|
.command('delete-profile')
|
|
211
212
|
.description('Delete a saved profile')
|
|
212
213
|
.argument('<name>', 'Profile name')
|
|
213
|
-
.action((name) => {
|
|
214
|
+
.action((name: string) => {
|
|
214
215
|
const profiles = listProfiles();
|
|
215
216
|
if (!profiles.includes(name)) {
|
|
216
217
|
console.error(chalk.red(`Profile "${name}" not found.`));
|
|
@@ -220,5 +221,53 @@ export function registerConfigCommand(program) {
|
|
|
220
221
|
console.log(chalk.green(`Profile "${name}" deleted.`));
|
|
221
222
|
});
|
|
222
223
|
|
|
224
|
+
// ── CUSTOM FIELD MANAGEMENT ─────────────────────────────────────
|
|
225
|
+
const fieldCmd = new Command('field')
|
|
226
|
+
.description('Manage custom field aliases');
|
|
227
|
+
|
|
228
|
+
fieldCmd
|
|
229
|
+
.command('set')
|
|
230
|
+
.description('Set a custom field alias')
|
|
231
|
+
.argument('<alias>', 'Field Alias (e.g. storyPoints)')
|
|
232
|
+
.argument('<fieldId>', 'Field ID (e.g. customfield_10011)')
|
|
233
|
+
.action((alias, fieldId) => {
|
|
234
|
+
const config = new ConfigStore('jira-pilot');
|
|
235
|
+
config.set(`customFields.${alias}`, fieldId);
|
|
236
|
+
console.log(chalk.green(`Alias "${chalk.bold(alias)}" mapped to ${chalk.bold(fieldId)}.`));
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
fieldCmd
|
|
240
|
+
.command('list')
|
|
241
|
+
.description('List custom field aliases')
|
|
242
|
+
.action(() => {
|
|
243
|
+
const config = new ConfigStore('jira-pilot');
|
|
244
|
+
const fields = config.get('customFields') || {};
|
|
245
|
+
if (Object.keys(fields).length === 0) {
|
|
246
|
+
console.log(chalk.yellow('No custom field aliases defined.'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.log(chalk.bold('\nCustom Field Aliases:\n'));
|
|
250
|
+
for (const [alias, id] of Object.entries(fields)) {
|
|
251
|
+
console.log(` ${chalk.cyan(alias)}: ${id}`);
|
|
252
|
+
}
|
|
253
|
+
console.log('');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
fieldCmd
|
|
257
|
+
.command('delete')
|
|
258
|
+
.description('Delete a custom field alias')
|
|
259
|
+
.argument('<alias>', 'Field Alias')
|
|
260
|
+
.action((alias) => {
|
|
261
|
+
const config = new ConfigStore('jira-pilot');
|
|
262
|
+
if (!config.get(`customFields.${alias}`)) {
|
|
263
|
+
console.error(chalk.red(`Alias "${alias}" not found.`));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
config.delete(`customFields.${alias}`);
|
|
267
|
+
console.log(chalk.green(`Alias "${chalk.bold(alias)}" deleted.`));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
configCmd.addCommand(fieldCmd);
|
|
271
|
+
|
|
223
272
|
program.addCommand(configCmd);
|
|
224
273
|
}
|