@falldownthesystem/devtool 0.1.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/LICENSE +15 -0
- package/README.md +59 -0
- package/package.json +40 -0
- package/src/clients/bitbucket.js +85 -0
- package/src/clients/confluence.js +63 -0
- package/src/clients/jira.js +61 -0
- package/src/commands/config.js +68 -0
- package/src/commands/confluence.js +113 -0
- package/src/commands/jira.js +95 -0
- package/src/commands/pr.js +132 -0
- package/src/config/store.js +156 -0
- package/src/formatters/confluence.js +212 -0
- package/src/formatters/jira.js +236 -0
- package/src/formatters/pr.js +306 -0
- package/src/index.js +40 -0
- package/src/utils/errors.js +43 -0
- package/src/utils/mode.js +10 -0
- package/src/utils/output.js +68 -0
|
@@ -0,0 +1,236 @@
|
|
|
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') return pc.blue(name);
|
|
8
|
+
if (lower === 'to do' || lower === 'open' || lower === 'backlog') 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') return pc.dim(name);
|
|
18
|
+
if (lower === 'lowest' || lower === 'trivial') return pc.dim(name);
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function relativeTime(dateStr) {
|
|
23
|
+
if (!dateStr) return '';
|
|
24
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
25
|
+
const mins = Math.floor(diff / 60000);
|
|
26
|
+
if (mins < 60) return `${mins}m ago`;
|
|
27
|
+
const hours = Math.floor(mins / 60);
|
|
28
|
+
if (hours < 24) return `${hours}h ago`;
|
|
29
|
+
const days = Math.floor(hours / 24);
|
|
30
|
+
if (days < 30) return `${days}d ago`;
|
|
31
|
+
return new Date(dateStr).toLocaleDateString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractSprint(fields) {
|
|
35
|
+
for (const key of Object.keys(fields)) {
|
|
36
|
+
if (!key.startsWith('customfield_')) continue;
|
|
37
|
+
const val = fields[key];
|
|
38
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && val[0].name) {
|
|
39
|
+
if ('state' in val[0] || 'goal' in val[0]) {
|
|
40
|
+
return val[val.length - 1].name;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (typeof val === 'string' && val.includes('com.atlassian.greenhopper')) {
|
|
44
|
+
const match = val.match(/name=([^,\]]+)/);
|
|
45
|
+
if (match) return match[1];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractStoryPoints(fields) {
|
|
52
|
+
for (const key of Object.keys(fields)) {
|
|
53
|
+
if (!key.startsWith('customfield_')) continue;
|
|
54
|
+
const val = fields[key];
|
|
55
|
+
if (typeof val === 'number' && val > 0 && val <= 100) {
|
|
56
|
+
const keyNum = parseInt(key.replace('customfield_', ''), 10);
|
|
57
|
+
if (keyNum > 10000) return val;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function descriptionText(desc) {
|
|
64
|
+
if (!desc) return '';
|
|
65
|
+
return typeof desc === 'string' ? desc : JSON.stringify(desc);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- JSON curators ---
|
|
69
|
+
|
|
70
|
+
export function curateIssueJson(issue) {
|
|
71
|
+
const f = issue.fields;
|
|
72
|
+
return {
|
|
73
|
+
key: issue.key,
|
|
74
|
+
summary: f.summary,
|
|
75
|
+
status: f.status?.name || null,
|
|
76
|
+
priority: f.priority?.name || null,
|
|
77
|
+
type: f.issuetype?.name || null,
|
|
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
|
+
sprint: extractSprint(f),
|
|
83
|
+
storyPoints: extractStoryPoints(f),
|
|
84
|
+
created: f.created,
|
|
85
|
+
updated: f.updated,
|
|
86
|
+
description: descriptionText(f.description),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function curateCommentsJson(comments) {
|
|
91
|
+
return comments.map((c) => ({
|
|
92
|
+
author: c.author?.displayName || null,
|
|
93
|
+
date: c.created,
|
|
94
|
+
body: typeof c.body === 'string' ? c.body : JSON.stringify(c.body || ''),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function curateSearchJson(result) {
|
|
99
|
+
return {
|
|
100
|
+
total: result.total,
|
|
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
|
+
// --- Plain formatters ---
|
|
113
|
+
|
|
114
|
+
export function plainIssueView(issue) {
|
|
115
|
+
const f = issue.fields;
|
|
116
|
+
console.log(`${issue.key} ${f.summary}`);
|
|
117
|
+
console.log(`Status: ${f.status?.name || 'Unknown'}`);
|
|
118
|
+
console.log(`Priority: ${f.priority?.name || 'None'}`);
|
|
119
|
+
console.log(`Type: ${f.issuetype?.name || 'Unknown'}`);
|
|
120
|
+
console.log(`Assignee: ${f.assignee?.displayName || 'Unassigned'}`);
|
|
121
|
+
console.log(`Reporter: ${f.reporter?.displayName || 'Unknown'}`);
|
|
122
|
+
if (f.labels?.length > 0) console.log(`Labels: ${f.labels.join(', ')}`);
|
|
123
|
+
if (f.components?.length > 0) console.log(`Components: ${f.components.map((c) => c.name).join(', ')}`);
|
|
124
|
+
const sprint = extractSprint(f);
|
|
125
|
+
if (sprint) console.log(`Sprint: ${sprint}`);
|
|
126
|
+
const points = extractStoryPoints(f);
|
|
127
|
+
if (points) console.log(`Story Points: ${points}`);
|
|
128
|
+
console.log(`Created: ${f.created}`);
|
|
129
|
+
console.log(`Updated: ${f.updated}`);
|
|
130
|
+
const desc = descriptionText(f.description);
|
|
131
|
+
if (desc) console.log(`Description:\n${desc.trim()}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function plainIssueComments(comments) {
|
|
135
|
+
if (comments.length === 0) { console.log('No comments.'); return; }
|
|
136
|
+
for (const c of comments) {
|
|
137
|
+
const body = typeof c.body === 'string' ? c.body : JSON.stringify(c.body || '');
|
|
138
|
+
console.log(`${c.author?.displayName || 'Unknown'} (${relativeTime(c.created)}): ${body.trim()}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function plainIssueSearch(result) {
|
|
143
|
+
if (result.issues.length === 0) { console.log('No issues found.'); return; }
|
|
144
|
+
for (const issue of result.issues) {
|
|
145
|
+
const f = issue.fields;
|
|
146
|
+
console.log(`${issue.key} [${f.status?.name}] "${f.summary}" - ${f.assignee?.displayName || 'Unassigned'} (${f.priority?.name}, ${relativeTime(f.updated)})`);
|
|
147
|
+
}
|
|
148
|
+
const showing = result.startAt + result.issues.length;
|
|
149
|
+
if (result.total > showing) console.log(`Showing ${result.startAt + 1}-${showing} of ${result.total}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Rich formatters (original) ---
|
|
153
|
+
|
|
154
|
+
export function formatIssueView(issue) {
|
|
155
|
+
const f = issue.fields;
|
|
156
|
+
|
|
157
|
+
heading(`${issue.key} ${f.summary}`);
|
|
158
|
+
divider();
|
|
159
|
+
field('Status', statusColor(f.status?.name));
|
|
160
|
+
field('Priority', priorityColor(f.priority?.name));
|
|
161
|
+
field('Type', f.issuetype?.name);
|
|
162
|
+
field('Assignee', f.assignee?.displayName || pc.dim('Unassigned'));
|
|
163
|
+
field('Reporter', f.reporter?.displayName);
|
|
164
|
+
field('Labels', f.labels?.length > 0 ? f.labels.join(', ') : null);
|
|
165
|
+
field('Components', f.components?.map((c) => c.name).join(', ') || null);
|
|
166
|
+
|
|
167
|
+
const sprint = extractSprint(f);
|
|
168
|
+
if (sprint) field('Sprint', sprint);
|
|
169
|
+
|
|
170
|
+
const points = extractStoryPoints(f);
|
|
171
|
+
if (points) field('Story Points', points);
|
|
172
|
+
|
|
173
|
+
field('Created', new Date(f.created).toLocaleString());
|
|
174
|
+
field('Updated', new Date(f.updated).toLocaleString());
|
|
175
|
+
|
|
176
|
+
if (f.description) {
|
|
177
|
+
heading('Description');
|
|
178
|
+
const desc = descriptionText(f.description);
|
|
179
|
+
const lines = desc.trim().split('\n');
|
|
180
|
+
const truncated = lines.slice(0, 30);
|
|
181
|
+
for (const line of truncated) {
|
|
182
|
+
console.log(` ${line}`);
|
|
183
|
+
}
|
|
184
|
+
if (lines.length > 30) {
|
|
185
|
+
console.log(pc.dim(` ... (${lines.length - 30} more lines)`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatIssueComments(comments) {
|
|
193
|
+
if (comments.length === 0) {
|
|
194
|
+
console.log(pc.dim(' No comments.'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const c of comments) {
|
|
199
|
+
console.log(`\n ${pc.bold(c.author?.displayName || 'Unknown')} ${pc.dim(relativeTime(c.created))}`);
|
|
200
|
+
if (c.body) {
|
|
201
|
+
const lines = (typeof c.body === 'string' ? c.body : JSON.stringify(c.body)).trim().split('\n');
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
console.log(` ${line}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.log('');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function formatIssueSearch(result) {
|
|
211
|
+
const { issues, total, startAt, maxResults } = result;
|
|
212
|
+
|
|
213
|
+
if (issues.length === 0) {
|
|
214
|
+
console.log(pc.dim(' No issues found.'));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const rows = issues.map((issue) => [
|
|
219
|
+
pc.bold(issue.key),
|
|
220
|
+
(issue.fields.summary || '').length > 55
|
|
221
|
+
? issue.fields.summary.slice(0, 52) + '...'
|
|
222
|
+
: issue.fields.summary || '',
|
|
223
|
+
statusColor(issue.fields.status?.name),
|
|
224
|
+
issue.fields.assignee?.displayName || pc.dim('Unassigned'),
|
|
225
|
+
priorityColor(issue.fields.priority?.name),
|
|
226
|
+
relativeTime(issue.fields.updated),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
console.log(table(['Key', 'Summary', 'Status', 'Assignee', 'Priority', 'Updated'], rows));
|
|
230
|
+
|
|
231
|
+
const showing = startAt + issues.length;
|
|
232
|
+
if (total > showing) {
|
|
233
|
+
console.log(pc.dim(` Showing ${startAt + 1}-${showing} of ${total} results`));
|
|
234
|
+
}
|
|
235
|
+
console.log('');
|
|
236
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { table, heading, field, divider, pc } from '../utils/output.js';
|
|
2
|
+
|
|
3
|
+
function stateLabel(state, draft) {
|
|
4
|
+
if (draft) return pc.yellow('DRAFT');
|
|
5
|
+
switch (state) {
|
|
6
|
+
case 'OPEN': return pc.green(state);
|
|
7
|
+
case 'MERGED': return pc.blue(state);
|
|
8
|
+
case 'DECLINED': return pc.red(state);
|
|
9
|
+
case 'SUPERSEDED': return pc.dim(state);
|
|
10
|
+
default: return state;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stateText(state, draft) {
|
|
15
|
+
return draft ? 'DRAFT' : state;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function approvalStatus(participant) {
|
|
19
|
+
if (participant.approved) return pc.green('APPROVED');
|
|
20
|
+
if (participant.state === 'changes_requested') return pc.red('CHANGES REQUESTED');
|
|
21
|
+
return pc.dim('PENDING');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function approvalStatusText(participant) {
|
|
25
|
+
if (participant.approved) return 'APPROVED';
|
|
26
|
+
if (participant.state === 'changes_requested') return 'CHANGES_REQUESTED';
|
|
27
|
+
return 'PENDING';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function approvalCount(pr) {
|
|
31
|
+
const approved = (pr.participants || []).filter((p) => p.approved).length;
|
|
32
|
+
const total = (pr.participants || []).filter((p) => p.role === 'REVIEWER').length;
|
|
33
|
+
if (total === 0) return pc.dim('0');
|
|
34
|
+
if (approved === total) return pc.green(`${approved}/${total}`);
|
|
35
|
+
return `${approved}/${total}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function approvalNumbers(pr) {
|
|
39
|
+
const approved = (pr.participants || []).filter((p) => p.approved).length;
|
|
40
|
+
const total = (pr.participants || []).filter((p) => p.role === 'REVIEWER').length;
|
|
41
|
+
return { approved, total };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function relativeTime(dateStr) {
|
|
45
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
46
|
+
const mins = Math.floor(diff / 60000);
|
|
47
|
+
if (mins < 60) return `${mins}m ago`;
|
|
48
|
+
const hours = Math.floor(mins / 60);
|
|
49
|
+
if (hours < 24) return `${hours}h ago`;
|
|
50
|
+
const days = Math.floor(hours / 24);
|
|
51
|
+
if (days < 30) return `${days}d ago`;
|
|
52
|
+
return new Date(dateStr).toLocaleDateString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function reviewers(pr) {
|
|
56
|
+
return (pr.participants || []).filter((p) => p.role === 'REVIEWER');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- JSON curators ---
|
|
60
|
+
|
|
61
|
+
export function curateListJson(prs) {
|
|
62
|
+
return prs.map((pr) => {
|
|
63
|
+
const { approved, total } = approvalNumbers(pr);
|
|
64
|
+
return {
|
|
65
|
+
id: pr.id,
|
|
66
|
+
title: pr.title,
|
|
67
|
+
author: pr.author.display_name,
|
|
68
|
+
state: pr.state,
|
|
69
|
+
draft: pr.draft || false,
|
|
70
|
+
approvals: { approved, total },
|
|
71
|
+
source: pr.source.branch.name,
|
|
72
|
+
destination: pr.destination.branch.name,
|
|
73
|
+
updated: pr.updated_on,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function curateViewJson(pr) {
|
|
79
|
+
return {
|
|
80
|
+
id: pr.id,
|
|
81
|
+
title: pr.title,
|
|
82
|
+
state: pr.state,
|
|
83
|
+
draft: pr.draft || false,
|
|
84
|
+
author: pr.author.display_name,
|
|
85
|
+
source: pr.source.branch.name,
|
|
86
|
+
destination: pr.destination.branch.name,
|
|
87
|
+
created: pr.created_on,
|
|
88
|
+
updated: pr.updated_on,
|
|
89
|
+
description: pr.description || '',
|
|
90
|
+
reviewers: reviewers(pr).map((r) => ({
|
|
91
|
+
name: r.user.display_name,
|
|
92
|
+
status: approvalStatusText(r),
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function curateCommentsJson(comments) {
|
|
98
|
+
return comments
|
|
99
|
+
.filter((c) => !c.deleted)
|
|
100
|
+
.sort((a, b) => new Date(a.created_on) - new Date(b.created_on))
|
|
101
|
+
.map((c) => ({
|
|
102
|
+
author: c.user.display_name,
|
|
103
|
+
date: c.created_on,
|
|
104
|
+
content: c.content?.raw || '',
|
|
105
|
+
inline: c.inline ? { path: c.inline.path, line: c.inline.from || c.inline.to } : null,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function curateDiffstatJson(files) {
|
|
110
|
+
return files.map((f) => ({
|
|
111
|
+
file: f.new?.path || f.old?.path || null,
|
|
112
|
+
status: f.status,
|
|
113
|
+
added: f.lines_added || 0,
|
|
114
|
+
removed: f.lines_removed || 0,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function curateActivityJson(activities) {
|
|
119
|
+
return activities.map((a) => {
|
|
120
|
+
if (a.approval) return { type: 'approval', user: a.approval.user.display_name, date: a.approval.date };
|
|
121
|
+
if (a.update) return { type: 'update', user: a.update.author?.display_name, date: a.update.date, state: a.update.state };
|
|
122
|
+
if (a.comment) return { type: 'comment', user: a.comment.user.display_name, date: a.comment.created_on, content: (a.comment.content?.raw || '').split('\n')[0] };
|
|
123
|
+
if (a.changes_requested) return { type: 'changes_requested', user: a.changes_requested.user.display_name, date: a.changes_requested.date };
|
|
124
|
+
return { type: 'unknown' };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- Plain formatters ---
|
|
129
|
+
|
|
130
|
+
export function plainPrList(prs) {
|
|
131
|
+
if (prs.length === 0) { console.log('No pull requests found.'); return; }
|
|
132
|
+
for (const pr of prs) {
|
|
133
|
+
const { approved, total } = approvalNumbers(pr);
|
|
134
|
+
console.log(`#${pr.id} ${stateText(pr.state, pr.draft)} "${pr.title}" by ${pr.author.display_name} (${approved}/${total} approvals, ${relativeTime(pr.updated_on)})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function plainPrView(pr) {
|
|
139
|
+
console.log(`#${pr.id} ${pr.title}`);
|
|
140
|
+
console.log(`State: ${stateText(pr.state, pr.draft)}`);
|
|
141
|
+
console.log(`Author: ${pr.author.display_name}`);
|
|
142
|
+
console.log(`Source: ${pr.source.branch.name} -> ${pr.destination.branch.name}`);
|
|
143
|
+
console.log(`Created: ${pr.created_on}`);
|
|
144
|
+
console.log(`Updated: ${pr.updated_on}`);
|
|
145
|
+
const revs = reviewers(pr);
|
|
146
|
+
if (revs.length > 0) {
|
|
147
|
+
console.log(`Reviewers: ${revs.map((r) => `${r.user.display_name} (${approvalStatusText(r)})`).join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
if (pr.description) {
|
|
150
|
+
console.log(`Description:\n${pr.description.trim()}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function plainPrComments(comments) {
|
|
155
|
+
const sorted = comments.filter((c) => !c.deleted).sort((a, b) => new Date(a.created_on) - new Date(b.created_on));
|
|
156
|
+
if (sorted.length === 0) { console.log('No comments.'); return; }
|
|
157
|
+
for (const c of sorted) {
|
|
158
|
+
const inline = c.inline ? ` [${c.inline.path}:${c.inline.from || c.inline.to}]` : '';
|
|
159
|
+
console.log(`${c.user.display_name} (${relativeTime(c.created_on)})${inline}: ${(c.content?.raw || '').trim()}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function plainPrDiffstat(files) {
|
|
164
|
+
if (files.length === 0) { console.log('No file changes.'); return; }
|
|
165
|
+
for (const f of files) {
|
|
166
|
+
console.log(`${f.status} ${f.new?.path || f.old?.path} +${f.lines_added || 0} -${f.lines_removed || 0}`);
|
|
167
|
+
}
|
|
168
|
+
const added = files.reduce((s, f) => s + (f.lines_added || 0), 0);
|
|
169
|
+
const removed = files.reduce((s, f) => s + (f.lines_removed || 0), 0);
|
|
170
|
+
console.log(`${files.length} files changed, +${added}, -${removed}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function plainPrActivity(activities) {
|
|
174
|
+
if (activities.length === 0) { console.log('No activity.'); return; }
|
|
175
|
+
for (const a of activities) {
|
|
176
|
+
if (a.approval) console.log(`APPROVED by ${a.approval.user.display_name} (${relativeTime(a.approval.date)})`);
|
|
177
|
+
else if (a.update) console.log(`UPDATED by ${a.update.author?.display_name || 'Unknown'} (${relativeTime(a.update.date)})${a.update.state ? ` -> ${a.update.state}` : ''}`);
|
|
178
|
+
else if (a.comment) console.log(`COMMENT by ${a.comment.user.display_name} (${relativeTime(a.comment.created_on)}): ${(a.comment.content?.raw || '').split('\n')[0].slice(0, 80)}`);
|
|
179
|
+
else if (a.changes_requested) console.log(`CHANGES_REQUESTED by ${a.changes_requested.user.display_name} (${relativeTime(a.changes_requested.date)})`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Rich formatters (original) ---
|
|
184
|
+
|
|
185
|
+
export function formatPrList(prs) {
|
|
186
|
+
if (prs.length === 0) {
|
|
187
|
+
console.log(pc.dim(' No pull requests found.'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const rows = prs.map((pr) => [
|
|
192
|
+
pc.bold(`#${pr.id}`),
|
|
193
|
+
pr.title.length > 60 ? pr.title.slice(0, 57) + '...' : pr.title,
|
|
194
|
+
pr.author.display_name,
|
|
195
|
+
stateLabel(pr.state, pr.draft),
|
|
196
|
+
approvalCount(pr),
|
|
197
|
+
relativeTime(pr.updated_on),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
console.log(table(['ID', 'Title', 'Author', 'State', 'Approvals', 'Updated'], rows));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatPrView(pr) {
|
|
204
|
+
heading(`#${pr.id} ${pr.title}`);
|
|
205
|
+
divider();
|
|
206
|
+
field('State', stateLabel(pr.state, pr.draft));
|
|
207
|
+
field('Author', pr.author.display_name);
|
|
208
|
+
field('Source', `${pr.source.branch.name} (${pr.source.repository?.full_name || ''})`);
|
|
209
|
+
field('Destination', `${pr.destination.branch.name} (${pr.destination.repository?.full_name || ''})`);
|
|
210
|
+
field('Created', new Date(pr.created_on).toLocaleString());
|
|
211
|
+
field('Updated', new Date(pr.updated_on).toLocaleString());
|
|
212
|
+
|
|
213
|
+
if (pr.description) {
|
|
214
|
+
heading('Description');
|
|
215
|
+
console.log(pr.description.trim());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const revs = reviewers(pr);
|
|
219
|
+
if (revs.length > 0) {
|
|
220
|
+
heading('Reviewers');
|
|
221
|
+
for (const r of revs) {
|
|
222
|
+
console.log(` ${r.user.display_name}: ${approvalStatus(r)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log('');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function formatPrComments(comments) {
|
|
230
|
+
if (comments.length === 0) {
|
|
231
|
+
console.log(pc.dim(' No comments.'));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sorted = comments
|
|
236
|
+
.filter((c) => !c.deleted)
|
|
237
|
+
.sort((a, b) => new Date(a.created_on) - new Date(b.created_on));
|
|
238
|
+
|
|
239
|
+
for (const c of sorted) {
|
|
240
|
+
const inline = c.inline ? pc.dim(` [${c.inline.path}:${c.inline.from || c.inline.to}]`) : '';
|
|
241
|
+
console.log(`\n ${pc.bold(c.user.display_name)} ${pc.dim(relativeTime(c.created_on))}${inline}`);
|
|
242
|
+
if (c.content?.raw) {
|
|
243
|
+
const lines = c.content.raw.trim().split('\n');
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
console.log(` ${line}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
console.log('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function formatPrDiffstat(files) {
|
|
253
|
+
if (files.length === 0) {
|
|
254
|
+
console.log(pc.dim(' No file changes.'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const rows = files.map((f) => {
|
|
259
|
+
const added = f.lines_added || 0;
|
|
260
|
+
const removed = f.lines_removed || 0;
|
|
261
|
+
return [
|
|
262
|
+
f.new?.path || f.old?.path || '(unknown)',
|
|
263
|
+
f.status,
|
|
264
|
+
pc.green(`+${added}`),
|
|
265
|
+
pc.red(`-${removed}`),
|
|
266
|
+
];
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
console.log(table(['File', 'Status', 'Added', 'Removed'], rows));
|
|
270
|
+
|
|
271
|
+
const totalAdded = files.reduce((s, f) => s + (f.lines_added || 0), 0);
|
|
272
|
+
const totalRemoved = files.reduce((s, f) => s + (f.lines_removed || 0), 0);
|
|
273
|
+
console.log(` ${files.length} files changed, ${pc.green(`+${totalAdded}`)}, ${pc.red(`-${totalRemoved}`)}`);
|
|
274
|
+
console.log('');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function formatPrActivity(activities) {
|
|
278
|
+
if (activities.length === 0) {
|
|
279
|
+
console.log(pc.dim(' No activity.'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const a of activities) {
|
|
284
|
+
if (a.approval) {
|
|
285
|
+
const user = a.approval.user.display_name;
|
|
286
|
+
const date = relativeTime(a.approval.date);
|
|
287
|
+
console.log(` ${pc.green('APPROVED')} by ${pc.bold(user)} ${pc.dim(date)}`);
|
|
288
|
+
} else if (a.update) {
|
|
289
|
+
const user = a.update.author?.display_name || 'Unknown';
|
|
290
|
+
const date = relativeTime(a.update.date);
|
|
291
|
+
const state = a.update.state ? ` → ${stateLabel(a.update.state, false)}` : '';
|
|
292
|
+
console.log(` ${pc.blue('UPDATED')} by ${pc.bold(user)} ${pc.dim(date)}${state}`);
|
|
293
|
+
} else if (a.comment) {
|
|
294
|
+
const user = a.comment.user.display_name;
|
|
295
|
+
const date = relativeTime(a.comment.created_on);
|
|
296
|
+
const snippet = (a.comment.content?.raw || '').split('\n')[0].slice(0, 80);
|
|
297
|
+
console.log(` ${pc.yellow('COMMENT')} by ${pc.bold(user)} ${pc.dim(date)}`);
|
|
298
|
+
if (snippet) console.log(` ${snippet}`);
|
|
299
|
+
} else if (a.changes_requested) {
|
|
300
|
+
const user = a.changes_requested.user.display_name;
|
|
301
|
+
const date = relativeTime(a.changes_requested.date);
|
|
302
|
+
console.log(` ${pc.red('CHANGES REQUESTED')} by ${pc.bold(user)} ${pc.dim(date)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
console.log('');
|
|
306
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { isConfigured, runSetup } from './config/store.js';
|
|
5
|
+
import configCmd from './commands/config.js';
|
|
6
|
+
import prCmd from './commands/pr.js';
|
|
7
|
+
import jiraCmd from './commands/jira.js';
|
|
8
|
+
import confluenceCmd from './commands/confluence.js';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('dev')
|
|
14
|
+
.description('CLI for Bitbucket PRs, Jira tickets, and Confluence pages')
|
|
15
|
+
.version('0.2.0')
|
|
16
|
+
.option('--json', 'Output raw JSON')
|
|
17
|
+
.option('--plain', 'Output compact plain text (no tables, no colors)');
|
|
18
|
+
|
|
19
|
+
program.addCommand(configCmd);
|
|
20
|
+
program.addCommand(prCmd);
|
|
21
|
+
program.addCommand(jiraCmd);
|
|
22
|
+
program.addCommand(confluenceCmd);
|
|
23
|
+
|
|
24
|
+
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
25
|
+
const cmdChain = [];
|
|
26
|
+
let cmd = actionCommand;
|
|
27
|
+
while (cmd) {
|
|
28
|
+
cmdChain.unshift(cmd.name());
|
|
29
|
+
cmd = cmd.parent;
|
|
30
|
+
}
|
|
31
|
+
if (cmdChain.includes('config')) return;
|
|
32
|
+
|
|
33
|
+
if (!isConfigured()) {
|
|
34
|
+
console.log('First run detected. Let\'s set up your configuration.\n');
|
|
35
|
+
await runSetup();
|
|
36
|
+
console.log('');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program.parse();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { error } from './output.js';
|
|
2
|
+
|
|
3
|
+
export function handleError(err, context = '') {
|
|
4
|
+
if (err.status === 401 || err.statusCode === 401) {
|
|
5
|
+
const service = context.includes('bitbucket') ? 'Bitbucket' : context.includes('jira') ? 'Jira' : context.includes('confluence') ? 'Confluence' : '';
|
|
6
|
+
error(`Authentication failed${service ? ` for ${service}` : ''}. Check your API token.`);
|
|
7
|
+
if (context) error(`Endpoint: ${context}`);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (err.status === 403 || err.statusCode === 403) {
|
|
13
|
+
error('Access denied. Your token may lack the required scopes.');
|
|
14
|
+
if (context) error(`Endpoint: ${context}`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (err.status === 404 || err.statusCode === 404) {
|
|
20
|
+
error('Not found. Check that the resource exists and you have access.');
|
|
21
|
+
if (context) error(`Endpoint: ${context}`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
27
|
+
error(`Could not connect to server.`);
|
|
28
|
+
if (context) error(`URL: ${context}`);
|
|
29
|
+
error('Check that the URL is correct and the server is reachable.');
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' || err.code === 'SELF_SIGNED_CERT_IN_CHAIN') {
|
|
35
|
+
error('SSL certificate error. If using a self-signed cert, set NODE_TLS_REJECT_UNAUTHORIZED=0');
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
error(err.message || String(err));
|
|
41
|
+
if (context) error(`Context: ${context}`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function getOutputMode(cmd) {
|
|
2
|
+
const globals = cmd.optsWithGlobals ? cmd.optsWithGlobals() : cmd.opts();
|
|
3
|
+
if (globals.json) return 'json';
|
|
4
|
+
if (globals.plain) return 'plain';
|
|
5
|
+
return 'rich';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function outputJson(data) {
|
|
9
|
+
console.log(JSON.stringify(data, null, 2));
|
|
10
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
|
|
4
|
+
const HIDE_CURSOR = '\x1B[?25l';
|
|
5
|
+
const SHOW_CURSOR = '\x1B[?25h';
|
|
6
|
+
const CLEAR_LINE = '\x1B[2K\r';
|
|
7
|
+
|
|
8
|
+
export function spinner(text) {
|
|
9
|
+
return {
|
|
10
|
+
start() {
|
|
11
|
+
process.stdout.write(`${HIDE_CURSOR}${pc.dim('–')} ${text}`);
|
|
12
|
+
return this;
|
|
13
|
+
},
|
|
14
|
+
success({ text: msg } = {}) {
|
|
15
|
+
process.stdout.write(`${CLEAR_LINE}${pc.green('✓')} ${msg || text}${SHOW_CURSOR}\n`);
|
|
16
|
+
return this;
|
|
17
|
+
},
|
|
18
|
+
error({ text: msg } = {}) {
|
|
19
|
+
process.stdout.write(`${CLEAR_LINE}${pc.red('✗')} ${msg || text}${SHOW_CURSOR}\n`);
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function table(head, rows, options = {}) {
|
|
26
|
+
const t = new Table({
|
|
27
|
+
head: head.map((h) => pc.bold(pc.cyan(h))),
|
|
28
|
+
style: { head: [], border: [] },
|
|
29
|
+
wordWrap: true,
|
|
30
|
+
...options,
|
|
31
|
+
});
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
t.push(row);
|
|
34
|
+
}
|
|
35
|
+
return t.toString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function heading(text) {
|
|
39
|
+
console.log(`\n${pc.bold(pc.cyan(text))}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function field(label, value) {
|
|
43
|
+
if (value === undefined || value === null || value === '') return;
|
|
44
|
+
console.log(` ${pc.dim(label + ':')} ${value}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function divider() {
|
|
48
|
+
console.log(pc.dim('─'.repeat(60)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function success(text) {
|
|
52
|
+
console.log(pc.green(`✓ ${text}`));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function warn(text) {
|
|
56
|
+
console.log(pc.yellow(`⚠ ${text}`));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function error(text) {
|
|
60
|
+
console.log(pc.red(`✗ ${text}`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function mask(str) {
|
|
64
|
+
if (!str || str.length < 8) return '****';
|
|
65
|
+
return str.slice(0, 4) + '•'.repeat(Math.min(str.length - 4, 16));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { pc };
|