jira-pilot 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/jira.js +1 -1
- package/package.json +1 -1
- package/src/commands/ai.js +11 -2
- package/src/commands/config.js +63 -0
- package/src/commands/issue.js +47 -4
- package/src/commands/sprint.js +41 -2
- package/src/services/ai-service.js +6 -2
- package/src/utils/adf-parser.js +49 -0
- package/src/utils/config.js +8 -2
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ jira issue list --export json # Creates issues-TIMESTAMP.json
|
|
|
87
87
|
jira issue list --export md # Creates issues-TIMESTAMP.md
|
|
88
88
|
|
|
89
89
|
# Combine filters and export (Power User)
|
|
90
|
-
jira issue list --project
|
|
90
|
+
jira issue list --project "project-name" --assignee "assignee_name" --status Done --export json
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
### Projects & Sprints
|
package/bin/jira.js
CHANGED
package/package.json
CHANGED
package/src/commands/ai.js
CHANGED
|
@@ -42,8 +42,17 @@ export function registerAiCommand(program) {
|
|
|
42
42
|
console.log(aiResponse);
|
|
43
43
|
|
|
44
44
|
} catch (e) {
|
|
45
|
-
spinner.
|
|
46
|
-
|
|
45
|
+
spinner.stop(); // Ensure spinner stops
|
|
46
|
+
if (e.response && e.response.config && e.response.config.url.includes('/issue/')) {
|
|
47
|
+
console.error(chalk.red(`\nError: Issue "${issueKey}" not found.`));
|
|
48
|
+
} else {
|
|
49
|
+
console.error(chalk.red('\nFailed to generate summary:'));
|
|
50
|
+
if (e.response) {
|
|
51
|
+
console.error(chalk.red(`API Error ${e.response.status}: `), e.response.data);
|
|
52
|
+
} else {
|
|
53
|
+
console.error(chalk.red(e.message));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
});
|
|
49
58
|
|
package/src/commands/config.js
CHANGED
|
@@ -36,6 +36,27 @@ export function registerConfigCommand(program) {
|
|
|
36
36
|
name: 'apiToken',
|
|
37
37
|
message: 'Jira API Token:',
|
|
38
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'], // Add 'gemini', 'anthropic' when ready
|
|
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
|
|
39
60
|
}
|
|
40
61
|
]);
|
|
41
62
|
|
|
@@ -87,5 +108,47 @@ export function registerConfigCommand(program) {
|
|
|
87
108
|
console.log(chalk.green('Credentials cleared.'));
|
|
88
109
|
});
|
|
89
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
|
+
|
|
90
153
|
program.addCommand(configCmd);
|
|
91
154
|
}
|
package/src/commands/issue.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { table } from 'table';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import { parseADF } from '../utils/adf-parser.js';
|
|
6
7
|
|
|
7
8
|
export function registerIssueCommand(program) {
|
|
8
9
|
const issueCmd = new Command('issue')
|
|
@@ -38,9 +39,9 @@ export function registerIssueCommand(program) {
|
|
|
38
39
|
|
|
39
40
|
const searchApi = '/search/jql';
|
|
40
41
|
const body = {
|
|
41
|
-
jql: jql || '',
|
|
42
|
+
jql: jql || 'created is not empty',
|
|
42
43
|
maxResults: parseInt(options.limit),
|
|
43
|
-
fields: ['summary', 'status', 'assignee', 'created', 'updated']
|
|
44
|
+
fields: ['summary', 'status', 'assignee', 'created', 'updated', 'description']
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
const data = await api.post(searchApi, body);
|
|
@@ -84,7 +85,7 @@ export function registerIssueCommand(program) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const tableData = [
|
|
87
|
-
[chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee')]
|
|
88
|
+
[chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Created'), chalk.bold('Updated')]
|
|
88
89
|
];
|
|
89
90
|
|
|
90
91
|
data.issues.forEach(i => {
|
|
@@ -92,7 +93,9 @@ export function registerIssueCommand(program) {
|
|
|
92
93
|
chalk.cyan(i.key),
|
|
93
94
|
i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
|
|
94
95
|
i.fields.status ? i.fields.status.name : '',
|
|
95
|
-
i.fields.assignee ? i.fields.assignee.displayName : 'Unassigned'
|
|
96
|
+
i.fields.assignee ? i.fields.assignee.displayName : 'Unassigned',
|
|
97
|
+
i.fields.created ? i.fields.created.split('T')[0] : '',
|
|
98
|
+
i.fields.updated ? i.fields.updated.split('T')[0] : ''
|
|
96
99
|
]);
|
|
97
100
|
});
|
|
98
101
|
|
|
@@ -108,5 +111,45 @@ export function registerIssueCommand(program) {
|
|
|
108
111
|
}
|
|
109
112
|
});
|
|
110
113
|
|
|
114
|
+
issueCmd
|
|
115
|
+
.command('view')
|
|
116
|
+
.description('View issue details')
|
|
117
|
+
.argument('<issueKey>', 'Issue Key')
|
|
118
|
+
.action(async (issueKey) => {
|
|
119
|
+
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
120
|
+
try {
|
|
121
|
+
const issue = await api.get(`/issue/${issueKey}`);
|
|
122
|
+
spinner.stop();
|
|
123
|
+
|
|
124
|
+
console.log(chalk.bold(`\n${issue.key}: ${issue.fields.summary}`));
|
|
125
|
+
console.log(chalk.grey(`${issue.fields.issuetype.name} - ${issue.fields.status.name} - ${issue.fields.priority ? issue.fields.priority.name : 'No Priority'}`));
|
|
126
|
+
console.log(chalk.bold('\nDescription:'));
|
|
127
|
+
console.log(parseADF(issue.fields.description) || 'No description provided.');
|
|
128
|
+
|
|
129
|
+
if (issue.fields.assignee) {
|
|
130
|
+
console.log(chalk.bold('\nAssignee: ') + issue.fields.assignee.displayName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (issue.fields.comment && issue.fields.comment.comments.length > 0) {
|
|
134
|
+
console.log(chalk.bold('\nComments:'));
|
|
135
|
+
issue.fields.comment.comments.forEach(c => {
|
|
136
|
+
console.log(chalk.cyan(c.author.displayName) + ': ' + c.body);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
console.log('');
|
|
140
|
+
} catch (e) {
|
|
141
|
+
spinner.fail('Failed to fetch issue');
|
|
142
|
+
if (e.response) {
|
|
143
|
+
if (e.response.status === 404) {
|
|
144
|
+
console.error(chalk.red(`Issue "${issueKey}" not found.`));
|
|
145
|
+
} else {
|
|
146
|
+
console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
console.error(chalk.red(e.message));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
111
154
|
program.addCommand(issueCmd);
|
|
112
155
|
}
|
package/src/commands/sprint.js
CHANGED
|
@@ -37,7 +37,41 @@ export function registerSprintCommand(program) {
|
|
|
37
37
|
// Using full path:
|
|
38
38
|
const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
|
|
39
39
|
const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
let boardId = options.board;
|
|
42
|
+
|
|
43
|
+
// If board option is not a number, try to look it up using the Board Name/Key
|
|
44
|
+
if (isNaN(boardId)) {
|
|
45
|
+
spinner.text = `Looking up board "${options.board}"...`;
|
|
46
|
+
const boardSearchUrl = `${domain}/rest/agile/1.0/board?name=${encodeURIComponent(options.board)}`;
|
|
47
|
+
const boardData = await api.get(boardSearchUrl);
|
|
48
|
+
|
|
49
|
+
if (!boardData.values || boardData.values.length === 0) {
|
|
50
|
+
// Fallback: It might be a project key. Let's try searching for boards associated with this project.
|
|
51
|
+
// But the API doesn't support projectKey filter directly on /board easily without iterating.
|
|
52
|
+
// For now, fail if name match doesn't work.
|
|
53
|
+
throw new Error(`Board with name "${options.board}" not found. Please provide the numeric Board ID.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Strict match or pick first? Let's pick the first one but warn if multiple
|
|
57
|
+
if (boardData.values.length > 1) {
|
|
58
|
+
// Try to find exact match
|
|
59
|
+
const exact = boardData.values.find(b => b.name.toLowerCase() === options.board.toLowerCase());
|
|
60
|
+
if (exact) {
|
|
61
|
+
boardId = exact.id;
|
|
62
|
+
} else {
|
|
63
|
+
// Just pick first? Or error?
|
|
64
|
+
// Let's pick first but log
|
|
65
|
+
console.log(chalk.yellow(`\nMultiple boards found for "${options.board}". Using "${boardData.values[0].name}" (ID: ${boardData.values[0].id}).`));
|
|
66
|
+
boardId = boardData.values[0].id;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
boardId = boardData.values[0].id;
|
|
70
|
+
}
|
|
71
|
+
spinner.text = `Fetching sprints for board ${options.board} (ID: ${boardId})...`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fullUrl = `${domain}/rest/agile/1.0/board/${boardId}/sprint?state=${options.state}`;
|
|
41
75
|
|
|
42
76
|
const data = await api.get(fullUrl);
|
|
43
77
|
spinner.stop();
|
|
@@ -65,7 +99,12 @@ export function registerSprintCommand(program) {
|
|
|
65
99
|
} catch (e) {
|
|
66
100
|
spinner.fail('Failed to list sprints');
|
|
67
101
|
if (e.response) {
|
|
68
|
-
|
|
102
|
+
if (e.response.status === 404) {
|
|
103
|
+
console.error(chalk.red(`\nError: Board with ID "${options.board}" not found or you do not have permission to view it.`));
|
|
104
|
+
console.error(chalk.grey('Tip: Verify the Board ID in your Jira URL: /jira/software/c/projects/KEY/boards/ID'));
|
|
105
|
+
} else {
|
|
106
|
+
console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
|
|
107
|
+
}
|
|
69
108
|
} else {
|
|
70
109
|
console.error(chalk.red(e.message));
|
|
71
110
|
}
|
|
@@ -5,10 +5,14 @@ export class AiService {
|
|
|
5
5
|
constructor() { }
|
|
6
6
|
|
|
7
7
|
async generate(prompt) {
|
|
8
|
-
const { aiKey, aiProvider } = getCredentials();
|
|
8
|
+
const { aiKey, aiProvider, aiEnabled } = getCredentials();
|
|
9
|
+
|
|
10
|
+
if (!aiEnabled) {
|
|
11
|
+
throw new Error('AI features are disabled. Run "jira config ai enable" to enable.');
|
|
12
|
+
}
|
|
9
13
|
|
|
10
14
|
if (!aiKey) {
|
|
11
|
-
throw new Error('AI API Key not configured. Run "jira config
|
|
15
|
+
throw new Error('AI API Key not configured. Run "jira config ai enable" or "jira config setup".');
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
// Basic implementation for OpenAI - extensible for others
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function parseADF(content) {
|
|
2
|
+
if (!content) return '';
|
|
3
|
+
if (typeof content === 'string') return content;
|
|
4
|
+
|
|
5
|
+
if (content.type === 'doc') {
|
|
6
|
+
return content.content.map(node => parseNode(node)).join('\n');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return JSON.stringify(content);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseNode(node) {
|
|
13
|
+
if (!node) return '';
|
|
14
|
+
|
|
15
|
+
switch (node.type) {
|
|
16
|
+
case 'paragraph':
|
|
17
|
+
return parseParagraph(node);
|
|
18
|
+
case 'text':
|
|
19
|
+
return node.text;
|
|
20
|
+
case 'bulletList':
|
|
21
|
+
return parseList(node, '•');
|
|
22
|
+
case 'orderedList':
|
|
23
|
+
return parseList(node, '1.');
|
|
24
|
+
case 'heading':
|
|
25
|
+
return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map(c => parseNode(c)).join('')}\n`;
|
|
26
|
+
case 'codeBlock':
|
|
27
|
+
return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map(c => c.text).join('')}\n\`\`\`\n`;
|
|
28
|
+
case 'blockquote':
|
|
29
|
+
return `> ${node.content.map(c => parseNode(c)).join('')}`;
|
|
30
|
+
default:
|
|
31
|
+
if (node.content) {
|
|
32
|
+
return node.content.map(c => parseNode(c)).join('');
|
|
33
|
+
}
|
|
34
|
+
return ''; // Unknown node, skip or fallback
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseParagraph(node) {
|
|
39
|
+
if (!node.content) return '\n';
|
|
40
|
+
return node.content.map(c => parseNode(c)).join('') + '\n';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseList(node, marker) {
|
|
44
|
+
if (!node.content) return '';
|
|
45
|
+
return node.content.map((item, index) => {
|
|
46
|
+
const prefix = marker === '1.' ? `${index + 1}. ` : `${marker} `;
|
|
47
|
+
return `${prefix}${item.content.map(c => parseNode(c)).join('')}`;
|
|
48
|
+
}).join('\n') + '\n';
|
|
49
|
+
}
|
package/src/utils/config.js
CHANGED
|
@@ -18,6 +18,10 @@ const schema = {
|
|
|
18
18
|
aiProvider: {
|
|
19
19
|
type: 'string',
|
|
20
20
|
default: 'openai'
|
|
21
|
+
},
|
|
22
|
+
aiEnabled: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
default: false
|
|
21
25
|
}
|
|
22
26
|
};
|
|
23
27
|
|
|
@@ -32,16 +36,18 @@ export const getCredentials = () => {
|
|
|
32
36
|
email: config.get('email'),
|
|
33
37
|
apiToken: config.get('apiToken'),
|
|
34
38
|
aiKey: config.get('aiKey'),
|
|
35
|
-
aiProvider: config.get('aiProvider')
|
|
39
|
+
aiProvider: config.get('aiProvider'),
|
|
40
|
+
aiEnabled: config.get('aiEnabled')
|
|
36
41
|
};
|
|
37
42
|
};
|
|
38
43
|
|
|
39
|
-
export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider }) => {
|
|
44
|
+
export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider, aiEnabled }) => {
|
|
40
45
|
if (jiraUrl) config.set('jiraUrl', jiraUrl);
|
|
41
46
|
if (email) config.set('email', email);
|
|
42
47
|
if (apiToken) config.set('apiToken', apiToken);
|
|
43
48
|
if (aiKey) config.set('aiKey', aiKey);
|
|
44
49
|
if (aiProvider) config.set('aiProvider', aiProvider);
|
|
50
|
+
if (typeof aiEnabled !== 'undefined') config.set('aiEnabled', aiEnabled);
|
|
45
51
|
};
|
|
46
52
|
|
|
47
53
|
export const clearCredentials = () => {
|