reslop 0.2.1 → 0.2.3
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/CHANGELOG.md +14 -1
- package/README.md +11 -1
- package/lib/ansi.js +120 -4
- package/lib/cli.js +3 -0
- package/lib/deps.js +43 -3
- package/lib/git-branches.js +47 -15
- package/lib/keys.js +10 -4
- package/lib/npm-commands.js +513 -0
- package/lib/render/branches.js +21 -28
- package/lib/render/chrome.js +137 -122
- package/lib/render/commits.js +604 -54
- package/lib/render/frame.js +38 -4
- package/lib/render/npm.js +228 -0
- package/lib/render/primitives.js +7 -36
- package/lib/report-model.js +127 -0
- package/lib/report-parse.js +94 -0
- package/lib/report-render.js +87 -0
- package/lib/report-run.js +164 -0
- package/lib/report-test.js +506 -0
- package/lib/report-tools.js +311 -0
- package/lib/session/actions.js +51 -2
- package/lib/session/commit.js +94 -19
- package/lib/session/compose.js +48 -5
- package/lib/session/dispatch.js +30 -2
- package/lib/session/files-pane.js +20 -4
- package/lib/session/lifecycle.js +4 -1
- package/lib/session/navigation.js +1 -0
- package/lib/session/npm.js +335 -0
- package/lib/session/pointer.js +8 -0
- package/lib/session/view.js +14 -3
- package/lib/session.js +10 -2
- package/package.json +6 -6
package/lib/render/frame.js
CHANGED
|
@@ -8,18 +8,20 @@ const files = require('./files.js');
|
|
|
8
8
|
const branches = require('./branches.js');
|
|
9
9
|
const commits = require('./commits.js');
|
|
10
10
|
const notes = require('./notes.js');
|
|
11
|
+
const npm = require('./npm.js');
|
|
11
12
|
const diff = require('./diff.js');
|
|
12
13
|
const unit = require('./unit.js');
|
|
13
14
|
|
|
14
15
|
const { RESET } = ansi;
|
|
15
16
|
const { LIST_PANES, paintBodyFill, paneResult } = primitives;
|
|
16
|
-
const { paintHeader,
|
|
17
|
+
const { paintHeader, paintFooter, footerHits, paintStatusLine } = chrome;
|
|
17
18
|
const { layoutButtons, statusBranchHit, statusChoiceHits } = chrome;
|
|
18
19
|
const { hiddenActions, dimFooterActions, extraFooterActions } = chrome;
|
|
19
20
|
const { paintNotePanel, paintBodyTodo } = notes;
|
|
20
21
|
const { paintBodyFiles } = files;
|
|
21
22
|
const { paintBodyBranches } = branches;
|
|
22
23
|
const { paintBodyCommits } = commits;
|
|
24
|
+
const { paintBodyNpm } = npm;
|
|
23
25
|
const { paintBodyDiff } = diff;
|
|
24
26
|
const { paintBodyUnit } = unit;
|
|
25
27
|
|
|
@@ -28,6 +30,7 @@ const bodyKind = (view) => {
|
|
|
28
30
|
if (pane === 'files' || pane === 'branches' || pane === 'commits') {
|
|
29
31
|
return pane;
|
|
30
32
|
}
|
|
33
|
+
if (pane === 'npm') return 'npm';
|
|
31
34
|
if (pane === 'unit') return 'unit';
|
|
32
35
|
if (!view.item) return 'empty';
|
|
33
36
|
if (view.item.origin === 'todo') return 'todo';
|
|
@@ -47,6 +50,7 @@ const BODY_PAINT = {
|
|
|
47
50
|
files: paintBodyFiles,
|
|
48
51
|
branches: paintBodyBranches,
|
|
49
52
|
commits: paintBodyCommits,
|
|
53
|
+
npm: paintBodyNpm,
|
|
50
54
|
empty: paintBodyEmpty,
|
|
51
55
|
todo: paintBodyTodo,
|
|
52
56
|
diff: paintBodyDiff,
|
|
@@ -75,6 +79,33 @@ const bodyFillKind = (listPane, splitBody) => {
|
|
|
75
79
|
return 'plain';
|
|
76
80
|
};
|
|
77
81
|
|
|
82
|
+
const ownerSpan = (owners, focus) => {
|
|
83
|
+
let first = -1;
|
|
84
|
+
let last = -1;
|
|
85
|
+
for (let i = 0; i < owners.length; i++) {
|
|
86
|
+
if (owners[i] !== focus) continue;
|
|
87
|
+
if (first < 0) first = i;
|
|
88
|
+
last = i;
|
|
89
|
+
}
|
|
90
|
+
return { first, last };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const revealScroll = (owners, focus, height, prefer) => {
|
|
94
|
+
const span = ownerSpan(owners, focus);
|
|
95
|
+
if (span.first < 0) return prefer;
|
|
96
|
+
const room = Math.max(1, height);
|
|
97
|
+
let start = prefer;
|
|
98
|
+
if (span.first < start) start = span.first;
|
|
99
|
+
if (span.last >= start + room) {
|
|
100
|
+
const fits = span.last - span.first + 1 <= room;
|
|
101
|
+
start = fits ? span.last - room + 1 : span.first;
|
|
102
|
+
}
|
|
103
|
+
const maxStart = Math.max(0, owners.length - room);
|
|
104
|
+
if (start > maxStart) start = maxStart;
|
|
105
|
+
if (start < 0) start = 0;
|
|
106
|
+
return start;
|
|
107
|
+
};
|
|
108
|
+
|
|
78
109
|
const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
|
|
79
110
|
if (!cursor) return null;
|
|
80
111
|
const row = cursor.row + shift;
|
|
@@ -119,7 +150,10 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
|
|
|
119
150
|
}
|
|
120
151
|
const scrollRaw = view.scroll ?? 0;
|
|
121
152
|
const scrollMax = Math.max(0, body.length - bodyH);
|
|
122
|
-
|
|
153
|
+
let scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
|
|
154
|
+
if (view.item && view.item.origin === 'todo') {
|
|
155
|
+
scroll = revealScroll(todoOwners, view.todoFocus ?? 0, bodyH, scroll);
|
|
156
|
+
}
|
|
123
157
|
const end = Math.min(body.length, scroll + bodyH);
|
|
124
158
|
const copied = Math.max(0, end - scroll);
|
|
125
159
|
const slice = new Array(bodyH);
|
|
@@ -165,7 +199,7 @@ const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
|
|
|
165
199
|
for (const row of notes.noteRows) rows.push(row);
|
|
166
200
|
const status = view.status ?? '';
|
|
167
201
|
rows.push(paintStatusLine(view, status, width, color));
|
|
168
|
-
rows.push(
|
|
202
|
+
rows.push(paintFooter(view, layout, width, color));
|
|
169
203
|
return overlayRows(rows, view.selection);
|
|
170
204
|
};
|
|
171
205
|
|
|
@@ -207,7 +241,7 @@ const renderFrame = (view, options = {}) => {
|
|
|
207
241
|
return {
|
|
208
242
|
text,
|
|
209
243
|
rows: painted,
|
|
210
|
-
buttons: layout
|
|
244
|
+
buttons: footerHits(view, layout),
|
|
211
245
|
fileHits,
|
|
212
246
|
todoHits,
|
|
213
247
|
templateHits,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../ansi.js');
|
|
4
|
+
const chrome = require('./chrome.js');
|
|
5
|
+
const primitives = require('./primitives.js');
|
|
6
|
+
const npmCommands = require('../npm-commands.js');
|
|
7
|
+
|
|
8
|
+
const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
|
|
9
|
+
const { clipAnsi, stripAnsi, RESET, foregroundOn } = ansi;
|
|
10
|
+
const { formatBusyStatus } = chrome;
|
|
11
|
+
const { TABLE_KEY, TABLE_VAL } = npmCommands;
|
|
12
|
+
const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
|
|
13
|
+
const { paintListEdit } = primitives;
|
|
14
|
+
|
|
15
|
+
const EDGE = 2;
|
|
16
|
+
const TAIL = 1;
|
|
17
|
+
const LOG_PAD_Y = 1;
|
|
18
|
+
const LOG_PAD_X = 2;
|
|
19
|
+
const NPM_NAME_MAX = 24;
|
|
20
|
+
|
|
21
|
+
const logViewRows = (bodyH) => {
|
|
22
|
+
const height = Math.max(1, bodyH);
|
|
23
|
+
if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
|
|
24
|
+
return height;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const rowFg = (selected) => (selected ? THEME.chromeFg : THEME.mutedFg);
|
|
28
|
+
|
|
29
|
+
const npmNameWidth = (commands) => {
|
|
30
|
+
let width = 0;
|
|
31
|
+
for (const entry of commands) {
|
|
32
|
+
width = Math.max(width, visibleWidth(`${entry.name ?? ''}`));
|
|
33
|
+
}
|
|
34
|
+
return Math.min(NPM_NAME_MAX, width);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const paintNpmRow = (entry, width, color, selected, nameCol = 0) => {
|
|
38
|
+
const inner = Math.max(1, width - EDGE - TAIL);
|
|
39
|
+
const mark = selected ? FILE_MARK : ' ';
|
|
40
|
+
const prefix = ` ${mark} `;
|
|
41
|
+
const prefixW = visibleWidth(prefix);
|
|
42
|
+
const rest = Math.max(1, inner - prefixW);
|
|
43
|
+
const detail = entry.kind === 'bin' ? 'bin' : entry.command;
|
|
44
|
+
const nameW = Math.min(Math.max(0, nameCol), rest);
|
|
45
|
+
const name = truncateVisible(entry.name, nameW);
|
|
46
|
+
const gap = rest > nameW + 2 ? ' ' : '';
|
|
47
|
+
const detailW = Math.max(0, rest - nameW - visibleWidth(gap));
|
|
48
|
+
const shown = truncateVisible(detail, detailW);
|
|
49
|
+
const mid = `${name}${' '.repeat(Math.max(0, nameW - visibleWidth(name)))}`;
|
|
50
|
+
const after = `${gap}${shown}`;
|
|
51
|
+
const pad = Math.max(0, rest - visibleWidth(mid + after));
|
|
52
|
+
const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
|
|
53
|
+
const fgRgb = rowFg(selected);
|
|
54
|
+
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
55
|
+
const tail = ' '.repeat(TAIL);
|
|
56
|
+
const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
|
|
57
|
+
if (!color) return `${left}${tail}${edge}`;
|
|
58
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
59
|
+
out += paint(prefix, fgRgb, bgRgb, color);
|
|
60
|
+
const bin = entry.kind === 'bin';
|
|
61
|
+
const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
|
|
62
|
+
out += paint(mid, nameFg, bgRgb, color, !bin);
|
|
63
|
+
out += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
|
|
64
|
+
return out + edge;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const paintNpmList = (view, width, color, bodyH, headerLines) => {
|
|
68
|
+
const commands = view.npmCommands ?? [];
|
|
69
|
+
const nameW = npmNameWidth(commands);
|
|
70
|
+
const cursor = view.npmCursor ?? 0;
|
|
71
|
+
const compose = view.compose;
|
|
72
|
+
const editing = compose && compose.kind === 'npm';
|
|
73
|
+
const paintRow = (entry, rowWidth, rowColor, selected) =>
|
|
74
|
+
paintNpmRow(entry, rowWidth, rowColor, selected, nameW);
|
|
75
|
+
let extra = null;
|
|
76
|
+
if (editing) {
|
|
77
|
+
const painted = paintListEdit(compose, width, color, EDGE);
|
|
78
|
+
const at = view.npmEditName ? 'replace' : 'end';
|
|
79
|
+
extra = { row: painted.row, cursor: painted.cursor, at, index: cursor };
|
|
80
|
+
}
|
|
81
|
+
const painted = paintCursorList(
|
|
82
|
+
commands,
|
|
83
|
+
cursor,
|
|
84
|
+
width,
|
|
85
|
+
color,
|
|
86
|
+
bodyH,
|
|
87
|
+
headerLines,
|
|
88
|
+
paintRow,
|
|
89
|
+
extra,
|
|
90
|
+
view.listScroll,
|
|
91
|
+
);
|
|
92
|
+
return paneResult({
|
|
93
|
+
body: painted.body,
|
|
94
|
+
fileHits: painted.fileHits,
|
|
95
|
+
todoOwners: [],
|
|
96
|
+
cursor: painted.cursor,
|
|
97
|
+
splitBody: false,
|
|
98
|
+
listScroll: painted.offset,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const logStart = (view, count, rows) => {
|
|
103
|
+
const maxStart = Math.max(0, count - rows);
|
|
104
|
+
let start = maxStart;
|
|
105
|
+
if (!view.npmFollow) start = view.npmScroll ?? 0;
|
|
106
|
+
if (start > maxStart) start = maxStart;
|
|
107
|
+
if (start < 0) start = 0;
|
|
108
|
+
return start;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
|
|
112
|
+
|
|
113
|
+
const mixRgb = (from, to, part) => {
|
|
114
|
+
const out = [0, 0, 0];
|
|
115
|
+
for (let i = 0; i < 3; i++) {
|
|
116
|
+
const next = from[i] + (to[i] - from[i]) * part;
|
|
117
|
+
out[i] = Math.round(next);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const tableTone = (index) => {
|
|
123
|
+
const lift = index % 2 === 0 ? 0.035 : 0.07;
|
|
124
|
+
const valueBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift);
|
|
125
|
+
const keyBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift + 0.025);
|
|
126
|
+
const frame = mixRgb(THEME.ctxBg, THEME.chromeFg, lift * 0.4);
|
|
127
|
+
return { valueBg, keyBg, frame };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const tableIndex = (lines, index) => {
|
|
131
|
+
let at = index;
|
|
132
|
+
let row = 0;
|
|
133
|
+
while (at > 0 && isTableLine(lines[at - 1])) {
|
|
134
|
+
at -= 1;
|
|
135
|
+
row += 1;
|
|
136
|
+
}
|
|
137
|
+
return row;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const paintTableLine = (line, width, index) => {
|
|
141
|
+
const raw = line.slice(TABLE_KEY.length);
|
|
142
|
+
const at = raw.indexOf(TABLE_VAL);
|
|
143
|
+
const key = at < 0 ? raw : raw.slice(0, at);
|
|
144
|
+
const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
|
|
145
|
+
const gap = rest.startsWith(' ') ? ' ' : '';
|
|
146
|
+
const value = rest.slice(gap.length);
|
|
147
|
+
const edge = 1;
|
|
148
|
+
const inner = Math.max(1, width - edge * 2);
|
|
149
|
+
const keyW = visibleWidth(key);
|
|
150
|
+
const gapW = visibleWidth(gap);
|
|
151
|
+
const valueW = Math.max(0, inner - keyW - gapW);
|
|
152
|
+
const shown = truncateVisible(value, valueW);
|
|
153
|
+
const pad = ' '.repeat(Math.max(0, valueW - visibleWidth(shown)));
|
|
154
|
+
const tone = tableTone(index);
|
|
155
|
+
const ink = THEME.ctxFg;
|
|
156
|
+
let out = fill(edge, ink, tone.frame, true);
|
|
157
|
+
out += paint(key, ink, tone.keyBg, true);
|
|
158
|
+
out += paint(gap, ink, tone.frame, true);
|
|
159
|
+
out += paint(`${shown}${pad}`, ink, tone.valueBg, true);
|
|
160
|
+
return `${out}${fill(edge, ink, tone.frame, true)}`;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
|
|
164
|
+
const inner = Math.max(1, width - LOG_PAD_X * 2);
|
|
165
|
+
const source = color ? `${line ?? ''}` : stripAnsi(line);
|
|
166
|
+
const clip = (value) =>
|
|
167
|
+
color ? clipAnsi(value, inner) : truncateVisible(value, inner);
|
|
168
|
+
const body = color ? foregroundOn(clip(source), THEME.ctxBg) : clip(source);
|
|
169
|
+
const lead = ' '.repeat(LOG_PAD_X);
|
|
170
|
+
const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
|
|
171
|
+
const tail = ' '.repeat(pad);
|
|
172
|
+
if (!color) return `${lead}${body}${tail}`;
|
|
173
|
+
const base = seq(fgRgb, THEME.ctxBg);
|
|
174
|
+
let out = `${base}${EL}${lead}${body}`;
|
|
175
|
+
if (pad) out += `${RESET}${base}${tail}`;
|
|
176
|
+
return `${out}${RESET}`;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const paintRunBar = (width, color, frame) => {
|
|
180
|
+
const label = formatBusyStatus('running', frame);
|
|
181
|
+
return paintLogLine(label, width, color, THEME.chromeFg);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const paintNpmLog = (view, width, color, bodyH) => {
|
|
185
|
+
const text = view.npmOutput ?? '';
|
|
186
|
+
const lines = text.split('\n');
|
|
187
|
+
if (lines.length && lines.at(-1) === '') lines.pop();
|
|
188
|
+
const running = view.npmRunning === true;
|
|
189
|
+
const count = lines.length + (running ? 1 : 0);
|
|
190
|
+
const rows = logViewRows(bodyH);
|
|
191
|
+
const padY = bodyH - rows;
|
|
192
|
+
const top = padY ? 1 : 0;
|
|
193
|
+
const start = logStart(view, count, rows);
|
|
194
|
+
const blank = fill(width, THEME.ctxFg, THEME.ctxBg, color);
|
|
195
|
+
const body = [];
|
|
196
|
+
for (let i = 0; i < top; i++) body.push(blank);
|
|
197
|
+
for (let i = 0; i < rows; i++) {
|
|
198
|
+
const index = start + i;
|
|
199
|
+
if (index < lines.length) {
|
|
200
|
+
const line = lines[index];
|
|
201
|
+
if (color && isTableLine(line)) {
|
|
202
|
+
const row = tableIndex(lines, index);
|
|
203
|
+
body.push(paintTableLine(line, width, row));
|
|
204
|
+
} else {
|
|
205
|
+
body.push(paintLogLine(line, width, color));
|
|
206
|
+
}
|
|
207
|
+
} else if (running && index === lines.length) {
|
|
208
|
+
body.push(paintRunBar(width, color, view.progressFrame ?? 0));
|
|
209
|
+
} else {
|
|
210
|
+
body.push(blank);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < padY - top; i++) body.push(blank);
|
|
214
|
+
return paneResult({
|
|
215
|
+
body,
|
|
216
|
+
fileHits: [],
|
|
217
|
+
todoOwners: [],
|
|
218
|
+
cursor: null,
|
|
219
|
+
splitBody: false,
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
|
|
224
|
+
if (view.npmView) return paintNpmLog(view, width, color, bodyH);
|
|
225
|
+
return paintNpmList(view, width, color, bodyH, headerLines);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
module.exports = { paintBodyNpm, logViewRows };
|
package/lib/render/primitives.js
CHANGED
|
@@ -8,7 +8,7 @@ const GUTTER_W = 2;
|
|
|
8
8
|
const FILE_MARK = '▶';
|
|
9
9
|
const FILE_LIST_PAD = 1;
|
|
10
10
|
const CHECK_DONE = 'xX';
|
|
11
|
-
const LIST_PANES = ['files', 'branches', 'commits'];
|
|
11
|
+
const LIST_PANES = ['files', 'branches', 'commits', 'npm'];
|
|
12
12
|
const CHECK_TOKEN = /\[([ xX])\]/;
|
|
13
13
|
const CHECK_LEAD = /^\[[ xX]\] /;
|
|
14
14
|
|
|
@@ -156,38 +156,15 @@ const paintCursorList = (
|
|
|
156
156
|
return { body, fileHits, cursor: textCursor, offset: start };
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
-
const branchLabel = (name) => {
|
|
160
|
-
const branch = `${name ?? ''}`.trim();
|
|
161
|
-
if (!branch) return '';
|
|
162
|
-
return `[${branch}]`;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const splitBracketName = (label) => {
|
|
166
|
-
const text = `${label ?? ''}`;
|
|
167
|
-
const open = text.startsWith('[') ? '[' : '';
|
|
168
|
-
const rest = open ? text.slice(1) : text;
|
|
169
|
-
const close = rest.endsWith(']') ? ']' : '';
|
|
170
|
-
const name = close ? rest.slice(0, -1) : rest;
|
|
171
|
-
return { open, name, close };
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const paintBracketName = (label, nameFg, wrapFg, bgRgb, color, nameBold) => {
|
|
175
|
-
const { open, name, close } = splitBracketName(label);
|
|
176
|
-
return (
|
|
177
|
-
paint(open, wrapFg, bgRgb, color, true) +
|
|
178
|
-
paint(name, nameFg, bgRgb, color, nameBold) +
|
|
179
|
-
paint(close, wrapFg, bgRgb, color, true)
|
|
180
|
-
);
|
|
181
|
-
};
|
|
159
|
+
const branchLabel = (name) => `${name ?? ''}`.trim();
|
|
182
160
|
|
|
183
|
-
const paintListEdit = (compose, width, color, edgeW) => {
|
|
161
|
+
const paintListEdit = (compose, width, color, edgeW = 0) => {
|
|
184
162
|
const inner = Math.max(1, width - edgeW);
|
|
185
163
|
const prefix = ` ${FILE_MARK} `;
|
|
186
164
|
const prefixW = visibleWidth(prefix);
|
|
187
165
|
const rest = Math.max(1, inner - prefixW);
|
|
188
166
|
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
189
|
-
const
|
|
190
|
-
const clipped = truncateVisible(label, rest);
|
|
167
|
+
const clipped = truncateVisible(text, rest);
|
|
191
168
|
const pad = Math.max(0, rest - visibleWidth(clipped));
|
|
192
169
|
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
193
170
|
const fgRgb = THEME.chromeFg;
|
|
@@ -198,17 +175,11 @@ const paintListEdit = (compose, width, color, edgeW) => {
|
|
|
198
175
|
const gap = ' '.repeat(pad);
|
|
199
176
|
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
200
177
|
row += paint(prefix, fgRgb, bgRgb, color);
|
|
201
|
-
row +=
|
|
202
|
-
clipped,
|
|
203
|
-
THEME.buttonHotFg,
|
|
204
|
-
THEME.mutedFg,
|
|
205
|
-
bgRgb,
|
|
206
|
-
color,
|
|
207
|
-
);
|
|
178
|
+
row += paint(clipped, THEME.buttonHotFg, bgRgb, color);
|
|
208
179
|
row += paint(gap, fgRgb, bgRgb, color);
|
|
209
180
|
row += edge;
|
|
210
181
|
}
|
|
211
|
-
const col = Math.min(
|
|
182
|
+
const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
|
|
212
183
|
return { row, cursor: { x: prefixW + col + 1 } };
|
|
213
184
|
};
|
|
214
185
|
|
|
@@ -280,9 +251,9 @@ module.exports = {
|
|
|
280
251
|
paintSplitRule,
|
|
281
252
|
paintBodyFill,
|
|
282
253
|
windowRows,
|
|
254
|
+
listWindowStart,
|
|
283
255
|
paintCursorList,
|
|
284
256
|
paintListEdit,
|
|
285
257
|
branchLabel,
|
|
286
|
-
paintBracketName,
|
|
287
258
|
paneResult,
|
|
288
259
|
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { stripAnsi } = require('./ansi.js');
|
|
4
|
+
const commands = require('./npm-commands.js');
|
|
5
|
+
|
|
6
|
+
const { withoutNodeFrame, indentStack, relativize } = commands;
|
|
7
|
+
|
|
8
|
+
const summary = () => ({
|
|
9
|
+
tests: null,
|
|
10
|
+
passed: null,
|
|
11
|
+
failed: null,
|
|
12
|
+
skipped: null,
|
|
13
|
+
errors: null,
|
|
14
|
+
warnings: null,
|
|
15
|
+
duration: '',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const problem = (fields = {}) => {
|
|
19
|
+
const trace = fields.trace || '';
|
|
20
|
+
const traces = fields.traces || (trace ? [trace] : []);
|
|
21
|
+
return {
|
|
22
|
+
severity: fields.severity || 'error',
|
|
23
|
+
name: fields.name || '',
|
|
24
|
+
message: fields.message || '',
|
|
25
|
+
trace,
|
|
26
|
+
traces,
|
|
27
|
+
duration: fields.duration || '',
|
|
28
|
+
code: fields.code || '',
|
|
29
|
+
operator: fields.operator || '',
|
|
30
|
+
expected: fields.expected || '',
|
|
31
|
+
actual: fields.actual || '',
|
|
32
|
+
extra: fields.extra || [],
|
|
33
|
+
count: fields.count === undefined ? 1 : fields.count,
|
|
34
|
+
omitted: fields.omitted || 0,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const report = (tool, fields = {}) => ({
|
|
39
|
+
tool,
|
|
40
|
+
ok: fields.ok !== false,
|
|
41
|
+
summary: fields.summary || summary(),
|
|
42
|
+
problems: fields.problems || [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const userTrace = (text, root) => {
|
|
46
|
+
const lines = stripAnsi(`${text ?? ''}`).split(/\r?\n/);
|
|
47
|
+
const kept = [];
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const next = withoutNodeFrame(line);
|
|
50
|
+
if (next === null) continue;
|
|
51
|
+
kept.push(relativize(indentStack(next), root).trimEnd());
|
|
52
|
+
}
|
|
53
|
+
while (kept.length && kept[kept.length - 1] === '') kept.pop();
|
|
54
|
+
return kept.join('\n').trim();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const normalizeMessage = (message) => {
|
|
58
|
+
const text = `${message ?? ''}`.trim().replace(/\s+/g, ' ');
|
|
59
|
+
return text.replace(/\s+\S+:\d+:\d+$/, '');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const groupKey = (item) =>
|
|
63
|
+
[
|
|
64
|
+
item.severity,
|
|
65
|
+
item.code,
|
|
66
|
+
item.operator,
|
|
67
|
+
item.expected,
|
|
68
|
+
item.actual,
|
|
69
|
+
normalizeMessage(item.message),
|
|
70
|
+
].join('\0');
|
|
71
|
+
|
|
72
|
+
const groupProblems = (problems) => {
|
|
73
|
+
const groups = new Map();
|
|
74
|
+
const order = [];
|
|
75
|
+
for (const item of problems) {
|
|
76
|
+
const key = groupKey(item);
|
|
77
|
+
let group = groups.get(key);
|
|
78
|
+
if (!group) {
|
|
79
|
+
group = problem({ ...item, count: 0, traces: [], omitted: 0 });
|
|
80
|
+
groups.set(key, group);
|
|
81
|
+
order.push(group);
|
|
82
|
+
}
|
|
83
|
+
group.count += 1;
|
|
84
|
+
if (item.trace && group.traces.length < 3) group.traces.push(item.trace);
|
|
85
|
+
}
|
|
86
|
+
for (const group of order) {
|
|
87
|
+
group.trace = group.traces[0] || '';
|
|
88
|
+
group.omitted = Math.max(0, group.count - group.traces.length);
|
|
89
|
+
}
|
|
90
|
+
return order;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const groupReport = (item) =>
|
|
94
|
+
report(item.tool, {
|
|
95
|
+
ok: item.ok,
|
|
96
|
+
summary: item.summary,
|
|
97
|
+
problems: groupProblems(item.problems),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const reduceStderr = (text, root) => userTrace(text, root);
|
|
101
|
+
|
|
102
|
+
const shortTail = (text) => {
|
|
103
|
+
const lines = `${text ?? ''}`.trim().split(/\r?\n/);
|
|
104
|
+
if (lines.length <= 20) return lines.join('\n');
|
|
105
|
+
return lines.slice(-20).join('\n');
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const envelope = (reports, exit, stderr) => {
|
|
109
|
+
const status = exit === null || exit === undefined ? 1 : exit;
|
|
110
|
+
const ok = status === 0;
|
|
111
|
+
const items = [];
|
|
112
|
+
for (const item of reports) {
|
|
113
|
+
items.push(report(item.tool, { ...item, ok }));
|
|
114
|
+
}
|
|
115
|
+
return { ok, exit: status, reports: items, stderr: stderr || '' };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
summary,
|
|
120
|
+
problem,
|
|
121
|
+
report,
|
|
122
|
+
userTrace,
|
|
123
|
+
groupReport,
|
|
124
|
+
reduceStderr,
|
|
125
|
+
shortTail,
|
|
126
|
+
envelope,
|
|
127
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { stripAnsi } = require('./ansi.js');
|
|
4
|
+
const model = require('./report-model.js');
|
|
5
|
+
const testParse = require('./report-test.js');
|
|
6
|
+
const toolParse = require('./report-tools.js');
|
|
7
|
+
|
|
8
|
+
const { problem, report, groupReport } = model;
|
|
9
|
+
const { reduceStderr, shortTail, envelope } = model;
|
|
10
|
+
const { parseTap, parseNodeJsonl, parseTestJson, parseSpec } = testParse;
|
|
11
|
+
const { parseToolJson, parseToolText } = toolParse;
|
|
12
|
+
|
|
13
|
+
const KNOWN = new Set([
|
|
14
|
+
'test',
|
|
15
|
+
'tsc',
|
|
16
|
+
'eslint',
|
|
17
|
+
'prettier',
|
|
18
|
+
'audit',
|
|
19
|
+
'outdated',
|
|
20
|
+
'ls',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const toolHint = (program, args) => {
|
|
24
|
+
const name = `${program ?? ''}`.split(/[/\\]/).pop();
|
|
25
|
+
const list = args || [];
|
|
26
|
+
const joined = [name, ...list].join(' ');
|
|
27
|
+
if (name === 'eslint') return 'eslint';
|
|
28
|
+
if (name === 'tsc') return 'tsc';
|
|
29
|
+
if (name === 'prettier') return 'prettier';
|
|
30
|
+
if (/\baudit\b/.test(joined)) return 'audit';
|
|
31
|
+
if (/\boutdated\b/.test(joined)) return 'outdated';
|
|
32
|
+
if (name === 'npm' || name === 'pnpm' || name === 'npm.cmd') {
|
|
33
|
+
if (/\b(ls|list)\b/.test(joined)) return 'ls';
|
|
34
|
+
}
|
|
35
|
+
if (name === 'node' && (args || []).includes('--test')) return 'test';
|
|
36
|
+
return '';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseJson = (text) => {
|
|
40
|
+
const trimmed = `${text ?? ''}`.trim();
|
|
41
|
+
if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return null;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(trimmed);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const parseJsonReports = (text, root) => {
|
|
50
|
+
const node = parseNodeJsonl(text, root);
|
|
51
|
+
if (node) return [node];
|
|
52
|
+
const data = parseJson(text);
|
|
53
|
+
if (!data) return null;
|
|
54
|
+
const test = parseTestJson(data, root);
|
|
55
|
+
if (test) return [test];
|
|
56
|
+
const tools = parseToolJson(data, root);
|
|
57
|
+
if (tools) return tools;
|
|
58
|
+
return [];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const parseReports = (text, root) => {
|
|
62
|
+
const json = parseJsonReports(text, root);
|
|
63
|
+
if (json) return json;
|
|
64
|
+
const reports = [];
|
|
65
|
+
const tap = parseTap(text, root);
|
|
66
|
+
if (tap) reports.push(tap);
|
|
67
|
+
const tools = parseToolText(text, root);
|
|
68
|
+
for (const item of tools) reports.push(item);
|
|
69
|
+
if (!tap) {
|
|
70
|
+
const spec = parseSpec(text, root);
|
|
71
|
+
if (spec) reports.push(spec);
|
|
72
|
+
}
|
|
73
|
+
return reports;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const withHint = (reports, text, exit, hint) => {
|
|
77
|
+
if (reports.length || !`${text ?? ''}`.trim() || exit === 0) return reports;
|
|
78
|
+
const tool = KNOWN.has(hint) ? hint : 'test';
|
|
79
|
+
return [report(tool, { problems: [problem({ message: shortTail(text) })] })];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildDocument = (stdout, stderr, root, exit, hint) => {
|
|
83
|
+
const text = stripAnsi(stdout);
|
|
84
|
+
const reports = withHint(parseReports(text, root), text, exit, hint);
|
|
85
|
+
const grouped = [];
|
|
86
|
+
for (const item of reports) grouped.push(groupReport(item));
|
|
87
|
+
return envelope(grouped, exit, reduceStderr(stderr, root));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
toolHint,
|
|
92
|
+
parseReports,
|
|
93
|
+
buildDocument,
|
|
94
|
+
};
|