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.
Files changed (45) hide show
  1. package/README.md +364 -0
  2. package/dist/cli.js +87 -0
  3. package/dist/commands/about.js +83 -0
  4. package/dist/commands/add-comment.js +109 -0
  5. package/dist/commands/me.js +23 -0
  6. package/dist/commands/project-statuses.js +23 -0
  7. package/dist/commands/projects.js +35 -0
  8. package/dist/commands/run-jql.js +44 -0
  9. package/dist/commands/task-with-details.js +23 -0
  10. package/dist/commands/update-description.js +109 -0
  11. package/dist/lib/formatters.js +193 -0
  12. package/dist/lib/jira-client.js +216 -0
  13. package/dist/lib/settings.js +68 -0
  14. package/dist/lib/utils.js +79 -0
  15. package/jest.config.js +21 -0
  16. package/package.json +47 -0
  17. package/settings.yaml +24 -0
  18. package/src/cli.ts +97 -0
  19. package/src/commands/about.ts +98 -0
  20. package/src/commands/add-comment.ts +94 -0
  21. package/src/commands/me.ts +18 -0
  22. package/src/commands/project-statuses.ts +18 -0
  23. package/src/commands/projects.ts +32 -0
  24. package/src/commands/run-jql.ts +40 -0
  25. package/src/commands/task-with-details.ts +18 -0
  26. package/src/commands/update-description.ts +94 -0
  27. package/src/lib/formatters.ts +224 -0
  28. package/src/lib/jira-client.ts +319 -0
  29. package/src/lib/settings.ts +77 -0
  30. package/src/lib/utils.ts +76 -0
  31. package/src/types/md-to-adf.d.ts +14 -0
  32. package/tests/README.md +97 -0
  33. package/tests/__mocks__/jira.js.ts +4 -0
  34. package/tests/__mocks__/md-to-adf.ts +7 -0
  35. package/tests/__mocks__/mdast-util-from-adf.ts +4 -0
  36. package/tests/__mocks__/mdast-util-to-markdown.ts +1 -0
  37. package/tests/add-comment.test.ts +226 -0
  38. package/tests/cli-permissions.test.ts +156 -0
  39. package/tests/jira-client.test.ts +123 -0
  40. package/tests/projects.test.ts +205 -0
  41. package/tests/settings.test.ts +288 -0
  42. package/tests/task-with-details.test.ts +83 -0
  43. package/tests/update-description.test.ts +262 -0
  44. package/to-do.md +9 -0
  45. 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
+ }