@plosson/agentio 0.1.7 ā 0.1.9
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/package.json +2 -2
- package/src/auth/jira-oauth.ts +249 -0
- package/src/commands/jira.ts +319 -0
- package/src/commands/slack.ts +265 -0
- package/src/config/credentials.ts +9 -0
- package/src/index.ts +4 -0
- package/src/services/jira/client.ts +355 -0
- package/src/services/slack/client.ts +76 -0
- package/src/types/config.ts +3 -1
- package/src/types/jira.ts +87 -0
- package/src/types/slack.ts +17 -0
- package/src/utils/output.ts +88 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plosson/agentio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "CLI for LLM agents to interact with communication and tracking services",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"scripts": {
|
|
35
35
|
"dev": "bun run src/index.ts",
|
|
36
36
|
"build": "bun build src/index.ts --outdir dist --target node",
|
|
37
|
-
"build:native": "bun build src/index.ts --compile --outfile dist/agentio",
|
|
37
|
+
"build:native": "bun build src/index.ts --compile --minify --sourcemap --bytecode --outfile dist/agentio",
|
|
38
38
|
"typecheck": "tsc --noEmit"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createServer, type Server } from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { JIRA_OAUTH_CONFIG } from '../config/credentials';
|
|
4
|
+
|
|
5
|
+
const ATLASSIAN_AUTH_URL = 'https://auth.atlassian.com/authorize';
|
|
6
|
+
const ATLASSIAN_TOKEN_URL = 'https://auth.atlassian.com/oauth/token';
|
|
7
|
+
const ATLASSIAN_RESOURCES_URL = 'https://api.atlassian.com/oauth/token/accessible-resources';
|
|
8
|
+
|
|
9
|
+
const JIRA_SCOPES = [
|
|
10
|
+
'read:jira-work', // Read projects, issues
|
|
11
|
+
'write:jira-work', // Add comments, change status
|
|
12
|
+
'offline_access', // Get refresh tokens
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const PORT_RANGE_START = 3000;
|
|
16
|
+
const PORT_RANGE_END = 3010;
|
|
17
|
+
|
|
18
|
+
export interface JiraOAuthResult {
|
|
19
|
+
accessToken: string;
|
|
20
|
+
refreshToken: string;
|
|
21
|
+
expiryDate: number;
|
|
22
|
+
cloudId: string;
|
|
23
|
+
siteUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AtlassianSite {
|
|
27
|
+
id: string;
|
|
28
|
+
url: string;
|
|
29
|
+
name: string;
|
|
30
|
+
scopes: string[];
|
|
31
|
+
avatarUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function findAvailablePort(): Promise<number> {
|
|
35
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
36
|
+
try {
|
|
37
|
+
await new Promise<void>((resolve, reject) => {
|
|
38
|
+
const server = createServer();
|
|
39
|
+
server.listen(port, () => {
|
|
40
|
+
server.close(() => resolve());
|
|
41
|
+
});
|
|
42
|
+
server.on('error', reject);
|
|
43
|
+
});
|
|
44
|
+
return port;
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`No available port found in range ${PORT_RANGE_START}-${PORT_RANGE_END}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function exchangeCodeForTokens(
|
|
53
|
+
code: string,
|
|
54
|
+
clientId: string,
|
|
55
|
+
clientSecret: string,
|
|
56
|
+
redirectUri: string
|
|
57
|
+
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
|
58
|
+
const response = await fetch(ATLASSIAN_TOKEN_URL, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
grant_type: 'authorization_code',
|
|
65
|
+
client_id: clientId,
|
|
66
|
+
client_secret: clientSecret,
|
|
67
|
+
code,
|
|
68
|
+
redirect_uri: redirectUri,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const error = await response.text();
|
|
74
|
+
throw new Error(`Failed to exchange code for tokens: ${error}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = await response.json();
|
|
78
|
+
return {
|
|
79
|
+
accessToken: data.access_token,
|
|
80
|
+
refreshToken: data.refresh_token,
|
|
81
|
+
expiresIn: data.expires_in,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function getAccessibleResources(accessToken: string): Promise<AtlassianSite[]> {
|
|
86
|
+
const response = await fetch(ATLASSIAN_RESOURCES_URL, {
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${accessToken}`,
|
|
89
|
+
Accept: 'application/json',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const error = await response.text();
|
|
95
|
+
throw new Error(`Failed to get accessible resources: ${error}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function refreshJiraToken(
|
|
102
|
+
refreshToken: string
|
|
103
|
+
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
|
104
|
+
const response = await fetch(ATLASSIAN_TOKEN_URL, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
grant_type: 'refresh_token',
|
|
111
|
+
client_id: JIRA_OAUTH_CONFIG.clientId,
|
|
112
|
+
client_secret: JIRA_OAUTH_CONFIG.clientSecret,
|
|
113
|
+
refresh_token: refreshToken,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const error = await response.text();
|
|
119
|
+
throw new Error(`Failed to refresh token: ${error}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
return {
|
|
124
|
+
accessToken: data.access_token,
|
|
125
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
126
|
+
expiresIn: data.expires_in,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function performJiraOAuthFlow(
|
|
131
|
+
selectSite?: (sites: AtlassianSite[]) => Promise<AtlassianSite>
|
|
132
|
+
): Promise<JiraOAuthResult> {
|
|
133
|
+
const port = await findAvailablePort();
|
|
134
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
135
|
+
|
|
136
|
+
const state = Math.random().toString(36).substring(2);
|
|
137
|
+
const authUrl = new URL(ATLASSIAN_AUTH_URL);
|
|
138
|
+
authUrl.searchParams.set('audience', 'api.atlassian.com');
|
|
139
|
+
authUrl.searchParams.set('client_id', JIRA_OAUTH_CONFIG.clientId);
|
|
140
|
+
authUrl.searchParams.set('scope', JIRA_SCOPES.join(' '));
|
|
141
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
142
|
+
authUrl.searchParams.set('state', state);
|
|
143
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
144
|
+
authUrl.searchParams.set('prompt', 'consent');
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
let server: Server;
|
|
148
|
+
|
|
149
|
+
const timeout = setTimeout(() => {
|
|
150
|
+
server?.close();
|
|
151
|
+
reject(new Error('OAuth flow timed out after 5 minutes'));
|
|
152
|
+
}, 5 * 60 * 1000);
|
|
153
|
+
|
|
154
|
+
server = createServer(async (req, res) => {
|
|
155
|
+
const url = new URL(req.url || '', `http://localhost:${port}`);
|
|
156
|
+
|
|
157
|
+
if (url.pathname !== '/callback') {
|
|
158
|
+
res.writeHead(404);
|
|
159
|
+
res.end('Not found');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const code = url.searchParams.get('code');
|
|
164
|
+
const error = url.searchParams.get('error');
|
|
165
|
+
const returnedState = url.searchParams.get('state');
|
|
166
|
+
|
|
167
|
+
if (error) {
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
169
|
+
res.end('<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>');
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
server.close();
|
|
172
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (returnedState !== state) {
|
|
177
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
178
|
+
res.end('<html><body><h1>State Mismatch</h1><p>You can close this window.</p></body></html>');
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
server.close();
|
|
181
|
+
reject(new Error('OAuth state mismatch - possible CSRF attack'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!code) {
|
|
186
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
187
|
+
res.end('<html><body><h1>Missing Authorization Code</h1><p>You can close this window.</p></body></html>');
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
server.close();
|
|
190
|
+
reject(new Error('Missing authorization code in OAuth callback'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
196
|
+
res.end('<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>');
|
|
197
|
+
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
server.close();
|
|
200
|
+
|
|
201
|
+
// Exchange code for tokens
|
|
202
|
+
const tokens = await exchangeCodeForTokens(code, JIRA_OAUTH_CONFIG.clientId, JIRA_OAUTH_CONFIG.clientSecret, redirectUri);
|
|
203
|
+
|
|
204
|
+
// Get accessible resources to find cloud ID
|
|
205
|
+
const sites = await getAccessibleResources(tokens.accessToken);
|
|
206
|
+
|
|
207
|
+
if (sites.length === 0) {
|
|
208
|
+
throw new Error('No accessible Jira sites found. Make sure your app has the correct permissions.');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Let user select site if multiple, otherwise use the first one
|
|
212
|
+
let selectedSite: AtlassianSite;
|
|
213
|
+
if (sites.length === 1) {
|
|
214
|
+
selectedSite = sites[0];
|
|
215
|
+
} else if (selectSite) {
|
|
216
|
+
selectedSite = await selectSite(sites);
|
|
217
|
+
} else {
|
|
218
|
+
selectedSite = sites[0];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
resolve({
|
|
222
|
+
accessToken: tokens.accessToken,
|
|
223
|
+
refreshToken: tokens.refreshToken,
|
|
224
|
+
expiryDate: Date.now() + tokens.expiresIn * 1000,
|
|
225
|
+
cloudId: selectedSite.id,
|
|
226
|
+
siteUrl: selectedSite.url,
|
|
227
|
+
});
|
|
228
|
+
} catch (err) {
|
|
229
|
+
reject(err);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
server.listen(port, () => {
|
|
234
|
+
console.error(`\nOpening browser for Atlassian authorization...`);
|
|
235
|
+
console.error(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);
|
|
236
|
+
|
|
237
|
+
// Open browser
|
|
238
|
+
const open = process.platform === 'darwin' ? 'open' :
|
|
239
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
240
|
+
Bun.spawn([open, authUrl.toString()], { stdout: 'ignore', stderr: 'ignore' });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
server.on('error', (err) => {
|
|
244
|
+
clearTimeout(timeout);
|
|
245
|
+
server?.close();
|
|
246
|
+
reject(err);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { setCredentials, removeCredentials, getCredentials, setCredentials as updateCredentials } from '../auth/token-store';
|
|
4
|
+
import { setProfile, removeProfile, listProfiles, getProfile } from '../config/config-manager';
|
|
5
|
+
import { performJiraOAuthFlow, refreshJiraToken, type AtlassianSite } from '../auth/jira-oauth';
|
|
6
|
+
import { JiraClient } from '../services/jira/client';
|
|
7
|
+
import { CliError, handleError } from '../utils/errors';
|
|
8
|
+
import { readStdin } from '../utils/stdin';
|
|
9
|
+
import {
|
|
10
|
+
printJiraProjectList,
|
|
11
|
+
printJiraIssueList,
|
|
12
|
+
printJiraIssue,
|
|
13
|
+
printJiraTransitions,
|
|
14
|
+
printJiraCommentResult,
|
|
15
|
+
printJiraTransitionResult,
|
|
16
|
+
} from '../utils/output';
|
|
17
|
+
import type { JiraCredentials } from '../types/jira';
|
|
18
|
+
|
|
19
|
+
function prompt(question: string): Promise<string> {
|
|
20
|
+
const rl = createInterface({
|
|
21
|
+
input: process.stdin,
|
|
22
|
+
output: process.stderr,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
rl.question(question, (answer) => {
|
|
27
|
+
rl.close();
|
|
28
|
+
resolve(answer.trim());
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureValidToken(credentials: JiraCredentials, profile: string): Promise<JiraCredentials> {
|
|
34
|
+
// Check if token is expired or about to expire (within 5 minutes)
|
|
35
|
+
const bufferTime = 5 * 60 * 1000;
|
|
36
|
+
if (credentials.expiryDate && Date.now() + bufferTime >= credentials.expiryDate) {
|
|
37
|
+
console.error('Access token expired, refreshing...');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const refreshed = await refreshJiraToken(credentials.refreshToken);
|
|
41
|
+
|
|
42
|
+
const newCredentials: JiraCredentials = {
|
|
43
|
+
...credentials,
|
|
44
|
+
accessToken: refreshed.accessToken,
|
|
45
|
+
refreshToken: refreshed.refreshToken,
|
|
46
|
+
expiryDate: Date.now() + refreshed.expiresIn * 1000,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
await updateCredentials('jira', profile, newCredentials);
|
|
50
|
+
return newCredentials;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new CliError(
|
|
53
|
+
'AUTH_FAILED',
|
|
54
|
+
'Failed to refresh access token. Please re-authenticate.',
|
|
55
|
+
`Run: agentio jira profile add --profile ${profile}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return credentials;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getJiraClient(profileName?: string): Promise<{ client: JiraClient; profile: string }> {
|
|
64
|
+
const profile = await getProfile('jira', profileName);
|
|
65
|
+
|
|
66
|
+
if (!profile) {
|
|
67
|
+
throw new CliError(
|
|
68
|
+
'PROFILE_NOT_FOUND',
|
|
69
|
+
profileName
|
|
70
|
+
? `Profile "${profileName}" not found for jira`
|
|
71
|
+
: 'No default profile configured for jira',
|
|
72
|
+
'Run: agentio jira profile add'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let credentials = await getCredentials<JiraCredentials>('jira', profile);
|
|
77
|
+
|
|
78
|
+
if (!credentials) {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
'AUTH_FAILED',
|
|
81
|
+
`No credentials found for jira profile "${profile}"`,
|
|
82
|
+
`Run: agentio jira profile add --profile ${profile}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Ensure token is valid
|
|
87
|
+
credentials = await ensureValidToken(credentials, profile);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
client: new JiraClient(credentials),
|
|
91
|
+
profile,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function registerJiraCommands(program: Command): void {
|
|
96
|
+
const jira = program
|
|
97
|
+
.command('jira')
|
|
98
|
+
.description('JIRA operations');
|
|
99
|
+
|
|
100
|
+
// List projects
|
|
101
|
+
jira
|
|
102
|
+
.command('projects')
|
|
103
|
+
.description('List JIRA projects')
|
|
104
|
+
.option('--profile <name>', 'Profile name')
|
|
105
|
+
.option('--limit <number>', 'Maximum number of projects', '50')
|
|
106
|
+
.action(async (options) => {
|
|
107
|
+
try {
|
|
108
|
+
const { client } = await getJiraClient(options.profile);
|
|
109
|
+
const projects = await client.listProjects({
|
|
110
|
+
maxResults: parseInt(options.limit, 10),
|
|
111
|
+
});
|
|
112
|
+
printJiraProjectList(projects);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
handleError(error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Search issues
|
|
119
|
+
jira
|
|
120
|
+
.command('search')
|
|
121
|
+
.description('Search JIRA issues')
|
|
122
|
+
.option('--profile <name>', 'Profile name')
|
|
123
|
+
.option('--jql <query>', 'JQL query')
|
|
124
|
+
.option('--project <key>', 'Project key')
|
|
125
|
+
.option('--status <status>', 'Issue status')
|
|
126
|
+
.option('--assignee <name>', 'Assignee name')
|
|
127
|
+
.option('--limit <number>', 'Maximum number of issues', '50')
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
try {
|
|
130
|
+
const { client } = await getJiraClient(options.profile);
|
|
131
|
+
const issues = await client.searchIssues({
|
|
132
|
+
jql: options.jql,
|
|
133
|
+
project: options.project,
|
|
134
|
+
status: options.status,
|
|
135
|
+
assignee: options.assignee,
|
|
136
|
+
maxResults: parseInt(options.limit, 10),
|
|
137
|
+
});
|
|
138
|
+
printJiraIssueList(issues);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
handleError(error);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Get issue details
|
|
145
|
+
jira
|
|
146
|
+
.command('get')
|
|
147
|
+
.description('Get JIRA issue details')
|
|
148
|
+
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
149
|
+
.option('--profile <name>', 'Profile name')
|
|
150
|
+
.action(async (issueKey: string, options) => {
|
|
151
|
+
try {
|
|
152
|
+
const { client } = await getJiraClient(options.profile);
|
|
153
|
+
const issue = await client.getIssue(issueKey);
|
|
154
|
+
printJiraIssue(issue);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
handleError(error);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Add comment
|
|
161
|
+
jira
|
|
162
|
+
.command('comment')
|
|
163
|
+
.description('Add a comment to an issue')
|
|
164
|
+
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
165
|
+
.argument('[body]', 'Comment body (or pipe via stdin)')
|
|
166
|
+
.option('--profile <name>', 'Profile name')
|
|
167
|
+
.action(async (issueKey: string, body: string | undefined, options) => {
|
|
168
|
+
try {
|
|
169
|
+
let text = body;
|
|
170
|
+
|
|
171
|
+
if (!text) {
|
|
172
|
+
text = await readStdin() || undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!text) {
|
|
176
|
+
throw new CliError('INVALID_PARAMS', 'Comment body is required. Provide as argument or pipe via stdin.');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { client } = await getJiraClient(options.profile);
|
|
180
|
+
const result = await client.addComment(issueKey, text);
|
|
181
|
+
printJiraCommentResult(result);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
handleError(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// List transitions
|
|
188
|
+
jira
|
|
189
|
+
.command('transitions')
|
|
190
|
+
.description('List available transitions for an issue')
|
|
191
|
+
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
192
|
+
.option('--profile <name>', 'Profile name')
|
|
193
|
+
.action(async (issueKey: string, options) => {
|
|
194
|
+
try {
|
|
195
|
+
const { client } = await getJiraClient(options.profile);
|
|
196
|
+
const transitions = await client.getTransitions(issueKey);
|
|
197
|
+
printJiraTransitions(issueKey, transitions);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
handleError(error);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Transition issue (change status)
|
|
204
|
+
jira
|
|
205
|
+
.command('transition')
|
|
206
|
+
.description('Transition an issue to a new status')
|
|
207
|
+
.argument('<issue-key>', 'Issue key (e.g., PROJ-123)')
|
|
208
|
+
.argument('<transition-id>', 'Transition ID (use "transitions" command to see available)')
|
|
209
|
+
.option('--profile <name>', 'Profile name')
|
|
210
|
+
.action(async (issueKey: string, transitionId: string, options) => {
|
|
211
|
+
try {
|
|
212
|
+
const { client } = await getJiraClient(options.profile);
|
|
213
|
+
const result = await client.transitionIssue(issueKey, transitionId);
|
|
214
|
+
printJiraTransitionResult(result);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
handleError(error);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Profile management
|
|
221
|
+
const profile = jira
|
|
222
|
+
.command('profile')
|
|
223
|
+
.description('Manage JIRA profiles');
|
|
224
|
+
|
|
225
|
+
profile
|
|
226
|
+
.command('add')
|
|
227
|
+
.description('Add a new JIRA profile with OAuth authentication')
|
|
228
|
+
.option('--profile <name>', 'Profile name', 'default')
|
|
229
|
+
.action(async (options) => {
|
|
230
|
+
try {
|
|
231
|
+
const profileName = options.profile;
|
|
232
|
+
|
|
233
|
+
console.error('\nš§ JIRA OAuth Setup\n');
|
|
234
|
+
|
|
235
|
+
// Site selection callback
|
|
236
|
+
const selectSite = async (sites: AtlassianSite[]): Promise<AtlassianSite> => {
|
|
237
|
+
console.error('\nMultiple JIRA sites found:\n');
|
|
238
|
+
sites.forEach((site, index) => {
|
|
239
|
+
console.error(` [${index + 1}] ${site.name} (${site.url})`);
|
|
240
|
+
});
|
|
241
|
+
console.error('');
|
|
242
|
+
|
|
243
|
+
const choice = await prompt(`? Select a site (1-${sites.length}): `);
|
|
244
|
+
const index = parseInt(choice, 10) - 1;
|
|
245
|
+
|
|
246
|
+
if (isNaN(index) || index < 0 || index >= sites.length) {
|
|
247
|
+
throw new CliError('INVALID_PARAMS', 'Invalid selection');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return sites[index];
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const result = await performJiraOAuthFlow(selectSite);
|
|
254
|
+
|
|
255
|
+
console.error(`\nā Authorized for site: ${result.siteUrl}\n`);
|
|
256
|
+
|
|
257
|
+
// Save credentials
|
|
258
|
+
const credentials: JiraCredentials = {
|
|
259
|
+
accessToken: result.accessToken,
|
|
260
|
+
refreshToken: result.refreshToken,
|
|
261
|
+
expiryDate: result.expiryDate,
|
|
262
|
+
cloudId: result.cloudId,
|
|
263
|
+
siteUrl: result.siteUrl,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
await setProfile('jira', profileName);
|
|
267
|
+
await setCredentials('jira', profileName, credentials);
|
|
268
|
+
|
|
269
|
+
console.log(`\nā
Profile "${profileName}" configured!`);
|
|
270
|
+
console.log(` Test with: agentio jira projects --profile ${profileName}`);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
handleError(error);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
profile
|
|
277
|
+
.command('list')
|
|
278
|
+
.description('List JIRA profiles')
|
|
279
|
+
.action(async () => {
|
|
280
|
+
try {
|
|
281
|
+
const result = await listProfiles('jira');
|
|
282
|
+
const { profiles, default: defaultProfile } = result[0];
|
|
283
|
+
|
|
284
|
+
if (profiles.length === 0) {
|
|
285
|
+
console.log('No profiles configured');
|
|
286
|
+
} else {
|
|
287
|
+
for (const name of profiles) {
|
|
288
|
+
const marker = name === defaultProfile ? ' (default)' : '';
|
|
289
|
+
const credentials = await getCredentials<JiraCredentials>('jira', name);
|
|
290
|
+
const siteInfo = credentials?.siteUrl ? ` - ${credentials.siteUrl}` : '';
|
|
291
|
+
console.log(`${name}${marker}${siteInfo}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
handleError(error);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
profile
|
|
300
|
+
.command('remove')
|
|
301
|
+
.description('Remove a JIRA profile')
|
|
302
|
+
.requiredOption('--profile <name>', 'Profile name')
|
|
303
|
+
.action(async (options) => {
|
|
304
|
+
try {
|
|
305
|
+
const profileName = options.profile;
|
|
306
|
+
|
|
307
|
+
const removed = await removeProfile('jira', profileName);
|
|
308
|
+
await removeCredentials('jira', profileName);
|
|
309
|
+
|
|
310
|
+
if (removed) {
|
|
311
|
+
console.error(`Removed profile "${profileName}"`);
|
|
312
|
+
} else {
|
|
313
|
+
console.error(`Profile "${profileName}" not found`);
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
handleError(error);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|