jira-ai 0.2.0 → 0.2.5

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.
@@ -76,7 +76,7 @@ async function aboutCommand() {
76
76
  console.log(chalk_1.default.bold('For detailed help on any command, run:'));
77
77
  console.log(chalk_1.default.green(' jira-ai <command> --help\n'));
78
78
  console.log(chalk_1.default.bold('Configuration:'));
79
- console.log(' Settings are managed in settings.yaml');
79
+ console.log(` Settings file: ${chalk_1.default.cyan((0, settings_1.getSettingsPath)())}`);
80
80
  const allowedProjects = (0, settings_1.getAllowedProjects)();
81
81
  console.log(` - Projects: ${allowedProjects.includes('all') ? 'All allowed' : allowedProjects.join(', ')}`);
82
82
  console.log(` - Commands: ${isAllAllowed ? 'All allowed' : allowedCommandsList.join(', ')}\n`);
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getSettingsPath = getSettingsPath;
6
7
  exports.loadSettings = loadSettings;
7
8
  exports.isProjectAllowed = isProjectAllowed;
8
9
  exports.isCommandAllowed = isCommandAllowed;
@@ -11,23 +12,55 @@ exports.getAllowedCommands = getAllowedCommands;
11
12
  exports.__resetCache__ = __resetCache__;
12
13
  const fs_1 = __importDefault(require("fs"));
13
14
  const path_1 = __importDefault(require("path"));
15
+ const os_1 = __importDefault(require("os"));
14
16
  const js_yaml_1 = __importDefault(require("js-yaml"));
17
+ const chalk_1 = __importDefault(require("chalk"));
18
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.jira-ai');
19
+ const SETTINGS_FILE = path_1.default.join(CONFIG_DIR, 'settings.yaml');
15
20
  let cachedSettings = null;
21
+ function getSettingsPath() {
22
+ return SETTINGS_FILE;
23
+ }
16
24
  function loadSettings() {
17
25
  if (cachedSettings) {
18
26
  return cachedSettings;
19
27
  }
20
- const settingsPath = path_1.default.join(process.cwd(), 'settings.yaml');
21
- if (!fs_1.default.existsSync(settingsPath)) {
22
- console.warn('Warning: settings.yaml not found. Using default settings (all allowed).');
23
- cachedSettings = {
24
- projects: ['all'],
25
- commands: ['all']
26
- };
27
- return cachedSettings;
28
+ // Ensure config directory exists
29
+ if (!fs_1.default.existsSync(CONFIG_DIR)) {
30
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
31
+ }
32
+ if (!fs_1.default.existsSync(SETTINGS_FILE)) {
33
+ // Check if settings.yaml exists in current working directory (migration/backward compatibility)
34
+ const localSettingsPath = path_1.default.join(process.cwd(), 'settings.yaml');
35
+ if (fs_1.default.existsSync(localSettingsPath)) {
36
+ try {
37
+ const fileContents = fs_1.default.readFileSync(localSettingsPath, 'utf8');
38
+ fs_1.default.writeFileSync(SETTINGS_FILE, fileContents);
39
+ console.log(chalk_1.default?.cyan ? chalk_1.default.cyan(`Migrated settings.yaml to ${SETTINGS_FILE}`) : `Migrated settings.yaml to ${SETTINGS_FILE}`);
40
+ }
41
+ catch (error) {
42
+ console.error('Error migrating settings.yaml:', error);
43
+ }
44
+ }
45
+ else {
46
+ // Create default settings.yaml if it doesn't exist anywhere
47
+ const defaultSettings = {
48
+ projects: ['all'],
49
+ commands: ['all']
50
+ };
51
+ try {
52
+ const yamlStr = js_yaml_1.default.dump(defaultSettings);
53
+ fs_1.default.writeFileSync(SETTINGS_FILE, yamlStr);
54
+ }
55
+ catch (error) {
56
+ console.error('Error creating default settings.yaml:', error);
57
+ }
58
+ cachedSettings = defaultSettings;
59
+ return cachedSettings;
60
+ }
28
61
  }
29
62
  try {
30
- const fileContents = fs_1.default.readFileSync(settingsPath, 'utf8');
63
+ const fileContents = fs_1.default.readFileSync(SETTINGS_FILE, 'utf8');
31
64
  const settings = js_yaml_1.default.load(fileContents);
32
65
  cachedSettings = {
33
66
  projects: settings.projects || ['all'],
@@ -36,7 +69,7 @@ function loadSettings() {
36
69
  return cachedSettings;
37
70
  }
38
71
  catch (error) {
39
- console.error('Error loading settings.yaml:', error);
72
+ console.error(`Error loading ${SETTINGS_FILE}:`, error);
40
73
  process.exit(1);
41
74
  }
42
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jira-ai",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "CLI tool for interacting with Atlassian Jira",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -43,5 +43,9 @@
43
43
  "ts-jest": "^29.4.6",
44
44
  "ts-node": "^10.9.2",
45
45
  "typescript": "^5.9.3"
46
+ },
47
+ "overrides": {
48
+ "adf-builder": "npm:@atlaskit/adf-utils@^19.26.4",
49
+ "uuid": "^10.0.0"
46
50
  }
47
51
  }
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { getAllowedCommands, getAllowedProjects, isCommandAllowed } from '../lib/settings';
2
+ import { getAllowedCommands, getAllowedProjects, isCommandAllowed, getSettingsPath } from '../lib/settings';
3
3
 
4
4
  interface CommandInfo {
5
5
  name: string;
@@ -91,7 +91,7 @@ export async function aboutCommand() {
91
91
  console.log(chalk.green(' jira-ai <command> --help\n'));
92
92
 
93
93
  console.log(chalk.bold('Configuration:'));
94
- console.log(' Settings are managed in settings.yaml');
94
+ console.log(` Settings file: ${chalk.cyan(getSettingsPath())}`);
95
95
  const allowedProjects = getAllowedProjects();
96
96
  console.log(` - Projects: ${allowedProjects.includes('all') ? 'All allowed' : allowedProjects.join(', ')}`);
97
97
  console.log(` - Commands: ${isAllAllowed ? 'All allowed' : allowedCommandsList.join(', ')}\n`);
@@ -1,32 +1,64 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import yaml from 'js-yaml';
5
+ import chalk from 'chalk';
4
6
 
5
7
  export interface Settings {
6
8
  projects: string[];
7
9
  commands: string[];
8
10
  }
9
11
 
12
+ const CONFIG_DIR = path.join(os.homedir(), '.jira-ai');
13
+ const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.yaml');
14
+
10
15
  let cachedSettings: Settings | null = null;
11
16
 
17
+ export function getSettingsPath(): string {
18
+ return SETTINGS_FILE;
19
+ }
20
+
12
21
  export function loadSettings(): Settings {
13
22
  if (cachedSettings) {
14
23
  return cachedSettings;
15
24
  }
16
25
 
17
- const settingsPath = path.join(process.cwd(), 'settings.yaml');
26
+ // Ensure config directory exists
27
+ if (!fs.existsSync(CONFIG_DIR)) {
28
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ }
18
30
 
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;
31
+ if (!fs.existsSync(SETTINGS_FILE)) {
32
+ // Check if settings.yaml exists in current working directory (migration/backward compatibility)
33
+ const localSettingsPath = path.join(process.cwd(), 'settings.yaml');
34
+ if (fs.existsSync(localSettingsPath)) {
35
+ try {
36
+ const fileContents = fs.readFileSync(localSettingsPath, 'utf8');
37
+ fs.writeFileSync(SETTINGS_FILE, fileContents);
38
+ console.log(chalk?.cyan ? chalk.cyan(`Migrated settings.yaml to ${SETTINGS_FILE}`) : `Migrated settings.yaml to ${SETTINGS_FILE}`);
39
+ } catch (error) {
40
+ console.error('Error migrating settings.yaml:', error);
41
+ }
42
+ } else {
43
+ // Create default settings.yaml if it doesn't exist anywhere
44
+ const defaultSettings: Settings = {
45
+ projects: ['all'],
46
+ commands: ['all']
47
+ };
48
+ try {
49
+ const yamlStr = yaml.dump(defaultSettings);
50
+ fs.writeFileSync(SETTINGS_FILE, yamlStr);
51
+ } catch (error) {
52
+ console.error('Error creating default settings.yaml:', error);
53
+ }
54
+
55
+ cachedSettings = defaultSettings;
56
+ return cachedSettings;
57
+ }
26
58
  }
27
59
 
28
60
  try {
29
- const fileContents = fs.readFileSync(settingsPath, 'utf8');
61
+ const fileContents = fs.readFileSync(SETTINGS_FILE, 'utf8');
30
62
  const settings = yaml.load(fileContents) as Settings;
31
63
 
32
64
  cachedSettings = {
@@ -36,7 +68,7 @@ export function loadSettings(): Settings {
36
68
 
37
69
  return cachedSettings;
38
70
  } catch (error) {
39
- console.error('Error loading settings.yaml:', error);
71
+ console.error(`Error loading ${SETTINGS_FILE}:`, error);
40
72
  process.exit(1);
41
73
  }
42
74
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import {
4
5
  loadSettings,
5
6
  isProjectAllowed,
@@ -14,7 +15,9 @@ jest.mock('fs');
14
15
  const mockFs = fs as jest.Mocked<typeof fs>;
15
16
 
16
17
  describe('Settings Module', () => {
17
- const mockSettingsPath = path.join(process.cwd(), 'settings.yaml');
18
+ const mockConfigDir = path.join(os.homedir(), '.jira-ai');
19
+ const mockSettingsPath = path.join(mockConfigDir, 'settings.yaml');
20
+ const mockLocalSettingsPath = path.join(process.cwd(), 'settings.yaml');
18
21
 
19
22
  beforeEach(() => {
20
23
  jest.clearAllMocks();
@@ -33,28 +36,38 @@ commands:
33
36
  - me
34
37
  - projects
35
38
  `;
36
- mockFs.existsSync.mockReturnValue(true);
39
+ // Mock that config dir exists and settings file exists
40
+ mockFs.existsSync.mockImplementation((path) => {
41
+ if (path === mockConfigDir) return true;
42
+ if (path === mockSettingsPath) return true;
43
+ return false;
44
+ });
37
45
  mockFs.readFileSync.mockReturnValue(mockYaml);
38
46
 
39
47
  const settings = loadSettings();
40
48
 
41
49
  expect(settings.projects).toEqual(['BP', 'PM', 'PS']);
42
50
  expect(settings.commands).toEqual(['me', 'projects']);
43
- expect(mockFs.existsSync).toHaveBeenCalledWith(mockSettingsPath);
44
51
  expect(mockFs.readFileSync).toHaveBeenCalledWith(mockSettingsPath, 'utf8');
45
52
  });
46
53
 
47
54
  it('should return default settings when file does not exist', () => {
55
+ // Mock that neither config dir nor settings files exist
48
56
  mockFs.existsSync.mockReturnValue(false);
57
+ mockFs.mkdirSync.mockReturnValue(undefined);
58
+ mockFs.writeFileSync.mockReturnValue(undefined);
49
59
 
50
- const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
51
60
  const settings = loadSettings();
52
61
 
53
62
  expect(settings.projects).toEqual(['all']);
54
63
  expect(settings.commands).toEqual(['all']);
55
- expect(consoleWarnSpy).toHaveBeenCalled();
56
-
57
- consoleWarnSpy.mockRestore();
64
+ // Should create the config directory
65
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(mockConfigDir, { recursive: true });
66
+ // Should create default settings file
67
+ expect(mockFs.writeFileSync).toHaveBeenCalledWith(
68
+ mockSettingsPath,
69
+ expect.stringContaining('projects')
70
+ );
58
71
  });
59
72
 
60
73
  it('should handle null/undefined projects/commands by defaulting to all', () => {
@@ -62,7 +75,11 @@ commands:
62
75
  projects:
63
76
  commands:
64
77
  `;
65
- mockFs.existsSync.mockReturnValue(true);
78
+ mockFs.existsSync.mockImplementation((path) => {
79
+ if (path === mockConfigDir) return true;
80
+ if (path === mockSettingsPath) return true;
81
+ return false;
82
+ });
66
83
  mockFs.readFileSync.mockReturnValue(mockYaml);
67
84
 
68
85
  const settings = loadSettings();
@@ -72,7 +89,11 @@ commands:
72
89
  });
73
90
 
74
91
  it('should exit process on invalid YAML', () => {
75
- mockFs.existsSync.mockReturnValue(true);
92
+ mockFs.existsSync.mockImplementation((path) => {
93
+ if (path === mockConfigDir) return true;
94
+ if (path === mockSettingsPath) return true;
95
+ return false;
96
+ });
76
97
  mockFs.readFileSync.mockReturnValue('invalid: yaml: content:');
77
98
 
78
99
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
@@ -97,7 +118,11 @@ projects:
97
118
  commands:
98
119
  - me
99
120
  `;
100
- mockFs.existsSync.mockReturnValue(true);
121
+ mockFs.existsSync.mockImplementation((path) => {
122
+ if (path === mockConfigDir) return true;
123
+ if (path === mockSettingsPath) return true;
124
+ return false;
125
+ });
101
126
  mockFs.readFileSync.mockReturnValue(mockYaml);
102
127
 
103
128
  expect(isProjectAllowed('BP')).toBe(true);
@@ -114,7 +139,11 @@ projects:
114
139
  commands:
115
140
  - me
116
141
  `;
117
- mockFs.existsSync.mockReturnValue(true);
142
+ mockFs.existsSync.mockImplementation((path) => {
143
+ if (path === mockConfigDir) return true;
144
+ if (path === mockSettingsPath) return true;
145
+ return false;
146
+ });
118
147
  mockFs.readFileSync.mockReturnValue(mockYaml);
119
148
 
120
149
  expect(isProjectAllowed('BP')).toBe(true);
@@ -131,7 +160,11 @@ projects:
131
160
  commands:
132
161
  - me
133
162
  `;
134
- mockFs.existsSync.mockReturnValue(true);
163
+ mockFs.existsSync.mockImplementation((path) => {
164
+ if (path === mockConfigDir) return true;
165
+ if (path === mockSettingsPath) return true;
166
+ return false;
167
+ });
135
168
  mockFs.readFileSync.mockReturnValue(mockYaml);
136
169
 
137
170
  expect(isProjectAllowed('XYZ')).toBe(false);
@@ -146,7 +179,11 @@ projects:
146
179
  commands:
147
180
  - me
148
181
  `;
149
- mockFs.existsSync.mockReturnValue(true);
182
+ mockFs.existsSync.mockImplementation((path) => {
183
+ if (path === mockConfigDir) return true;
184
+ if (path === mockSettingsPath) return true;
185
+ return false;
186
+ });
150
187
  mockFs.readFileSync.mockReturnValue(mockYaml);
151
188
 
152
189
  expect(isProjectAllowed('BP')).toBe(true);
@@ -163,7 +200,11 @@ projects:
163
200
  commands:
164
201
  - all
165
202
  `;
166
- mockFs.existsSync.mockReturnValue(true);
203
+ mockFs.existsSync.mockImplementation((path) => {
204
+ if (path === mockConfigDir) return true;
205
+ if (path === mockSettingsPath) return true;
206
+ return false;
207
+ });
167
208
  mockFs.readFileSync.mockReturnValue(mockYaml);
168
209
 
169
210
  expect(isCommandAllowed('me')).toBe(true);
@@ -179,7 +220,11 @@ commands:
179
220
  - me
180
221
  - projects
181
222
  `;
182
- mockFs.existsSync.mockReturnValue(true);
223
+ mockFs.existsSync.mockImplementation((path) => {
224
+ if (path === mockConfigDir) return true;
225
+ if (path === mockSettingsPath) return true;
226
+ return false;
227
+ });
183
228
  mockFs.readFileSync.mockReturnValue(mockYaml);
184
229
 
185
230
  expect(isCommandAllowed('me')).toBe(true);
@@ -194,7 +239,11 @@ commands:
194
239
  - me
195
240
  - projects
196
241
  `;
197
- mockFs.existsSync.mockReturnValue(true);
242
+ mockFs.existsSync.mockImplementation((path) => {
243
+ if (path === mockConfigDir) return true;
244
+ if (path === mockSettingsPath) return true;
245
+ return false;
246
+ });
198
247
  mockFs.readFileSync.mockReturnValue(mockYaml);
199
248
 
200
249
  expect(isCommandAllowed('task-with-details')).toBe(false);
@@ -212,7 +261,11 @@ projects:
212
261
  commands:
213
262
  - me
214
263
  `;
215
- mockFs.existsSync.mockReturnValue(true);
264
+ mockFs.existsSync.mockImplementation((path) => {
265
+ if (path === mockConfigDir) return true;
266
+ if (path === mockSettingsPath) return true;
267
+ return false;
268
+ });
216
269
  mockFs.readFileSync.mockReturnValue(mockYaml);
217
270
 
218
271
  const projects = getAllowedProjects();
@@ -226,7 +279,11 @@ projects:
226
279
  commands:
227
280
  - me
228
281
  `;
229
- mockFs.existsSync.mockReturnValue(true);
282
+ mockFs.existsSync.mockImplementation((path) => {
283
+ if (path === mockConfigDir) return true;
284
+ if (path === mockSettingsPath) return true;
285
+ return false;
286
+ });
230
287
  mockFs.readFileSync.mockReturnValue(mockYaml);
231
288
 
232
289
  const projects = getAllowedProjects();
@@ -243,7 +300,11 @@ commands:
243
300
  - me
244
301
  - projects
245
302
  `;
246
- mockFs.existsSync.mockReturnValue(true);
303
+ mockFs.existsSync.mockImplementation((path) => {
304
+ if (path === mockConfigDir) return true;
305
+ if (path === mockSettingsPath) return true;
306
+ return false;
307
+ });
247
308
  mockFs.readFileSync.mockReturnValue(mockYaml);
248
309
 
249
310
  const commands = getAllowedCommands();
@@ -257,7 +318,11 @@ projects:
257
318
  commands:
258
319
  - all
259
320
  `;
260
- mockFs.existsSync.mockReturnValue(true);
321
+ mockFs.existsSync.mockImplementation((path) => {
322
+ if (path === mockConfigDir) return true;
323
+ if (path === mockSettingsPath) return true;
324
+ return false;
325
+ });
261
326
  mockFs.readFileSync.mockReturnValue(mockYaml);
262
327
 
263
328
  const commands = getAllowedCommands();
@@ -273,7 +338,11 @@ projects:
273
338
  commands:
274
339
  - me
275
340
  `;
276
- mockFs.existsSync.mockReturnValue(true);
341
+ mockFs.existsSync.mockImplementation((path) => {
342
+ if (path === mockConfigDir) return true;
343
+ if (path === mockSettingsPath) return true;
344
+ return false;
345
+ });
277
346
  mockFs.readFileSync.mockReturnValue(mockYaml);
278
347
 
279
348
  // Call multiple times