jira-pilot 2.0.0 → 2.0.2
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/LICENSE +5 -5
- package/README.md +465 -404
- package/bin/jira.js +62 -55
- package/package.json +90 -84
- package/src/commands/ai-actions/plan.js +119 -0
- package/src/commands/ai-actions/review.js +109 -0
- package/src/commands/ai-actions/standup.js +42 -0
- package/src/commands/ai.js +232 -204
- package/src/commands/board.js +75 -66
- package/src/commands/bulk.js +108 -0
- package/src/commands/config.js +224 -154
- package/src/commands/dashboard.js +89 -0
- package/src/commands/git.js +63 -60
- package/src/commands/issue.js +985 -698
- package/src/commands/mcp.js +20 -20
- package/src/commands/project.js +59 -50
- package/src/commands/sprint.js +153 -78
- package/src/server/mcp-server.js +332 -332
- package/src/services/ai-service.js +165 -107
- package/src/services/api-service.js +115 -115
- package/src/utils/adf-parser.js +49 -49
- package/src/utils/config.js +97 -60
- package/src/utils/error-handler.js +41 -41
- package/src/utils/text-to-adf.js +34 -34
- package/src/utils/validators.js +88 -0
|
@@ -0,0 +1,108 @@
|
|
|
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) {
|
|
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) => {
|
|
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 => {
|
|
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;
|
|
55
|
+
const statusSelect = new Select({
|
|
56
|
+
name: 'status',
|
|
57
|
+
message: 'Target status',
|
|
58
|
+
choices: transData.transitions.map(t => ({ name: t.name, message: t.name }))
|
|
59
|
+
});
|
|
60
|
+
targetStatus = await statusSelect.run();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!options.yes) {
|
|
64
|
+
const { Confirm } = enquirer;
|
|
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 => 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) {
|
|
103
|
+
handleCommandError(spinner, e, 'Bulk transition failed');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
program.addCommand(bulkCmd);
|
|
108
|
+
}
|
package/src/commands/config.js
CHANGED
|
@@ -1,154 +1,224 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import enquirer from 'enquirer';
|
|
4
|
-
import { setCredentials, getCredentials, clearCredentials } from '../utils/config.js';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
import { api } from '../services/api-service.js';
|
|
7
|
-
|
|
8
|
-
export function registerConfigCommand(program) {
|
|
9
|
-
const configCmd = new Command('config')
|
|
10
|
-
.description('Configure Jira credentials');
|
|
11
|
-
|
|
12
|
-
configCmd
|
|
13
|
-
.command('setup')
|
|
14
|
-
.description('Interactive setup of Jira credentials')
|
|
15
|
-
.action(async () => {
|
|
16
|
-
console.log(chalk.blue('Configuring jira-pilot...'));
|
|
17
|
-
|
|
18
|
-
const current = getCredentials();
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const answers = await enquirer.prompt([
|
|
22
|
-
{
|
|
23
|
-
type: 'input',
|
|
24
|
-
name: 'jiraUrl',
|
|
25
|
-
message: 'Jira Site URL (e.g., https://your-domain.atlassian.net):',
|
|
26
|
-
initial: current.jiraUrl
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
type: 'input',
|
|
30
|
-
name: 'email',
|
|
31
|
-
message: 'Jira Email Address:',
|
|
32
|
-
initial: current.email
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
type: 'password',
|
|
36
|
-
name: 'apiToken',
|
|
37
|
-
message: 'Jira API Token:',
|
|
38
|
-
initial: current.apiToken ? '*****' : undefined
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
type: 'confirm',
|
|
42
|
-
name: 'aiEnabled',
|
|
43
|
-
message: 'Enable AI features?',
|
|
44
|
-
initial: current.aiEnabled || false
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
type: 'select',
|
|
48
|
-
name: 'aiProvider',
|
|
49
|
-
message: 'Select AI Provider:',
|
|
50
|
-
choices: ['openai', 'gemini', 'anthropic'],
|
|
51
|
-
initial: current.aiProvider || 'openai',
|
|
52
|
-
skip: (state) => !state.answers.aiEnabled
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
type: 'password',
|
|
56
|
-
name: 'aiKey',
|
|
57
|
-
message: 'AI API Key:',
|
|
58
|
-
initial: current.aiKey ? '*****' : undefined,
|
|
59
|
-
skip: (state) => !state.answers.aiEnabled
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.description('
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import enquirer from 'enquirer';
|
|
4
|
+
import { setCredentials, getCredentials, clearCredentials, saveProfile, loadProfile, deleteProfile, listProfiles, getActiveProfile } from '../utils/config.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { api } from '../services/api-service.js';
|
|
7
|
+
|
|
8
|
+
export function registerConfigCommand(program) {
|
|
9
|
+
const configCmd = new Command('config')
|
|
10
|
+
.description('Configure Jira credentials');
|
|
11
|
+
|
|
12
|
+
configCmd
|
|
13
|
+
.command('setup')
|
|
14
|
+
.description('Interactive setup of Jira credentials')
|
|
15
|
+
.action(async () => {
|
|
16
|
+
console.log(chalk.blue('Configuring jira-pilot...'));
|
|
17
|
+
|
|
18
|
+
const current = getCredentials();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const answers = await enquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'jiraUrl',
|
|
25
|
+
message: 'Jira Site URL (e.g., https://your-domain.atlassian.net):',
|
|
26
|
+
initial: current.jiraUrl
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: 'input',
|
|
30
|
+
name: 'email',
|
|
31
|
+
message: 'Jira Email Address:',
|
|
32
|
+
initial: current.email
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'password',
|
|
36
|
+
name: 'apiToken',
|
|
37
|
+
message: 'Jira API Token:',
|
|
38
|
+
initial: current.apiToken ? '*****' : undefined
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'confirm',
|
|
42
|
+
name: 'aiEnabled',
|
|
43
|
+
message: 'Enable AI features?',
|
|
44
|
+
initial: current.aiEnabled || false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'select',
|
|
48
|
+
name: 'aiProvider',
|
|
49
|
+
message: 'Select AI Provider:',
|
|
50
|
+
choices: ['openai', 'gemini', 'anthropic'],
|
|
51
|
+
initial: current.aiProvider || 'openai',
|
|
52
|
+
skip: (state) => !state.answers.aiEnabled
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'password',
|
|
56
|
+
name: 'aiKey',
|
|
57
|
+
message: 'AI API Key:',
|
|
58
|
+
initial: current.aiKey ? '*****' : undefined,
|
|
59
|
+
skip: (state) => !state.answers.aiEnabled
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'password',
|
|
63
|
+
name: 'githubToken',
|
|
64
|
+
message: 'GitHub Personal Access Token (for AI Code Review):',
|
|
65
|
+
initial: current.githubToken ? '*****' : undefined,
|
|
66
|
+
skip: (state) => !state.answers.aiEnabled
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// Keep existing token if user didn't change it (and entered ***** which is not real)
|
|
71
|
+
// Actually prompt returns text. If they leave it blank?
|
|
72
|
+
// Let's assume if they type nothing, we keep old? Enquirer behavior depends.
|
|
73
|
+
// Better to just save what we get.
|
|
74
|
+
|
|
75
|
+
// Validation check
|
|
76
|
+
const spinner = ora('Verifying credentials...').start();
|
|
77
|
+
|
|
78
|
+
// Temporarily set config to test
|
|
79
|
+
setCredentials(answers);
|
|
80
|
+
api.init(); // Refresh api client with new creds
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await api.get('/myself');
|
|
84
|
+
spinner.succeed(chalk.green('Credentials verified and saved!'));
|
|
85
|
+
} catch (e) {
|
|
86
|
+
spinner.fail(chalk.red('Verification failed! Credentials saved but might be incorrect.'));
|
|
87
|
+
console.error(e.message);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(chalk.red('Setup cancelled or failed'), e);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
configCmd
|
|
96
|
+
.command('view')
|
|
97
|
+
.description('View current configuration')
|
|
98
|
+
.action(() => {
|
|
99
|
+
const { jiraUrl, email } = getCredentials();
|
|
100
|
+
if (jiraUrl) {
|
|
101
|
+
console.log(chalk.green('Current Configuration:'));
|
|
102
|
+
console.log(`URL: ${jiraUrl}`);
|
|
103
|
+
console.log(`Email: ${email}`);
|
|
104
|
+
console.log(`Token: ************`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(chalk.yellow('No configuration found. Run "jira config setup"'));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
configCmd
|
|
111
|
+
.command('clear')
|
|
112
|
+
.description('Clear saved credentials')
|
|
113
|
+
.action(() => {
|
|
114
|
+
clearCredentials();
|
|
115
|
+
console.log(chalk.green('Credentials cleared.'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const aiConfigCmd = new Command('ai')
|
|
119
|
+
.description('Manage AI settings');
|
|
120
|
+
|
|
121
|
+
aiConfigCmd
|
|
122
|
+
.command('enable')
|
|
123
|
+
.description('Enable AI features')
|
|
124
|
+
.action(async () => {
|
|
125
|
+
const current = getCredentials();
|
|
126
|
+
let key = current.aiKey;
|
|
127
|
+
|
|
128
|
+
if (!key) {
|
|
129
|
+
const response = await enquirer.prompt({
|
|
130
|
+
type: 'password',
|
|
131
|
+
name: 'aiKey',
|
|
132
|
+
message: 'Enter AI API Key:'
|
|
133
|
+
});
|
|
134
|
+
key = response.aiKey;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setCredentials({ aiEnabled: true, aiKey: key });
|
|
138
|
+
console.log(chalk.green('AI features enabled!'));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
aiConfigCmd
|
|
142
|
+
.command('disable')
|
|
143
|
+
.description('Disable AI features')
|
|
144
|
+
.action(() => {
|
|
145
|
+
setCredentials({ aiEnabled: false });
|
|
146
|
+
console.log(chalk.yellow('AI features disabled.'));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
aiConfigCmd
|
|
150
|
+
.command('status')
|
|
151
|
+
.description('Check AI feature status')
|
|
152
|
+
.action(() => {
|
|
153
|
+
const { aiEnabled, aiProvider } = getCredentials();
|
|
154
|
+
console.log(`AI Enabled: ${aiEnabled ? chalk.green('Yes') : chalk.red('No')}`);
|
|
155
|
+
console.log(`Provider: ${aiProvider || 'None'}`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
configCmd.addCommand(aiConfigCmd);
|
|
159
|
+
|
|
160
|
+
// ── PROFILE MANAGEMENT ───────────────────────────────────────────
|
|
161
|
+
configCmd
|
|
162
|
+
.command('save')
|
|
163
|
+
.description('Save current config as a named profile')
|
|
164
|
+
.argument('<name>', 'Profile name')
|
|
165
|
+
.action((name) => {
|
|
166
|
+
saveProfile(name);
|
|
167
|
+
console.log(chalk.green(`Profile "${name}" saved and set as active.`));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
configCmd
|
|
171
|
+
.command('use')
|
|
172
|
+
.description('Switch to a saved profile')
|
|
173
|
+
.argument('<name>', 'Profile name')
|
|
174
|
+
.action((name) => {
|
|
175
|
+
if (loadProfile(name)) {
|
|
176
|
+
console.log(chalk.green(`Switched to profile "${name}".`));
|
|
177
|
+
const creds = getCredentials();
|
|
178
|
+
console.log(` URL: ${creds.jiraUrl}`);
|
|
179
|
+
console.log(` Email: ${creds.email}`);
|
|
180
|
+
} else {
|
|
181
|
+
console.error(chalk.red(`Profile "${name}" not found.`));
|
|
182
|
+
const profiles = listProfiles();
|
|
183
|
+
if (profiles.length > 0) {
|
|
184
|
+
console.log(chalk.grey(`Available: ${profiles.join(', ')}`));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
configCmd
|
|
190
|
+
.command('profiles')
|
|
191
|
+
.description('List saved profiles')
|
|
192
|
+
.action(() => {
|
|
193
|
+
const profiles = listProfiles();
|
|
194
|
+
const active = getActiveProfile();
|
|
195
|
+
|
|
196
|
+
if (profiles.length === 0) {
|
|
197
|
+
console.log(chalk.yellow('No profiles saved. Use "jira config save <name>" to save one.'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(chalk.bold('\nSaved Profiles:\n'));
|
|
202
|
+
profiles.forEach(p => {
|
|
203
|
+
const marker = p === active ? chalk.green(' ✓ (active)') : '';
|
|
204
|
+
console.log(` ${chalk.cyan(p)}${marker}`);
|
|
205
|
+
});
|
|
206
|
+
console.log('');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
configCmd
|
|
210
|
+
.command('delete-profile')
|
|
211
|
+
.description('Delete a saved profile')
|
|
212
|
+
.argument('<name>', 'Profile name')
|
|
213
|
+
.action((name) => {
|
|
214
|
+
const profiles = listProfiles();
|
|
215
|
+
if (!profiles.includes(name)) {
|
|
216
|
+
console.error(chalk.red(`Profile "${name}" not found.`));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
deleteProfile(name);
|
|
220
|
+
console.log(chalk.green(`Profile "${name}" deleted.`));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
program.addCommand(configCmd);
|
|
224
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { table } from 'table';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { api } from '../services/api-service.js';
|
|
6
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
7
|
+
|
|
8
|
+
export function registerDashboardCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('dashboard')
|
|
11
|
+
.description('Show a quick overview of your Jira activity')
|
|
12
|
+
.option('-o, --output <format>', 'Output format (json)')
|
|
13
|
+
.addHelpText('after', `
|
|
14
|
+
Examples:
|
|
15
|
+
$ jira dashboard
|
|
16
|
+
$ jira dashboard --output json
|
|
17
|
+
`)
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const spinner = ora('Loading dashboard...').start();
|
|
20
|
+
try {
|
|
21
|
+
// Fetch in parallel: my open issues + recently updated
|
|
22
|
+
const [myIssues, recentIssues] = await Promise.all([
|
|
23
|
+
api.post('/search/jql', {
|
|
24
|
+
jql: 'assignee = currentUser() AND statusCategory != Done ORDER BY priority ASC, updated DESC',
|
|
25
|
+
maxResults: 8,
|
|
26
|
+
fields: ['summary', 'status', 'priority', 'updated']
|
|
27
|
+
}),
|
|
28
|
+
api.post('/search/jql', {
|
|
29
|
+
jql: 'assignee = currentUser() ORDER BY updated DESC',
|
|
30
|
+
maxResults: 5,
|
|
31
|
+
fields: ['summary', 'status', 'updated']
|
|
32
|
+
})
|
|
33
|
+
]);
|
|
34
|
+
spinner.stop();
|
|
35
|
+
|
|
36
|
+
if (options.output === 'json') {
|
|
37
|
+
console.log(JSON.stringify({
|
|
38
|
+
openIssues: (myIssues.issues || []).map(i => ({
|
|
39
|
+
key: i.key, summary: i.fields.summary,
|
|
40
|
+
status: i.fields.status?.name, priority: i.fields.priority?.name
|
|
41
|
+
})),
|
|
42
|
+
recentActivity: (recentIssues.issues || []).map(i => ({
|
|
43
|
+
key: i.key, summary: i.fields.summary,
|
|
44
|
+
status: i.fields.status?.name, updated: i.fields.updated
|
|
45
|
+
}))
|
|
46
|
+
}, null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Open Issues ──────────────────────────────────────
|
|
51
|
+
console.log(chalk.bold('\n📋 Your Open Issues') + chalk.grey(` (${myIssues.total || 0} total)`));
|
|
52
|
+
|
|
53
|
+
if (myIssues.issues && myIssues.issues.length > 0) {
|
|
54
|
+
const openTable = [
|
|
55
|
+
[chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Priority')]
|
|
56
|
+
];
|
|
57
|
+
myIssues.issues.forEach(i => {
|
|
58
|
+
const prio = i.fields.priority?.name || '';
|
|
59
|
+
const prioColor = prio === 'Highest' || prio === 'High' ? chalk.red(prio) : prio === 'Low' || prio === 'Lowest' ? chalk.blue(prio) : prio;
|
|
60
|
+
openTable.push([
|
|
61
|
+
chalk.cyan(i.key),
|
|
62
|
+
i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
|
|
63
|
+
i.fields.status?.name || '',
|
|
64
|
+
prioColor
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
console.log(table(openTable));
|
|
68
|
+
} else {
|
|
69
|
+
console.log(chalk.green(' 🎉 No open issues — nice work!\n'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Recent Activity ──────────────────────────────────
|
|
73
|
+
console.log(chalk.bold('🕐 Recent Activity'));
|
|
74
|
+
|
|
75
|
+
if (recentIssues.issues && recentIssues.issues.length > 0) {
|
|
76
|
+
recentIssues.issues.forEach(i => {
|
|
77
|
+
const updated = i.fields.updated ? new Date(i.fields.updated).toLocaleDateString() : '';
|
|
78
|
+
console.log(` ${chalk.cyan(i.key)} ${i.fields.summary || ''} ${chalk.grey(`[${i.fields.status?.name}] ${updated}`)}`);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
console.log(chalk.grey(' No recent activity.'));
|
|
82
|
+
}
|
|
83
|
+
console.log('');
|
|
84
|
+
|
|
85
|
+
} catch (e) {
|
|
86
|
+
handleCommandError(spinner, e, 'Failed to load dashboard');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|