jira-pilot 2.1.2 → 2.1.3
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 +23 -0
- package/bin/jira.ts +2 -0
- package/dist/bin/jira.js +2 -0
- package/dist/bin/jira.js.map +1 -1
- package/dist/src/commands/ai-actions/plan.js +1 -1
- package/dist/src/commands/ai-actions/plan.js.map +1 -1
- package/dist/src/commands/ai-actions/review.js +5 -4
- package/dist/src/commands/ai-actions/review.js.map +1 -1
- package/dist/src/commands/ai-actions/standup.js +1 -1
- package/dist/src/commands/ai-actions/standup.js.map +1 -1
- package/dist/src/commands/ai.js +1 -1
- package/dist/src/commands/ai.js.map +1 -1
- package/dist/src/commands/board.js +10 -5
- package/dist/src/commands/board.js.map +1 -1
- package/dist/src/commands/bulk.js +1 -1
- package/dist/src/commands/bulk.js.map +1 -1
- package/dist/src/commands/config.js +1 -1
- package/dist/src/commands/config.js.map +1 -1
- package/dist/src/commands/dashboard.js +11 -5
- package/dist/src/commands/dashboard.js.map +1 -1
- package/dist/src/commands/filter.js +7 -4
- package/dist/src/commands/filter.js.map +1 -1
- package/dist/src/commands/git.js +1 -1
- package/dist/src/commands/git.js.map +1 -1
- package/dist/src/commands/issue-attach.js +1 -1
- package/dist/src/commands/issue-attach.js.map +1 -1
- package/dist/src/commands/issue-pr.js +1 -1
- package/dist/src/commands/issue-pr.js.map +1 -1
- package/dist/src/commands/issue-worklog.js +10 -5
- package/dist/src/commands/issue-worklog.js.map +1 -1
- package/dist/src/commands/issue.js +142 -87
- package/dist/src/commands/issue.js.map +1 -1
- package/dist/src/commands/project.js +10 -5
- package/dist/src/commands/project.js.map +1 -1
- package/dist/src/commands/sprint.js +19 -8
- package/dist/src/commands/sprint.js.map +1 -1
- package/dist/src/commands/tui.d.ts +2 -0
- package/dist/src/commands/tui.js +10 -0
- package/dist/src/commands/tui.js.map +1 -0
- package/dist/src/services/ai-service.js +7 -4
- package/dist/src/services/ai-service.js.map +1 -1
- package/dist/src/services/api-service.d.ts +2 -0
- package/dist/src/services/api-service.js +29 -20
- package/dist/src/services/api-service.js.map +1 -1
- package/dist/src/tui/App.d.ts +1 -0
- package/dist/src/tui/App.js +26 -0
- package/dist/src/tui/App.js.map +1 -0
- package/dist/src/tui/index.d.ts +1 -0
- package/dist/src/tui/index.js +8 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/screens/BoardList.d.ts +1 -0
- package/dist/src/tui/screens/BoardList.js +71 -0
- package/dist/src/tui/screens/BoardList.js.map +1 -0
- package/dist/src/tui/screens/Dashboard.d.ts +1 -0
- package/dist/src/tui/screens/Dashboard.js +41 -0
- package/dist/src/tui/screens/Dashboard.js.map +1 -0
- package/dist/src/tui/screens/IssueDetail.d.ts +6 -0
- package/dist/src/tui/screens/IssueDetail.js +40 -0
- package/dist/src/tui/screens/IssueDetail.js.map +1 -0
- package/dist/src/tui/screens/IssueList.d.ts +1 -0
- package/dist/src/tui/screens/IssueList.js +72 -0
- package/dist/src/tui/screens/IssueList.js.map +1 -0
- package/dist/src/tui/screens/KanbanBoard.d.ts +6 -0
- package/dist/src/tui/screens/KanbanBoard.js +86 -0
- package/dist/src/tui/screens/KanbanBoard.js.map +1 -0
- package/dist/src/tui/utils/adf-render.d.ts +1 -0
- package/dist/src/tui/utils/adf-render.js +29 -0
- package/dist/src/tui/utils/adf-render.js.map +1 -0
- package/dist/src/utils/error-handler.d.ts +2 -2
- package/dist/src/utils/error-handler.js.map +1 -1
- package/dist/src/utils/http.d.ts +27 -0
- package/dist/src/utils/http.js +95 -0
- package/dist/src/utils/http.js.map +1 -0
- package/dist/src/utils/spinner.d.ts +21 -0
- package/dist/src/utils/spinner.js +79 -0
- package/dist/src/utils/spinner.js.map +1 -0
- package/package.json +10 -5
- package/src/commands/ai-actions/plan.ts +1 -1
- package/src/commands/ai-actions/review.ts +5 -4
- package/src/commands/ai-actions/standup.ts +1 -1
- package/src/commands/ai.ts +1 -1
- package/src/commands/board.ts +10 -5
- package/src/commands/bulk.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/dashboard.ts +11 -5
- package/src/commands/filter.ts +8 -5
- package/src/commands/git.ts +1 -1
- package/src/commands/issue-attach.ts +1 -1
- package/src/commands/issue-pr.ts +1 -1
- package/src/commands/issue-worklog.ts +10 -5
- package/src/commands/issue.ts +149 -89
- package/src/commands/project.ts +10 -5
- package/src/commands/sprint.ts +19 -8
- package/src/commands/tui.ts +11 -0
- package/src/services/ai-service.ts +7 -4
- package/src/services/api-service.ts +29 -21
- package/src/tui/App.tsx +61 -0
- package/src/tui/index.tsx +8 -0
- package/src/tui/screens/BoardList.tsx +102 -0
- package/src/tui/screens/Dashboard.tsx +75 -0
- package/src/tui/screens/IssueDetail.tsx +93 -0
- package/src/tui/screens/IssueList.tsx +116 -0
- package/src/tui/screens/KanbanBoard.tsx +133 -0
- package/src/tui/utils/adf-render.ts +30 -0
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/http.ts +128 -0
- package/src/utils/spinner.ts +87 -0
package/src/commands/sprint.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import Table from '
|
|
3
|
+
import { Table } from 'cmd-table';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
|
-
import ora from '
|
|
5
|
+
import ora from '../utils/spinner.js';
|
|
6
6
|
import enquirer from 'enquirer';
|
|
7
7
|
import { handleCommandError } from '../utils/error-handler.js';
|
|
8
8
|
|
|
@@ -56,11 +56,16 @@ Common Actions:
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const table = new Table({
|
|
59
|
-
|
|
59
|
+
columns: [
|
|
60
|
+
{ name: chalk.bold('ID') },
|
|
61
|
+
{ name: chalk.bold('Name') },
|
|
62
|
+
{ name: chalk.bold('State') },
|
|
63
|
+
{ name: chalk.bold('Dates') }
|
|
64
|
+
]
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
data.values.forEach((s: any) => {
|
|
63
|
-
table.
|
|
68
|
+
table.addRow([
|
|
64
69
|
s.id,
|
|
65
70
|
s.name,
|
|
66
71
|
s.state === 'active' ? chalk.green(s.state) : s.state,
|
|
@@ -68,7 +73,7 @@ Common Actions:
|
|
|
68
73
|
]);
|
|
69
74
|
});
|
|
70
75
|
|
|
71
|
-
console.log(table.
|
|
76
|
+
console.log(table.render());
|
|
72
77
|
|
|
73
78
|
} catch (e: any) {
|
|
74
79
|
handleCommandError(spinner, e, 'Failed to list sprints');
|
|
@@ -131,10 +136,16 @@ Examples:
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
const table = new Table({
|
|
134
|
-
|
|
139
|
+
columns: [
|
|
140
|
+
{ name: chalk.bold('Key') },
|
|
141
|
+
{ name: chalk.bold('Summary') },
|
|
142
|
+
{ name: chalk.bold('Status') },
|
|
143
|
+
{ name: chalk.bold('Assignee') },
|
|
144
|
+
{ name: chalk.bold('Priority') }
|
|
145
|
+
]
|
|
135
146
|
});
|
|
136
147
|
issues.issues.forEach((i: any) => {
|
|
137
|
-
table.
|
|
148
|
+
table.addRow([
|
|
138
149
|
chalk.cyan(i.key),
|
|
139
150
|
i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
|
|
140
151
|
i.fields.status?.name || '',
|
|
@@ -142,7 +153,7 @@ Examples:
|
|
|
142
153
|
i.fields.priority?.name || ''
|
|
143
154
|
]);
|
|
144
155
|
});
|
|
145
|
-
console.log(table.
|
|
156
|
+
console.log(table.render());
|
|
146
157
|
console.log(chalk.grey(`${issues.issues.length} issue(s) in sprint`));
|
|
147
158
|
|
|
148
159
|
} catch (e: any) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { startTui } from '../tui/index.js';
|
|
3
|
+
|
|
4
|
+
export function registerTuiCommand(program: Command) {
|
|
5
|
+
program
|
|
6
|
+
.command('tui')
|
|
7
|
+
.description('Start the interactive TUI mode')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
startTui();
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HttpClient } from '../utils/http.js';
|
|
2
2
|
import { getCredentials } from '../utils/config.js';
|
|
3
3
|
|
|
4
4
|
export class AiService {
|
|
@@ -89,7 +89,8 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
89
89
|
|
|
90
90
|
async callOpenAI(key: string, prompt: string): Promise<string> {
|
|
91
91
|
try {
|
|
92
|
-
const
|
|
92
|
+
const client = new HttpClient();
|
|
93
|
+
const response = await client.post('https://api.openai.com/v1/chat/completions', {
|
|
93
94
|
model: 'gpt-4o',
|
|
94
95
|
messages: [{ role: 'user', content: prompt }],
|
|
95
96
|
temperature: 0.7
|
|
@@ -107,8 +108,9 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
107
108
|
// Gemini REST API — uses generativelanguage.googleapis.com
|
|
108
109
|
const model = 'gemini-2.0-flash';
|
|
109
110
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
|
|
111
|
+
const client = new HttpClient();
|
|
110
112
|
|
|
111
|
-
const response = await
|
|
113
|
+
const response = await client.post(url, {
|
|
112
114
|
contents: [{
|
|
113
115
|
parts: [{ text: prompt }]
|
|
114
116
|
}],
|
|
@@ -137,7 +139,8 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
|
|
|
137
139
|
async callAnthropic(key: string, prompt: string): Promise<string> {
|
|
138
140
|
try {
|
|
139
141
|
// Anthropic Messages API
|
|
140
|
-
const
|
|
142
|
+
const client = new HttpClient();
|
|
143
|
+
const response = await client.post('https://api.anthropic.com/v1/messages', {
|
|
141
144
|
model: 'claude-sonnet-4-20250514',
|
|
142
145
|
max_tokens: 2048,
|
|
143
146
|
messages: [{ role: 'user', content: prompt }]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HttpClient } from '../utils/http.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { getCredentials } from '../utils/config.js';
|
|
4
4
|
|
|
@@ -28,27 +28,28 @@ export class ApiService {
|
|
|
28
28
|
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`;
|
|
29
29
|
|
|
30
30
|
// Standard REST API v3 client
|
|
31
|
-
this.client =
|
|
31
|
+
this.client = new HttpClient({
|
|
32
32
|
baseURL: `${this._domain}/rest/api/3`,
|
|
33
33
|
headers: {
|
|
34
34
|
'Authorization': authHeader,
|
|
35
|
-
'Accept': 'application/json'
|
|
36
|
-
'Content-Type': 'application/json'
|
|
35
|
+
'Accept': 'application/json'
|
|
37
36
|
}
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
// Agile REST API v1 client (for boards, sprints, etc.)
|
|
41
|
-
this.agileClient =
|
|
40
|
+
this.agileClient = new HttpClient({
|
|
42
41
|
baseURL: `${this._domain}/rest/agile/1.0`,
|
|
43
42
|
headers: {
|
|
44
43
|
'Authorization': authHeader,
|
|
45
|
-
'Accept': 'application/json'
|
|
46
|
-
'Content-Type': 'application/json'
|
|
44
|
+
'Accept': 'application/json'
|
|
47
45
|
}
|
|
48
46
|
});
|
|
47
|
+
}
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
private async handleRequest(request: Promise<any>) {
|
|
50
|
+
try {
|
|
51
|
+
return await request;
|
|
52
|
+
} catch (error: any) {
|
|
52
53
|
if (error.response) {
|
|
53
54
|
if (error.response.status === 401) {
|
|
54
55
|
console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
|
|
@@ -56,11 +57,8 @@ export class ApiService {
|
|
|
56
57
|
console.error(chalk.red('Access denied. You may not have permission for this resource.'));
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
this.client.interceptors.response.use((r: any) => r, errorInterceptor);
|
|
63
|
-
this.agileClient.interceptors.response.use((r: any) => r, errorInterceptor);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/** @returns {string} The Jira domain URL */
|
|
@@ -81,28 +79,38 @@ export class ApiService {
|
|
|
81
79
|
|
|
82
80
|
async get(url: string, config: any = {}) {
|
|
83
81
|
this.ensureClient();
|
|
84
|
-
const response = await this.client.get(url, config);
|
|
82
|
+
const response = await this.handleRequest(this.client.get(url, config));
|
|
85
83
|
return response.data;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
async post(url: string, data: any, config: any = {}) {
|
|
89
87
|
this.ensureClient();
|
|
90
|
-
const response = await this.client.post(url, data, config);
|
|
88
|
+
const response = await this.handleRequest(this.client.post(url, data, config));
|
|
91
89
|
return response.data;
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
async put(url: string, data: any, config: any = {}) {
|
|
95
93
|
this.ensureClient();
|
|
96
|
-
const response = await this.client.put(url, data, config);
|
|
94
|
+
const response = await this.handleRequest(this.client.put(url, data, config));
|
|
97
95
|
return response.data;
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
async delete(url: string, config: any = {}) {
|
|
101
99
|
this.ensureClient();
|
|
102
|
-
const response = await this.client.delete(url, config);
|
|
100
|
+
const response = await this.handleRequest(this.client.delete(url, config));
|
|
103
101
|
return response.data;
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
async search(jql: string, startAt: number = 0, maxResults: number = 50) {
|
|
105
|
+
return this.post('/search/jql', {
|
|
106
|
+
jql,
|
|
107
|
+
startAt,
|
|
108
|
+
maxResults,
|
|
109
|
+
fields: ['summary', 'status', 'assignee', 'priority', 'issuetype', 'created', 'updated', 'project'],
|
|
110
|
+
validation: 'warn'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
106
114
|
async upload(url: string, formData: any) {
|
|
107
115
|
this.ensureClient();
|
|
108
116
|
// Jira requires this header for attachments
|
|
@@ -117,7 +125,7 @@ export class ApiService {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
const config = { headers };
|
|
120
|
-
const response = await this.client.post(url, formData, config);
|
|
128
|
+
const response = await this.handleRequest(this.client.post(url, formData, config));
|
|
121
129
|
return response.data;
|
|
122
130
|
}
|
|
123
131
|
|
|
@@ -125,13 +133,13 @@ export class ApiService {
|
|
|
125
133
|
|
|
126
134
|
async agileGet(url: string, config: any = {}) {
|
|
127
135
|
this.ensureClient();
|
|
128
|
-
const response = await this.agileClient.get(url, config);
|
|
136
|
+
const response = await this.handleRequest(this.agileClient.get(url, config));
|
|
129
137
|
return response.data;
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
async agilePost(url: string, data: any, config: any = {}) {
|
|
133
141
|
this.ensureClient();
|
|
134
|
-
const response = await this.agileClient.post(url, data, config);
|
|
142
|
+
const response = await this.handleRequest(this.agileClient.post(url, data, config));
|
|
135
143
|
return response.data;
|
|
136
144
|
}
|
|
137
145
|
}
|
package/src/tui/App.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import Dashboard from './screens/Dashboard.js';
|
|
4
|
+
import IssueList from './screens/IssueList.js';
|
|
5
|
+
import BoardList from './screens/BoardList.js';
|
|
6
|
+
|
|
7
|
+
type View = 'dashboard' | 'issues' | 'boards';
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
const [view, setView] = useState<View>('dashboard');
|
|
11
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
12
|
+
const tabs: View[] = ['dashboard', 'issues', 'boards'];
|
|
13
|
+
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.leftArrow) {
|
|
16
|
+
setActiveTab(prev => Math.max(0, prev - 1));
|
|
17
|
+
setView(tabs[Math.max(0, activeTab - 1)]);
|
|
18
|
+
}
|
|
19
|
+
if (key.rightArrow) {
|
|
20
|
+
setActiveTab(prev => Math.min(tabs.length - 1, prev + 1));
|
|
21
|
+
setView(tabs[Math.min(tabs.length - 1, activeTab + 1)]);
|
|
22
|
+
}
|
|
23
|
+
if (input === 'q') {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Box flexDirection="column" height="100%">
|
|
30
|
+
{/* Header */}
|
|
31
|
+
<Box borderStyle="classic" borderColor="blue" paddingX={1}>
|
|
32
|
+
<Text bold color="blue">Jira Pilot</Text>
|
|
33
|
+
<Box marginLeft={2} flexDirection="column" justifyContent="center">
|
|
34
|
+
<Text>Use <Text color="green">←/→</Text> to navigate tabs. Press <Text color="red">q</Text> to quit.</Text>
|
|
35
|
+
</Box>
|
|
36
|
+
</Box>
|
|
37
|
+
|
|
38
|
+
{/* Tabs */}
|
|
39
|
+
<Box paddingX={1} marginBottom={1}>
|
|
40
|
+
{tabs.map((tab, index) => (
|
|
41
|
+
<Box key={tab} marginRight={2}>
|
|
42
|
+
<Text
|
|
43
|
+
color={activeTab === index ? 'green' : 'white'}
|
|
44
|
+
bold={activeTab === index}
|
|
45
|
+
underline={activeTab === index}
|
|
46
|
+
>
|
|
47
|
+
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
48
|
+
</Text>
|
|
49
|
+
</Box>
|
|
50
|
+
))}
|
|
51
|
+
</Box>
|
|
52
|
+
|
|
53
|
+
{/* Content Area */}
|
|
54
|
+
<Box flexGrow={1} borderStyle="round" padding={1} borderColor="gray">
|
|
55
|
+
{view === 'dashboard' && <Dashboard />}
|
|
56
|
+
{view === 'issues' && <IssueList />}
|
|
57
|
+
{view === 'boards' && <BoardList />}
|
|
58
|
+
</Box>
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SpinnerOriginal from 'ink-spinner';
|
|
4
|
+
const Spinner = SpinnerOriginal as any;
|
|
5
|
+
import { api } from '../../services/api-service.js';
|
|
6
|
+
import KanbanBoard from './KanbanBoard.js';
|
|
7
|
+
|
|
8
|
+
export default function BoardList() {
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [boards, setBoards] = useState<any[]>([]);
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
+
const [selectedBoardId, setSelectedBoardId] = useState<string | null>(null);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const fetchBoards = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const results = await api.agileGet('/board');
|
|
19
|
+
setBoards(results.values);
|
|
20
|
+
setLoading(false);
|
|
21
|
+
} catch (err: any) {
|
|
22
|
+
setError(err.message);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
fetchBoards();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
useInput((input, key) => {
|
|
30
|
+
if (selectedBoardId) return;
|
|
31
|
+
|
|
32
|
+
if (key.upArrow) {
|
|
33
|
+
setSelectedIndex((prev: number) => Math.max(0, prev - 1));
|
|
34
|
+
}
|
|
35
|
+
if (key.downArrow) {
|
|
36
|
+
setSelectedIndex((prev: number) => Math.min(boards.length - 1, prev + 1));
|
|
37
|
+
}
|
|
38
|
+
if (key.return) {
|
|
39
|
+
if (boards[selectedIndex]) {
|
|
40
|
+
setSelectedBoardId(boards[selectedIndex].id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (selectedBoardId) {
|
|
46
|
+
return <KanbanBoard boardId={selectedBoardId} onBack={() => setSelectedBoardId(null)} />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (loading) {
|
|
50
|
+
return (
|
|
51
|
+
<Box>
|
|
52
|
+
<Text color="green"><Spinner type="dots" /></Text>
|
|
53
|
+
<Text> Loading Boards...</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (error) {
|
|
59
|
+
return <Text color="red">Error: {error}</Text>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (boards.length === 0) {
|
|
63
|
+
return <Text>No boards found.</Text>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const windowSize = 15;
|
|
67
|
+
const halfWindow = Math.floor(windowSize / 2);
|
|
68
|
+
|
|
69
|
+
// Ensure the selected item is always visible
|
|
70
|
+
let start = 0;
|
|
71
|
+
if (selectedIndex > halfWindow) {
|
|
72
|
+
start = Math.min(selectedIndex - halfWindow, boards.length - windowSize);
|
|
73
|
+
}
|
|
74
|
+
start = Math.max(0, start);
|
|
75
|
+
const end = Math.min(boards.length, start + windowSize);
|
|
76
|
+
|
|
77
|
+
const visibleBoards = boards.slice(start, end);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Box flexDirection="column">
|
|
81
|
+
<Box marginBottom={1}>
|
|
82
|
+
<Text bold underline>Select a Board ({selectedIndex + 1}/{boards.length})</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
|
|
85
|
+
{visibleBoards.map((board: any, index: number) => {
|
|
86
|
+
const globalIndex = start + index;
|
|
87
|
+
const isSelected = globalIndex === selectedIndex;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Box key={board.id}>
|
|
91
|
+
<Text color={isSelected ? 'green' : 'white'} bold={isSelected}>
|
|
92
|
+
{isSelected ? '> ' : ' '}
|
|
93
|
+
</Text>
|
|
94
|
+
<Text color={isSelected ? 'green' : 'white'}>
|
|
95
|
+
{board.name} ({board.type})
|
|
96
|
+
</Text>
|
|
97
|
+
</Box>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</Box>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import SpinnerOriginal from 'ink-spinner';
|
|
4
|
+
const Spinner = SpinnerOriginal as any;
|
|
5
|
+
import { api } from '../../services/api-service.js';
|
|
6
|
+
|
|
7
|
+
export default function Dashboard() {
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
const [user, setUser] = useState<any>(null);
|
|
10
|
+
const [issueCount, setIssueCount] = useState<number>(0);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const fetchData = async () => {
|
|
15
|
+
try {
|
|
16
|
+
// Fetch issue count (maxResults must be >= 1)
|
|
17
|
+
const searchResults = await api.search(
|
|
18
|
+
`assignee = currentUser() AND resolution = Unresolved`,
|
|
19
|
+
0,
|
|
20
|
+
1
|
|
21
|
+
);
|
|
22
|
+
const myself = await api.get('/myself');
|
|
23
|
+
|
|
24
|
+
setUser(myself);
|
|
25
|
+
setIssueCount(searchResults.total);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
// detailed error inspection
|
|
29
|
+
const msg = err.response
|
|
30
|
+
? `API Error ${err.response.status}: ${JSON.stringify(err.response.data)}`
|
|
31
|
+
: err.message || JSON.stringify(err);
|
|
32
|
+
setError(msg);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
fetchData();
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
if (loading) {
|
|
41
|
+
return (
|
|
42
|
+
<Box>
|
|
43
|
+
<Text color="green"><Spinner type="dots" /></Text>
|
|
44
|
+
<Text> Loading Dashboard...</Text>
|
|
45
|
+
</Box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (error) {
|
|
50
|
+
return (
|
|
51
|
+
<Box flexDirection="column" borderColor="red" borderStyle="round" padding={1}>
|
|
52
|
+
<Text color="red" bold>Error Loading Dashboard:</Text>
|
|
53
|
+
<Text color="red">{error}</Text>
|
|
54
|
+
<Box marginTop={1}>
|
|
55
|
+
<Text color="gray">
|
|
56
|
+
Tip: Check your internet connection and run `jira config` to verify credentials.
|
|
57
|
+
</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Box flexDirection="column">
|
|
65
|
+
<Text bold>Welcome back, {user?.displayName}!</Text>
|
|
66
|
+
<Box marginTop={1} borderStyle="single" padding={1} borderColor="yellow">
|
|
67
|
+
<Text>You have <Text bold color="red">{issueCount}</Text> unresolved issues assigned to you.</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
|
|
70
|
+
<Box marginTop={1}>
|
|
71
|
+
<Text color="gray">Press ←/→ to navigate. Try the 'Issues' tab to see details.</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
</Box>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SpinnerOriginal from 'ink-spinner';
|
|
4
|
+
const Spinner = SpinnerOriginal as any;
|
|
5
|
+
import { api } from '../../services/api-service.js';
|
|
6
|
+
import { renderADF } from '../utils/adf-render.js';
|
|
7
|
+
|
|
8
|
+
interface IssueDetailProps {
|
|
9
|
+
issueKey: string;
|
|
10
|
+
onBack: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function IssueDetail({ issueKey, onBack }: IssueDetailProps) {
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [issue, setIssue] = useState<any>(null);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const fetchIssue = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const results = await api.get(`/issue/${issueKey}`);
|
|
22
|
+
setIssue(results);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
} catch (err: any) {
|
|
25
|
+
setError(err.message);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
fetchIssue();
|
|
30
|
+
}, [issueKey]);
|
|
31
|
+
|
|
32
|
+
useInput((input, key) => {
|
|
33
|
+
if (key.escape || input === 'b') {
|
|
34
|
+
onBack();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (loading) {
|
|
39
|
+
return (
|
|
40
|
+
<Box>
|
|
41
|
+
<Text color="green"><Spinner type="dots" /></Text>
|
|
42
|
+
<Text> Loading Issue {issueKey}...</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (error) {
|
|
48
|
+
return <Text color="red">Error: {error}</Text>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { summary, description, status, priority, assignee, reporter, comment } = issue.fields;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Box flexDirection="column" padding={1} borderStyle="single">
|
|
55
|
+
<Box marginBottom={1}>
|
|
56
|
+
<Text bold color="cyan">{issue.key}: {summary}</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
|
|
59
|
+
<Box flexDirection="row" marginBottom={1}>
|
|
60
|
+
<Box width="50%">
|
|
61
|
+
<Text><Text bold>Status:</Text> {status.name}</Text>
|
|
62
|
+
<Text><Text bold>Priority:</Text> {priority.name}</Text>
|
|
63
|
+
</Box>
|
|
64
|
+
<Box width="50%">
|
|
65
|
+
<Text><Text bold>Assignee:</Text> {assignee ? assignee.displayName : 'Unassigned'}</Text>
|
|
66
|
+
<Text><Text bold>Reporter:</Text> {reporter ? reporter.displayName : 'Unknown'}</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
</Box>
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
<Box marginBottom={1}>
|
|
72
|
+
<Text bold underline>Description:</Text>
|
|
73
|
+
<Text>{renderADF(description) || 'No description provided.'}</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
|
|
76
|
+
{comment && comment.comments && comment.comments.length > 0 && (
|
|
77
|
+
<Box flexDirection="column" marginTop={1}>
|
|
78
|
+
<Text bold underline>Comments ({comment.total}):</Text>
|
|
79
|
+
{comment.comments.slice(-3).map((c: any) => (
|
|
80
|
+
<Box key={c.id} borderStyle="round" borderColor="gray" padding={1} marginTop={1}>
|
|
81
|
+
<Text bold color="blue">{c.author.displayName}:</Text>
|
|
82
|
+
<Text>{renderADF(c.body)}</Text>
|
|
83
|
+
</Box>
|
|
84
|
+
))}
|
|
85
|
+
</Box>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<Box marginTop={1}>
|
|
89
|
+
<Text color="gray">Press <Text bold>Esc</Text> or <Text bold>b</Text> to go back.</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
</Box>
|
|
92
|
+
);
|
|
93
|
+
}
|