reslop 0.2.4 → 0.2.6

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -2,67 +2,30 @@
2
2
 
3
3
  const ansi = require('../ansi.js');
4
4
  const actions = require('../session/actions.js');
5
- const { LIST_PANES } = actions;
6
5
 
7
- const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
6
+ const { THEME, paint, visibleWidth, graphemes, graphemeWidth } = ansi;
7
+ const { LIST_PANES } = actions;
8
8
 
9
9
  const GUTTER_W = 2;
10
10
  const FILE_MARK = '▶';
11
- const FILE_LIST_PAD = 1;
12
- const CHECK_DONE = 'xX';
13
- const CHECK_TOKEN = /\[([ xX])\]/;
14
- const CHECK_LEAD = /^\[[ xX]\] /;
11
+ const MARK_W = visibleWidth(` ${FILE_MARK} `);
12
+ const LIST_PAD = 1;
13
+ const NO_EDITS = [];
15
14
 
16
15
  const fill = (width, fgRgb, bgRgb, color) =>
17
16
  paint(' '.repeat(Math.max(0, width)), fgRgb, bgRgb, color);
18
17
 
19
- const paintBar = (text, width, fgRgb, bgRgb, color) => {
20
- const clipped = truncateVisible(text, width);
21
- const pad = Math.max(0, width - visibleWidth(clipped));
22
- const filled = clipped + ' '.repeat(pad);
23
- if (!color) return filled;
24
- return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
25
- };
18
+ const markPrefix = (selected) => ` ${selected ? FILE_MARK : ' '} `;
26
19
 
27
- const splitCheck = (text) => {
28
- const match = CHECK_TOKEN.exec(text);
29
- if (!match) return null;
30
- const start = match.index;
31
- const token = match[0];
32
- const before = text.slice(0, start);
33
- const mark = match[1];
34
- const after = text.slice(start + token.length);
35
- return { before, mark, token, after };
36
- };
20
+ const isListPane = (pane) => LIST_PANES.includes(pane) || pane === 'dashboard';
37
21
 
38
- const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
39
- const clipped = truncateVisible(text, width);
40
- const parts = color ? splitCheck(clipped) : null;
41
- if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
42
- const pad = Math.max(0, width - visibleWidth(clipped));
43
- const done = CHECK_DONE.includes(parts.mark);
44
- let chipFg = THEME.checkFg;
45
- let chipBg = THEME.checkBg;
46
- if (plain) {
47
- chipFg = fgRgb;
48
- chipBg = bgRgb;
49
- } else if (done) {
50
- chipFg = THEME.checkDoneFg;
51
- chipBg = THEME.checkDoneBg;
52
- }
53
- const after = `${parts.after}${' '.repeat(pad)}`;
54
- let out = `${seq(fgRgb, bgRgb)}${EL}`;
55
- if (parts.before) out += paint(parts.before, fgRgb, bgRgb, color);
56
- out += paint(parts.token, chipFg, chipBg, color);
57
- if (after) out += paint(after, fgRgb, bgRgb, color);
58
- return out;
59
- };
22
+ const isTaskView = (view) => view.item?.origin === 'task';
23
+
24
+ const branchLabel = (name) => `${name ?? ''}`.trim();
60
25
 
61
26
  const padVisible = (text, width, align) => {
62
- const n = Math.max(0, width - visibleWidth(text));
63
- const gap = ' '.repeat(n);
64
- if (align === 'end') return `${text}${gap}`;
65
- return `${gap}${text}`;
27
+ const gap = ' '.repeat(Math.max(0, width - visibleWidth(text)));
28
+ return align === 'end' ? `${text}${gap}` : `${gap}${text}`;
66
29
  };
67
30
 
68
31
  const splitWidths = (width) => {
@@ -74,136 +37,132 @@ const splitWidths = (width) => {
74
37
  const paintSplitRule = (color) =>
75
38
  fill(1, THEME.splitGutterFg, THEME.splitGutterBg, color);
76
39
 
77
- const paintSplitBlank = (width, color) => {
40
+ const paintBodyFill = (width, color, kind) => {
41
+ const ctx = (n) => fill(n, THEME.ctxFg, THEME.ctxBg, color);
42
+ if (kind === 'files') return ctx(Math.max(1, width - 1)) + ctx(1);
43
+ if (kind !== 'split') return ctx(width);
78
44
  const { leftW, rightW } = splitWidths(width);
79
- return (
80
- fill(leftW, THEME.ctxFg, THEME.ctxBg, color) +
81
- paintSplitRule(color) +
82
- fill(rightW, THEME.ctxFg, THEME.ctxBg, color)
83
- );
45
+ return ctx(leftW) + paintSplitRule(color) + ctx(rightW);
84
46
  };
85
47
 
86
- const paintBodyFill = (width, color, kind) => {
87
- if (kind === 'files') {
88
- const inner = Math.max(1, width - 1);
89
- const main = fill(inner, THEME.ctxFg, THEME.ctxBg, color);
90
- const edge = fill(1, THEME.ctxFg, THEME.ctxBg, color);
91
- return main + edge;
92
- }
93
- if (kind === 'split') return paintSplitBlank(width, color);
94
- return fill(width, THEME.ctxFg, THEME.ctxBg, color);
95
- };
48
+ const paneResult = (fields = {}) => ({
49
+ body: fields.body ?? [],
50
+ fileHits: fields.fileHits ?? [],
51
+ taskOwners: fields.taskOwners ?? [],
52
+ taskChecks: fields.taskChecks ?? [],
53
+ cursor: fields.cursor ?? null,
54
+ splitBody: fields.splitBody === true,
55
+ listScroll: fields.listScroll ?? 0,
56
+ editHits: fields.editHits ?? [],
57
+ });
96
58
 
97
- const windowRows = (rows, focus, cap) => {
98
- if (rows.length <= cap) return { rows, offset: 0 };
99
- let start = focus - cap + 1;
100
- if (start < 0) start = 0;
101
- if (start + cap > rows.length) start = rows.length - cap;
102
- return { rows: rows.slice(start, start + cap), offset: start };
103
- };
59
+ const editSpan = (x0, textX, x1, start, text) => ({
60
+ x0,
61
+ textX,
62
+ x1,
63
+ start,
64
+ text,
65
+ limit: start + text.length,
66
+ live: true,
67
+ });
68
+
69
+ const listLine = (row, index, cursor = null, edits = NO_EDITS) => ({
70
+ row,
71
+ index,
72
+ hit: index >= 0,
73
+ cursor,
74
+ edits,
75
+ });
104
76
 
105
77
  const listWindowStart = (focus, offset, listH, count) => {
106
78
  let start = offset ?? 0;
107
79
  if (focus < start) start = focus;
108
80
  if (focus >= start + listH) start = focus - listH + 1;
109
- const maxStart = Math.max(0, count - listH);
110
- if (start > maxStart) start = maxStart;
111
- if (start < 0) start = 0;
112
- return start;
81
+ return Math.max(0, Math.min(start, count - listH));
113
82
  };
114
83
 
115
- const extraIdx = (extraRow, cursor, count) => {
116
- if (!extraRow) return -1;
117
- if (extraRow.at === 'start') return 0;
118
- if (extraRow.at === 'replace') return extraRow.index ?? cursor;
119
- return count;
84
+ const rowCursor = (cursor, row) => {
85
+ const out = { x: cursor.x, row };
86
+ if (typeof cursor.scroll === 'number') out.scroll = cursor.scroll;
87
+ return out;
120
88
  };
121
89
 
122
- const paneResult = (fields = {}) => ({
123
- body: fields.body ?? [],
124
- fileHits: fields.fileHits ?? [],
125
- todoOwners: fields.todoOwners ?? [],
126
- todoChecks: fields.todoChecks ?? [],
127
- cursor: fields.cursor ?? null,
128
- splitBody: fields.splitBody === true,
129
- listScroll: fields.listScroll ?? 0,
130
- });
131
-
132
- const paintCursorList = (
133
- items,
134
- cursor,
135
- width,
136
- color,
137
- bodyH,
138
- headerLines,
139
- paintRow,
140
- extraRow,
141
- offset = 0,
142
- ) => {
143
- const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
90
+ const paintListWindow = (count, focus, pane, lineAt) => {
91
+ const { width, color, bodyH, headerLines, offset } = pane;
92
+ const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
144
93
  const listH = bodyH - pad * 2;
145
- const inserting = extraRow && extraRow.at === 'start';
146
- const extra = extraRow && extraRow.at !== 'replace' ? 1 : 0;
147
- const count = items.length + extra;
148
- const extraAt = extraIdx(extraRow, cursor, items.length);
149
- const focus = extraRow ? extraAt : cursor;
150
94
  const start = listWindowStart(focus, offset, listH, count);
151
95
  const blank = paintBodyFill(width, color, 'files');
152
- const body = [];
96
+ const body = pad ? [blank] : [];
153
97
  const fileHits = [];
154
- let textCursor = null;
155
- if (pad) body.push(blank);
98
+ const editHits = [];
99
+ let cursor = null;
156
100
  for (let i = 0; i < listH; i++) {
157
- const idx = start + i;
158
- if (extraRow && idx === extraAt) {
159
- body.push(extraRow.row);
160
- if (extraRow.cursor) {
161
- textCursor = { x: extraRow.cursor.x, row: body.length - 1 };
162
- }
163
- continue;
101
+ const line = lineAt(start + i);
102
+ if (!line) break;
103
+ const row = body.push(line.row) - 1;
104
+ if (line.cursor) cursor = rowCursor(line.cursor, row);
105
+ for (const edit of line.edits) editHits.push({ ...edit, row });
106
+ if (line.hit) {
107
+ fileHits.push({ y: headerLines + pad + i + 1, cursor: line.index });
164
108
  }
165
- const itemIdx = inserting ? idx - 1 : idx;
166
- const entry = items[itemIdx];
167
- if (!entry) break;
168
- const selected = extraRow ? false : itemIdx === cursor;
169
- body.push(paintRow(entry, width, color, selected));
170
- const y = headerLines + pad + i + 1;
171
- fileHits.push({ y, cursor: itemIdx });
172
109
  }
173
110
  if (pad) body.push(blank);
174
- return paneResult({
175
- body,
176
- fileHits,
177
- cursor: textCursor,
178
- listScroll: start,
179
- });
111
+ return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
112
+ };
113
+
114
+ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
115
+ const lineAt = (index) => {
116
+ const entry = items[index];
117
+ if (!entry) return null;
118
+ return listLine(paintRow(entry, !extra && index === cursor), index);
119
+ };
120
+ if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
121
+ const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
122
+ if (extra.at === 'replace') {
123
+ const at = extra.index;
124
+ const replaced = (i) => (i === at ? edit : lineAt(i));
125
+ return paintListWindow(items.length, at, pane, replaced);
126
+ }
127
+ const first = extra.at === 'start';
128
+ const at = first ? 0 : items.length;
129
+ const shift = first ? 1 : 0;
130
+ const inserted = (i) => (i === at ? edit : lineAt(i - shift));
131
+ return paintListWindow(items.length + 1, at, pane, inserted);
132
+ };
133
+
134
+ const sliceVisible = (text, skip, width) => {
135
+ if (width <= 0) return '';
136
+ const parts = graphemes(text);
137
+ let used = 0;
138
+ let i = 0;
139
+ const drop = Math.max(0, skip);
140
+ while (i < parts.length) {
141
+ const w = graphemeWidth(parts[i], used);
142
+ if (used + w > drop) break;
143
+ used += w;
144
+ i += 1;
145
+ }
146
+ let out = '';
147
+ let taken = 0;
148
+ for (; i < parts.length; i++) {
149
+ const w = graphemeWidth(parts[i], drop + taken);
150
+ if (taken + w > width) break;
151
+ out += parts[i];
152
+ taken += w;
153
+ }
154
+ return out;
180
155
  };
181
156
 
182
- const branchLabel = (name) => `${name ?? ''}`.trim();
183
-
184
- const paintListEdit = (compose, width, color, edgeW = 0) => {
185
- const inner = Math.max(1, width - edgeW);
186
- const prefix = ` ${FILE_MARK} `;
187
- const prefixW = visibleWidth(prefix);
188
- const rest = Math.max(1, inner - prefixW);
189
- const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
190
- const clipped = truncateVisible(text, rest);
191
- const pad = Math.max(0, rest - visibleWidth(clipped));
192
- const left = `${prefix}${clipped}${' '.repeat(pad)}`;
193
- const fgRgb = THEME.chromeFg;
194
- const bgRgb = THEME.buttonBg;
195
- const edge = fill(edgeW, fgRgb, THEME.ctxBg, color);
196
- let row = `${left}${edge}`;
197
- if (color) {
198
- const gap = ' '.repeat(pad);
199
- row = `${seq(fgRgb, bgRgb)}${EL}`;
200
- row += paint(prefix, fgRgb, bgRgb, color);
201
- row += paint(clipped, THEME.buttonHotFg, bgRgb, color);
202
- row += paint(gap, fgRgb, bgRgb, color);
203
- row += edge;
204
- }
205
- const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
206
- return { row, cursor: { x: prefixW + col + 1 } };
157
+ const fieldWindow = (text, cursor, scroll, width) => {
158
+ if (width <= 0) return { text: '', col: 0, scroll: 0 };
159
+ const at = Math.max(0, Math.min(cursor ?? 0, text.length));
160
+ const col = visibleWidth(text.slice(0, at));
161
+ let next = Math.max(0, scroll ?? 0);
162
+ if (col < next) next = col;
163
+ if (col >= next + width) next = col - width + 1;
164
+ const shown = sliceVisible(text, next, width);
165
+ return { text: shown, col: col - next, scroll: next };
207
166
  };
208
167
 
209
168
  const measureColumns = (entries, rowFields, align) => {
@@ -221,7 +180,7 @@ const measureColumns = (entries, rowFields, align) => {
221
180
  const padColumns = (fields, widths, align) => {
222
181
  const parts = {};
223
182
  for (const key of Object.keys(align)) {
224
- const width = widths?.[key] ?? visibleWidth(fields[key]);
183
+ const width = widths[key] ?? visibleWidth(fields[key]);
225
184
  parts[key] = padVisible(fields[key], width, align[key]);
226
185
  }
227
186
  return parts;
@@ -248,25 +207,27 @@ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
248
207
  };
249
208
 
250
209
  module.exports = {
251
- measureColumns,
252
- padColumns,
253
- joinColumns,
254
- paintColumns,
255
210
  GUTTER_W,
256
211
  FILE_MARK,
257
- LIST_PANES,
258
- CHECK_LEAD,
212
+ MARK_W,
259
213
  fill,
260
- paintBar,
261
- paintCheckBar,
214
+ markPrefix,
215
+ isListPane,
216
+ isTaskView,
217
+ branchLabel,
262
218
  padVisible,
263
219
  splitWidths,
264
220
  paintSplitRule,
265
221
  paintBodyFill,
266
- windowRows,
267
- listWindowStart,
268
- paintCursorList,
269
- paintListEdit,
270
- branchLabel,
271
222
  paneResult,
223
+ editSpan,
224
+ listLine,
225
+ paintListWindow,
226
+ paintCursorList,
227
+ sliceVisible,
228
+ fieldWindow,
229
+ measureColumns,
230
+ padColumns,
231
+ joinColumns,
232
+ paintColumns,
272
233
  };
@@ -5,30 +5,21 @@ const notes = require('./notes.js');
5
5
  const diff = require('./diff.js');
6
6
  const present = require('./present.js');
7
7
  const frame = require('./frame.js');
8
- const { renderFrame } = frame;
9
8
 
10
- const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
11
- const { updatePrompt, dropPrompt } = chrome;
12
9
  const { formatBusyStatus, headerText, layoutButtons } = chrome;
13
- const { delCharBg, addCharBg, paintDiffLine, codeInnerWidth } = diff;
14
- const { todoInnerWidth, todoBodyWidth, noteInnerWidth } = notes;
10
+ const { todoBodyWidth, noteInnerWidth } = notes;
11
+ const { paintDiffLine, codeInnerWidth } = diff;
15
12
  const { presentCursor, presentRows } = present;
13
+ const { renderFrame } = frame;
16
14
 
17
15
  module.exports = {
18
- QUIT_PROMPT,
19
- PUSH_PROMPT,
20
- updatePrompt,
21
- dropPrompt,
22
- delCharBg,
23
- addCharBg,
24
- paintDiffLine,
16
+ formatBusyStatus,
25
17
  headerText,
26
18
  layoutButtons,
27
- todoInnerWidth,
28
19
  todoBodyWidth,
29
20
  noteInnerWidth,
21
+ paintDiffLine,
30
22
  codeInnerWidth,
31
- formatBusyStatus,
32
23
  renderFrame,
33
24
  presentCursor,
34
25
  presentRows,
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const commands = require('../npm-commands.js');
4
+ const { formatTable } = commands;
5
+ const reportRender = require('../report-render.js');
6
+ const { summaryRows, problemRows, displayMessage, quoteLines } = reportRender;
7
+
8
+ const reportProblem = (item) => {
9
+ const title = item.name || item.message || item.code || 'problem';
10
+ const mark = item.severity === 'warning' ? '△' : '✖';
11
+ const count = item.count > 1 ? ` × ${item.count}` : '';
12
+ const lines = ['', `${mark} ${title.split('\n')[0]}${count}`, ''];
13
+ lines.push(...formatTable(problemRows(item)));
14
+ const message = displayMessage(item);
15
+ if (message) lines.push('', ...quoteLines(message));
16
+ if (item.trace) {
17
+ lines.push('', 'Stack');
18
+ for (const row of item.trace.split(/\r?\n/)) {
19
+ const frame = row.trim().replace(/^at\s+/, '');
20
+ if (frame) lines.push(` at ${frame}`);
21
+ }
22
+ }
23
+ if (item.related.length) {
24
+ lines.push('', 'Related failures');
25
+ for (const row of item.related) lines.push(` ↳ ${row}`);
26
+ }
27
+ return lines;
28
+ };
29
+
30
+ const renderReport = (doc) => {
31
+ const lines = [];
32
+ for (const item of doc.reports) {
33
+ if (lines.length) lines.push('');
34
+ const mark = item.ok ? '✔' : '✖';
35
+ lines.push(`${mark} ${item.tool} · exit ${doc.exit}`, '');
36
+ lines.push(...formatTable(summaryRows(item.summary)));
37
+ for (const entry of item.problems) lines.push(...reportProblem(entry));
38
+ }
39
+ if (doc.stderr) lines.push('', 'Stderr', doc.stderr);
40
+ return `${lines.join('\n')}\n`;
41
+ };
42
+
43
+ module.exports = { renderReport };