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.
Files changed (35) hide show
  1. package/README.md +216 -173
  2. package/bin/{jira.js → jira.ts} +10 -1
  3. package/dist/bin/jira.js +64 -0
  4. package/package.json +21 -15
  5. package/src/commands/ai-actions/{plan.js → plan.ts} +9 -9
  6. package/src/commands/ai-actions/{review.js → review.ts} +2 -2
  7. package/src/commands/ai-actions/{standup.js → standup.ts} +4 -4
  8. package/src/commands/{ai.js → ai.ts} +11 -11
  9. package/src/commands/{board.js → board.ts} +11 -11
  10. package/src/commands/bulk.ts +230 -0
  11. package/src/commands/{config.js → config.ts} +57 -8
  12. package/src/commands/dashboard.ts +222 -0
  13. package/src/commands/filter.ts +84 -0
  14. package/src/commands/{git.js → git.ts} +4 -4
  15. package/src/commands/issue-attach.ts +44 -0
  16. package/src/commands/issue-pr.ts +87 -0
  17. package/src/commands/issue-worklog.ts +90 -0
  18. package/src/commands/{issue.js → issue.ts} +359 -68
  19. package/src/commands/{mcp.js → mcp.ts} +2 -2
  20. package/src/commands/{project.js → project.ts} +11 -11
  21. package/src/commands/sprint.ts +269 -0
  22. package/src/server/{mcp-server.js → mcp-server.ts} +235 -8
  23. package/src/services/{ai-service.js → ai-service.ts} +16 -16
  24. package/src/services/{api-service.js → api-service.ts} +33 -9
  25. package/src/services/config-service.ts +21 -0
  26. package/src/types.ts +68 -0
  27. package/src/utils/{adf-parser.js → adf-parser.ts} +12 -12
  28. package/src/utils/config-store.ts +109 -0
  29. package/src/utils/{config.js → config.ts} +14 -41
  30. package/src/utils/{error-handler.js → error-handler.ts} +2 -1
  31. package/src/utils/{text-to-adf.js → text-to-adf.ts} +1 -1
  32. package/src/utils/{validators.js → validators.ts} +4 -4
  33. package/src/commands/bulk.js +0 -108
  34. package/src/commands/dashboard.js +0 -89
  35. 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
+ }