jira-pilot 1.1.0 → 2.0.1
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 +262 -53
- package/bin/jira.js +5 -1
- package/package.json +52 -11
- package/src/commands/ai.js +5 -0
- package/src/commands/board.js +66 -0
- package/src/commands/git.js +3 -0
- package/src/commands/issue.js +9 -0
- package/src/commands/sprint.js +6 -47
- package/src/server/mcp-server.js +1 -8
- package/src/services/api-service.js +54 -33
- package/src/utils/error-handler.js +41 -0
- package/src/utils/validators.js +88 -0
- package/.github/workflows/publish.yml +0 -22
package/README.md
CHANGED
|
@@ -4,24 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
`jira-pilot` is a next-generation Command Line Interface for Jira. It bridges the gap between traditional developer tools and modern AI Agents.
|
|
6
6
|
|
|
7
|
-
- **For Humans:** A beautiful, interactive CLI to manage issues, sprints, and code without leaving your terminal.
|
|
8
|
-
- **For Agents:** A fully compliant **Model Context Protocol (MCP)** server that lets AI assistants (like Claude Desktop
|
|
7
|
+
- **For Humans:** A beautiful, interactive CLI to manage issues, sprints, boards, and code without leaving your terminal.
|
|
8
|
+
- **For Agents:** A fully compliant **Model Context Protocol (MCP)** server with 8 tools that lets AI assistants (like Claude Desktop or Gemini) interact with your Jira instance safely.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Features at a Glance
|
|
11
13
|
|
|
12
14
|
### 👤 Human-Centric Features
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
| Feature | Description |
|
|
16
|
+
|---------|-------------|
|
|
17
|
+
| **Issue Management** | Create, view, list, transition, assign, and comment on issues |
|
|
18
|
+
| **Interactive Wizards** | Step-by-step prompts with `enquirer` — no flags required |
|
|
19
|
+
| **Board & Sprint Management** | List boards, view sprints by state |
|
|
20
|
+
| **Git Integration** | Create feature branches from issues with smart naming |
|
|
21
|
+
| **AI Copilot** | Summarize issues, draft descriptions, get next-action suggestions |
|
|
22
|
+
| **Rich Visualization** | Beautiful tables, spinners, and formatted output |
|
|
23
|
+
| **Export** | Export issues list to JSON or Markdown files |
|
|
20
24
|
|
|
21
25
|
### 🤖 Agentic Features (MCP)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
| Feature | Description |
|
|
27
|
+
|---------|-------------|
|
|
28
|
+
| **8 MCP Tools** | list_issues, get_issue, create_issue, transition_issue, assign_issue, add_comment, list_projects, list_sprints |
|
|
29
|
+
| **LLM-Optimized** | Clean, structured JSON responses for efficient token usage |
|
|
30
|
+
| **Stdio Transport** | Standard MCP stdio server — works with any MCP client |
|
|
31
|
+
|
|
32
|
+
### 🧠 Multi-Provider AI
|
|
33
|
+
| Provider | Model |
|
|
34
|
+
|----------|-------|
|
|
35
|
+
| **OpenAI** | GPT-4o |
|
|
36
|
+
| **Google Gemini** | gemini-2.0-flash |
|
|
37
|
+
| **Anthropic** | claude-sonnet (claude-sonnet-4-20250514) |
|
|
25
38
|
|
|
26
39
|
---
|
|
27
40
|
|
|
@@ -32,12 +45,14 @@
|
|
|
32
45
|
npm install -g jira-pilot
|
|
33
46
|
```
|
|
34
47
|
|
|
48
|
+
After installing, the `jira` command is available globally.
|
|
49
|
+
|
|
35
50
|
### Local Development
|
|
36
51
|
```bash
|
|
37
|
-
git clone https://github.com/
|
|
52
|
+
git clone https://github.com/Aarul5/jira-pilot.git
|
|
38
53
|
cd jira-pilot
|
|
39
54
|
npm install
|
|
40
|
-
npm link
|
|
55
|
+
npm link # Makes 'jira' command available globally
|
|
41
56
|
```
|
|
42
57
|
|
|
43
58
|
---
|
|
@@ -46,80 +61,211 @@ npm link
|
|
|
46
61
|
|
|
47
62
|
Before using the tool, set up your credentials. You can get an API Token from [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens).
|
|
48
63
|
|
|
64
|
+
### Initial Setup
|
|
49
65
|
```bash
|
|
50
66
|
jira config setup
|
|
51
67
|
```
|
|
52
68
|
|
|
53
69
|
You will be prompted for:
|
|
54
|
-
1.
|
|
55
|
-
2.
|
|
56
|
-
3.
|
|
57
|
-
4.
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
1. **Jira Site URL** — e.g., `https://your-company.atlassian.net`
|
|
71
|
+
2. **Email** — Your Atlassian account email
|
|
72
|
+
3. **API Token** — The token you generated from Atlassian
|
|
73
|
+
4. **Enable AI** — Toggle AI features on/off
|
|
74
|
+
5. **AI Provider** — Choose between `openai`, `gemini`, or `anthropic`
|
|
75
|
+
6. **AI API Key** — Your API key for the selected provider
|
|
76
|
+
|
|
77
|
+
### View / Clear Configuration
|
|
60
78
|
```bash
|
|
61
|
-
jira config view
|
|
79
|
+
jira config view # Show current configuration (keys are masked)
|
|
80
|
+
jira config clear # Remove all stored credentials
|
|
62
81
|
```
|
|
63
82
|
|
|
83
|
+
> **Note:** Credentials are stored securely using the `conf` library in your system's config directory.
|
|
84
|
+
|
|
64
85
|
---
|
|
65
86
|
|
|
66
87
|
## 📖 Usage
|
|
67
88
|
|
|
68
|
-
###
|
|
89
|
+
### 📋 Issue Management
|
|
90
|
+
|
|
91
|
+
#### List Issues
|
|
69
92
|
```bash
|
|
70
|
-
# List issues
|
|
93
|
+
# List issues assigned to you in active sprints
|
|
71
94
|
jira issue list
|
|
72
95
|
|
|
73
96
|
# List with custom JQL
|
|
74
97
|
jira issue list --jql "project = PROJ AND priority = High"
|
|
75
98
|
|
|
76
|
-
#
|
|
77
|
-
jira issue
|
|
99
|
+
# Filter by project, assignee, or status
|
|
100
|
+
jira issue list --project PROJ --assignee "john.doe" --status "In Progress"
|
|
101
|
+
|
|
102
|
+
# Limit results
|
|
103
|
+
jira issue list --limit 20
|
|
78
104
|
|
|
79
|
-
#
|
|
105
|
+
# Export results to file
|
|
106
|
+
jira issue list --export json # Creates issues-TIMESTAMP.json
|
|
107
|
+
jira issue list --export md # Creates issues-TIMESTAMP.md
|
|
108
|
+
|
|
109
|
+
# Combine filters and export
|
|
110
|
+
jira issue list --project PROJ --status Done --export json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### View Issue Details
|
|
114
|
+
```bash
|
|
80
115
|
jira issue view PROJ-123
|
|
116
|
+
```
|
|
117
|
+
Displays: summary, status, priority, assignee, description, and recent comments.
|
|
118
|
+
|
|
119
|
+
#### Create Issue
|
|
120
|
+
```bash
|
|
121
|
+
# Interactive wizard (recommended)
|
|
122
|
+
jira issue create
|
|
123
|
+
|
|
124
|
+
# Non-interactive with flags
|
|
125
|
+
jira issue create -p PROJ -s "Fix login bug"
|
|
126
|
+
jira issue create -p PROJ -t Bug -s "Crash on save" --priority High
|
|
127
|
+
jira issue create -p PROJ -t Story -s "Add dark mode" -d "Users want a dark theme" -a me
|
|
128
|
+
```
|
|
81
129
|
|
|
82
|
-
|
|
130
|
+
**Interactive Wizard Steps:**
|
|
131
|
+
1. **Select Project** — Choose from your accessible projects
|
|
132
|
+
2. **Select Issue Type** — Bug, Story, Task, Epic, etc.
|
|
133
|
+
3. **Enter Summary** — Required issue title
|
|
134
|
+
4. **Enter Description** — Optional, converted to Jira ADF format
|
|
135
|
+
5. **Select Priority** — High, Medium, Low, etc.
|
|
136
|
+
6. **Assign** — Myself, Unassigned, or search by name/email
|
|
137
|
+
|
|
138
|
+
#### Transition Issue Status
|
|
139
|
+
```bash
|
|
140
|
+
# Interactive — shows available transitions
|
|
83
141
|
jira issue transition PROJ-123
|
|
84
142
|
|
|
85
|
-
#
|
|
86
|
-
jira issue
|
|
87
|
-
jira issue
|
|
143
|
+
# Direct — specify target status
|
|
144
|
+
jira issue transition PROJ-123 --status "In Progress"
|
|
145
|
+
jira issue transition PROJ-123 -s Done
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Assign / Reassign Issue
|
|
149
|
+
```bash
|
|
150
|
+
# Interactive — choose Myself, Unassign, or Search
|
|
151
|
+
jira issue assign PROJ-123
|
|
88
152
|
|
|
89
|
-
#
|
|
90
|
-
jira issue
|
|
153
|
+
# Quick assign
|
|
154
|
+
jira issue assign PROJ-123 -a me # Assign to yourself
|
|
155
|
+
jira issue assign PROJ-123 -a none # Unassign
|
|
91
156
|
```
|
|
92
157
|
|
|
93
|
-
|
|
158
|
+
#### Add Comment
|
|
159
|
+
```bash
|
|
160
|
+
# Interactive — prompts for comment text
|
|
161
|
+
jira issue comment PROJ-123
|
|
162
|
+
|
|
163
|
+
# Inline comment
|
|
164
|
+
jira issue comment PROJ-123 -m "Fixed in latest build"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### 📂 Projects & Boards
|
|
170
|
+
|
|
171
|
+
#### List Projects
|
|
94
172
|
```bash
|
|
95
|
-
# List projects
|
|
96
173
|
jira project list
|
|
174
|
+
```
|
|
175
|
+
Displays: project key, name, lead, and style in a formatted table.
|
|
97
176
|
|
|
98
|
-
|
|
177
|
+
#### List Boards
|
|
178
|
+
```bash
|
|
179
|
+
# List all boards
|
|
180
|
+
jira board list
|
|
181
|
+
|
|
182
|
+
# Filter by project
|
|
183
|
+
jira board list -p PROJ
|
|
184
|
+
|
|
185
|
+
# Filter by type
|
|
186
|
+
jira board list -t scrum
|
|
187
|
+
jira board list -t kanban
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### List Sprints
|
|
191
|
+
```bash
|
|
192
|
+
# List active and future sprints
|
|
99
193
|
jira sprint list --board 5
|
|
194
|
+
|
|
195
|
+
# List by board name
|
|
196
|
+
jira sprint list --board "My Team Board"
|
|
197
|
+
|
|
198
|
+
# Filter by state
|
|
199
|
+
jira sprint list --board 5 --state active
|
|
200
|
+
jira sprint list --board 5 --state closed
|
|
100
201
|
```
|
|
101
202
|
|
|
102
|
-
|
|
103
|
-
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### 🌿 Git Integration
|
|
206
|
+
|
|
207
|
+
Create feature branches automatically named from the issue summary:
|
|
104
208
|
```bash
|
|
105
209
|
jira git branch PROJ-123
|
|
106
210
|
# Output: Switched to a new branch 'feature/PROJ-123-fix-login-modal'
|
|
107
211
|
```
|
|
108
212
|
|
|
109
|
-
|
|
110
|
-
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### 🤖 AI Features
|
|
216
|
+
|
|
217
|
+
> **Requires:** AI features must be enabled via `jira config setup` with a valid API key for your chosen provider (OpenAI, Gemini, or Anthropic).
|
|
218
|
+
|
|
219
|
+
#### Summarize an Issue
|
|
220
|
+
Get an AI-generated TL;DR of long issue threads with comments:
|
|
111
221
|
```bash
|
|
112
222
|
jira ai summarize PROJ-123
|
|
113
223
|
```
|
|
114
|
-
|
|
224
|
+
|
|
225
|
+
#### Draft an Issue Description
|
|
226
|
+
Generate a structured issue description from rough notes or bullet points:
|
|
227
|
+
```bash
|
|
228
|
+
# Interactive — prompts for your notes
|
|
229
|
+
jira ai draft
|
|
230
|
+
|
|
231
|
+
# Inline with issue type context
|
|
232
|
+
jira ai draft -i "login fails, returns 500, only on mobile" -t bug
|
|
233
|
+
jira ai draft -i "add dark mode toggle to settings" -t story
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Suggest Next Actions
|
|
237
|
+
Analyze an issue and get AI-powered suggestions for what to do next:
|
|
238
|
+
```bash
|
|
239
|
+
jira ai suggest PROJ-123
|
|
240
|
+
```
|
|
241
|
+
Returns: **Immediate Next Action**, **Potential Blockers**, **Suggested Status Transition**, and **Recommendations**.
|
|
115
242
|
|
|
116
243
|
---
|
|
117
244
|
|
|
118
|
-
## 🧠 Using with AI Agents (
|
|
245
|
+
## 🧠 Using with AI Agents (MCP)
|
|
119
246
|
|
|
120
247
|
`jira-pilot` implements the **Model Context Protocol (MCP)**, making it plug-and-play for AI assistants.
|
|
121
248
|
|
|
249
|
+
### Starting the MCP Server
|
|
250
|
+
```bash
|
|
251
|
+
jira mcp
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Available MCP Tools
|
|
255
|
+
|
|
256
|
+
| Tool | Description | Required Args |
|
|
257
|
+
|------|-------------|---------------|
|
|
258
|
+
| `jira_list_issues` | Search issues via JQL | `jql` |
|
|
259
|
+
| `jira_get_issue` | Get full issue details | `issueKey` |
|
|
260
|
+
| `jira_create_issue` | Create a new issue (ADF) | `projectKey`, `summary` |
|
|
261
|
+
| `jira_transition_issue` | List or execute transitions | `issueKey` |
|
|
262
|
+
| `jira_assign_issue` | Assign/unassign an issue | `issueKey` |
|
|
263
|
+
| `jira_add_comment` | Add a comment (ADF) | `issueKey`, `body` |
|
|
264
|
+
| `jira_list_projects` | List accessible projects | — |
|
|
265
|
+
| `jira_list_sprints` | List sprints for a board | `boardId` |
|
|
266
|
+
|
|
122
267
|
### Claude Desktop Configuration
|
|
268
|
+
|
|
123
269
|
Add the following to your `claude_desktop_config.json`:
|
|
124
270
|
|
|
125
271
|
```json
|
|
@@ -133,12 +279,32 @@ Add the following to your `claude_desktop_config.json`:
|
|
|
133
279
|
}
|
|
134
280
|
```
|
|
135
281
|
|
|
136
|
-
|
|
137
|
-
|
|
282
|
+
### VS Code / Cursor Configuration
|
|
283
|
+
|
|
284
|
+
Add to your `.vscode/mcp.json` or equivalent:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"servers": {
|
|
289
|
+
"jira-pilot": {
|
|
290
|
+
"command": "jira",
|
|
291
|
+
"args": ["mcp"]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Example Agent Prompts
|
|
298
|
+
Once connected, you can ask your AI assistant things like:
|
|
299
|
+
- *"Show me my high-priority Jira issues"*
|
|
300
|
+
- *"Create a bug for the login crash on mobile in project PROJ"*
|
|
301
|
+
- *"Transition PROJ-123 to In Progress and assign it to me"*
|
|
302
|
+
- *"Add a comment to PROJ-456 saying the fix is deployed"*
|
|
303
|
+
- *"What sprints are active on board 5?"*
|
|
138
304
|
|
|
139
305
|
---
|
|
140
306
|
|
|
141
|
-
##
|
|
307
|
+
## 🧪 Testing & Verification
|
|
142
308
|
|
|
143
309
|
### Testing the MCP Server
|
|
144
310
|
You can test the MCP server functionality using the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
|
|
@@ -148,17 +314,60 @@ npx @modelcontextprotocol/inspector node ./bin/jira.js mcp
|
|
|
148
314
|
```
|
|
149
315
|
|
|
150
316
|
This will launch a web interface where you can:
|
|
151
|
-
1.
|
|
152
|
-
2.
|
|
153
|
-
3.
|
|
317
|
+
1. View all 8 available tools and their schemas
|
|
318
|
+
2. Execute tools with custom arguments
|
|
319
|
+
3. Inspect request/response logs
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 📦 CLI Command Reference
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
jira [command]
|
|
327
|
+
|
|
328
|
+
Commands:
|
|
329
|
+
config Configure Jira credentials
|
|
330
|
+
issue Manage Jira issues
|
|
331
|
+
project Manage Jira projects
|
|
332
|
+
board Manage Jira boards
|
|
333
|
+
sprint Manage Sprints
|
|
334
|
+
git Git integration for Jira
|
|
335
|
+
ai AI Helper commands
|
|
336
|
+
mcp Start MCP Agent Server (Stdio)
|
|
337
|
+
|
|
338
|
+
Issue Subcommands:
|
|
339
|
+
issue list List issues (JQL, filters, export)
|
|
340
|
+
issue view View issue details
|
|
341
|
+
issue create Create a new issue (wizard or flags)
|
|
342
|
+
issue transition Transition issue status
|
|
343
|
+
issue assign Assign or reassign an issue
|
|
344
|
+
issue comment Add a comment to an issue
|
|
345
|
+
|
|
346
|
+
AI Subcommands:
|
|
347
|
+
ai summarize Summarize an issue using AI
|
|
348
|
+
ai draft Draft issue description from notes
|
|
349
|
+
ai suggest Suggest next actions for an issue
|
|
350
|
+
|
|
351
|
+
Board Subcommands:
|
|
352
|
+
board list List Jira boards
|
|
353
|
+
|
|
354
|
+
Sprint Subcommands:
|
|
355
|
+
sprint list List sprints for a board
|
|
356
|
+
```
|
|
154
357
|
|
|
155
358
|
---
|
|
156
359
|
|
|
157
|
-
##
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
360
|
+
## 🤝 Contributing
|
|
361
|
+
|
|
362
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
git clone https://github.com/Aarul5/jira-pilot.git
|
|
366
|
+
cd jira-pilot
|
|
367
|
+
npm install
|
|
368
|
+
npm link
|
|
369
|
+
```
|
|
162
370
|
|
|
163
371
|
## 📄 License
|
|
372
|
+
|
|
164
373
|
ISC
|
package/bin/jira.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
@@ -19,6 +19,8 @@ program
|
|
|
19
19
|
Examples:
|
|
20
20
|
$ jira issue list
|
|
21
21
|
$ jira issue view PROJ-123
|
|
22
|
+
$ jira issue create
|
|
23
|
+
$ jira board list
|
|
22
24
|
$ jira sprint list --board 123
|
|
23
25
|
$ jira ai summarize PROJ-123
|
|
24
26
|
`);
|
|
@@ -27,6 +29,7 @@ import { registerConfigCommand } from '../src/commands/config.js';
|
|
|
27
29
|
import { registerIssueCommand } from '../src/commands/issue.js';
|
|
28
30
|
import { registerProjectCommand } from '../src/commands/project.js';
|
|
29
31
|
import { registerSprintCommand } from '../src/commands/sprint.js';
|
|
32
|
+
import { registerBoardCommand } from '../src/commands/board.js';
|
|
30
33
|
import { registerGitCommand } from '../src/commands/git.js';
|
|
31
34
|
import { registerAiCommand } from '../src/commands/ai.js';
|
|
32
35
|
import { registerMcpCommand } from '../src/commands/mcp.js';
|
|
@@ -36,6 +39,7 @@ registerConfigCommand(program);
|
|
|
36
39
|
registerIssueCommand(program);
|
|
37
40
|
registerProjectCommand(program);
|
|
38
41
|
registerSprintCommand(program);
|
|
42
|
+
registerBoardCommand(program);
|
|
39
43
|
registerGitCommand(program);
|
|
40
44
|
registerAiCommand(program);
|
|
41
45
|
registerMcpCommand(program);
|
package/package.json
CHANGED
|
@@ -1,32 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jira-pilot",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "AI-powered Jira CLI for humans and agents",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "AI-powered Jira CLI for humans and agents — manage issues, sprints, boards with interactive wizards, multi-provider AI (OpenAI/Gemini/Anthropic), and an 8-tool MCP server for AI assistants",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"jira": "./bin/jira.js"
|
|
8
|
+
"jira": "./bin/jira.js",
|
|
9
|
+
"jira-pilot": "./bin/jira.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
+
"start": "node bin/jira.js",
|
|
13
|
+
"mcp": "node bin/jira.js mcp",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:coverage": "vitest run --coverage",
|
|
12
17
|
"link": "npm link"
|
|
13
18
|
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"src/",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
14
28
|
"keywords": [
|
|
15
29
|
"jira",
|
|
16
30
|
"cli",
|
|
17
|
-
"
|
|
18
|
-
"agent",
|
|
19
|
-
"mcp",
|
|
31
|
+
"jira-cli",
|
|
20
32
|
"jira-client",
|
|
21
|
-
"jiracli",
|
|
22
|
-
"atlassian",
|
|
23
33
|
"jira-api",
|
|
34
|
+
"atlassian",
|
|
35
|
+
"atlassian-jira",
|
|
36
|
+
"ai",
|
|
37
|
+
"ai-cli",
|
|
38
|
+
"openai",
|
|
39
|
+
"gemini",
|
|
40
|
+
"anthropic",
|
|
41
|
+
"claude",
|
|
42
|
+
"gpt",
|
|
43
|
+
"mcp",
|
|
44
|
+
"model-context-protocol",
|
|
45
|
+
"ai-agent",
|
|
46
|
+
"agent",
|
|
24
47
|
"issue-tracker",
|
|
48
|
+
"project-management",
|
|
25
49
|
"task-management",
|
|
50
|
+
"sprint",
|
|
51
|
+
"kanban",
|
|
52
|
+
"scrum",
|
|
53
|
+
"agile",
|
|
26
54
|
"command-line",
|
|
27
|
-
"
|
|
55
|
+
"interactive",
|
|
56
|
+
"terminal",
|
|
57
|
+
"developer-tools",
|
|
58
|
+
"devtools",
|
|
59
|
+
"productivity",
|
|
60
|
+
"workflow",
|
|
61
|
+
"git-integration"
|
|
28
62
|
],
|
|
29
|
-
"author":
|
|
63
|
+
"author": {
|
|
64
|
+
"name": "Arul",
|
|
65
|
+
"url": "https://github.com/Aarul5"
|
|
66
|
+
},
|
|
30
67
|
"repository": {
|
|
31
68
|
"type": "git",
|
|
32
69
|
"url": "git+https://github.com/Aarul5/jira-pilot.git"
|
|
@@ -46,5 +83,9 @@
|
|
|
46
83
|
"open": "^10.0.0",
|
|
47
84
|
"ora": "^8.0.0",
|
|
48
85
|
"table": "^6.8.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
89
|
+
"vitest": "^4.0.18"
|
|
49
90
|
}
|
|
50
91
|
}
|
package/src/commands/ai.js
CHANGED
|
@@ -5,6 +5,7 @@ import enquirer from 'enquirer';
|
|
|
5
5
|
import { api } from '../services/api-service.js';
|
|
6
6
|
import { aiService } from '../services/ai-service.js';
|
|
7
7
|
import { parseADF } from '../utils/adf-parser.js';
|
|
8
|
+
import { validateIssueKey } from '../utils/validators.js';
|
|
8
9
|
|
|
9
10
|
export function registerAiCommand(program) {
|
|
10
11
|
const aiCmd = new Command('ai')
|
|
@@ -22,6 +23,8 @@ Common Actions:
|
|
|
22
23
|
.description('Summarize an issue using AI')
|
|
23
24
|
.argument('<issueKey>', 'Jira Issue Key')
|
|
24
25
|
.action(async (issueKey) => {
|
|
26
|
+
const check = validateIssueKey(issueKey);
|
|
27
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
25
28
|
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
26
29
|
try {
|
|
27
30
|
const issue = await api.get(`/issue/${issueKey}?fields=summary,description,comment`);
|
|
@@ -141,6 +144,8 @@ Keep it professional and concise. Output in plain text (not markdown headers, us
|
|
|
141
144
|
.description('Suggest next actions for an issue based on its context')
|
|
142
145
|
.argument('<issueKey>', 'Jira Issue Key')
|
|
143
146
|
.action(async (issueKey) => {
|
|
147
|
+
const check = validateIssueKey(issueKey);
|
|
148
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
144
149
|
const spinner = ora(`Analyzing issue ${issueKey}...`).start();
|
|
145
150
|
try {
|
|
146
151
|
const issue = await api.get(`/issue/${issueKey}?fields=summary,description,status,assignee,priority,comment,issuetype`);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { table } from 'table';
|
|
4
|
+
import { api } from '../services/api-service.js';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
7
|
+
|
|
8
|
+
export function registerBoardCommand(program) {
|
|
9
|
+
const boardCmd = new Command('board')
|
|
10
|
+
.description('Manage Jira boards')
|
|
11
|
+
.addHelpText('after', `
|
|
12
|
+
Common Actions:
|
|
13
|
+
$ jira board list # List all boards
|
|
14
|
+
$ jira board list -p PROJ # List boards for a project
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
boardCmd
|
|
18
|
+
.command('list')
|
|
19
|
+
.description('List Jira boards')
|
|
20
|
+
.option('-p, --project <key>', 'Filter by project key')
|
|
21
|
+
.option('-t, --type <type>', 'Filter by board type (scrum, kanban, simple)')
|
|
22
|
+
.option('-l, --limit <n>', 'Max results', '50')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const spinner = ora('Fetching boards...').start();
|
|
25
|
+
try {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
params.set('maxResults', options.limit);
|
|
28
|
+
|
|
29
|
+
if (options.project) {
|
|
30
|
+
params.set('projectKeyOrId', options.project);
|
|
31
|
+
}
|
|
32
|
+
if (options.type) {
|
|
33
|
+
params.set('type', options.type);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const data = await api.agileGet(`/board?${params.toString()}`);
|
|
37
|
+
spinner.stop();
|
|
38
|
+
|
|
39
|
+
if (!data.values || data.values.length === 0) {
|
|
40
|
+
console.log(chalk.yellow('No boards found.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tableData = [
|
|
45
|
+
[chalk.bold('ID'), chalk.bold('Name'), chalk.bold('Type'), chalk.bold('Project')]
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
data.values.forEach(b => {
|
|
49
|
+
tableData.push([
|
|
50
|
+
b.id,
|
|
51
|
+
b.name,
|
|
52
|
+
b.type,
|
|
53
|
+
b.location?.projectKey || '-'
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(table(tableData));
|
|
58
|
+
console.log(chalk.grey(`Showing ${data.values.length} board(s)`));
|
|
59
|
+
|
|
60
|
+
} catch (e) {
|
|
61
|
+
handleCommandError(spinner, e, 'Failed to list boards');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program.addCommand(boardCmd);
|
|
66
|
+
}
|
package/src/commands/git.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
|
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import enquirer from 'enquirer';
|
|
7
|
+
import { validateIssueKey } from '../utils/validators.js';
|
|
7
8
|
|
|
8
9
|
export function registerGitCommand(program) {
|
|
9
10
|
const gitCmd = new Command('git')
|
|
@@ -15,6 +16,8 @@ export function registerGitCommand(program) {
|
|
|
15
16
|
.argument('<issueKey>', 'Jira Issue Key (e.g., PROJ-123)')
|
|
16
17
|
.option('-t, --type <type>', 'Branch type (feature, bugfix, hotfix)', 'feature')
|
|
17
18
|
.action(async (issueKey, options) => {
|
|
19
|
+
const check = validateIssueKey(issueKey);
|
|
20
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
18
21
|
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
19
22
|
try {
|
|
20
23
|
const issue = await api.get(`/issue/${issueKey}`);
|
package/src/commands/issue.js
CHANGED
|
@@ -6,6 +6,7 @@ import ora from 'ora';
|
|
|
6
6
|
import enquirer from 'enquirer';
|
|
7
7
|
import { parseADF } from '../utils/adf-parser.js';
|
|
8
8
|
import { textToADF } from '../utils/text-to-adf.js';
|
|
9
|
+
import { validateIssueKey } from '../utils/validators.js';
|
|
9
10
|
|
|
10
11
|
export function registerIssueCommand(program) {
|
|
11
12
|
const issueCmd = new Command('issue')
|
|
@@ -136,6 +137,8 @@ Examples:
|
|
|
136
137
|
$ jira issue view PROJ-123
|
|
137
138
|
`)
|
|
138
139
|
.action(async (issueKey) => {
|
|
140
|
+
const check = validateIssueKey(issueKey);
|
|
141
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
139
142
|
const spinner = ora(`Fetching issue ${issueKey}...`).start();
|
|
140
143
|
try {
|
|
141
144
|
const issue = await api.get(`/issue/${issueKey}`);
|
|
@@ -464,6 +467,8 @@ Examples:
|
|
|
464
467
|
$ jira issue transition PROJ-123 -s Done
|
|
465
468
|
`)
|
|
466
469
|
.action(async (issueKey, options) => {
|
|
470
|
+
const check = validateIssueKey(issueKey);
|
|
471
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
467
472
|
const spinner = ora(`Fetching transitions for ${issueKey}...`).start();
|
|
468
473
|
try {
|
|
469
474
|
// Fetch current issue to show context
|
|
@@ -554,6 +559,8 @@ Examples:
|
|
|
554
559
|
$ jira issue assign PROJ-123 -a none # Unassign
|
|
555
560
|
`)
|
|
556
561
|
.action(async (issueKey, options) => {
|
|
562
|
+
const check = validateIssueKey(issueKey);
|
|
563
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
557
564
|
try {
|
|
558
565
|
let assigneeId = options.assignee;
|
|
559
566
|
|
|
@@ -649,6 +656,8 @@ Examples:
|
|
|
649
656
|
$ jira issue comment PROJ-123 -m "Fixed in latest build"
|
|
650
657
|
`)
|
|
651
658
|
.action(async (issueKey, options) => {
|
|
659
|
+
const check = validateIssueKey(issueKey);
|
|
660
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
652
661
|
try {
|
|
653
662
|
let commentText = options.message;
|
|
654
663
|
|
package/src/commands/sprint.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { table } from 'table';
|
|
4
4
|
import { api } from '../services/api-service.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import { handleCommandError } from '../utils/error-handler.js';
|
|
6
7
|
|
|
7
8
|
export function registerSprintCommand(program) {
|
|
8
9
|
const sprintCmd = new Command('sprint')
|
|
@@ -15,57 +16,27 @@ Common Actions:
|
|
|
15
16
|
sprintCmd
|
|
16
17
|
.command('list')
|
|
17
18
|
.description('List sprints for a board')
|
|
18
|
-
.requiredOption('-b, --board <id>', 'Board ID')
|
|
19
|
+
.requiredOption('-b, --board <id>', 'Board ID or name')
|
|
19
20
|
.option('-s, --state <state>', 'State (active, future, closed)', 'active,future')
|
|
20
21
|
.action(async (options) => {
|
|
21
22
|
const spinner = ora(`Fetching sprints for board ${options.board}...`).start();
|
|
22
23
|
try {
|
|
23
|
-
// Agile API usually involves /rest/agile/1.0
|
|
24
|
-
// My default ApiService is /rest/api/3. I might need to override or allow full path?
|
|
25
|
-
// ApiService handles baseURL. I should make it flexible or add Agile support.
|
|
26
|
-
|
|
27
|
-
// HACK: ApiService constructor sets base to /rest/api/3.
|
|
28
|
-
// I need to use a different client or hack the URL.
|
|
29
|
-
// Axios allows absolute URLs to override baseURL.
|
|
30
|
-
// So if I pass full URL it works.
|
|
31
|
-
|
|
32
|
-
const { jiraUrl } = (await import('../utils/config.js')).getCredentials();
|
|
33
|
-
// Assuming api-service exposes client or get method.
|
|
34
|
-
// But get method prepend baseURL? No, axios usually supports absolute URL.
|
|
35
|
-
|
|
36
|
-
// Let's modify ApiService later to support 'type' or just use full path if needed.
|
|
37
|
-
// Or simpler: /rest/agile/1.0/board/${id}/sprint
|
|
38
|
-
// But api service baseURL is fixed.
|
|
39
|
-
|
|
40
|
-
// To fix this proper: I'll modify ApiService to allow changing API version/path or just use full path.
|
|
41
|
-
// Using full path:
|
|
42
|
-
const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
|
|
43
|
-
const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
|
|
44
|
-
|
|
45
24
|
let boardId = options.board;
|
|
46
25
|
|
|
47
|
-
// If board option is not a number,
|
|
26
|
+
// If board option is not a number, look it up by name
|
|
48
27
|
if (isNaN(boardId)) {
|
|
49
28
|
spinner.text = `Looking up board "${options.board}"...`;
|
|
50
|
-
const
|
|
51
|
-
const boardData = await api.get(boardSearchUrl);
|
|
29
|
+
const boardData = await api.agileGet(`/board?name=${encodeURIComponent(options.board)}`);
|
|
52
30
|
|
|
53
31
|
if (!boardData.values || boardData.values.length === 0) {
|
|
54
|
-
// Fallback: It might be a project key. Let's try searching for boards associated with this project.
|
|
55
|
-
// But the API doesn't support projectKey filter directly on /board easily without iterating.
|
|
56
|
-
// For now, fail if name match doesn't work.
|
|
57
32
|
throw new Error(`Board with name "${options.board}" not found. Please provide the numeric Board ID.`);
|
|
58
33
|
}
|
|
59
34
|
|
|
60
|
-
// Strict match or pick first? Let's pick the first one but warn if multiple
|
|
61
35
|
if (boardData.values.length > 1) {
|
|
62
|
-
// Try to find exact match
|
|
63
36
|
const exact = boardData.values.find(b => b.name.toLowerCase() === options.board.toLowerCase());
|
|
64
37
|
if (exact) {
|
|
65
38
|
boardId = exact.id;
|
|
66
39
|
} else {
|
|
67
|
-
// Just pick first? Or error?
|
|
68
|
-
// Let's pick first but log
|
|
69
40
|
console.log(chalk.yellow(`\nMultiple boards found for "${options.board}". Using "${boardData.values[0].name}" (ID: ${boardData.values[0].id}).`));
|
|
70
41
|
boardId = boardData.values[0].id;
|
|
71
42
|
}
|
|
@@ -75,9 +46,7 @@ Common Actions:
|
|
|
75
46
|
spinner.text = `Fetching sprints for board ${options.board} (ID: ${boardId})...`;
|
|
76
47
|
}
|
|
77
48
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const data = await api.get(fullUrl);
|
|
49
|
+
const data = await api.agileGet(`/board/${boardId}/sprint?state=${options.state}`);
|
|
81
50
|
spinner.stop();
|
|
82
51
|
|
|
83
52
|
if (!data.values || data.values.length === 0) {
|
|
@@ -101,17 +70,7 @@ Common Actions:
|
|
|
101
70
|
console.log(table(tableData));
|
|
102
71
|
|
|
103
72
|
} catch (e) {
|
|
104
|
-
spinner
|
|
105
|
-
if (e.response) {
|
|
106
|
-
if (e.response.status === 404) {
|
|
107
|
-
console.error(chalk.red(`\nError: Board with ID "${options.board}" not found or you do not have permission to view it.`));
|
|
108
|
-
console.error(chalk.grey('Tip: Verify the Board ID in your Jira URL: /jira/software/c/projects/KEY/boards/ID'));
|
|
109
|
-
} else {
|
|
110
|
-
console.error(chalk.red(`Error ${e.response.status}: `), e.response.data);
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
console.error(chalk.red(e.message));
|
|
114
|
-
}
|
|
73
|
+
handleCommandError(spinner, e, 'Failed to list sprints');
|
|
115
74
|
}
|
|
116
75
|
});
|
|
117
76
|
|
package/src/server/mcp-server.js
CHANGED
|
@@ -3,7 +3,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { api } from "../services/api-service.js";
|
|
5
5
|
import { textToADF } from "../utils/text-to-adf.js";
|
|
6
|
-
import { getCredentials } from "../utils/config.js";
|
|
7
6
|
|
|
8
7
|
// Initialize MCP Server
|
|
9
8
|
const server = new Server(
|
|
@@ -298,14 +297,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
298
297
|
|
|
299
298
|
// ── jira_list_sprints ───────────────────────────────
|
|
300
299
|
if (name === "jira_list_sprints") {
|
|
301
|
-
const { jiraUrl } = getCredentials();
|
|
302
|
-
const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
|
|
303
|
-
const domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
|
|
304
|
-
|
|
305
300
|
const state = args.state || 'active,future';
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
const data = await api.get(fullUrl);
|
|
301
|
+
const data = await api.agileGet(`/board/${args.boardId}/sprint?state=${state}`);
|
|
309
302
|
|
|
310
303
|
const sprints = (data.values || []).map(s => ({
|
|
311
304
|
id: s.id,
|
|
@@ -11,45 +11,59 @@ export class ApiService {
|
|
|
11
11
|
const { jiraUrl, email, apiToken } = getCredentials();
|
|
12
12
|
|
|
13
13
|
if (!jiraUrl || !email || !apiToken) {
|
|
14
|
-
// Don't throw here, allow initialization for 'config' command usage
|
|
15
14
|
this.client = null;
|
|
15
|
+
this._domain = null;
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
|
|
20
|
-
|
|
20
|
+
this._domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
|
|
21
21
|
|
|
22
|
+
const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`;
|
|
23
|
+
|
|
24
|
+
// Standard REST API v3 client
|
|
22
25
|
this.client = axios.create({
|
|
23
|
-
baseURL: `${
|
|
26
|
+
baseURL: `${this._domain}/rest/api/3`,
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization': authHeader,
|
|
29
|
+
'Accept': 'application/json',
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Agile REST API v1 client (for boards, sprints, etc.)
|
|
35
|
+
this.agileClient = axios.create({
|
|
36
|
+
baseURL: `${this._domain}/rest/agile/1.0`,
|
|
24
37
|
headers: {
|
|
25
|
-
'Authorization':
|
|
38
|
+
'Authorization': authHeader,
|
|
26
39
|
'Accept': 'application/json',
|
|
27
40
|
'Content-Type': 'application/json'
|
|
28
41
|
}
|
|
29
42
|
});
|
|
30
43
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
response
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} else if (error.response.status === 403) {
|
|
39
|
-
console.error(chalk.red('Access denied. You may not have permission for this resource.'));
|
|
40
|
-
} else if (error.response.status === 404) {
|
|
41
|
-
// Sometime 404 is valid (issues not found), let caller handle?
|
|
42
|
-
// Or log generic error? For now rethrow with clean message property if possible.
|
|
43
|
-
}
|
|
44
|
+
// Shared response interceptor
|
|
45
|
+
const errorInterceptor = (error) => {
|
|
46
|
+
if (error.response) {
|
|
47
|
+
if (error.response.status === 401) {
|
|
48
|
+
console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
|
|
49
|
+
} else if (error.response.status === 403) {
|
|
50
|
+
console.error(chalk.red('Access denied. You may not have permission for this resource.'));
|
|
44
51
|
}
|
|
45
|
-
return Promise.reject(error);
|
|
46
52
|
}
|
|
47
|
-
|
|
53
|
+
return Promise.reject(error);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.client.interceptors.response.use(r => r, errorInterceptor);
|
|
57
|
+
this.agileClient.interceptors.response.use(r => r, errorInterceptor);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @returns {string} The Jira domain URL */
|
|
61
|
+
get domain() {
|
|
62
|
+
return this._domain;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
ensureClient() {
|
|
51
66
|
if (!this.client) {
|
|
52
|
-
// Try to re-init in case config was just set
|
|
53
67
|
this.init();
|
|
54
68
|
if (!this.client) {
|
|
55
69
|
throw new Error('Jira credentials not configured. Run "jira config" first.');
|
|
@@ -57,25 +71,18 @@ export class ApiService {
|
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
|
|
74
|
+
// ── Standard REST API v3 Methods ────────────────────────────────
|
|
75
|
+
|
|
60
76
|
async get(url, config = {}) {
|
|
61
77
|
this.ensureClient();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return response.data;
|
|
65
|
-
} catch (e) {
|
|
66
|
-
// Optional: Wrap error
|
|
67
|
-
throw e;
|
|
68
|
-
}
|
|
78
|
+
const response = await this.client.get(url, config);
|
|
79
|
+
return response.data;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
async post(url, data, config = {}) {
|
|
72
83
|
this.ensureClient();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return response.data;
|
|
76
|
-
} catch (e) {
|
|
77
|
-
throw e;
|
|
78
|
-
}
|
|
84
|
+
const response = await this.client.post(url, data, config);
|
|
85
|
+
return response.data;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
async put(url, data, config = {}) {
|
|
@@ -89,6 +96,20 @@ export class ApiService {
|
|
|
89
96
|
const response = await this.client.delete(url, config);
|
|
90
97
|
return response.data;
|
|
91
98
|
}
|
|
99
|
+
|
|
100
|
+
// ── Agile REST API v1 Methods ───────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async agileGet(url, config = {}) {
|
|
103
|
+
this.ensureClient();
|
|
104
|
+
const response = await this.agileClient.get(url, config);
|
|
105
|
+
return response.data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async agilePost(url, data, config = {}) {
|
|
109
|
+
this.ensureClient();
|
|
110
|
+
const response = await this.agileClient.post(url, data, config);
|
|
111
|
+
return response.data;
|
|
112
|
+
}
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
export const api = new ApiService();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standardized error handler for CLI commands.
|
|
5
|
+
* Stops the spinner and prints a formatted error message.
|
|
6
|
+
*
|
|
7
|
+
* @param {object|null} spinner - Ora spinner instance (will be stopped/failed)
|
|
8
|
+
* @param {Error} error - The error object
|
|
9
|
+
* @param {string} [context] - Optional context (e.g., "Failed to list issues")
|
|
10
|
+
*/
|
|
11
|
+
export function handleCommandError(spinner, error, context = 'Operation failed') {
|
|
12
|
+
// Handle user cancellation (Ctrl+C in enquirer)
|
|
13
|
+
if (error === '' || (error && error.message === '')) {
|
|
14
|
+
if (spinner) spinner.stop();
|
|
15
|
+
console.log(chalk.yellow('\nCancelled.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (spinner) {
|
|
20
|
+
spinner.fail(context);
|
|
21
|
+
} else {
|
|
22
|
+
console.error(chalk.red(`\n${context}:`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (error.response) {
|
|
26
|
+
const status = error.response.status;
|
|
27
|
+
if (status === 404) {
|
|
28
|
+
console.error(chalk.red('Resource not found. Check the ID or key.'));
|
|
29
|
+
} else if (status === 400) {
|
|
30
|
+
const data = error.response.data;
|
|
31
|
+
const messages = data?.errorMessages?.join(', ') || (data?.errors
|
|
32
|
+
? Object.entries(data.errors).map(([k, v]) => `${k}: ${v}`).join(', ')
|
|
33
|
+
: JSON.stringify(data));
|
|
34
|
+
console.error(chalk.red(`Bad Request: ${messages}`));
|
|
35
|
+
} else {
|
|
36
|
+
console.error(chalk.red(`Error ${status}: `), error.response.data);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
console.error(chalk.red(error.message));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validators for CLI commands.
|
|
3
|
+
* Validates user input before API calls to catch errors early.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates a Jira issue key format (e.g., PROJ-123).
|
|
8
|
+
* @param {string} key - The issue key to validate.
|
|
9
|
+
* @returns {{ valid: boolean, message?: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function validateIssueKey(key) {
|
|
12
|
+
if (!key || typeof key !== 'string') {
|
|
13
|
+
return { valid: false, message: 'Issue key is required.' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const trimmed = key.trim().toUpperCase();
|
|
17
|
+
const pattern = /^[A-Z][A-Z0-9_]+-\d+$/;
|
|
18
|
+
|
|
19
|
+
if (!pattern.test(trimmed)) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
message: `Invalid issue key "${key}". Expected format: PROJ-123 (letters/numbers, dash, digits).`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { valid: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates a Jira project key (e.g., PROJ).
|
|
31
|
+
* @param {string} key - The project key to validate.
|
|
32
|
+
* @returns {{ valid: boolean, message?: string }}
|
|
33
|
+
*/
|
|
34
|
+
export function validateProjectKey(key) {
|
|
35
|
+
if (!key || typeof key !== 'string') {
|
|
36
|
+
return { valid: false, message: 'Project key is required.' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const trimmed = key.trim().toUpperCase();
|
|
40
|
+
const pattern = /^[A-Z][A-Z0-9_]+$/;
|
|
41
|
+
|
|
42
|
+
if (!pattern.test(trimmed)) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
message: `Invalid project key "${key}". Must start with a letter and contain only uppercase letters, digits, or underscores.`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { valid: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validates a Jira site URL.
|
|
54
|
+
* @param {string} url - The URL to validate.
|
|
55
|
+
* @returns {{ valid: boolean, message?: string }}
|
|
56
|
+
*/
|
|
57
|
+
export function validateUrl(url) {
|
|
58
|
+
if (!url || typeof url !== 'string') {
|
|
59
|
+
return { valid: false, message: 'URL is required.' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const trimmed = url.trim();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(trimmed);
|
|
66
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
67
|
+
return { valid: false, message: 'URL must use http or https protocol.' };
|
|
68
|
+
}
|
|
69
|
+
return { valid: true };
|
|
70
|
+
} catch {
|
|
71
|
+
return { valid: false, message: `Invalid URL: "${url}". Example: https://your-company.atlassian.net` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sanitizes a JQL string by escaping potentially dangerous characters.
|
|
77
|
+
* This is a basic sanitization — Jira's API does its own validation too.
|
|
78
|
+
* @param {string} jql - The JQL query string.
|
|
79
|
+
* @returns {string} The sanitized JQL string.
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizeJql(jql) {
|
|
82
|
+
if (!jql || typeof jql !== 'string') {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Remove null bytes and control characters
|
|
87
|
+
return jql.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim();
|
|
88
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
name: Publish Package
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
publish:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
permissions:
|
|
11
|
-
contents: read
|
|
12
|
-
id-token: write
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: '20.x'
|
|
18
|
-
registry-url: 'https://registry.npmjs.org'
|
|
19
|
-
- run: npm ci
|
|
20
|
-
- run: npm publish --provenance --access public
|
|
21
|
-
env:
|
|
22
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|