jira-pilot 2.0.1 → 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.
@@ -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
+ }
@@ -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
- // Keep existing token if user didn't change it (and entered ***** which is not real)
64
- // Actually prompt returns text. If they leave it blank?
65
- // Let's assume if they type nothing, we keep old? Enquirer behavior depends.
66
- // Better to just save what we get.
67
-
68
- // Validation check
69
- const spinner = ora('Verifying credentials...').start();
70
-
71
- // Temporarily set config to test
72
- setCredentials(answers);
73
- api.init(); // Refresh api client with new creds
74
-
75
- try {
76
- await api.get('/myself');
77
- spinner.succeed(chalk.green('Credentials verified and saved!'));
78
- } catch (e) {
79
- spinner.fail(chalk.red('Verification failed! Credentials saved but might be incorrect.'));
80
- console.error(e.message);
81
- }
82
-
83
- } catch (e) {
84
- console.error(chalk.red('Setup cancelled or failed'), e);
85
- }
86
- });
87
-
88
- configCmd
89
- .command('view')
90
- .description('View current configuration')
91
- .action(() => {
92
- const { jiraUrl, email } = getCredentials();
93
- if (jiraUrl) {
94
- console.log(chalk.green('Current Configuration:'));
95
- console.log(`URL: ${jiraUrl}`);
96
- console.log(`Email: ${email}`);
97
- console.log(`Token: ************`);
98
- } else {
99
- console.log(chalk.yellow('No configuration found. Run "jira config setup"'));
100
- }
101
- });
102
-
103
- configCmd
104
- .command('clear')
105
- .description('Clear saved credentials')
106
- .action(() => {
107
- clearCredentials();
108
- console.log(chalk.green('Credentials cleared.'));
109
- });
110
-
111
- const aiConfigCmd = new Command('ai')
112
- .description('Manage AI settings');
113
-
114
- aiConfigCmd
115
- .command('enable')
116
- .description('Enable AI features')
117
- .action(async () => {
118
- const current = getCredentials();
119
- let key = current.aiKey;
120
-
121
- if (!key) {
122
- const response = await enquirer.prompt({
123
- type: 'password',
124
- name: 'aiKey',
125
- message: 'Enter AI API Key:'
126
- });
127
- key = response.aiKey;
128
- }
129
-
130
- setCredentials({ aiEnabled: true, aiKey: key });
131
- console.log(chalk.green('AI features enabled!'));
132
- });
133
-
134
- aiConfigCmd
135
- .command('disable')
136
- .description('Disable AI features')
137
- .action(() => {
138
- setCredentials({ aiEnabled: false });
139
- console.log(chalk.yellow('AI features disabled.'));
140
- });
141
-
142
- aiConfigCmd
143
- .command('status')
144
- .description('Check AI feature status')
145
- .action(() => {
146
- const { aiEnabled, aiProvider } = getCredentials();
147
- console.log(`AI Enabled: ${aiEnabled ? chalk.green('Yes') : chalk.red('No')}`);
148
- console.log(`Provider: ${aiProvider || 'None'}`);
149
- });
150
-
151
- configCmd.addCommand(aiConfigCmd);
152
-
153
- program.addCommand(configCmd);
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
+ }