jira-ai 0.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 +364 -0
- package/dist/cli.js +87 -0
- package/dist/commands/about.js +83 -0
- package/dist/commands/add-comment.js +109 -0
- package/dist/commands/me.js +23 -0
- package/dist/commands/project-statuses.js +23 -0
- package/dist/commands/projects.js +35 -0
- package/dist/commands/run-jql.js +44 -0
- package/dist/commands/task-with-details.js +23 -0
- package/dist/commands/update-description.js +109 -0
- package/dist/lib/formatters.js +193 -0
- package/dist/lib/jira-client.js +216 -0
- package/dist/lib/settings.js +68 -0
- package/dist/lib/utils.js +79 -0
- package/jest.config.js +21 -0
- package/package.json +47 -0
- package/settings.yaml +24 -0
- package/src/cli.ts +97 -0
- package/src/commands/about.ts +98 -0
- package/src/commands/add-comment.ts +94 -0
- package/src/commands/me.ts +18 -0
- package/src/commands/project-statuses.ts +18 -0
- package/src/commands/projects.ts +32 -0
- package/src/commands/run-jql.ts +40 -0
- package/src/commands/task-with-details.ts +18 -0
- package/src/commands/update-description.ts +94 -0
- package/src/lib/formatters.ts +224 -0
- package/src/lib/jira-client.ts +319 -0
- package/src/lib/settings.ts +77 -0
- package/src/lib/utils.ts +76 -0
- package/src/types/md-to-adf.d.ts +14 -0
- package/tests/README.md +97 -0
- package/tests/__mocks__/jira.js.ts +4 -0
- package/tests/__mocks__/md-to-adf.ts +7 -0
- package/tests/__mocks__/mdast-util-from-adf.ts +4 -0
- package/tests/__mocks__/mdast-util-to-markdown.ts +1 -0
- package/tests/add-comment.test.ts +226 -0
- package/tests/cli-permissions.test.ts +156 -0
- package/tests/jira-client.test.ts +123 -0
- package/tests/projects.test.ts +205 -0
- package/tests/settings.test.ts +288 -0
- package/tests/task-with-details.test.ts +83 -0
- package/tests/update-description.test.ts +262 -0
- package/to-do.md +9 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import mdToAdf from 'md-to-adf';
|
|
6
|
+
import { updateIssueDescription } from '../lib/jira-client';
|
|
7
|
+
|
|
8
|
+
export async function updateDescriptionCommand(
|
|
9
|
+
taskId: string,
|
|
10
|
+
options: { fromFile: string }
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
// Validate taskId
|
|
13
|
+
if (!taskId || taskId.trim() === '') {
|
|
14
|
+
console.error(chalk.red('\nError: Task ID cannot be empty'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Validate file path
|
|
19
|
+
const filePath = options.fromFile;
|
|
20
|
+
if (!filePath || filePath.trim() === '') {
|
|
21
|
+
console.error(chalk.red('\nError: File path is required (use --from-file)'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Resolve file path to absolute
|
|
26
|
+
const absolutePath = path.resolve(filePath);
|
|
27
|
+
|
|
28
|
+
// Check file exists
|
|
29
|
+
if (!fs.existsSync(absolutePath)) {
|
|
30
|
+
console.error(chalk.red(`\nError: File not found: ${absolutePath}`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Read file
|
|
35
|
+
let markdownContent: string;
|
|
36
|
+
try {
|
|
37
|
+
markdownContent = fs.readFileSync(absolutePath, 'utf-8');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(
|
|
40
|
+
chalk.red(
|
|
41
|
+
'\nError reading file: ' +
|
|
42
|
+
(error instanceof Error ? error.message : 'Unknown error')
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate file is not empty
|
|
49
|
+
if (markdownContent.trim() === '') {
|
|
50
|
+
console.error(chalk.red('\nError: File is empty'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Convert Markdown to ADF
|
|
55
|
+
let adfContent: any;
|
|
56
|
+
try {
|
|
57
|
+
adfContent = mdToAdf(markdownContent);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(
|
|
60
|
+
chalk.red(
|
|
61
|
+
'\nError converting Markdown to ADF: ' +
|
|
62
|
+
(error instanceof Error ? error.message : 'Unknown error')
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Update issue description with spinner
|
|
69
|
+
const spinner = ora(`Updating description for ${taskId}...`).start();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await updateIssueDescription(taskId, adfContent);
|
|
73
|
+
spinner.succeed(chalk.green(`Description updated successfully for ${taskId}`));
|
|
74
|
+
console.log(chalk.gray(`\nFile: ${absolutePath}`));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
spinner.fail(chalk.red('Failed to update description'));
|
|
77
|
+
console.error(
|
|
78
|
+
chalk.red(
|
|
79
|
+
'\nError: ' + (error instanceof Error ? error.message : 'Unknown error')
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Provide helpful hints based on error
|
|
84
|
+
if (error instanceof Error && error.message.includes('404')) {
|
|
85
|
+
console.log(chalk.yellow('\nHint: Check that the task ID is correct'));
|
|
86
|
+
} else if (error instanceof Error && error.message.includes('403')) {
|
|
87
|
+
console.log(
|
|
88
|
+
chalk.yellow('\nHint: You may not have permission to edit this issue')
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { UserInfo, Project, TaskDetails, Status, JqlIssue } from './jira-client';
|
|
4
|
+
import { formatTimestamp, truncate } from './utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a styled table
|
|
8
|
+
*/
|
|
9
|
+
function createTable(headers: string[], colWidths?: number[]): Table.Table {
|
|
10
|
+
return new Table({
|
|
11
|
+
head: headers.map((h) => chalk.cyan.bold(h)),
|
|
12
|
+
style: {
|
|
13
|
+
head: [],
|
|
14
|
+
border: ['gray'],
|
|
15
|
+
},
|
|
16
|
+
colWidths,
|
|
17
|
+
wordWrap: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format user info
|
|
23
|
+
*/
|
|
24
|
+
export function formatUserInfo(user: UserInfo): string {
|
|
25
|
+
const table = createTable(['Property', 'Value'], [20, 50]);
|
|
26
|
+
|
|
27
|
+
table.push(
|
|
28
|
+
['Host', process.env.JIRA_HOST || 'N/A'],
|
|
29
|
+
['Display Name', user.displayName],
|
|
30
|
+
['Email', user.emailAddress],
|
|
31
|
+
['Account ID', user.accountId],
|
|
32
|
+
['Status', user.active ? chalk.green('Active') : chalk.red('Inactive')],
|
|
33
|
+
['Time Zone', user.timeZone]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return '\n' + chalk.bold('User Information:') + '\n' + table.toString() + '\n';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format projects list
|
|
41
|
+
*/
|
|
42
|
+
export function formatProjects(projects: Project[]): string {
|
|
43
|
+
if (projects.length === 0) {
|
|
44
|
+
return chalk.yellow('No projects found.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const table = createTable(['Key', 'Name', 'Type', 'Lead'], [12, 35, 15, 25]);
|
|
48
|
+
|
|
49
|
+
projects.forEach((project) => {
|
|
50
|
+
table.push([
|
|
51
|
+
chalk.cyan(project.key),
|
|
52
|
+
truncate(project.name, 35),
|
|
53
|
+
project.projectTypeKey,
|
|
54
|
+
project.lead?.displayName || chalk.gray('N/A'),
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let output = '\n' + chalk.bold(`Projects (${projects.length} total)`) + '\n\n';
|
|
59
|
+
output += table.toString() + '\n';
|
|
60
|
+
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format task details with comments
|
|
66
|
+
*/
|
|
67
|
+
export function formatTaskDetails(task: TaskDetails): string {
|
|
68
|
+
let output = '\n' + chalk.bold.cyan(`${task.key}: ${task.summary}`) + '\n\n';
|
|
69
|
+
|
|
70
|
+
// Basic info table
|
|
71
|
+
const infoTable = createTable(['Property', 'Value'], [15, 65]);
|
|
72
|
+
|
|
73
|
+
infoTable.push(
|
|
74
|
+
['Status', chalk.green(task.status.name)],
|
|
75
|
+
['Assignee', task.assignee?.displayName || chalk.gray('Unassigned')],
|
|
76
|
+
['Reporter', task.reporter?.displayName || chalk.gray('N/A')],
|
|
77
|
+
['Created', formatTimestamp(task.created)],
|
|
78
|
+
['Updated', formatTimestamp(task.updated)]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
output += infoTable.toString() + '\n\n';
|
|
82
|
+
|
|
83
|
+
// Parent Task
|
|
84
|
+
if (task.parent) {
|
|
85
|
+
output += chalk.bold('Parent Task:') + '\n';
|
|
86
|
+
const parentTable = createTable(['Key', 'Summary', 'Status'], [12, 50, 18]);
|
|
87
|
+
parentTable.push([
|
|
88
|
+
chalk.cyan(task.parent.key),
|
|
89
|
+
truncate(task.parent.summary, 50),
|
|
90
|
+
task.parent.status.name,
|
|
91
|
+
]);
|
|
92
|
+
output += parentTable.toString() + '\n\n';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Subtasks
|
|
96
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
97
|
+
output += chalk.bold(`Subtasks (${task.subtasks.length}):`) + '\n';
|
|
98
|
+
const subtasksTable = createTable(['Key', 'Summary', 'Status'], [12, 50, 18]);
|
|
99
|
+
task.subtasks.forEach((subtask) => {
|
|
100
|
+
subtasksTable.push([
|
|
101
|
+
chalk.cyan(subtask.key),
|
|
102
|
+
truncate(subtask.summary, 50),
|
|
103
|
+
subtask.status.name,
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
output += subtasksTable.toString() + '\n\n';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Description
|
|
110
|
+
if (task.description) {
|
|
111
|
+
output += chalk.bold('Description:') + '\n';
|
|
112
|
+
output += chalk.dim('─'.repeat(80)) + '\n';
|
|
113
|
+
output += task.description + '\n';
|
|
114
|
+
output += chalk.dim('─'.repeat(80)) + '\n\n';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Comments
|
|
118
|
+
if (task.comments.length > 0) {
|
|
119
|
+
output += chalk.bold(`Comments (${task.comments.length}):`) + '\n\n';
|
|
120
|
+
|
|
121
|
+
task.comments.forEach((comment, index) => {
|
|
122
|
+
output += chalk.cyan(`${index + 1}. ${comment.author.displayName}`) +
|
|
123
|
+
chalk.gray(` - ${formatTimestamp(comment.created)}`) + '\n';
|
|
124
|
+
output += chalk.dim('─'.repeat(80)) + '\n';
|
|
125
|
+
output += comment.body + '\n';
|
|
126
|
+
output += chalk.dim('─'.repeat(80)) + '\n\n';
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
output += chalk.gray('No comments yet.\n\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format project statuses list
|
|
137
|
+
*/
|
|
138
|
+
export function formatProjectStatuses(projectKey: string, statuses: Status[]): string {
|
|
139
|
+
if (statuses.length === 0) {
|
|
140
|
+
return chalk.yellow('No statuses found for this project.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const table = createTable(['Status Name', 'Category', 'Description'], [25, 20, 45]);
|
|
144
|
+
|
|
145
|
+
// Sort statuses by category for better readability
|
|
146
|
+
const sortedStatuses = [...statuses].sort((a, b) => {
|
|
147
|
+
const categoryOrder = ['TODO', 'IN_PROGRESS', 'DONE'];
|
|
148
|
+
const aIndex = categoryOrder.indexOf(a.statusCategory.key.toUpperCase());
|
|
149
|
+
const bIndex = categoryOrder.indexOf(b.statusCategory.key.toUpperCase());
|
|
150
|
+
return aIndex - bIndex;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
sortedStatuses.forEach((status) => {
|
|
154
|
+
// Color code based on status category
|
|
155
|
+
let categoryColor = chalk.white;
|
|
156
|
+
if (status.statusCategory.key.toLowerCase() === 'done') {
|
|
157
|
+
categoryColor = chalk.green;
|
|
158
|
+
} else if (status.statusCategory.key.toLowerCase() === 'indeterminate') {
|
|
159
|
+
categoryColor = chalk.yellow;
|
|
160
|
+
} else if (status.statusCategory.key.toLowerCase() === 'new') {
|
|
161
|
+
categoryColor = chalk.blue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
table.push([
|
|
165
|
+
chalk.cyan(status.name),
|
|
166
|
+
categoryColor(status.statusCategory.name),
|
|
167
|
+
truncate(status.description || chalk.gray('No description'), 45),
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let output = '\n' + chalk.bold(`Project ${projectKey} - Available Statuses (${statuses.length} total)`) + '\n\n';
|
|
172
|
+
output += table.toString() + '\n';
|
|
173
|
+
|
|
174
|
+
return output;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format JQL query results
|
|
179
|
+
*/
|
|
180
|
+
export function formatJqlResults(issues: JqlIssue[]): string {
|
|
181
|
+
if (issues.length === 0) {
|
|
182
|
+
return chalk.yellow('\nNo issues found matching your JQL query.\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const table = createTable(['Key', 'Summary', 'Status', 'Assignee', 'Priority'], [12, 40, 18, 20, 12]);
|
|
186
|
+
|
|
187
|
+
issues.forEach((issue) => {
|
|
188
|
+
// Color-code status
|
|
189
|
+
let statusColor = chalk.white;
|
|
190
|
+
const statusLower = issue.status.name.toLowerCase();
|
|
191
|
+
if (statusLower.includes('done') || statusLower.includes('closed') || statusLower.includes('resolved')) {
|
|
192
|
+
statusColor = chalk.green;
|
|
193
|
+
} else if (statusLower.includes('progress') || statusLower.includes('review')) {
|
|
194
|
+
statusColor = chalk.yellow;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Color-code priority
|
|
198
|
+
let priorityColor = chalk.white;
|
|
199
|
+
const priorityName = issue.priority?.name || chalk.gray('None');
|
|
200
|
+
if (issue.priority) {
|
|
201
|
+
const priorityLower = issue.priority.name.toLowerCase();
|
|
202
|
+
if (priorityLower.includes('highest') || priorityLower.includes('high')) {
|
|
203
|
+
priorityColor = chalk.red;
|
|
204
|
+
} else if (priorityLower.includes('medium')) {
|
|
205
|
+
priorityColor = chalk.yellow;
|
|
206
|
+
} else if (priorityLower.includes('low')) {
|
|
207
|
+
priorityColor = chalk.blue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
table.push([
|
|
212
|
+
chalk.cyan(issue.key),
|
|
213
|
+
truncate(issue.summary, 40),
|
|
214
|
+
statusColor(issue.status.name),
|
|
215
|
+
issue.assignee ? truncate(issue.assignee.displayName, 20) : chalk.gray('Unassigned'),
|
|
216
|
+
typeof priorityName === 'string' ? priorityColor(priorityName) : priorityName,
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let output = '\n' + chalk.bold(`Results (${issues.length} total)`) + '\n\n';
|
|
221
|
+
output += table.toString() + '\n';
|
|
222
|
+
|
|
223
|
+
return output;
|
|
224
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Version3Client } from 'jira.js';
|
|
2
|
+
import { convertADFToMarkdown } from './utils';
|
|
3
|
+
|
|
4
|
+
export interface UserInfo {
|
|
5
|
+
accountId: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
emailAddress: string;
|
|
8
|
+
active: boolean;
|
|
9
|
+
timeZone: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Project {
|
|
13
|
+
id: string;
|
|
14
|
+
key: string;
|
|
15
|
+
name: string;
|
|
16
|
+
projectTypeKey: string;
|
|
17
|
+
lead?: {
|
|
18
|
+
displayName: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Comment {
|
|
23
|
+
id: string;
|
|
24
|
+
author: {
|
|
25
|
+
displayName: string;
|
|
26
|
+
emailAddress?: string;
|
|
27
|
+
};
|
|
28
|
+
body: string;
|
|
29
|
+
created: string;
|
|
30
|
+
updated: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Status {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
statusCategory: {
|
|
38
|
+
id: string;
|
|
39
|
+
key: string;
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LinkedIssue {
|
|
45
|
+
id: string;
|
|
46
|
+
key: string;
|
|
47
|
+
summary: string;
|
|
48
|
+
status: {
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TaskDetails {
|
|
54
|
+
id: string;
|
|
55
|
+
key: string;
|
|
56
|
+
summary: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
status: {
|
|
59
|
+
name: string;
|
|
60
|
+
};
|
|
61
|
+
assignee?: {
|
|
62
|
+
displayName: string;
|
|
63
|
+
};
|
|
64
|
+
reporter?: {
|
|
65
|
+
displayName: string;
|
|
66
|
+
};
|
|
67
|
+
created: string;
|
|
68
|
+
updated: string;
|
|
69
|
+
comments: Comment[];
|
|
70
|
+
parent?: LinkedIssue;
|
|
71
|
+
subtasks: LinkedIssue[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface JqlIssue {
|
|
75
|
+
key: string;
|
|
76
|
+
summary: string;
|
|
77
|
+
status: {
|
|
78
|
+
name: string;
|
|
79
|
+
};
|
|
80
|
+
assignee: {
|
|
81
|
+
displayName: string;
|
|
82
|
+
} | null;
|
|
83
|
+
priority: {
|
|
84
|
+
name: string;
|
|
85
|
+
} | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let jiraClient: Version3Client | null = null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get or create Jira client instance
|
|
92
|
+
*/
|
|
93
|
+
export function getJiraClient(): Version3Client {
|
|
94
|
+
if (!jiraClient) {
|
|
95
|
+
jiraClient = new Version3Client({
|
|
96
|
+
host: process.env.JIRA_HOST!,
|
|
97
|
+
authentication: {
|
|
98
|
+
basic: {
|
|
99
|
+
email: process.env.JIRA_USER_EMAIL!,
|
|
100
|
+
apiToken: process.env.JIRA_API_TOKEN!,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return jiraClient;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get current user information
|
|
110
|
+
*/
|
|
111
|
+
export async function getCurrentUser(): Promise<UserInfo> {
|
|
112
|
+
const client = getJiraClient();
|
|
113
|
+
const user = await client.myself.getCurrentUser();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
accountId: user.accountId || '',
|
|
117
|
+
displayName: user.displayName || '',
|
|
118
|
+
emailAddress: user.emailAddress || '',
|
|
119
|
+
active: user.active || false,
|
|
120
|
+
timeZone: user.timeZone || '',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all projects
|
|
126
|
+
*/
|
|
127
|
+
export async function getProjects(): Promise<Project[]> {
|
|
128
|
+
const client = getJiraClient();
|
|
129
|
+
const response = await client.projects.searchProjects({
|
|
130
|
+
expand: 'lead',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return response.values.map((project: any) => ({
|
|
134
|
+
id: project.id,
|
|
135
|
+
key: project.key,
|
|
136
|
+
name: project.name,
|
|
137
|
+
projectTypeKey: project.projectTypeKey,
|
|
138
|
+
lead: project.lead ? {
|
|
139
|
+
displayName: project.lead.displayName,
|
|
140
|
+
} : undefined,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get task details with comments
|
|
146
|
+
*/
|
|
147
|
+
export async function getTaskWithDetails(taskId: string): Promise<TaskDetails> {
|
|
148
|
+
const client = getJiraClient();
|
|
149
|
+
|
|
150
|
+
// Get issue details
|
|
151
|
+
const issue = await client.issues.getIssue({
|
|
152
|
+
issueIdOrKey: taskId,
|
|
153
|
+
fields: [
|
|
154
|
+
'summary',
|
|
155
|
+
'description',
|
|
156
|
+
'status',
|
|
157
|
+
'assignee',
|
|
158
|
+
'reporter',
|
|
159
|
+
'created',
|
|
160
|
+
'updated',
|
|
161
|
+
'comment',
|
|
162
|
+
'parent',
|
|
163
|
+
'subtasks',
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Extract comments
|
|
168
|
+
const comments: Comment[] = issue.fields.comment?.comments?.map((comment: any) => ({
|
|
169
|
+
id: comment.id,
|
|
170
|
+
author: {
|
|
171
|
+
displayName: comment.author?.displayName || 'Unknown',
|
|
172
|
+
emailAddress: comment.author?.emailAddress,
|
|
173
|
+
},
|
|
174
|
+
body: convertADFToMarkdown(comment.body),
|
|
175
|
+
created: comment.created || '',
|
|
176
|
+
updated: comment.updated || '',
|
|
177
|
+
})) || [];
|
|
178
|
+
|
|
179
|
+
// Convert description from ADF to Markdown
|
|
180
|
+
const descriptionMarkdown = convertADFToMarkdown(issue.fields.description);
|
|
181
|
+
const description = descriptionMarkdown || undefined;
|
|
182
|
+
|
|
183
|
+
// Extract parent if exists
|
|
184
|
+
const parent: LinkedIssue | undefined = issue.fields.parent ? {
|
|
185
|
+
id: issue.fields.parent.id,
|
|
186
|
+
key: issue.fields.parent.key,
|
|
187
|
+
summary: issue.fields.parent.fields?.summary || '',
|
|
188
|
+
status: {
|
|
189
|
+
name: issue.fields.parent.fields?.status?.name || 'Unknown',
|
|
190
|
+
},
|
|
191
|
+
} : undefined;
|
|
192
|
+
|
|
193
|
+
// Extract subtasks
|
|
194
|
+
const subtasks: LinkedIssue[] = issue.fields.subtasks?.map((subtask: any) => ({
|
|
195
|
+
id: subtask.id,
|
|
196
|
+
key: subtask.key,
|
|
197
|
+
summary: subtask.fields?.summary || '',
|
|
198
|
+
status: {
|
|
199
|
+
name: subtask.fields?.status?.name || 'Unknown',
|
|
200
|
+
},
|
|
201
|
+
})) || [];
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
id: issue.id || '',
|
|
205
|
+
key: issue.key || '',
|
|
206
|
+
summary: issue.fields.summary || '',
|
|
207
|
+
description,
|
|
208
|
+
status: {
|
|
209
|
+
name: issue.fields.status?.name || 'Unknown',
|
|
210
|
+
},
|
|
211
|
+
assignee: issue.fields.assignee ? {
|
|
212
|
+
displayName: issue.fields.assignee.displayName || 'Unknown',
|
|
213
|
+
} : undefined,
|
|
214
|
+
reporter: issue.fields.reporter ? {
|
|
215
|
+
displayName: issue.fields.reporter.displayName || 'Unknown',
|
|
216
|
+
} : undefined,
|
|
217
|
+
created: issue.fields.created || '',
|
|
218
|
+
updated: issue.fields.updated || '',
|
|
219
|
+
comments,
|
|
220
|
+
parent,
|
|
221
|
+
subtasks,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get all possible statuses for a project
|
|
227
|
+
*/
|
|
228
|
+
export async function getProjectStatuses(projectIdOrKey: string): Promise<Status[]> {
|
|
229
|
+
const client = getJiraClient();
|
|
230
|
+
|
|
231
|
+
// Get all statuses for the project
|
|
232
|
+
const statuses = await client.projects.getAllStatuses({
|
|
233
|
+
projectIdOrKey,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Flatten and deduplicate statuses from all issue types
|
|
237
|
+
const statusMap = new Map<string, Status>();
|
|
238
|
+
|
|
239
|
+
statuses.forEach((issueTypeStatuses: any) => {
|
|
240
|
+
issueTypeStatuses.statuses?.forEach((status: any) => {
|
|
241
|
+
if (!statusMap.has(status.id)) {
|
|
242
|
+
statusMap.set(status.id, {
|
|
243
|
+
id: status.id || '',
|
|
244
|
+
name: status.name || '',
|
|
245
|
+
description: status.description,
|
|
246
|
+
statusCategory: {
|
|
247
|
+
id: status.statusCategory?.id || '',
|
|
248
|
+
key: status.statusCategory?.key || '',
|
|
249
|
+
name: status.statusCategory?.name || '',
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return Array.from(statusMap.values());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Search for issues using JQL query
|
|
261
|
+
*/
|
|
262
|
+
export async function searchIssuesByJql(jqlQuery: string, maxResults: number): Promise<JqlIssue[]> {
|
|
263
|
+
const client = getJiraClient();
|
|
264
|
+
|
|
265
|
+
const response = await client.issueSearch.searchForIssuesUsingJqlEnhancedSearch({
|
|
266
|
+
jql: jqlQuery,
|
|
267
|
+
maxResults,
|
|
268
|
+
fields: ['summary', 'status', 'assignee', 'priority'],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return response.issues?.map((issue: any) => ({
|
|
272
|
+
key: issue.key || '',
|
|
273
|
+
summary: issue.fields?.summary || '',
|
|
274
|
+
status: {
|
|
275
|
+
name: issue.fields?.status?.name || 'Unknown',
|
|
276
|
+
},
|
|
277
|
+
assignee: issue.fields?.assignee ? {
|
|
278
|
+
displayName: issue.fields.assignee.displayName || 'Unknown',
|
|
279
|
+
} : null,
|
|
280
|
+
priority: issue.fields?.priority ? {
|
|
281
|
+
name: issue.fields.priority.name || 'Unknown',
|
|
282
|
+
} : null,
|
|
283
|
+
})) || [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update the description of a Jira issue
|
|
288
|
+
* @param taskId - The issue key (e.g., "PROJ-123")
|
|
289
|
+
* @param adfContent - The description content in ADF format
|
|
290
|
+
*/
|
|
291
|
+
export async function updateIssueDescription(
|
|
292
|
+
taskId: string,
|
|
293
|
+
adfContent: any
|
|
294
|
+
): Promise<void> {
|
|
295
|
+
const client = getJiraClient();
|
|
296
|
+
await client.issues.editIssue({
|
|
297
|
+
issueIdOrKey: taskId,
|
|
298
|
+
fields: {
|
|
299
|
+
description: adfContent,
|
|
300
|
+
},
|
|
301
|
+
notifyUsers: false,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Add a comment to a Jira issue
|
|
307
|
+
* @param taskId - The issue key (e.g., "PROJ-123")
|
|
308
|
+
* @param adfContent - The comment content in ADF format
|
|
309
|
+
*/
|
|
310
|
+
export async function addIssueComment(
|
|
311
|
+
taskId: string,
|
|
312
|
+
adfContent: any
|
|
313
|
+
): Promise<void> {
|
|
314
|
+
const client = getJiraClient();
|
|
315
|
+
await client.issueComments.addComment({
|
|
316
|
+
issueIdOrKey: taskId,
|
|
317
|
+
comment: adfContent,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
export interface Settings {
|
|
6
|
+
projects: string[];
|
|
7
|
+
commands: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let cachedSettings: Settings | null = null;
|
|
11
|
+
|
|
12
|
+
export function loadSettings(): Settings {
|
|
13
|
+
if (cachedSettings) {
|
|
14
|
+
return cachedSettings;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const settingsPath = path.join(process.cwd(), 'settings.yaml');
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(settingsPath)) {
|
|
20
|
+
console.warn('Warning: settings.yaml not found. Using default settings (all allowed).');
|
|
21
|
+
cachedSettings = {
|
|
22
|
+
projects: ['all'],
|
|
23
|
+
commands: ['all']
|
|
24
|
+
};
|
|
25
|
+
return cachedSettings;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const fileContents = fs.readFileSync(settingsPath, 'utf8');
|
|
30
|
+
const settings = yaml.load(fileContents) as Settings;
|
|
31
|
+
|
|
32
|
+
cachedSettings = {
|
|
33
|
+
projects: settings.projects || ['all'],
|
|
34
|
+
commands: settings.commands || ['all']
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return cachedSettings;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Error loading settings.yaml:', error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isProjectAllowed(projectKey: string): boolean {
|
|
45
|
+
const settings = loadSettings();
|
|
46
|
+
|
|
47
|
+
if (settings.projects.includes('all')) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return settings.projects.includes(projectKey);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isCommandAllowed(commandName: string): boolean {
|
|
55
|
+
const settings = loadSettings();
|
|
56
|
+
|
|
57
|
+
if (settings.commands.includes('all')) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return settings.commands.includes(commandName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getAllowedProjects(): string[] {
|
|
65
|
+
const settings = loadSettings();
|
|
66
|
+
return settings.projects;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getAllowedCommands(): string[] {
|
|
70
|
+
const settings = loadSettings();
|
|
71
|
+
return settings.commands;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// For testing purposes only
|
|
75
|
+
export function __resetCache__(): void {
|
|
76
|
+
cachedSettings = null;
|
|
77
|
+
}
|