jira-pilot 2.0.0 ā 2.0.2
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/LICENSE +5 -5
- package/README.md +465 -404
- package/bin/jira.js +62 -55
- package/package.json +90 -84
- package/src/commands/ai-actions/plan.js +119 -0
- package/src/commands/ai-actions/review.js +109 -0
- package/src/commands/ai-actions/standup.js +42 -0
- package/src/commands/ai.js +232 -204
- package/src/commands/board.js +75 -66
- package/src/commands/bulk.js +108 -0
- package/src/commands/config.js +224 -154
- package/src/commands/dashboard.js +89 -0
- package/src/commands/git.js +63 -60
- package/src/commands/issue.js +985 -698
- package/src/commands/mcp.js +20 -20
- package/src/commands/project.js +59 -50
- package/src/commands/sprint.js +153 -78
- package/src/server/mcp-server.js +332 -332
- package/src/services/ai-service.js +165 -107
- package/src/services/api-service.js +115 -115
- package/src/utils/adf-parser.js +49 -49
- package/src/utils/config.js +97 -60
- package/src/utils/error-handler.js +41 -41
- package/src/utils/text-to-adf.js +34 -34
- package/src/utils/validators.js +88 -0
package/bin/jira.js
CHANGED
|
@@ -1,56 +1,63 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
5
|
-
import { join, dirname } from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
|
|
8
|
-
// Load package.json for version
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
11
|
-
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.name('jira')
|
|
16
|
-
.description('AI-powered Jira CLI for humans and agents')
|
|
17
|
-
.version(pkg.version)
|
|
18
|
-
.addHelpText('after', `
|
|
19
|
-
Examples:
|
|
20
|
-
$ jira
|
|
21
|
-
$ jira issue
|
|
22
|
-
$ jira issue
|
|
23
|
-
$ jira
|
|
24
|
-
$ jira
|
|
25
|
-
$ jira
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
program
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
// Load package.json for version
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('jira')
|
|
16
|
+
.description('AI-powered Jira CLI for humans and agents')
|
|
17
|
+
.version(pkg.version)
|
|
18
|
+
.addHelpText('after', `
|
|
19
|
+
Examples:
|
|
20
|
+
$ jira dashboard
|
|
21
|
+
$ jira issue list
|
|
22
|
+
$ jira issue view PROJ-123
|
|
23
|
+
$ jira issue create
|
|
24
|
+
$ jira issue search "login bug"
|
|
25
|
+
$ jira board list
|
|
26
|
+
$ jira sprint list --board 123
|
|
27
|
+
$ jira bulk transition -j "project = PROJ" -s Done
|
|
28
|
+
$ jira ai summarize PROJ-123
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
import { registerConfigCommand } from '../src/commands/config.js';
|
|
32
|
+
import { registerIssueCommand } from '../src/commands/issue.js';
|
|
33
|
+
import { registerProjectCommand } from '../src/commands/project.js';
|
|
34
|
+
import { registerSprintCommand } from '../src/commands/sprint.js';
|
|
35
|
+
import { registerBoardCommand } from '../src/commands/board.js';
|
|
36
|
+
import { registerGitCommand } from '../src/commands/git.js';
|
|
37
|
+
import { registerAiCommand } from '../src/commands/ai.js';
|
|
38
|
+
import { registerMcpCommand } from '../src/commands/mcp.js';
|
|
39
|
+
import { registerBulkCommand } from '../src/commands/bulk.js';
|
|
40
|
+
import { registerDashboardCommand } from '../src/commands/dashboard.js';
|
|
41
|
+
|
|
42
|
+
// Register Commands
|
|
43
|
+
registerConfigCommand(program);
|
|
44
|
+
registerIssueCommand(program);
|
|
45
|
+
registerProjectCommand(program);
|
|
46
|
+
registerSprintCommand(program);
|
|
47
|
+
registerBoardCommand(program);
|
|
48
|
+
registerGitCommand(program);
|
|
49
|
+
registerAiCommand(program);
|
|
50
|
+
registerMcpCommand(program);
|
|
51
|
+
registerBulkCommand(program);
|
|
52
|
+
registerDashboardCommand(program);
|
|
53
|
+
|
|
54
|
+
program.on('command:*', () => {
|
|
55
|
+
console.error(chalk.red('Invalid command: %s\nSee --help for a list of available commands.'), program.args.join(' '));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!process.argv.slice(2).length) {
|
|
60
|
+
program.outputHelp();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,85 +1,91 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "jira-pilot",
|
|
3
|
-
"version": "2.0.
|
|
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
|
-
"main": "src/index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"jira": "./bin/jira.js",
|
|
9
|
-
"jira-pilot": "./bin/jira.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"start": "node bin/jira.js",
|
|
13
|
-
"mcp": "node bin/jira.js mcp",
|
|
14
|
-
"test": "
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"jira
|
|
30
|
-
"
|
|
31
|
-
"jira-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "jira-pilot",
|
|
3
|
+
"version": "2.0.2",
|
|
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
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jira": "./bin/jira.js",
|
|
9
|
+
"jira-pilot": "./bin/jira.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
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",
|
|
17
|
+
"link": "npm link"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"src/",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"jira",
|
|
30
|
+
"cli",
|
|
31
|
+
"jira-cli",
|
|
32
|
+
"jira-client",
|
|
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",
|
|
47
|
+
"issue-tracker",
|
|
48
|
+
"project-management",
|
|
49
|
+
"task-management",
|
|
50
|
+
"sprint",
|
|
51
|
+
"kanban",
|
|
52
|
+
"scrum",
|
|
53
|
+
"agile",
|
|
54
|
+
"command-line",
|
|
55
|
+
"interactive",
|
|
56
|
+
"terminal",
|
|
57
|
+
"developer-tools",
|
|
58
|
+
"devtools",
|
|
59
|
+
"productivity",
|
|
60
|
+
"workflow",
|
|
61
|
+
"git-integration"
|
|
62
|
+
],
|
|
63
|
+
"author": {
|
|
64
|
+
"name": "Arul",
|
|
65
|
+
"url": "https://github.com/Aarul5"
|
|
66
|
+
},
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/Aarul5/jira-pilot.git"
|
|
70
|
+
},
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/Aarul5/jira-pilot/issues"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://github.com/Aarul5/jira-pilot#readme",
|
|
75
|
+
"license": "ISC",
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
78
|
+
"axios": "^1.6.0",
|
|
79
|
+
"chalk": "^5.3.0",
|
|
80
|
+
"commander": "^11.1.0",
|
|
81
|
+
"conf": "^12.0.0",
|
|
82
|
+
"enquirer": "^2.4.1",
|
|
83
|
+
"open": "^10.0.0",
|
|
84
|
+
"ora": "^8.0.0",
|
|
85
|
+
"table": "^6.8.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
89
|
+
"vitest": "^4.0.18"
|
|
90
|
+
}
|
|
85
91
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import enquirer from 'enquirer';
|
|
4
|
+
import { api } from '../../services/api-service.js';
|
|
5
|
+
import { aiService } from '../../services/ai-service.js';
|
|
6
|
+
import { validateIssueKey } from '../../utils/validators.js';
|
|
7
|
+
import { parseADF } from '../../utils/adf-parser.js';
|
|
8
|
+
|
|
9
|
+
export async function planAction(epicKey, options) {
|
|
10
|
+
const check = validateIssueKey(epicKey);
|
|
11
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
12
|
+
|
|
13
|
+
const spinner = ora(`Fetching Epic ${epicKey}...`).start();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const issue = await api.get(`/issue/${epicKey}`);
|
|
17
|
+
const summary = issue.fields.summary;
|
|
18
|
+
const description = issue.fields.description ? parseADF(issue.fields.description) : 'No description';
|
|
19
|
+
const projectKey = issue.fields.project.key;
|
|
20
|
+
|
|
21
|
+
spinner.text = 'AI is breaking down the Epic...';
|
|
22
|
+
|
|
23
|
+
const plan = await aiService.breakdownEpic(summary, description);
|
|
24
|
+
|
|
25
|
+
spinner.stop();
|
|
26
|
+
|
|
27
|
+
if (!plan || plan.length === 0) {
|
|
28
|
+
console.log(chalk.yellow('AI could not generate a plan.'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.cyan(`\nProposed Breakdown for ${epicKey} (${summary}):\n`));
|
|
33
|
+
|
|
34
|
+
// Let user select items to create
|
|
35
|
+
const choices = plan.map((item, index) => ({
|
|
36
|
+
name: `${item.type}: ${item.summary}`,
|
|
37
|
+
value: index, // store index to retrieve item
|
|
38
|
+
checked: true
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { selectedIndices } = await enquirer.prompt({
|
|
42
|
+
type: 'multiselect',
|
|
43
|
+
name: 'selectedIndices',
|
|
44
|
+
message: 'Select issues to create:',
|
|
45
|
+
choices: choices.map((c, i) => ({ ...c, value: i })), // ensure value is index
|
|
46
|
+
result(names) {
|
|
47
|
+
// map names back to indices
|
|
48
|
+
return names.map(name => this.map(name)); // 'this.map' returns value (index)
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Loop through selected and create
|
|
53
|
+
// Convert map result (object/array) to array of indices
|
|
54
|
+
const indicesToCreate = Object.values(selectedIndices);
|
|
55
|
+
|
|
56
|
+
if (indicesToCreate.length === 0) {
|
|
57
|
+
console.log(chalk.yellow('No items selected.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.dim('\nCreating issues...'));
|
|
62
|
+
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const idx of indicesToCreate) {
|
|
65
|
+
const item = plan[idx];
|
|
66
|
+
const itemSpinner = ora(`Creating ${item.type}: ${item.summary}`).start();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const payload = {
|
|
70
|
+
fields: {
|
|
71
|
+
project: { key: projectKey },
|
|
72
|
+
summary: item.summary,
|
|
73
|
+
description: item.description, // Simple string, Jira converts to ADF or accepts text depending on config.
|
|
74
|
+
// Note: v3 API often needs ADF. If description is simple text, it might fail.
|
|
75
|
+
// We should construct a basic paragraph ADF document.
|
|
76
|
+
description: {
|
|
77
|
+
version: 1,
|
|
78
|
+
type: 'doc',
|
|
79
|
+
content: [{
|
|
80
|
+
type: 'paragraph',
|
|
81
|
+
content: [{ type: 'text', text: item.description || '' }]
|
|
82
|
+
}]
|
|
83
|
+
},
|
|
84
|
+
issuetype: { name: item.type },
|
|
85
|
+
parent: { key: epicKey } // Try to link to Epic
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const res = await api.post('/issue', payload);
|
|
90
|
+
itemSpinner.succeed(`${res.key} created.`);
|
|
91
|
+
results.push(res.key);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
itemSpinner.fail(`Failed to create ${item.summary}.`);
|
|
94
|
+
// Fallback: try without description if ADF error
|
|
95
|
+
try {
|
|
96
|
+
const payloadNoDesc = {
|
|
97
|
+
fields: {
|
|
98
|
+
project: { key: projectKey },
|
|
99
|
+
summary: item.summary,
|
|
100
|
+
issuetype: { name: item.type },
|
|
101
|
+
parent: { key: epicKey }
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const res = await api.post('/issue', payloadNoDesc);
|
|
105
|
+
itemSpinner.succeed(`${res.key} created (without description).`);
|
|
106
|
+
results.push(res.key);
|
|
107
|
+
} catch (e2) {
|
|
108
|
+
// ignore
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.green(`\nDone! Created ${results.length} issues linked to ${epicKey}.`));
|
|
114
|
+
|
|
115
|
+
} catch (e) {
|
|
116
|
+
spinner.stop();
|
|
117
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { api } from '../../services/api-service.js';
|
|
6
|
+
import { aiService } from '../../services/ai-service.js';
|
|
7
|
+
import { validateIssueKey } from '../../utils/validators.js';
|
|
8
|
+
import { getCredentials } from '../../utils/config.js';
|
|
9
|
+
import { parseADF } from '../../utils/adf-parser.js';
|
|
10
|
+
|
|
11
|
+
export async function reviewAction(issueKey, options) {
|
|
12
|
+
const check = validateIssueKey(issueKey);
|
|
13
|
+
if (!check.valid) { console.error(chalk.red(check.message)); return; }
|
|
14
|
+
|
|
15
|
+
const { githubToken } = getCredentials();
|
|
16
|
+
if (!githubToken) {
|
|
17
|
+
console.error(chalk.red('GitHub Token not found. Run "jira config setup" or manually add githubToken to config.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spinner = ora(`Fetching issue ${issueKey} and searching for PRs...`).start();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// 1. Fetch Issue Context
|
|
25
|
+
const issue = await api.get(`/issue/${issueKey}?fields=summary,description,acceptanceCriteria`); // basic fields
|
|
26
|
+
const summary = issue.fields.summary;
|
|
27
|
+
const description = issue.fields.description ? parseADF(issue.fields.description) : 'No description';
|
|
28
|
+
const context = `Title: ${summary}\nDescription: ${description}`;
|
|
29
|
+
|
|
30
|
+
// 2. Determine GitHub Repo from local git
|
|
31
|
+
let repoOwner, repoName;
|
|
32
|
+
try {
|
|
33
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
|
|
34
|
+
// Parse: https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
35
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)/);
|
|
36
|
+
if (match) {
|
|
37
|
+
repoOwner = match[1];
|
|
38
|
+
repoName = match[2];
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Ignore, maybe not in a repo
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!repoOwner || !repoName) {
|
|
45
|
+
spinner.fail('Could not detect GitHub repository from "git remote get-url origin".');
|
|
46
|
+
console.log(chalk.yellow('Ensure you are in the git repository folder.'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
spinner.text = `Searching PRs in ${repoOwner}/${repoName} for ${issueKey}...`;
|
|
51
|
+
|
|
52
|
+
// 3. Search GitHub PRs
|
|
53
|
+
// We search for PRs that mention the issue key in title or branch name
|
|
54
|
+
const searchRes = await axios.get(`https://api.github.com/search/issues`, {
|
|
55
|
+
params: {
|
|
56
|
+
q: `repo:${repoOwner}/${repoName} is:pr ${issueKey}`
|
|
57
|
+
},
|
|
58
|
+
headers: {
|
|
59
|
+
'Authorization': `token ${githubToken}`,
|
|
60
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const prs = searchRes.data.items;
|
|
65
|
+
if (prs.length === 0) {
|
|
66
|
+
spinner.fail(`No open PRs found for ${issueKey} in ${repoOwner}/${repoName}.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Use the most recent PR
|
|
71
|
+
const pr = prs[0];
|
|
72
|
+
spinner.text = `Analyzing PR #${pr.number}: ${pr.title}...`;
|
|
73
|
+
|
|
74
|
+
// 4. Fetch Diff
|
|
75
|
+
const diffRes = await axios.get(pr.pull_request.url, {
|
|
76
|
+
headers: {
|
|
77
|
+
'Authorization': `token ${githubToken}`,
|
|
78
|
+
'Accept': 'application/vnd.github.v3.diff'
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const diff = diffRes.data;
|
|
82
|
+
|
|
83
|
+
if (!diff) {
|
|
84
|
+
spinner.fail('Empty diff or failed to fetch diff.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5. AI Review
|
|
89
|
+
spinner.text = 'AI is reviewing the code changes...';
|
|
90
|
+
const review = await aiService.reviewCode(diff, context);
|
|
91
|
+
|
|
92
|
+
spinner.stop();
|
|
93
|
+
|
|
94
|
+
console.log(chalk.green(`\nš¤ AI Code Review for PR #${pr.number} (${issueKey}):\n`));
|
|
95
|
+
console.log(review);
|
|
96
|
+
console.log(chalk.dim(`\nPR Link: ${pr.html_url}`));
|
|
97
|
+
|
|
98
|
+
} catch (e) {
|
|
99
|
+
spinner.stop();
|
|
100
|
+
if (e.response?.status === 404) {
|
|
101
|
+
console.error(chalk.red(`Resource not found (Issue or Repo). Check permissions.`));
|
|
102
|
+
} else if (e.response?.status === 401) {
|
|
103
|
+
console.error(chalk.red(`GitHub/Jira Authentication failed. Check your tokens.`));
|
|
104
|
+
} else {
|
|
105
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
106
|
+
if (e.response?.data) console.error(chalk.dim(JSON.stringify(e.response.data)));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { api } from '../../services/api-service.js';
|
|
4
|
+
import { aiService } from '../../services/ai-service.js';
|
|
5
|
+
import { parseADF } from '../../utils/adf-parser.js';
|
|
6
|
+
|
|
7
|
+
export async function standupAction(options) {
|
|
8
|
+
const spinner = ora('Analyzing your recent activity...').start();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// 1. Fetch User Info
|
|
12
|
+
const myself = await api.get('/myself');
|
|
13
|
+
const accountId = myself.accountId;
|
|
14
|
+
const displayName = myself.displayName;
|
|
15
|
+
|
|
16
|
+
// 2. Fetch Issues Updated/Commented by User in last 24h
|
|
17
|
+
// JQL: updated >= -24h AND (assignee = currentUser() OR watcher = currentUser() OR comment ~ currentUser())
|
|
18
|
+
// Simplification: JQL: assignee = currentUser() AND updated >= -1d
|
|
19
|
+
const yesterdayJql = `assignee = currentUser() AND updated >= -1d ORDER BY updated DESC`;
|
|
20
|
+
|
|
21
|
+
const yesterdayIssuesRes = await api.get(`/search?jql=${encodeURIComponent(yesterdayJql)}&fields=summary,status,comment,worklog`);
|
|
22
|
+
const yesterdayIssues = yesterdayIssuesRes.issues.map(i => `- ${i.key} ${i.fields.summary} (${i.fields.status.name})`).join('\n');
|
|
23
|
+
|
|
24
|
+
// 3. Fetch Issues Assigned for Today (In Progress or To Do)
|
|
25
|
+
const todayJql = `assignee = currentUser() AND statusCategory IN ("To Do", "In Progress") ORDER BY priority DESC`;
|
|
26
|
+
const todayIssuesRes = await api.get(`/search?jql=${encodeURIComponent(todayJql)}&fields=summary,status,priority`);
|
|
27
|
+
const todayIssues = todayIssuesRes.issues.map(i => `- ${i.key} ${i.fields.summary} [${i.fields.priority.name}]`).join('\n');
|
|
28
|
+
|
|
29
|
+
spinner.text = 'Generating standup report...';
|
|
30
|
+
|
|
31
|
+
const report = await aiService.generateStandup(yesterdayIssues, todayIssues);
|
|
32
|
+
|
|
33
|
+
spinner.stop();
|
|
34
|
+
|
|
35
|
+
console.log(chalk.green(`\nš¢ Standup Report for ${displayName}:\n`));
|
|
36
|
+
console.log(report);
|
|
37
|
+
|
|
38
|
+
} catch (e) {
|
|
39
|
+
spinner.stop();
|
|
40
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
41
|
+
}
|
|
42
|
+
}
|