jira-pilot 2.0.4 → 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.
- package/README.md +216 -173
- package/bin/{jira.js → jira.ts} +10 -1
- package/dist/bin/jira.js +64 -0
- package/package.json +21 -15
- package/src/commands/ai-actions/{plan.js → plan.ts} +9 -9
- package/src/commands/ai-actions/{review.js → review.ts} +2 -2
- package/src/commands/ai-actions/{standup.js → standup.ts} +4 -4
- package/src/commands/{ai.js → ai.ts} +11 -11
- package/src/commands/{board.js → board.ts} +11 -11
- package/src/commands/bulk.ts +230 -0
- package/src/commands/{config.js → config.ts} +57 -8
- package/src/commands/dashboard.ts +222 -0
- package/src/commands/filter.ts +84 -0
- package/src/commands/{git.js → git.ts} +4 -4
- package/src/commands/issue-attach.ts +44 -0
- package/src/commands/issue-pr.ts +87 -0
- package/src/commands/issue-worklog.ts +90 -0
- package/src/commands/{issue.js → issue.ts} +359 -68
- package/src/commands/{mcp.js → mcp.ts} +2 -2
- package/src/commands/{project.js → project.ts} +11 -11
- package/src/commands/sprint.ts +269 -0
- package/src/server/{mcp-server.js → mcp-server.ts} +235 -8
- package/src/services/{ai-service.js → ai-service.ts} +16 -16
- package/src/services/{api-service.js → api-service.ts} +33 -9
- package/src/services/config-service.ts +21 -0
- package/src/types.ts +68 -0
- package/src/utils/{adf-parser.js → adf-parser.ts} +12 -12
- package/src/utils/config-store.ts +109 -0
- package/src/utils/{config.js → config.ts} +14 -41
- package/src/utils/{error-handler.js → error-handler.ts} +2 -1
- package/src/utils/{text-to-adf.js → text-to-adf.ts} +1 -1
- package/src/utils/{validators.js → validators.ts} +4 -4
- package/src/commands/bulk.js +0 -108
- package/src/commands/dashboard.js +0 -89
- package/src/commands/sprint.js +0 -153
|
@@ -4,10 +4,10 @@ import { getCredentials } from '../utils/config.js';
|
|
|
4
4
|
export class AiService {
|
|
5
5
|
constructor() { }
|
|
6
6
|
|
|
7
|
-
async generate(prompt) {
|
|
8
|
-
const { aiKey, aiProvider,
|
|
7
|
+
async generate(prompt: string): Promise<string> {
|
|
8
|
+
const { aiKey, aiProvider, enableAi } = getCredentials();
|
|
9
9
|
|
|
10
|
-
if (!
|
|
10
|
+
if (!enableAi) {
|
|
11
11
|
throw new Error('AI features are disabled. Run "jira config ai enable" to enable.');
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -29,7 +29,7 @@ export class AiService {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async reviewCode(diff, context) {
|
|
32
|
+
async reviewCode(diff: string, context: string): Promise<string> {
|
|
33
33
|
const prompt = `Review the following code changes against the issue context.
|
|
34
34
|
Issue Context: ${context}
|
|
35
35
|
|
|
@@ -47,7 +47,7 @@ Provide a concise code review. Focus on:
|
|
|
47
47
|
return this.generate(prompt);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async breakdownEpic(summary, description) {
|
|
50
|
+
async breakdownEpic(summary: string, description: string): Promise<any> {
|
|
51
51
|
const prompt = `Break down the following Epic into child Stories and Tasks.
|
|
52
52
|
Epic Summary: ${summary}
|
|
53
53
|
Epic Description: ${description || 'N/A'}
|
|
@@ -60,7 +60,7 @@ Example: [{"type": "Story", "summary": "...", "description": "..."}]`;
|
|
|
60
60
|
return JSON.parse(jsonStr);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async generateStandup(yesterday, today) {
|
|
63
|
+
async generateStandup(yesterday: string, today: string): Promise<string> {
|
|
64
64
|
const prompt = `Generate a daily standup update based on my Jira activity.
|
|
65
65
|
Activity (Last 24h):
|
|
66
66
|
${yesterday}
|
|
@@ -77,7 +77,7 @@ Keep it concise and professional.`;
|
|
|
77
77
|
return this.generate(prompt);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
async generateJql(query) {
|
|
80
|
+
async generateJql(query: string): Promise<string> {
|
|
81
81
|
const prompt = `Convert the following natural language query into Jira JQL.
|
|
82
82
|
Query: "${query}"
|
|
83
83
|
|
|
@@ -87,7 +87,7 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
87
87
|
return response.replace(/```/g, '').trim();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async callOpenAI(key, prompt) {
|
|
90
|
+
async callOpenAI(key: string, prompt: string): Promise<string> {
|
|
91
91
|
try {
|
|
92
92
|
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
|
|
93
93
|
model: 'gpt-4o',
|
|
@@ -97,12 +97,12 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
97
97
|
headers: { 'Authorization': `Bearer ${key}` }
|
|
98
98
|
});
|
|
99
99
|
return response.data.choices[0].message.content;
|
|
100
|
-
} catch (e) {
|
|
100
|
+
} catch (e: any) {
|
|
101
101
|
throw new Error(`OpenAI API Error: ${e.response?.data?.error?.message || e.message}`);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
async callGemini(key, prompt) {
|
|
105
|
+
async callGemini(key: string, prompt: string): Promise<string> {
|
|
106
106
|
try {
|
|
107
107
|
// Gemini REST API — uses generativelanguage.googleapis.com
|
|
108
108
|
const model = 'gemini-2.0-flash';
|
|
@@ -125,8 +125,8 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
125
125
|
throw new Error('No response generated by Gemini.');
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
return candidates[0].content.parts.map(p => p.text).join('');
|
|
129
|
-
} catch (e) {
|
|
128
|
+
return candidates[0].content.parts.map((p: any) => p.text).join('');
|
|
129
|
+
} catch (e: any) {
|
|
130
130
|
if (e.response?.data?.error) {
|
|
131
131
|
throw new Error(`Gemini API Error: ${e.response.data.error.message}`);
|
|
132
132
|
}
|
|
@@ -134,7 +134,7 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
async callAnthropic(key, prompt) {
|
|
137
|
+
async callAnthropic(key: string, prompt: string): Promise<string> {
|
|
138
138
|
try {
|
|
139
139
|
// Anthropic Messages API
|
|
140
140
|
const response = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
@@ -150,10 +150,10 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
return response.data.content
|
|
153
|
-
.filter(block => block.type === 'text')
|
|
154
|
-
.map(block => block.text)
|
|
153
|
+
.filter((block: any) => block.type === 'text')
|
|
154
|
+
.map((block: any) => block.text)
|
|
155
155
|
.join('');
|
|
156
|
-
} catch (e) {
|
|
156
|
+
} catch (e: any) {
|
|
157
157
|
if (e.response?.data?.error) {
|
|
158
158
|
throw new Error(`Anthropic API Error: ${e.response.data.error.message}`);
|
|
159
159
|
}
|
|
@@ -3,10 +3,16 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { getCredentials } from '../utils/config.js';
|
|
4
4
|
|
|
5
5
|
export class ApiService {
|
|
6
|
+
private client: any;
|
|
7
|
+
private agileClient: any;
|
|
8
|
+
private _domain: string | null = null;
|
|
9
|
+
|
|
6
10
|
constructor() {
|
|
7
11
|
this.init();
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
|
|
15
|
+
|
|
10
16
|
init() {
|
|
11
17
|
const { jiraUrl, email, apiToken } = getCredentials();
|
|
12
18
|
|
|
@@ -42,7 +48,7 @@ export class ApiService {
|
|
|
42
48
|
});
|
|
43
49
|
|
|
44
50
|
// Shared response interceptor
|
|
45
|
-
const errorInterceptor = (error) => {
|
|
51
|
+
const errorInterceptor = (error: any) => {
|
|
46
52
|
if (error.response) {
|
|
47
53
|
if (error.response.status === 401) {
|
|
48
54
|
console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
|
|
@@ -53,8 +59,8 @@ export class ApiService {
|
|
|
53
59
|
return Promise.reject(error);
|
|
54
60
|
};
|
|
55
61
|
|
|
56
|
-
this.client.interceptors.response.use(r => r, errorInterceptor);
|
|
57
|
-
this.agileClient.interceptors.response.use(r => r, errorInterceptor);
|
|
62
|
+
this.client.interceptors.response.use((r: any) => r, errorInterceptor);
|
|
63
|
+
this.agileClient.interceptors.response.use((r: any) => r, errorInterceptor);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
/** @returns {string} The Jira domain URL */
|
|
@@ -73,39 +79,57 @@ export class ApiService {
|
|
|
73
79
|
|
|
74
80
|
// ── Standard REST API v3 Methods ────────────────────────────────
|
|
75
81
|
|
|
76
|
-
async get(url, config = {}) {
|
|
82
|
+
async get(url: string, config: any = {}) {
|
|
77
83
|
this.ensureClient();
|
|
78
84
|
const response = await this.client.get(url, config);
|
|
79
85
|
return response.data;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
async post(url, data, config = {}) {
|
|
88
|
+
async post(url: string, data: any, config: any = {}) {
|
|
83
89
|
this.ensureClient();
|
|
84
90
|
const response = await this.client.post(url, data, config);
|
|
85
91
|
return response.data;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
async put(url, data, config = {}) {
|
|
94
|
+
async put(url: string, data: any, config: any = {}) {
|
|
89
95
|
this.ensureClient();
|
|
90
96
|
const response = await this.client.put(url, data, config);
|
|
91
97
|
return response.data;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
async delete(url, config = {}) {
|
|
100
|
+
async delete(url: string, config: any = {}) {
|
|
95
101
|
this.ensureClient();
|
|
96
102
|
const response = await this.client.delete(url, config);
|
|
97
103
|
return response.data;
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
async upload(url: string, formData: any) {
|
|
107
|
+
this.ensureClient();
|
|
108
|
+
// Jira requires this header for attachments
|
|
109
|
+
const headers: any = {
|
|
110
|
+
'X-Atlassian-Token': 'no-check'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// If using 'form-data' package, it has getHeaders().
|
|
114
|
+
// If using native FormData, axios/adapter handles Content-Type + boundary.
|
|
115
|
+
if (formData.getHeaders) {
|
|
116
|
+
Object.assign(headers, formData.getHeaders());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const config = { headers };
|
|
120
|
+
const response = await this.client.post(url, formData, config);
|
|
121
|
+
return response.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
// ── Agile REST API v1 Methods ───────────────────────────────────
|
|
101
125
|
|
|
102
|
-
async agileGet(url, config = {}) {
|
|
126
|
+
async agileGet(url: string, config: any = {}) {
|
|
103
127
|
this.ensureClient();
|
|
104
128
|
const response = await this.agileClient.get(url, config);
|
|
105
129
|
return response.data;
|
|
106
130
|
}
|
|
107
131
|
|
|
108
|
-
async agilePost(url, data, config = {}) {
|
|
132
|
+
async agilePost(url: string, data: any, config: any = {}) {
|
|
109
133
|
this.ensureClient();
|
|
110
134
|
const response = await this.agileClient.post(url, data, config);
|
|
111
135
|
return response.data;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ConfigStore from '../utils/config-store.js';
|
|
2
|
+
|
|
3
|
+
const config = new ConfigStore('jira-pilot');
|
|
4
|
+
|
|
5
|
+
export class ConfigService {
|
|
6
|
+
static getConfig() {
|
|
7
|
+
return config.all;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static saveConfig(newConfig: any) {
|
|
11
|
+
config.all = newConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static get(key: string) {
|
|
15
|
+
return config.get(key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static set(key: string, value: any) {
|
|
19
|
+
config.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface JiraIssue {
|
|
2
|
+
key: string;
|
|
3
|
+
fields: {
|
|
4
|
+
summary: string;
|
|
5
|
+
description?: any; // ADF
|
|
6
|
+
status: { name: string; id?: string };
|
|
7
|
+
priority?: { name: string; id?: string };
|
|
8
|
+
assignee?: { accountId: string; displayName: string; emailAddress?: string };
|
|
9
|
+
issuetype: { name: string; id?: string; subtask?: boolean };
|
|
10
|
+
created: string;
|
|
11
|
+
updated: string;
|
|
12
|
+
comment?: {
|
|
13
|
+
comments: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
body: string;
|
|
16
|
+
author: { displayName: string; accountId: string };
|
|
17
|
+
created: string;
|
|
18
|
+
}>;
|
|
19
|
+
total: number;
|
|
20
|
+
};
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface JiraProject {
|
|
26
|
+
key: string;
|
|
27
|
+
name: string;
|
|
28
|
+
id: string;
|
|
29
|
+
lead?: { displayName: string };
|
|
30
|
+
style?: string; // classic / next-gen
|
|
31
|
+
issueTypes?: Array<{ name: string; subtask?: boolean; id: string }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface JiraSprint {
|
|
35
|
+
id: number;
|
|
36
|
+
name: string;
|
|
37
|
+
state: 'active' | 'future' | 'closed';
|
|
38
|
+
startDate?: string;
|
|
39
|
+
endDate?: string;
|
|
40
|
+
originBoardId?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface JiraBoard {
|
|
44
|
+
id: number;
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
location?: { projectId: number; displayName: string; projectKey: string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ConfigProfile {
|
|
51
|
+
jiraUrl: string;
|
|
52
|
+
email: string;
|
|
53
|
+
apiToken: string;
|
|
54
|
+
aiEnabled?: boolean; // Unified key
|
|
55
|
+
enableAi?: boolean; // Legacy key support
|
|
56
|
+
aiProvider?: 'openai' | 'gemini' | 'anthropic';
|
|
57
|
+
aiKey?: string; // Legacy
|
|
58
|
+
aiConfig?: {
|
|
59
|
+
apiKey?: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
};
|
|
62
|
+
githubToken?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GlobalConfig {
|
|
66
|
+
profiles: Record<string, ConfigProfile>;
|
|
67
|
+
currentProfile?: string;
|
|
68
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
export function parseADF(content) {
|
|
1
|
+
export function parseADF(content: any) {
|
|
2
2
|
if (!content) return '';
|
|
3
3
|
if (typeof content === 'string') return content;
|
|
4
4
|
|
|
5
5
|
if (content.type === 'doc') {
|
|
6
|
-
return content.content.map(node => parseNode(node)).join('\n');
|
|
6
|
+
return content.content.map((node: any) => parseNode(node)).join('\n');
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
return JSON.stringify(content);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function parseNode(node) {
|
|
12
|
+
function parseNode(node: any): string {
|
|
13
13
|
if (!node) return '';
|
|
14
14
|
|
|
15
15
|
switch (node.type) {
|
|
@@ -22,28 +22,28 @@ function parseNode(node) {
|
|
|
22
22
|
case 'orderedList':
|
|
23
23
|
return parseList(node, '1.');
|
|
24
24
|
case 'heading':
|
|
25
|
-
return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map(c => parseNode(c)).join('')}\n`;
|
|
25
|
+
return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map((c: any) => parseNode(c)).join('')}\n`;
|
|
26
26
|
case 'codeBlock':
|
|
27
|
-
return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map(c => c.text).join('')}\n\`\`\`\n`;
|
|
27
|
+
return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map((c: any) => c.text).join('')}\n\`\`\`\n`;
|
|
28
28
|
case 'blockquote':
|
|
29
|
-
return `> ${node.content.map(c => parseNode(c)).join('')}`;
|
|
29
|
+
return `> ${node.content.map((c: any) => parseNode(c)).join('')}`;
|
|
30
30
|
default:
|
|
31
31
|
if (node.content) {
|
|
32
|
-
return node.content.map(c => parseNode(c)).join('');
|
|
32
|
+
return node.content.map((c: any) => parseNode(c)).join('');
|
|
33
33
|
}
|
|
34
34
|
return ''; // Unknown node, skip or fallback
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function parseParagraph(node) {
|
|
38
|
+
function parseParagraph(node: any) {
|
|
39
39
|
if (!node.content) return '\n';
|
|
40
|
-
return node.content.map(c => parseNode(c)).join('') + '\n';
|
|
40
|
+
return node.content.map((c: any) => parseNode(c)).join('') + '\n';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function parseList(node, marker) {
|
|
43
|
+
function parseList(node: any, marker: string) {
|
|
44
44
|
if (!node.content) return '';
|
|
45
|
-
return node.content.map((item, index) => {
|
|
45
|
+
return node.content.map((item: any, index: number) => {
|
|
46
46
|
const prefix = marker === '1.' ? `${index + 1}. ` : `${marker} `;
|
|
47
|
-
return `${prefix}${item.content.map(c => parseNode(c)).join('')}`;
|
|
47
|
+
return `${prefix}${item.content.map((c: any) => parseNode(c)).join('')}`;
|
|
48
48
|
}).join('\n') + '\n';
|
|
49
49
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
function getAppDataPath() {
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
8
|
+
}
|
|
9
|
+
if (process.platform === 'darwin') {
|
|
10
|
+
return path.join(os.homedir(), 'Library', 'Preferences');
|
|
11
|
+
}
|
|
12
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class ConfigStore {
|
|
16
|
+
private dir: string;
|
|
17
|
+
private path: string;
|
|
18
|
+
|
|
19
|
+
constructor(projectName: string) {
|
|
20
|
+
this.dir = path.join(getAppDataPath(), projectName);
|
|
21
|
+
this.path = path.join(this.dir, 'config.json');
|
|
22
|
+
this.ensureDir();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ensureDir() {
|
|
26
|
+
if (!fs.existsSync(this.dir)) {
|
|
27
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
load() {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(this.path)) return {};
|
|
34
|
+
return JSON.parse(fs.readFileSync(this.path, 'utf8'));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
save(data: any) {
|
|
41
|
+
this.ensureDir();
|
|
42
|
+
fs.writeFileSync(this.path, JSON.stringify(data, null, 4), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(key?: string): any {
|
|
46
|
+
const data = this.load();
|
|
47
|
+
if (!key) return data;
|
|
48
|
+
|
|
49
|
+
// Support dot notation: profiles.work
|
|
50
|
+
const parts = key.split('.');
|
|
51
|
+
let current = data;
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (current === undefined || current === null) return undefined;
|
|
54
|
+
current = current[part];
|
|
55
|
+
}
|
|
56
|
+
return current;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
set(key: string, value: any): void {
|
|
60
|
+
const data = this.load();
|
|
61
|
+
|
|
62
|
+
const parts = key.split('.');
|
|
63
|
+
const last = parts.pop();
|
|
64
|
+
if (!last) return;
|
|
65
|
+
|
|
66
|
+
let current = data;
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (typeof current[part] !== 'object' || current[part] === null) {
|
|
69
|
+
current[part] = {};
|
|
70
|
+
}
|
|
71
|
+
current = current[part];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
current[last] = value;
|
|
75
|
+
this.save(data);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
delete(key: string): void {
|
|
79
|
+
const data = this.load();
|
|
80
|
+
const parts = key.split('.');
|
|
81
|
+
const last = parts.pop();
|
|
82
|
+
if (!last) return; // Should not happen
|
|
83
|
+
|
|
84
|
+
let current = data;
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
if (current === undefined || current === null) return; // Key doesn't exist
|
|
87
|
+
current = current[part];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (current && typeof current === 'object') {
|
|
91
|
+
delete current[last];
|
|
92
|
+
this.save(data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
clear() {
|
|
97
|
+
this.save({});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get all() {
|
|
101
|
+
return this.load();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set all(data: any) {
|
|
105
|
+
this.save(data);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default ConfigStore;
|
|
@@ -1,57 +1,30 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ConfigStore from './config-store.js';
|
|
2
|
+
import { ConfigProfile } from '../types.js';
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
jiraUrl: {
|
|
5
|
-
type: 'string',
|
|
6
|
-
format: 'url'
|
|
7
|
-
},
|
|
8
|
-
email: {
|
|
9
|
-
type: 'string',
|
|
10
|
-
format: 'email'
|
|
11
|
-
},
|
|
12
|
-
apiToken: {
|
|
13
|
-
type: 'string'
|
|
14
|
-
},
|
|
15
|
-
aiKey: {
|
|
16
|
-
type: 'string'
|
|
17
|
-
},
|
|
18
|
-
aiProvider: {
|
|
19
|
-
type: 'string',
|
|
20
|
-
default: 'openai'
|
|
21
|
-
},
|
|
22
|
-
aiEnabled: {
|
|
23
|
-
type: 'boolean',
|
|
24
|
-
default: false
|
|
25
|
-
},
|
|
26
|
-
githubToken: {
|
|
27
|
-
type: 'string'
|
|
28
|
-
}
|
|
29
|
-
};
|
|
4
|
+
const config = new ConfigStore('jira-pilot');
|
|
30
5
|
|
|
31
|
-
const
|
|
32
|
-
projectName: 'jira-pilot',
|
|
33
|
-
schema
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
export const getCredentials = () => {
|
|
6
|
+
export const getCredentials = (): Partial<ConfigProfile> => {
|
|
37
7
|
return {
|
|
38
8
|
jiraUrl: config.get('jiraUrl'),
|
|
39
9
|
email: config.get('email'),
|
|
40
10
|
apiToken: config.get('apiToken'),
|
|
41
11
|
aiKey: config.get('aiKey'),
|
|
42
12
|
aiProvider: config.get('aiProvider'),
|
|
43
|
-
|
|
13
|
+
enableAi: config.get('aiEnabled'), // Map legacy key
|
|
44
14
|
githubToken: config.get('githubToken')
|
|
45
15
|
};
|
|
46
16
|
};
|
|
47
17
|
|
|
48
|
-
export const setCredentials = ({ jiraUrl, email, apiToken,
|
|
18
|
+
export const setCredentials = ({ jiraUrl, email, apiToken, aiConfig, aiProvider, enableAi, githubToken }: Partial<ConfigProfile>) => {
|
|
49
19
|
if (jiraUrl) config.set('jiraUrl', jiraUrl);
|
|
50
20
|
if (email) config.set('email', email);
|
|
51
21
|
if (apiToken) config.set('apiToken', apiToken);
|
|
52
|
-
if (
|
|
22
|
+
if (aiConfig) {
|
|
23
|
+
if (aiConfig.apiKey) config.set('aiKey', aiConfig.apiKey);
|
|
24
|
+
config.set('aiConfig', aiConfig);
|
|
25
|
+
}
|
|
53
26
|
if (aiProvider) config.set('aiProvider', aiProvider);
|
|
54
|
-
if (typeof
|
|
27
|
+
if (typeof enableAi !== 'undefined') config.set('enableAi', enableAi);
|
|
55
28
|
if (githubToken) config.set('githubToken', githubToken);
|
|
56
29
|
};
|
|
57
30
|
|
|
@@ -66,13 +39,13 @@ export const hasCredentials = () => {
|
|
|
66
39
|
|
|
67
40
|
// ── Profile Management ──────────────────────────────────────────────
|
|
68
41
|
|
|
69
|
-
export const saveProfile = (name) => {
|
|
42
|
+
export const saveProfile = (name: string): void => {
|
|
70
43
|
const creds = getCredentials();
|
|
71
44
|
config.set(`profiles.${name}`, creds);
|
|
72
45
|
config.set('activeProfile', name);
|
|
73
46
|
};
|
|
74
47
|
|
|
75
|
-
export const loadProfile = (name) => {
|
|
48
|
+
export const loadProfile = (name: string): boolean => {
|
|
76
49
|
const profile = config.get(`profiles.${name}`);
|
|
77
50
|
if (!profile) return false;
|
|
78
51
|
setCredentials(profile);
|
|
@@ -80,7 +53,7 @@ export const loadProfile = (name) => {
|
|
|
80
53
|
return true;
|
|
81
54
|
};
|
|
82
55
|
|
|
83
|
-
export const deleteProfile = (name) => {
|
|
56
|
+
export const deleteProfile = (name: string): void => {
|
|
84
57
|
config.delete(`profiles.${name}`);
|
|
85
58
|
if (config.get('activeProfile') === name) {
|
|
86
59
|
config.delete('activeProfile');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { Ora } from 'ora';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Standardized error handler for CLI commands.
|
|
@@ -8,7 +9,7 @@ import chalk from 'chalk';
|
|
|
8
9
|
* @param {Error} error - The error object
|
|
9
10
|
* @param {string} [context] - Optional context (e.g., "Failed to list issues")
|
|
10
11
|
*/
|
|
11
|
-
export function handleCommandError(spinner, error, context = 'Operation failed') {
|
|
12
|
+
export function handleCommandError(spinner: Ora | null, error: any, context = 'Operation failed') {
|
|
12
13
|
// Handle user cancellation (Ctrl+C in enquirer)
|
|
13
14
|
if (error === '' || (error && error.message === '')) {
|
|
14
15
|
if (spinner) spinner.stop();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @param {string} key - The issue key to validate.
|
|
9
9
|
* @returns {{ valid: boolean, message?: string }}
|
|
10
10
|
*/
|
|
11
|
-
export function validateIssueKey(key) {
|
|
11
|
+
export function validateIssueKey(key: any): { valid: boolean; message?: string } {
|
|
12
12
|
if (!key || typeof key !== 'string') {
|
|
13
13
|
return { valid: false, message: 'Issue key is required.' };
|
|
14
14
|
}
|
|
@@ -31,7 +31,7 @@ export function validateIssueKey(key) {
|
|
|
31
31
|
* @param {string} key - The project key to validate.
|
|
32
32
|
* @returns {{ valid: boolean, message?: string }}
|
|
33
33
|
*/
|
|
34
|
-
export function validateProjectKey(key) {
|
|
34
|
+
export function validateProjectKey(key: any): { valid: boolean; message?: string } {
|
|
35
35
|
if (!key || typeof key !== 'string') {
|
|
36
36
|
return { valid: false, message: 'Project key is required.' };
|
|
37
37
|
}
|
|
@@ -54,7 +54,7 @@ export function validateProjectKey(key) {
|
|
|
54
54
|
* @param {string} url - The URL to validate.
|
|
55
55
|
* @returns {{ valid: boolean, message?: string }}
|
|
56
56
|
*/
|
|
57
|
-
export function validateUrl(url) {
|
|
57
|
+
export function validateUrl(url: any): { valid: boolean; message?: string } {
|
|
58
58
|
if (!url || typeof url !== 'string') {
|
|
59
59
|
return { valid: false, message: 'URL is required.' };
|
|
60
60
|
}
|
|
@@ -78,7 +78,7 @@ export function validateUrl(url) {
|
|
|
78
78
|
* @param {string} jql - The JQL query string.
|
|
79
79
|
* @returns {string} The sanitized JQL string.
|
|
80
80
|
*/
|
|
81
|
-
export function sanitizeJql(jql) {
|
|
81
|
+
export function sanitizeJql(jql: any): string {
|
|
82
82
|
if (!jql || typeof jql !== 'string') {
|
|
83
83
|
return '';
|
|
84
84
|
}
|