@falldownthesystem/devtool 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/clients/serviceDesk.js +48 -1
- package/src/commands/sd.js +40 -0
- package/src/formatters/serviceDesk.js +45 -0
- package/src/index.js +1 -1
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ function authHeader() {
|
|
|
10
10
|
return 'Basic ' + Buffer.from(`${email}:${token}`).toString('base64');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async function request(path, params = {}) {
|
|
13
|
+
async function request(path, params = {}, extraHeaders = {}) {
|
|
14
14
|
const url = new URL(path.startsWith('http') ? path : `${baseUrl()}${path}`);
|
|
15
15
|
for (const [k, v] of Object.entries(params)) {
|
|
16
16
|
if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v);
|
|
@@ -20,6 +20,7 @@ async function request(path, params = {}) {
|
|
|
20
20
|
headers: {
|
|
21
21
|
'Authorization': authHeader(),
|
|
22
22
|
'Accept': 'application/json',
|
|
23
|
+
...extraHeaders,
|
|
23
24
|
},
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -70,3 +71,49 @@ export async function getMyIssues() {
|
|
|
70
71
|
: `assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC`;
|
|
71
72
|
return searchIssues(jql);
|
|
72
73
|
}
|
|
74
|
+
|
|
75
|
+
let _serviceDeskIdCache = null;
|
|
76
|
+
|
|
77
|
+
async function getServiceDeskId(projectKey) {
|
|
78
|
+
if (_serviceDeskIdCache && _serviceDeskIdCache.projectKey === projectKey) {
|
|
79
|
+
return _serviceDeskIdCache.id;
|
|
80
|
+
}
|
|
81
|
+
const data = await request('/rest/servicedeskapi/servicedesk', { start: 0, limit: 100 }, { 'X-ExperimentalApi': 'opt-in' });
|
|
82
|
+
const match = (data.values || []).find((sd) => sd.projectKey === projectKey);
|
|
83
|
+
if (!match) {
|
|
84
|
+
throw new Error(`No Service Desk found for project ${projectKey}`);
|
|
85
|
+
}
|
|
86
|
+
_serviceDeskIdCache = { projectKey, id: match.id };
|
|
87
|
+
return match.id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function listQueues() {
|
|
91
|
+
const projectKey = get('serviceDesk.defaultProject');
|
|
92
|
+
if (!projectKey) throw new Error('serviceDesk.defaultProject is not configured');
|
|
93
|
+
const sdId = await getServiceDeskId(projectKey);
|
|
94
|
+
const data = await request(`/rest/servicedeskapi/servicedesk/${sdId}/queue`, { start: 0, limit: 100 }, { 'X-ExperimentalApi': 'opt-in' });
|
|
95
|
+
return (data.values || []).map((q) => ({ id: q.id, name: q.name, jql: q.jql }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function findQueue(nameOrId) {
|
|
99
|
+
const queues = await listQueues();
|
|
100
|
+
const idMatch = queues.find((q) => String(q.id) === String(nameOrId));
|
|
101
|
+
if (idMatch) return idMatch;
|
|
102
|
+
const lower = nameOrId.toLowerCase();
|
|
103
|
+
const exact = queues.find((q) => q.name.toLowerCase() === lower);
|
|
104
|
+
if (exact) return exact;
|
|
105
|
+
const partial = queues.filter((q) => q.name.toLowerCase().includes(lower));
|
|
106
|
+
if (partial.length === 1) return partial[0];
|
|
107
|
+
if (partial.length > 1) {
|
|
108
|
+
const err = new Error(`Ambiguous queue "${nameOrId}" — matches: ${partial.map((q) => q.name).join(', ')}`);
|
|
109
|
+
err.candidates = partial;
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`No queue found matching "${nameOrId}"`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function searchByQueue(nameOrId, maxResults = 50, nextPageToken = '') {
|
|
116
|
+
const queue = await findQueue(nameOrId);
|
|
117
|
+
const result = await searchIssues(queue.jql, maxResults, nextPageToken);
|
|
118
|
+
return { ...result, queue };
|
|
119
|
+
}
|
package/src/commands/sd.js
CHANGED
|
@@ -4,11 +4,14 @@ import {
|
|
|
4
4
|
getIssueComments,
|
|
5
5
|
searchIssues,
|
|
6
6
|
getMyIssues,
|
|
7
|
+
listQueues,
|
|
8
|
+
searchByQueue,
|
|
7
9
|
} from '../clients/serviceDesk.js';
|
|
8
10
|
import {
|
|
9
11
|
formatIssueView, formatIssueComments, formatIssueSearch,
|
|
10
12
|
plainIssueView, plainIssueComments, plainIssueSearch,
|
|
11
13
|
curateIssueJson, curateCommentsJson, curateSearchJson,
|
|
14
|
+
formatQueueList, plainQueueList, curateQueuesJson,
|
|
12
15
|
} from '../formatters/serviceDesk.js';
|
|
13
16
|
import { spinner } from '../utils/output.js';
|
|
14
17
|
import { handleError } from '../utils/errors.js';
|
|
@@ -74,6 +77,43 @@ sd.command('search <jql>')
|
|
|
74
77
|
}
|
|
75
78
|
});
|
|
76
79
|
|
|
80
|
+
sd.command('queues')
|
|
81
|
+
.description('List Service Desk queues for the default project')
|
|
82
|
+
.action(async function () {
|
|
83
|
+
const mode = getOutputMode(this);
|
|
84
|
+
const s = mode === 'rich' ? spinner('Fetching queues...').start() : null;
|
|
85
|
+
try {
|
|
86
|
+
const queues = await listQueues();
|
|
87
|
+
if (s) s.success({ text: `${queues.length} queue(s)` });
|
|
88
|
+
if (mode === 'json') outputJson(curateQueuesJson(queues));
|
|
89
|
+
else if (mode === 'plain') plainQueueList(queues);
|
|
90
|
+
else formatQueueList(queues);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (s) s.error({ text: 'Failed to fetch queues' });
|
|
93
|
+
handleError(err, 'sd queues');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
sd.command('queue <nameOrId>')
|
|
98
|
+
.description('Show issues in a Service Desk queue (by name or id)')
|
|
99
|
+
.option('-n, --per-page <number>', 'Results per page', '20')
|
|
100
|
+
.option('--next <token>', 'Continuation token from a previous page')
|
|
101
|
+
.action(async function (nameOrId, opts) {
|
|
102
|
+
const mode = getOutputMode(this);
|
|
103
|
+
const perPage = parseInt(opts.perPage, 10);
|
|
104
|
+
const s = mode === 'rich' ? spinner(`Fetching queue "${nameOrId}"...`).start() : null;
|
|
105
|
+
try {
|
|
106
|
+
const result = await searchByQueue(nameOrId, perPage, opts.next || '');
|
|
107
|
+
if (s) s.success({ text: `${result.issues.length} issue(s) in "${result.queue.name}"` });
|
|
108
|
+
if (mode === 'json') outputJson(curateSearchJson(result));
|
|
109
|
+
else if (mode === 'plain') plainIssueSearch(result);
|
|
110
|
+
else formatIssueSearch(result);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (s) s.error({ text: `Failed to fetch queue "${nameOrId}"` });
|
|
113
|
+
handleError(err, `sd queue ${nameOrId}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
77
117
|
sd.command('mine')
|
|
78
118
|
.alias('my')
|
|
79
119
|
.description('Show my open Service Desk tickets')
|
|
@@ -59,6 +59,10 @@ function requestType(fields) {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function migrationServerKey(fields) {
|
|
63
|
+
return fields.customfield_10165 || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
62
66
|
// --- JSON curators ---
|
|
63
67
|
|
|
64
68
|
export function curateIssueJson(issue) {
|
|
@@ -70,6 +74,7 @@ export function curateIssueJson(issue) {
|
|
|
70
74
|
priority: f.priority?.name || null,
|
|
71
75
|
type: f.issuetype?.name || null,
|
|
72
76
|
requestType: requestType(f),
|
|
77
|
+
migrationServerKey: migrationServerKey(f),
|
|
73
78
|
assignee: f.assignee?.displayName || null,
|
|
74
79
|
reporter: f.reporter?.displayName || null,
|
|
75
80
|
labels: f.labels || [],
|
|
@@ -92,6 +97,7 @@ export function curateSearchJson(result) {
|
|
|
92
97
|
return {
|
|
93
98
|
nextPageToken: result.nextPageToken,
|
|
94
99
|
isLast: result.isLast,
|
|
100
|
+
queue: result.queue ? { id: result.queue.id, name: result.queue.name, jql: result.queue.jql } : undefined,
|
|
95
101
|
issues: result.issues.map((issue) => ({
|
|
96
102
|
key: issue.key,
|
|
97
103
|
summary: issue.fields.summary,
|
|
@@ -103,6 +109,31 @@ export function curateSearchJson(result) {
|
|
|
103
109
|
};
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
export function curateQueuesJson(queues) {
|
|
113
|
+
return queues.map((q) => ({ id: q.id, name: q.name, jql: q.jql }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function plainQueueList(queues) {
|
|
117
|
+
if (queues.length === 0) { console.log('No queues.'); return; }
|
|
118
|
+
for (const q of queues) {
|
|
119
|
+
console.log(`${q.id}\t${q.name}\t${q.jql}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function formatQueueList(queues) {
|
|
124
|
+
if (queues.length === 0) {
|
|
125
|
+
console.log(pc.dim(' No queues.'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const rows = queues.map((q) => [
|
|
129
|
+
pc.dim(String(q.id)),
|
|
130
|
+
pc.bold(q.name),
|
|
131
|
+
(q.jql || '').length > 80 ? q.jql.slice(0, 77) + '...' : q.jql || '',
|
|
132
|
+
]);
|
|
133
|
+
console.log(table(['ID', 'Name', 'JQL'], rows));
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
|
|
106
137
|
// --- Plain formatters ---
|
|
107
138
|
|
|
108
139
|
export function plainIssueView(issue) {
|
|
@@ -113,6 +144,8 @@ export function plainIssueView(issue) {
|
|
|
113
144
|
console.log(`Type: ${f.issuetype?.name || 'Unknown'}`);
|
|
114
145
|
const rt = requestType(f);
|
|
115
146
|
if (rt) console.log(`Request Type: ${rt}`);
|
|
147
|
+
const mk = migrationServerKey(f);
|
|
148
|
+
if (mk) console.log(`Migration Server Key: ${mk}`);
|
|
116
149
|
console.log(`Assignee: ${f.assignee?.displayName || 'Unassigned'}`);
|
|
117
150
|
console.log(`Reporter: ${f.reporter?.displayName || 'Unknown'}`);
|
|
118
151
|
if (f.labels?.length > 0) console.log(`Labels: ${f.labels.join(', ')}`);
|
|
@@ -131,6 +164,10 @@ export function plainIssueComments(comments) {
|
|
|
131
164
|
}
|
|
132
165
|
|
|
133
166
|
export function plainIssueSearch(result) {
|
|
167
|
+
if (result.queue) {
|
|
168
|
+
console.log(`Queue: ${result.queue.name} (${result.queue.id})`);
|
|
169
|
+
console.log(`JQL: ${result.queue.jql}`);
|
|
170
|
+
}
|
|
134
171
|
if (result.issues.length === 0) { console.log('No issues found.'); return; }
|
|
135
172
|
for (const issue of result.issues) {
|
|
136
173
|
const f = issue.fields;
|
|
@@ -153,6 +190,8 @@ export function formatIssueView(issue) {
|
|
|
153
190
|
field('Type', f.issuetype?.name);
|
|
154
191
|
const rt = requestType(f);
|
|
155
192
|
if (rt) field('Request Type', rt);
|
|
193
|
+
const mk = migrationServerKey(f);
|
|
194
|
+
if (mk) field('Migration Server Key', mk);
|
|
156
195
|
field('Assignee', f.assignee?.displayName || pc.dim('Unassigned'));
|
|
157
196
|
field('Reporter', f.reporter?.displayName);
|
|
158
197
|
field('Labels', f.labels?.length > 0 ? f.labels.join(', ') : null);
|
|
@@ -197,6 +236,12 @@ export function formatIssueComments(comments) {
|
|
|
197
236
|
export function formatIssueSearch(result) {
|
|
198
237
|
const { issues } = result;
|
|
199
238
|
|
|
239
|
+
if (result.queue) {
|
|
240
|
+
heading(`Queue: ${result.queue.name}`);
|
|
241
|
+
console.log(pc.dim(` ${result.queue.jql}`));
|
|
242
|
+
divider();
|
|
243
|
+
}
|
|
244
|
+
|
|
200
245
|
if (issues.length === 0) {
|
|
201
246
|
console.log(pc.dim(' No issues found.'));
|
|
202
247
|
return;
|
package/src/index.js
CHANGED
|
@@ -13,7 +13,7 @@ const program = new Command();
|
|
|
13
13
|
program
|
|
14
14
|
.name('dev')
|
|
15
15
|
.description('CLI for Bitbucket PRs, Jira tickets, Confluence pages, and Service Desk tickets')
|
|
16
|
-
.version('0.
|
|
16
|
+
.version('0.4.0')
|
|
17
17
|
.option('--json', 'Output raw JSON')
|
|
18
18
|
.option('--plain', 'Output compact plain text (no tables, no colors)');
|
|
19
19
|
|