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
|
@@ -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
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import enquirer from 'enquirer';
|
|
6
|
+
import { api } from '../services/api-service.js';
|
|
7
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
8
|
+
|
|
9
|
+
// Utility for status icons
|
|
10
|
+
const getStatusIcon = (status: string) => {
|
|
11
|
+
const s = status.toLowerCase();
|
|
12
|
+
if (s.includes('done') || s.includes('closed')) return '✅';
|
|
13
|
+
if (s.includes('progress')) return '🏃';
|
|
14
|
+
if (s.includes('review')) return '👀';
|
|
15
|
+
return '📝';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Utility for priority icons/colors
|
|
19
|
+
const getPriorityColor = (priority: string, text: string) => {
|
|
20
|
+
const p = priority.toLowerCase();
|
|
21
|
+
if (p.includes('highest')) return chalk.red.bold(text);
|
|
22
|
+
if (p.includes('high')) return chalk.red(text);
|
|
23
|
+
if (p.includes('medium')) return chalk.yellow(text);
|
|
24
|
+
if (p.includes('low')) return chalk.blue(text);
|
|
25
|
+
return chalk.grey(text);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function registerDashboardCommand(program: Command) {
|
|
29
|
+
program
|
|
30
|
+
.command('dashboard')
|
|
31
|
+
.description('Interactive Jira Dashboard')
|
|
32
|
+
.option('-o, --output <format>', 'Output format (json)')
|
|
33
|
+
.action(async (options: any) => {
|
|
34
|
+
if (options.output === 'json') {
|
|
35
|
+
try {
|
|
36
|
+
const [myIssues, recentIssues] = await Promise.all([
|
|
37
|
+
api.post('/search/jql', {
|
|
38
|
+
jql: 'assignee = currentUser() AND statusCategory != Done ORDER BY priority ASC, updated DESC',
|
|
39
|
+
maxResults: 15,
|
|
40
|
+
fields: ['summary', 'status', 'priority', 'updated']
|
|
41
|
+
}),
|
|
42
|
+
api.post('/search/jql', {
|
|
43
|
+
jql: 'assignee = currentUser() ORDER BY updated DESC',
|
|
44
|
+
maxResults: 5,
|
|
45
|
+
fields: ['summary', 'status', 'updated']
|
|
46
|
+
})
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
console.log(JSON.stringify({
|
|
50
|
+
openIssues: (myIssues.issues || []).map((i: any) => ({
|
|
51
|
+
key: i.key, summary: i.fields.summary,
|
|
52
|
+
status: i.fields.status?.name, priority: i.fields.priority?.name
|
|
53
|
+
})),
|
|
54
|
+
recentActivity: (recentIssues.issues || []).map((i: any) => ({
|
|
55
|
+
key: i.key, summary: i.fields.summary,
|
|
56
|
+
status: i.fields.status?.name, updated: i.fields.updated
|
|
57
|
+
}))
|
|
58
|
+
}, null, 2));
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
console.error(JSON.stringify({ error: e.message }));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Interactive Dashboard ────────────────────────────
|
|
66
|
+
while (true) {
|
|
67
|
+
console.clear();
|
|
68
|
+
const spinner = ora('Loading dashboard...').start();
|
|
69
|
+
try {
|
|
70
|
+
const myself = await api.get('/myself');
|
|
71
|
+
|
|
72
|
+
// Fetch in parallel: my open issues + recently updated
|
|
73
|
+
const [myIssues, recentIssues] = await Promise.all([
|
|
74
|
+
api.post('/search/jql', {
|
|
75
|
+
jql: 'assignee = currentUser() AND statusCategory != Done ORDER BY priority ASC, updated DESC', // Fixed Sort
|
|
76
|
+
maxResults: 15,
|
|
77
|
+
fields: ['summary', 'status', 'priority', 'updated', 'issuetype']
|
|
78
|
+
}),
|
|
79
|
+
api.post('/search/jql', {
|
|
80
|
+
jql: 'assignee = currentUser() ORDER BY updated DESC',
|
|
81
|
+
maxResults: 5,
|
|
82
|
+
fields: ['summary', 'status', 'updated']
|
|
83
|
+
})
|
|
84
|
+
]);
|
|
85
|
+
spinner.stop();
|
|
86
|
+
|
|
87
|
+
// ── Header ───────────────────────────────────────────
|
|
88
|
+
console.log(chalk.bold.blue('\n✈️ Jira Pilot Dashboard'));
|
|
89
|
+
console.log(chalk.grey(` User: ${myself.displayName} <${myself.emailAddress}>`));
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
// ── Open Issues Table ────────────────────────────────
|
|
93
|
+
console.log(chalk.bold('📋 Your Open Issues') + chalk.grey(` (${myIssues.total || 0} total)`));
|
|
94
|
+
|
|
95
|
+
const issues = myIssues.issues || [];
|
|
96
|
+
|
|
97
|
+
if (issues.length > 0) {
|
|
98
|
+
const table = new Table({
|
|
99
|
+
head: [chalk.bold('Key'), chalk.bold('Type'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Priority')]
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
issues.forEach((i: any) => {
|
|
103
|
+
table.push([
|
|
104
|
+
chalk.cyan(i.key),
|
|
105
|
+
i.fields.issuetype?.name || '',
|
|
106
|
+
i.fields.summary ? (i.fields.summary.length > 40 ? i.fields.summary.substring(0, 37) + '...' : i.fields.summary) : '',
|
|
107
|
+
i.fields.status?.name || '',
|
|
108
|
+
getPriorityColor(i.fields.priority?.name || '', i.fields.priority?.name || '')
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
console.log(table.toString());
|
|
112
|
+
} else {
|
|
113
|
+
console.log(chalk.green(' 🎉 No open issues — nice work!'));
|
|
114
|
+
}
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
// ── Interactive Menu ─────────────────────────────────
|
|
118
|
+
// Enhancing the selection UI as requested
|
|
119
|
+
const choices = [
|
|
120
|
+
...issues.map((i: any) => {
|
|
121
|
+
const statusIcon = getStatusIcon(i.fields.status?.name || '');
|
|
122
|
+
const priorityColor = getPriorityColor(i.fields.priority?.name || '', '●');
|
|
123
|
+
const key = chalk.cyan.bold(i.key.padEnd(10));
|
|
124
|
+
const summary = i.fields.summary.substring(0, 45);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
name: i.key,
|
|
128
|
+
message: `${priorityColor} ${key} ${statusIcon} ${summary}`,
|
|
129
|
+
value: i.key
|
|
130
|
+
};
|
|
131
|
+
}),
|
|
132
|
+
{ role: 'separator' },
|
|
133
|
+
{ name: 'refresh', message: '🔄 Refresh Dashboard' },
|
|
134
|
+
{ name: 'exit', message: '🚪 Exit' }
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const { action } = await enquirer.prompt({
|
|
138
|
+
type: 'select',
|
|
139
|
+
name: 'action',
|
|
140
|
+
message: 'Select an issue to manage:',
|
|
141
|
+
choices: choices as any
|
|
142
|
+
}) as any;
|
|
143
|
+
|
|
144
|
+
if (action === 'exit') {
|
|
145
|
+
console.log('Bye! 👋');
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (action === 'refresh') {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Issue Selected: Show Action Menu
|
|
154
|
+
const selectedKey = action;
|
|
155
|
+
const { issueAction } = await enquirer.prompt({
|
|
156
|
+
type: 'select',
|
|
157
|
+
name: 'issueAction',
|
|
158
|
+
message: `Action for ${chalk.cyan(selectedKey)}:`,
|
|
159
|
+
choices: [
|
|
160
|
+
{ name: 'view', message: '📄 View Details' },
|
|
161
|
+
{ name: 'comment', message: '💬 Add Comment' },
|
|
162
|
+
{ name: 'transition', message: '🚀 Transition Status' },
|
|
163
|
+
{ name: 'assign', message: '👤 Assign' },
|
|
164
|
+
{ name: 'back', message: '⬅️ Back to Dashboard' }
|
|
165
|
+
]
|
|
166
|
+
}) as any;
|
|
167
|
+
|
|
168
|
+
if (issueAction === 'back') continue;
|
|
169
|
+
|
|
170
|
+
if (issueAction === 'view') {
|
|
171
|
+
const issue = await api.get(`/issue/${selectedKey}`);
|
|
172
|
+
console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
|
|
173
|
+
console.log(chalk.grey('────────────────────────────────────────'));
|
|
174
|
+
console.log(`${getStatusIcon(issue.fields.status.name)} ${issue.fields.status.name} | ${getPriorityColor(issue.fields.priority?.name || '', issue.fields.priority?.name || '')}`);
|
|
175
|
+
console.log(`\n${issue.fields.description || chalk.italic('No description')}\n`);
|
|
176
|
+
await pause();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (issueAction === 'comment') {
|
|
180
|
+
const { inputComment } = await enquirer.prompt({
|
|
181
|
+
type: 'input',
|
|
182
|
+
name: 'inputComment',
|
|
183
|
+
message: 'Comment:',
|
|
184
|
+
}) as any;
|
|
185
|
+
if (inputComment) {
|
|
186
|
+
const { textToADF } = await import('../utils/text-to-adf.js');
|
|
187
|
+
await api.post(`/issue/${selectedKey}/comment`, { body: textToADF(inputComment) });
|
|
188
|
+
console.log(chalk.green('Comment added.'));
|
|
189
|
+
await pause();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (issueAction === 'transition') {
|
|
194
|
+
const transData = await api.get(`/issue/${selectedKey}/transitions`);
|
|
195
|
+
const { transId } = await enquirer.prompt({
|
|
196
|
+
type: 'select',
|
|
197
|
+
name: 'transId',
|
|
198
|
+
message: 'Select Status:',
|
|
199
|
+
choices: transData.transitions.map((t: any) => ({ name: t.id, message: t.to.name }))
|
|
200
|
+
}) as any;
|
|
201
|
+
await api.post(`/issue/${selectedKey}/transitions`, { transition: { id: transId } });
|
|
202
|
+
console.log(chalk.green('Transitioned.'));
|
|
203
|
+
await pause();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (issueAction === 'assign') {
|
|
207
|
+
await api.put(`/issue/${selectedKey}/assignee`, { accountId: myself.accountId });
|
|
208
|
+
console.log(chalk.green('Assigned to you.'));
|
|
209
|
+
await pause();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
} catch (e: any) {
|
|
213
|
+
handleCommandError(spinner, e, 'Dashboard Error');
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function pause() {
|
|
221
|
+
await enquirer.prompt({ type: 'input', name: 'cont', message: 'Press Enter to continue...' });
|
|
222
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import { api } from '../services/api-service.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import enquirer from 'enquirer';
|
|
7
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
8
|
+
import { ConfigService } from '../services/config-service.js';
|
|
9
|
+
|
|
10
|
+
export function registerFilterCommand(program: Command) {
|
|
11
|
+
const filterCmd = new Command('filter')
|
|
12
|
+
.description('Manage saved JQL filters (Local)')
|
|
13
|
+
.addHelpText('after', `
|
|
14
|
+
Examples:
|
|
15
|
+
$ jira filter list
|
|
16
|
+
$ jira filter save "My Bugs" "assignee = currentUser() AND issuetype = Bug"
|
|
17
|
+
$ jira filter delete "My Bugs"
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
filterCmd
|
|
21
|
+
.command('list')
|
|
22
|
+
.description('List saved filters')
|
|
23
|
+
.action(async () => {
|
|
24
|
+
const cfg = ConfigService.getConfig();
|
|
25
|
+
const filters = cfg.filters || {};
|
|
26
|
+
|
|
27
|
+
if (Object.keys(filters).length === 0) {
|
|
28
|
+
console.log(chalk.yellow('No local filters saved.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const table = new Table({
|
|
33
|
+
head: [chalk.bold('Name'), chalk.bold('JQL')]
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
for (const [name, jql] of Object.entries(filters)) {
|
|
37
|
+
table.push([chalk.cyan(name), jql as string] as any);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(table.toString());
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
filterCmd
|
|
44
|
+
.command('save')
|
|
45
|
+
.description('Save a JQL filter locally')
|
|
46
|
+
.argument('<name>', 'Filter Name')
|
|
47
|
+
.argument('<jql>', 'JQL Query')
|
|
48
|
+
.action(async (name, jql) => {
|
|
49
|
+
try {
|
|
50
|
+
const cfg = ConfigService.getConfig();
|
|
51
|
+
if (!cfg.filters) cfg.filters = {};
|
|
52
|
+
|
|
53
|
+
cfg.filters[name] = jql;
|
|
54
|
+
ConfigService.saveConfig(cfg);
|
|
55
|
+
|
|
56
|
+
console.log(chalk.green(`Filter "${chalk.bold(name)}" saved.`));
|
|
57
|
+
} catch (e: any) {
|
|
58
|
+
console.error(chalk.red(`Failed to save filter: ${e.message}`));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
filterCmd
|
|
63
|
+
.command('delete')
|
|
64
|
+
.description('Delete a saved filter')
|
|
65
|
+
.argument('<name>', 'Filter Name')
|
|
66
|
+
.action(async (name) => {
|
|
67
|
+
try {
|
|
68
|
+
const cfg = ConfigService.getConfig();
|
|
69
|
+
if (!cfg.filters || !cfg.filters[name]) {
|
|
70
|
+
console.log(chalk.yellow(`Filter "${name}" not found.`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
delete cfg.filters[name];
|
|
75
|
+
ConfigService.saveConfig(cfg);
|
|
76
|
+
|
|
77
|
+
console.log(chalk.green(`Filter "${chalk.bold(name)}" deleted.`));
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
console.error(chalk.red(`Failed to delete filter: ${e.message}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program.addCommand(filterCmd);
|
|
84
|
+
}
|
|
@@ -7,7 +7,7 @@ import enquirer from 'enquirer';
|
|
|
7
7
|
import { validateIssueKey } from '../utils/validators.js';
|
|
8
8
|
import { handleCommandError } from '../utils/error-handler.js';
|
|
9
9
|
|
|
10
|
-
export function registerGitCommand(program) {
|
|
10
|
+
export function registerGitCommand(program: Command) {
|
|
11
11
|
const gitCmd = new Command('git')
|
|
12
12
|
.description('Git integration for Jira');
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ export function registerGitCommand(program) {
|
|
|
16
16
|
.description('Create a git branch from a Jira issue')
|
|
17
17
|
.argument('<issueKey>', 'Jira Issue Key (e.g., PROJ-123)')
|
|
18
18
|
.option('-t, --type <type>', 'Branch type (feature, bugfix, hotfix)', 'feature')
|
|
19
|
-
.action(async (issueKey, options) => {
|
|
19
|
+
.action(async (issueKey: string, options: any) => {
|
|
20
20
|
const check = validateIssueKey(issueKey);
|
|
21
21
|
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
22
22
|
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
@@ -39,7 +39,7 @@ export function registerGitCommand(program) {
|
|
|
39
39
|
name: 'confirm',
|
|
40
40
|
message: 'Create and switch to this branch?',
|
|
41
41
|
initial: true
|
|
42
|
-
});
|
|
42
|
+
}) as any;
|
|
43
43
|
|
|
44
44
|
if (confirm) {
|
|
45
45
|
try {
|
|
@@ -50,7 +50,7 @@ export function registerGitCommand(program) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
} catch (e) {
|
|
53
|
+
} catch (e: any) {
|
|
54
54
|
handleCommandError(spinner, e, 'Failed to create branch');
|
|
55
55
|
}
|
|
56
56
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { api } from '../services/api-service.js';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { validateIssueKey } from '../utils/validators.js';
|
|
8
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
9
|
+
// Node 20+ has global FormData and File, but for type safety or older environments:
|
|
10
|
+
// We rely on global FormData.
|
|
11
|
+
|
|
12
|
+
export function registerAttachCommand(issueCmd: Command) {
|
|
13
|
+
issueCmd
|
|
14
|
+
.command('attach')
|
|
15
|
+
.description('Attach a file to an issue')
|
|
16
|
+
.argument('<issueKey>', 'Issue Key')
|
|
17
|
+
.argument('<filePath>', 'Path to file')
|
|
18
|
+
.action(async (issueKey, filePath) => {
|
|
19
|
+
const check = validateIssueKey(issueKey);
|
|
20
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(filePath)) {
|
|
23
|
+
console.error(chalk.red(`File not found: ${filePath}`));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const spinner = ora(`Uploading ${path.basename(filePath)} to ${issueKey}...`).start();
|
|
28
|
+
try {
|
|
29
|
+
// Use fs.openAsBlob (Node 20+)
|
|
30
|
+
// @ts-ignore - TS might not know openAsBlob if target is old, but engine is Node 20
|
|
31
|
+
const blob = await fs.openAsBlob(filePath);
|
|
32
|
+
|
|
33
|
+
const formData = new FormData();
|
|
34
|
+
formData.append('file', blob, path.basename(filePath));
|
|
35
|
+
|
|
36
|
+
await api.upload(`/issue/${issueKey}/attachments`, formData);
|
|
37
|
+
|
|
38
|
+
spinner.succeed(chalk.green(`Attached ${chalk.bold(path.basename(filePath))} to ${issueKey}`));
|
|
39
|
+
|
|
40
|
+
} catch (e: any) {
|
|
41
|
+
handleCommandError(spinner, e, 'Failed to attach file');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import enquirer from 'enquirer';
|
|
4
|
+
import { api } from '../services/api-service.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { validateIssueKey } from '../utils/validators.js';
|
|
9
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
10
|
+
import { parseADF } from '../utils/adf-parser.js';
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
14
|
+
export function registerPrCommand(issueCmd: Command) {
|
|
15
|
+
issueCmd
|
|
16
|
+
.command('pr')
|
|
17
|
+
.description('Create a GitHub Pull Request for an issue')
|
|
18
|
+
.argument('<issueKey>', 'Issue Key')
|
|
19
|
+
.action(async (issueKey) => {
|
|
20
|
+
const check = validateIssueKey(issueKey);
|
|
21
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
22
|
+
|
|
23
|
+
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
24
|
+
try {
|
|
25
|
+
// Check if gh CLI is installed
|
|
26
|
+
try {
|
|
27
|
+
await execAsync('gh --version');
|
|
28
|
+
} catch (e) {
|
|
29
|
+
spinner.fail('GitHub CLI (`gh`) is not installed or not in PATH.');
|
|
30
|
+
console.log(chalk.yellow('Please install GitHub CLI to use this feature: https://cli.github.com/'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const issue = await api.get(`/issue/${issueKey}?fields=summary,description,issuetype`);
|
|
35
|
+
spinner.stop();
|
|
36
|
+
|
|
37
|
+
const summary = issue.fields.summary;
|
|
38
|
+
const description = parseADF(issue.fields.description) || '';
|
|
39
|
+
const type = issue.fields.issuetype.name.toUpperCase();
|
|
40
|
+
|
|
41
|
+
// Construct PR Title and Body
|
|
42
|
+
const prTitle = `${issueKey}: ${summary}`;
|
|
43
|
+
const prBody = `## Related Issue\n\n[${issueKey}](${api.domain}/browse/${issueKey})\n\n## Description\n\n${description}\n\n## Type of Change\n\n- [ ] ${type}`;
|
|
44
|
+
|
|
45
|
+
console.log(chalk.bold('\nDraft PR Content:'));
|
|
46
|
+
console.log(chalk.cyan('Title: ') + prTitle);
|
|
47
|
+
console.log(chalk.cyan('Body: ') + prBody.substring(0, 100) + '...\n');
|
|
48
|
+
|
|
49
|
+
const { confirm } = await enquirer.prompt({
|
|
50
|
+
type: 'confirm',
|
|
51
|
+
name: 'confirm',
|
|
52
|
+
message: 'Create Pull Request with `gh`?',
|
|
53
|
+
initial: true
|
|
54
|
+
}) as any;
|
|
55
|
+
|
|
56
|
+
if (!confirm) {
|
|
57
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const prSpinner = ora('Running gh pr create...').start();
|
|
62
|
+
|
|
63
|
+
// Escape characters for shell is tricky, so we use --title and --body flags carefully.
|
|
64
|
+
// A better approach for robust CLI usage might be to spawn the process with args directly to avoid shell parsing issues.
|
|
65
|
+
// However, execAsync with proper escaping or spawn is needed. For simplicity in this demo, we can use a basic approach
|
|
66
|
+
// or write to a temp file. To be safe, let's just pass them as arguments but be mindful of quotes.
|
|
67
|
+
// For Windows compatibility, this can be complex.
|
|
68
|
+
// Alternative: Interactive mode of gh pr create?
|
|
69
|
+
// `gh pr create --title "..." --body "..."`
|
|
70
|
+
|
|
71
|
+
// We will try running interactive mode if we can't easily pass args, but interactive inside a child_process is hard.
|
|
72
|
+
// Let's try passing args.
|
|
73
|
+
|
|
74
|
+
// Sanitize title and body for shell execution (basic)
|
|
75
|
+
const safeTitle = prTitle.replace(/"/g, '\\"');
|
|
76
|
+
const safeBody = prBody.replace(/"/g, '\\"');
|
|
77
|
+
|
|
78
|
+
const command = `gh pr create --title "${safeTitle}" --body "${safeBody}" --web`;
|
|
79
|
+
|
|
80
|
+
await execAsync(command);
|
|
81
|
+
prSpinner.succeed('Pull Request created! (Browser opened)');
|
|
82
|
+
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
handleCommandError(spinner, e, 'Failed to create PR');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|