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.
@@ -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, paintButtons, paintStatusLine } = chrome;
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
- const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
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(paintButtons(layout, width, color));
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.hits,
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 };
@@ -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 label = `[${text}]`;
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 += paintBracketName(
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((compose.cursor ?? 0) + 1, visibleWidth(clipped));
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
+ };