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.
Files changed (34) hide show
  1. package/README.md +207 -158
  2. package/bin/{jira.js → jira.ts} +10 -1
  3. package/dist/bin/jira.js +64 -0
  4. package/package.json +14 -7
  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} +5 -5
  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} +348 -57
  19. package/src/commands/{mcp.js → mcp.ts} +2 -2
  20. package/src/commands/{project.js → project.ts} +5 -5
  21. package/src/commands/{sprint.js → sprint.ts} +125 -9
  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.js → config-store.ts} +18 -5
  29. package/src/utils/{config.js → config.ts} +12 -8
  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 -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
  }