@falldownthesystem/devtool 0.4.0 → 0.4.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/LICENSE +15 -0
- package/README.md +1 -2
- package/package.json +22 -4
- package/src/clients/confluence.js +12 -8
- package/src/clients/serviceDesk.js +181 -119
- package/src/commands/config.js +10 -27
- package/src/commands/sd.js +135 -135
- package/src/config/store.js +20 -16
- package/src/formatters/serviceDesk.js +267 -267
- package/src/index.js +1 -1
- package/CLAUDE.md +0 -54
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
import { table, heading, field, divider, pc } from '../utils/output.js';
|
|
2
|
-
|
|
3
|
-
function statusColor(name) {
|
|
4
|
-
if (!name) return pc.dim('Unknown');
|
|
5
|
-
const lower = name.toLowerCase();
|
|
6
|
-
if (lower === 'done' || lower === 'closed' || lower === 'resolved') return pc.green(name);
|
|
7
|
-
if (lower === 'in progress' || lower === 'in review' || lower === 'waiting for support') return pc.blue(name);
|
|
8
|
-
if (lower === 'to do' || lower === 'open' || lower === 'backlog' || lower === 'waiting for customer') return pc.yellow(name);
|
|
9
|
-
return name;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function priorityColor(name) {
|
|
13
|
-
if (!name) return pc.dim('None');
|
|
14
|
-
const lower = name.toLowerCase();
|
|
15
|
-
if (lower === 'highest' || lower === 'blocker') return pc.red(pc.bold(name));
|
|
16
|
-
if (lower === 'high' || lower === 'critical') return pc.red(name);
|
|
17
|
-
if (lower === 'low' || lower === 'minor' || lower === 'lowest' || lower === 'trivial') return pc.dim(name);
|
|
18
|
-
return name;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function relativeTime(dateStr) {
|
|
22
|
-
if (!dateStr) return '';
|
|
23
|
-
const diff = Date.now() - new Date(dateStr).getTime();
|
|
24
|
-
const mins = Math.floor(diff / 60000);
|
|
25
|
-
if (mins < 60) return `${mins}m ago`;
|
|
26
|
-
const hours = Math.floor(mins / 60);
|
|
27
|
-
if (hours < 24) return `${hours}h ago`;
|
|
28
|
-
const days = Math.floor(hours / 24);
|
|
29
|
-
if (days < 30) return `${days}d ago`;
|
|
30
|
-
return new Date(dateStr).toLocaleDateString();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function adfToText(node) {
|
|
34
|
-
if (!node) return '';
|
|
35
|
-
if (typeof node === 'string') return node;
|
|
36
|
-
if (Array.isArray(node)) return node.map(adfToText).join('');
|
|
37
|
-
if (node.type === 'text') return node.text || '';
|
|
38
|
-
if (node.type === 'hardBreak') return '\n';
|
|
39
|
-
const inner = adfToText(node.content);
|
|
40
|
-
const blockTypes = new Set(['paragraph', 'heading', 'bulletList', 'orderedList', 'listItem', 'codeBlock', 'blockquote']);
|
|
41
|
-
if (blockTypes.has(node.type)) return inner + '\n';
|
|
42
|
-
return inner;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function descriptionText(field) {
|
|
46
|
-
if (!field) return '';
|
|
47
|
-
if (typeof field === 'string') return field;
|
|
48
|
-
return adfToText(field).trim();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function requestType(fields) {
|
|
52
|
-
for (const key of Object.keys(fields)) {
|
|
53
|
-
if (!key.startsWith('customfield_')) continue;
|
|
54
|
-
const val = fields[key];
|
|
55
|
-
if (val && typeof val === 'object' && val.requestType?.name) {
|
|
56
|
-
return val.requestType.name;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function migrationServerKey(fields) {
|
|
63
|
-
return fields.customfield_10165 || null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// --- JSON curators ---
|
|
67
|
-
|
|
68
|
-
export function curateIssueJson(issue) {
|
|
69
|
-
const f = issue.fields;
|
|
70
|
-
return {
|
|
71
|
-
key: issue.key,
|
|
72
|
-
summary: f.summary,
|
|
73
|
-
status: f.status?.name || null,
|
|
74
|
-
priority: f.priority?.name || null,
|
|
75
|
-
type: f.issuetype?.name || null,
|
|
76
|
-
requestType: requestType(f),
|
|
77
|
-
migrationServerKey: migrationServerKey(f),
|
|
78
|
-
assignee: f.assignee?.displayName || null,
|
|
79
|
-
reporter: f.reporter?.displayName || null,
|
|
80
|
-
labels: f.labels || [],
|
|
81
|
-
components: (f.components || []).map((c) => c.name),
|
|
82
|
-
created: f.created,
|
|
83
|
-
updated: f.updated,
|
|
84
|
-
description: descriptionText(f.description),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function curateCommentsJson(comments) {
|
|
89
|
-
return comments.map((c) => ({
|
|
90
|
-
author: c.author?.displayName || null,
|
|
91
|
-
date: c.created,
|
|
92
|
-
body: descriptionText(c.body),
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function curateSearchJson(result) {
|
|
97
|
-
return {
|
|
98
|
-
nextPageToken: result.nextPageToken,
|
|
99
|
-
isLast: result.isLast,
|
|
100
|
-
queue: result.queue ? { id: result.queue.id, name: result.queue.name, jql: result.queue.jql } : undefined,
|
|
101
|
-
issues: result.issues.map((issue) => ({
|
|
102
|
-
key: issue.key,
|
|
103
|
-
summary: issue.fields.summary,
|
|
104
|
-
status: issue.fields.status?.name || null,
|
|
105
|
-
assignee: issue.fields.assignee?.displayName || null,
|
|
106
|
-
priority: issue.fields.priority?.name || null,
|
|
107
|
-
updated: issue.fields.updated,
|
|
108
|
-
})),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
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
|
-
|
|
137
|
-
// --- Plain formatters ---
|
|
138
|
-
|
|
139
|
-
export function plainIssueView(issue) {
|
|
140
|
-
const f = issue.fields;
|
|
141
|
-
console.log(`${issue.key} ${f.summary}`);
|
|
142
|
-
console.log(`Status: ${f.status?.name || 'Unknown'}`);
|
|
143
|
-
console.log(`Priority: ${f.priority?.name || 'None'}`);
|
|
144
|
-
console.log(`Type: ${f.issuetype?.name || 'Unknown'}`);
|
|
145
|
-
const rt = requestType(f);
|
|
146
|
-
if (rt) console.log(`Request Type: ${rt}`);
|
|
147
|
-
const mk = migrationServerKey(f);
|
|
148
|
-
if (mk) console.log(`Migration Server Key: ${mk}`);
|
|
149
|
-
console.log(`Assignee: ${f.assignee?.displayName || 'Unassigned'}`);
|
|
150
|
-
console.log(`Reporter: ${f.reporter?.displayName || 'Unknown'}`);
|
|
151
|
-
if (f.labels?.length > 0) console.log(`Labels: ${f.labels.join(', ')}`);
|
|
152
|
-
if (f.components?.length > 0) console.log(`Components: ${f.components.map((c) => c.name).join(', ')}`);
|
|
153
|
-
console.log(`Created: ${f.created}`);
|
|
154
|
-
console.log(`Updated: ${f.updated}`);
|
|
155
|
-
const desc = descriptionText(f.description);
|
|
156
|
-
if (desc) console.log(`Description:\n${desc}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function plainIssueComments(comments) {
|
|
160
|
-
if (comments.length === 0) { console.log('No comments.'); return; }
|
|
161
|
-
for (const c of comments) {
|
|
162
|
-
console.log(`${c.author?.displayName || 'Unknown'} (${relativeTime(c.created)}): ${descriptionText(c.body)}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
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
|
-
}
|
|
171
|
-
if (result.issues.length === 0) { console.log('No issues found.'); return; }
|
|
172
|
-
for (const issue of result.issues) {
|
|
173
|
-
const f = issue.fields;
|
|
174
|
-
console.log(`${issue.key} [${f.status?.name}] "${f.summary}" - ${f.assignee?.displayName || 'Unassigned'} (${f.priority?.name}, ${relativeTime(f.updated)})`);
|
|
175
|
-
}
|
|
176
|
-
if (!result.isLast && result.nextPageToken) {
|
|
177
|
-
console.log(`More results available (nextPageToken: ${result.nextPageToken})`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// --- Rich formatters ---
|
|
182
|
-
|
|
183
|
-
export function formatIssueView(issue) {
|
|
184
|
-
const f = issue.fields;
|
|
185
|
-
|
|
186
|
-
heading(`${issue.key} ${f.summary}`);
|
|
187
|
-
divider();
|
|
188
|
-
field('Status', statusColor(f.status?.name));
|
|
189
|
-
field('Priority', priorityColor(f.priority?.name));
|
|
190
|
-
field('Type', f.issuetype?.name);
|
|
191
|
-
const rt = requestType(f);
|
|
192
|
-
if (rt) field('Request Type', rt);
|
|
193
|
-
const mk = migrationServerKey(f);
|
|
194
|
-
if (mk) field('Migration Server Key', mk);
|
|
195
|
-
field('Assignee', f.assignee?.displayName || pc.dim('Unassigned'));
|
|
196
|
-
field('Reporter', f.reporter?.displayName);
|
|
197
|
-
field('Labels', f.labels?.length > 0 ? f.labels.join(', ') : null);
|
|
198
|
-
field('Components', f.components?.map((c) => c.name).join(', ') || null);
|
|
199
|
-
field('Created', new Date(f.created).toLocaleString());
|
|
200
|
-
field('Updated', new Date(f.updated).toLocaleString());
|
|
201
|
-
|
|
202
|
-
const desc = descriptionText(f.description);
|
|
203
|
-
if (desc) {
|
|
204
|
-
heading('Description');
|
|
205
|
-
const lines = desc.split('\n');
|
|
206
|
-
const truncated = lines.slice(0, 30);
|
|
207
|
-
for (const line of truncated) {
|
|
208
|
-
console.log(` ${line}`);
|
|
209
|
-
}
|
|
210
|
-
if (lines.length > 30) {
|
|
211
|
-
console.log(pc.dim(` ... (${lines.length - 30} more lines)`));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
console.log('');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function formatIssueComments(comments) {
|
|
219
|
-
if (comments.length === 0) {
|
|
220
|
-
console.log(pc.dim(' No comments.'));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
for (const c of comments) {
|
|
225
|
-
console.log(`\n ${pc.bold(c.author?.displayName || 'Unknown')} ${pc.dim(relativeTime(c.created))}`);
|
|
226
|
-
const body = descriptionText(c.body);
|
|
227
|
-
if (body) {
|
|
228
|
-
for (const line of body.split('\n')) {
|
|
229
|
-
console.log(` ${line}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
console.log('');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function formatIssueSearch(result) {
|
|
237
|
-
const { issues } = result;
|
|
238
|
-
|
|
239
|
-
if (result.queue) {
|
|
240
|
-
heading(`Queue: ${result.queue.name}`);
|
|
241
|
-
console.log(pc.dim(` ${result.queue.jql}`));
|
|
242
|
-
divider();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (issues.length === 0) {
|
|
246
|
-
console.log(pc.dim(' No issues found.'));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const rows = issues.map((issue) => [
|
|
251
|
-
pc.bold(issue.key),
|
|
252
|
-
(issue.fields.summary || '').length > 55
|
|
253
|
-
? issue.fields.summary.slice(0, 52) + '...'
|
|
254
|
-
: issue.fields.summary || '',
|
|
255
|
-
statusColor(issue.fields.status?.name),
|
|
256
|
-
issue.fields.assignee?.displayName || pc.dim('Unassigned'),
|
|
257
|
-
priorityColor(issue.fields.priority?.name),
|
|
258
|
-
relativeTime(issue.fields.updated),
|
|
259
|
-
]);
|
|
260
|
-
|
|
261
|
-
console.log(table(['Key', 'Summary', 'Status', 'Assignee', 'Priority', 'Updated'], rows));
|
|
262
|
-
|
|
263
|
-
if (!result.isLast && result.nextPageToken) {
|
|
264
|
-
console.log(pc.dim(` More results available — pass --next ${result.nextPageToken} to continue`));
|
|
265
|
-
}
|
|
266
|
-
console.log('');
|
|
267
|
-
}
|
|
1
|
+
import { table, heading, field, divider, pc } from '../utils/output.js';
|
|
2
|
+
|
|
3
|
+
function statusColor(name) {
|
|
4
|
+
if (!name) return pc.dim('Unknown');
|
|
5
|
+
const lower = name.toLowerCase();
|
|
6
|
+
if (lower === 'done' || lower === 'closed' || lower === 'resolved') return pc.green(name);
|
|
7
|
+
if (lower === 'in progress' || lower === 'in review' || lower === 'waiting for support') return pc.blue(name);
|
|
8
|
+
if (lower === 'to do' || lower === 'open' || lower === 'backlog' || lower === 'waiting for customer') return pc.yellow(name);
|
|
9
|
+
return name;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function priorityColor(name) {
|
|
13
|
+
if (!name) return pc.dim('None');
|
|
14
|
+
const lower = name.toLowerCase();
|
|
15
|
+
if (lower === 'highest' || lower === 'blocker') return pc.red(pc.bold(name));
|
|
16
|
+
if (lower === 'high' || lower === 'critical') return pc.red(name);
|
|
17
|
+
if (lower === 'low' || lower === 'minor' || lower === 'lowest' || lower === 'trivial') return pc.dim(name);
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function relativeTime(dateStr) {
|
|
22
|
+
if (!dateStr) return '';
|
|
23
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
24
|
+
const mins = Math.floor(diff / 60000);
|
|
25
|
+
if (mins < 60) return `${mins}m ago`;
|
|
26
|
+
const hours = Math.floor(mins / 60);
|
|
27
|
+
if (hours < 24) return `${hours}h ago`;
|
|
28
|
+
const days = Math.floor(hours / 24);
|
|
29
|
+
if (days < 30) return `${days}d ago`;
|
|
30
|
+
return new Date(dateStr).toLocaleDateString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function adfToText(node) {
|
|
34
|
+
if (!node) return '';
|
|
35
|
+
if (typeof node === 'string') return node;
|
|
36
|
+
if (Array.isArray(node)) return node.map(adfToText).join('');
|
|
37
|
+
if (node.type === 'text') return node.text || '';
|
|
38
|
+
if (node.type === 'hardBreak') return '\n';
|
|
39
|
+
const inner = adfToText(node.content);
|
|
40
|
+
const blockTypes = new Set(['paragraph', 'heading', 'bulletList', 'orderedList', 'listItem', 'codeBlock', 'blockquote']);
|
|
41
|
+
if (blockTypes.has(node.type)) return inner + '\n';
|
|
42
|
+
return inner;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function descriptionText(field) {
|
|
46
|
+
if (!field) return '';
|
|
47
|
+
if (typeof field === 'string') return field;
|
|
48
|
+
return adfToText(field).trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function requestType(fields) {
|
|
52
|
+
for (const key of Object.keys(fields)) {
|
|
53
|
+
if (!key.startsWith('customfield_')) continue;
|
|
54
|
+
const val = fields[key];
|
|
55
|
+
if (val && typeof val === 'object' && val.requestType?.name) {
|
|
56
|
+
return val.requestType.name;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function migrationServerKey(fields) {
|
|
63
|
+
return fields.customfield_10165 || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- JSON curators ---
|
|
67
|
+
|
|
68
|
+
export function curateIssueJson(issue) {
|
|
69
|
+
const f = issue.fields;
|
|
70
|
+
return {
|
|
71
|
+
key: issue.key,
|
|
72
|
+
summary: f.summary,
|
|
73
|
+
status: f.status?.name || null,
|
|
74
|
+
priority: f.priority?.name || null,
|
|
75
|
+
type: f.issuetype?.name || null,
|
|
76
|
+
requestType: requestType(f),
|
|
77
|
+
migrationServerKey: migrationServerKey(f),
|
|
78
|
+
assignee: f.assignee?.displayName || null,
|
|
79
|
+
reporter: f.reporter?.displayName || null,
|
|
80
|
+
labels: f.labels || [],
|
|
81
|
+
components: (f.components || []).map((c) => c.name),
|
|
82
|
+
created: f.created,
|
|
83
|
+
updated: f.updated,
|
|
84
|
+
description: descriptionText(f.description),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function curateCommentsJson(comments) {
|
|
89
|
+
return comments.map((c) => ({
|
|
90
|
+
author: c.author?.displayName || null,
|
|
91
|
+
date: c.created,
|
|
92
|
+
body: descriptionText(c.body),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function curateSearchJson(result) {
|
|
97
|
+
return {
|
|
98
|
+
nextPageToken: result.nextPageToken,
|
|
99
|
+
isLast: result.isLast,
|
|
100
|
+
queue: result.queue ? { id: result.queue.id, name: result.queue.name, jql: result.queue.jql } : undefined,
|
|
101
|
+
issues: result.issues.map((issue) => ({
|
|
102
|
+
key: issue.key,
|
|
103
|
+
summary: issue.fields.summary,
|
|
104
|
+
status: issue.fields.status?.name || null,
|
|
105
|
+
assignee: issue.fields.assignee?.displayName || null,
|
|
106
|
+
priority: issue.fields.priority?.name || null,
|
|
107
|
+
updated: issue.fields.updated,
|
|
108
|
+
})),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
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
|
+
|
|
137
|
+
// --- Plain formatters ---
|
|
138
|
+
|
|
139
|
+
export function plainIssueView(issue) {
|
|
140
|
+
const f = issue.fields;
|
|
141
|
+
console.log(`${issue.key} ${f.summary}`);
|
|
142
|
+
console.log(`Status: ${f.status?.name || 'Unknown'}`);
|
|
143
|
+
console.log(`Priority: ${f.priority?.name || 'None'}`);
|
|
144
|
+
console.log(`Type: ${f.issuetype?.name || 'Unknown'}`);
|
|
145
|
+
const rt = requestType(f);
|
|
146
|
+
if (rt) console.log(`Request Type: ${rt}`);
|
|
147
|
+
const mk = migrationServerKey(f);
|
|
148
|
+
if (mk) console.log(`Migration Server Key: ${mk}`);
|
|
149
|
+
console.log(`Assignee: ${f.assignee?.displayName || 'Unassigned'}`);
|
|
150
|
+
console.log(`Reporter: ${f.reporter?.displayName || 'Unknown'}`);
|
|
151
|
+
if (f.labels?.length > 0) console.log(`Labels: ${f.labels.join(', ')}`);
|
|
152
|
+
if (f.components?.length > 0) console.log(`Components: ${f.components.map((c) => c.name).join(', ')}`);
|
|
153
|
+
console.log(`Created: ${f.created}`);
|
|
154
|
+
console.log(`Updated: ${f.updated}`);
|
|
155
|
+
const desc = descriptionText(f.description);
|
|
156
|
+
if (desc) console.log(`Description:\n${desc}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function plainIssueComments(comments) {
|
|
160
|
+
if (comments.length === 0) { console.log('No comments.'); return; }
|
|
161
|
+
for (const c of comments) {
|
|
162
|
+
console.log(`${c.author?.displayName || 'Unknown'} (${relativeTime(c.created)}): ${descriptionText(c.body)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
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
|
+
}
|
|
171
|
+
if (result.issues.length === 0) { console.log('No issues found.'); return; }
|
|
172
|
+
for (const issue of result.issues) {
|
|
173
|
+
const f = issue.fields;
|
|
174
|
+
console.log(`${issue.key} [${f.status?.name}] "${f.summary}" - ${f.assignee?.displayName || 'Unassigned'} (${f.priority?.name}, ${relativeTime(f.updated)})`);
|
|
175
|
+
}
|
|
176
|
+
if (!result.isLast && result.nextPageToken) {
|
|
177
|
+
console.log(`More results available (nextPageToken: ${result.nextPageToken})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Rich formatters ---
|
|
182
|
+
|
|
183
|
+
export function formatIssueView(issue) {
|
|
184
|
+
const f = issue.fields;
|
|
185
|
+
|
|
186
|
+
heading(`${issue.key} ${f.summary}`);
|
|
187
|
+
divider();
|
|
188
|
+
field('Status', statusColor(f.status?.name));
|
|
189
|
+
field('Priority', priorityColor(f.priority?.name));
|
|
190
|
+
field('Type', f.issuetype?.name);
|
|
191
|
+
const rt = requestType(f);
|
|
192
|
+
if (rt) field('Request Type', rt);
|
|
193
|
+
const mk = migrationServerKey(f);
|
|
194
|
+
if (mk) field('Migration Server Key', mk);
|
|
195
|
+
field('Assignee', f.assignee?.displayName || pc.dim('Unassigned'));
|
|
196
|
+
field('Reporter', f.reporter?.displayName);
|
|
197
|
+
field('Labels', f.labels?.length > 0 ? f.labels.join(', ') : null);
|
|
198
|
+
field('Components', f.components?.map((c) => c.name).join(', ') || null);
|
|
199
|
+
field('Created', new Date(f.created).toLocaleString());
|
|
200
|
+
field('Updated', new Date(f.updated).toLocaleString());
|
|
201
|
+
|
|
202
|
+
const desc = descriptionText(f.description);
|
|
203
|
+
if (desc) {
|
|
204
|
+
heading('Description');
|
|
205
|
+
const lines = desc.split('\n');
|
|
206
|
+
const truncated = lines.slice(0, 30);
|
|
207
|
+
for (const line of truncated) {
|
|
208
|
+
console.log(` ${line}`);
|
|
209
|
+
}
|
|
210
|
+
if (lines.length > 30) {
|
|
211
|
+
console.log(pc.dim(` ... (${lines.length - 30} more lines)`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log('');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function formatIssueComments(comments) {
|
|
219
|
+
if (comments.length === 0) {
|
|
220
|
+
console.log(pc.dim(' No comments.'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const c of comments) {
|
|
225
|
+
console.log(`\n ${pc.bold(c.author?.displayName || 'Unknown')} ${pc.dim(relativeTime(c.created))}`);
|
|
226
|
+
const body = descriptionText(c.body);
|
|
227
|
+
if (body) {
|
|
228
|
+
for (const line of body.split('\n')) {
|
|
229
|
+
console.log(` ${line}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log('');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function formatIssueSearch(result) {
|
|
237
|
+
const { issues } = result;
|
|
238
|
+
|
|
239
|
+
if (result.queue) {
|
|
240
|
+
heading(`Queue: ${result.queue.name}`);
|
|
241
|
+
console.log(pc.dim(` ${result.queue.jql}`));
|
|
242
|
+
divider();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (issues.length === 0) {
|
|
246
|
+
console.log(pc.dim(' No issues found.'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const rows = issues.map((issue) => [
|
|
251
|
+
pc.bold(issue.key),
|
|
252
|
+
(issue.fields.summary || '').length > 55
|
|
253
|
+
? issue.fields.summary.slice(0, 52) + '...'
|
|
254
|
+
: issue.fields.summary || '',
|
|
255
|
+
statusColor(issue.fields.status?.name),
|
|
256
|
+
issue.fields.assignee?.displayName || pc.dim('Unassigned'),
|
|
257
|
+
priorityColor(issue.fields.priority?.name),
|
|
258
|
+
relativeTime(issue.fields.updated),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
console.log(table(['Key', 'Summary', 'Status', 'Assignee', 'Priority', 'Updated'], rows));
|
|
262
|
+
|
|
263
|
+
if (!result.isLast && result.nextPageToken) {
|
|
264
|
+
console.log(pc.dim(` More results available — pass --next ${result.nextPageToken} to continue`));
|
|
265
|
+
}
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
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.4.
|
|
16
|
+
.version('0.4.1')
|
|
17
17
|
.option('--json', 'Output raw JSON')
|
|
18
18
|
.option('--plain', 'Output compact plain text (no tables, no colors)');
|
|
19
19
|
|
package/CLAUDE.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
`devtool` — a Node.js CLI (`dev`) for interacting with Bitbucket pull requests and Jira tickets from the terminal. Uses ES modules, commander for CLI parsing, and the `conf` package for persistent config storage.
|
|
8
|
-
|
|
9
|
-
## Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# Run the CLI
|
|
13
|
-
node src/index.js <command>
|
|
14
|
-
|
|
15
|
-
# Install globally for `dev` command
|
|
16
|
-
npm link
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
No build step, no tests, no linter configured yet.
|
|
20
|
-
|
|
21
|
-
## Architecture
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
src/
|
|
25
|
-
index.js # Entry point — commander program, registers subcommands, auto-setup hook
|
|
26
|
-
config/store.js # Conf-based config (persisted to OS config dir via `conf` package)
|
|
27
|
-
clients/ # API clients (bitbucket.js uses fetch + Basic auth, jira.js uses jira.js SDK + Bearer token)
|
|
28
|
-
commands/ # Commander subcommands (config, pr, jira)
|
|
29
|
-
formatters/ # Output formatting — each has three variants: rich (colored tables), plain (compact text), JSON (curated objects)
|
|
30
|
-
utils/
|
|
31
|
-
output.js # Shared UI primitives: spinner, table (cli-table3), heading, field, divider, mask
|
|
32
|
-
errors.js # handleError — maps HTTP status codes and connection errors to user-friendly messages
|
|
33
|
-
mode.js # getOutputMode — reads --json/--plain global flags, returns 'json'|'plain'|'rich'
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Output Mode Pattern
|
|
37
|
-
|
|
38
|
-
Every command supports three output modes via global flags `--json` and `--plain`:
|
|
39
|
-
- **rich** (default): colored output with tables (cli-table3) and spinners
|
|
40
|
-
- **plain**: no colors, no tables, compact text — suitable for piping
|
|
41
|
-
- **json**: curated JSON (not raw API responses) via `curate*Json` functions in formatters
|
|
42
|
-
|
|
43
|
-
Commands follow this pattern: check mode → conditionally show spinner → fetch data → format via mode-specific function. The `withMode` helper in `commands/pr.js` abstracts this for simple cases.
|
|
44
|
-
|
|
45
|
-
### Config
|
|
46
|
-
|
|
47
|
-
Stored via `conf` package (projectName: `devtool`). Interactive setup runs automatically on first use (preAction hook in index.js). Config keys:
|
|
48
|
-
- `bitbucket.{workspace, defaultRepo, email, token}`
|
|
49
|
-
- `jira.{baseUrl, token, defaultProject}`
|
|
50
|
-
|
|
51
|
-
### API Clients
|
|
52
|
-
|
|
53
|
-
- **Bitbucket** (`clients/bitbucket.js`): Raw fetch against `api.bitbucket.org/2.0`, Basic auth, manual pagination via `next` links
|
|
54
|
-
- **Jira** (`clients/jira.js`): Uses `jira.js` SDK (Version2Client) with Bearer token auth (overrides SDK's built-in Basic auth via request interceptor)
|