reslop 0.2.1 → 0.2.4

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 (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
@@ -0,0 +1,507 @@
1
+ 'use strict';
2
+
3
+ const model = require('./report-model.js');
4
+ const npmCommands = require('./npm-commands.js');
5
+ const { isPassingTest } = npmCommands;
6
+
7
+ const { summary, problem, report, userTrace } = model;
8
+
9
+ const KNOWN = [
10
+ 'message',
11
+ 'error',
12
+ 'code',
13
+ 'operator',
14
+ 'expected',
15
+ 'actual',
16
+ 'stack',
17
+ 'duration_ms',
18
+ 'duration',
19
+ 'name',
20
+ 'location',
21
+ 'severity',
22
+ 'type',
23
+ ];
24
+
25
+ const show = (value) => {
26
+ if (value === null || value === undefined) return '';
27
+ if (typeof value === 'string') return value;
28
+ if (typeof value === 'number' || typeof value === 'boolean') {
29
+ return `${value}`;
30
+ }
31
+ return JSON.stringify(value);
32
+ };
33
+
34
+ const extraFrom = (fields) => {
35
+ const extra = [];
36
+ for (const field of fields) {
37
+ if (KNOWN.includes(field[0])) continue;
38
+ const value = show(field[1]);
39
+ if (!value) continue;
40
+ const shown = value.length > 500 ? `${value.slice(0, 500)}…` : value;
41
+ extra.push([field[0], shown]);
42
+ }
43
+ return extra;
44
+ };
45
+
46
+ const fieldMap = (fields) => {
47
+ const map = new Map();
48
+ for (const field of fields) map.set(field[0], field[1]);
49
+ return map;
50
+ };
51
+
52
+ const problemFromFields = (name, fields, root) => {
53
+ const map = fieldMap(fields);
54
+ const message = show(map.get('error') || map.get('message'));
55
+ const stack = show(map.get('stack'));
56
+ const location = show(map.get('location'));
57
+ const trace = userTrace(stack || location, root);
58
+ const duration = show(map.get('duration_ms') || map.get('duration'));
59
+ return problem({
60
+ name,
61
+ message,
62
+ trace,
63
+ duration,
64
+ code: show(map.get('code')),
65
+ operator: show(map.get('operator')),
66
+ expected: show(map.get('expected')),
67
+ actual: show(map.get('actual')),
68
+ extra: extraFrom(fields),
69
+ });
70
+ };
71
+
72
+ const yamlValue = (raw) => {
73
+ const text = `${raw ?? ''}`.trim();
74
+ if (text.length < 2) return text;
75
+ const open = text[0];
76
+ const close = text[text.length - 1];
77
+ const code = open.charCodeAt(0);
78
+ const quoted = open === close && (code === 34 || code === 39);
79
+ if (quoted) return text.slice(1, -1);
80
+ return text;
81
+ };
82
+
83
+ const yamlFields = (lines) => {
84
+ const fields = [];
85
+ let index = 0;
86
+ while (index < lines.length) {
87
+ const match = /^(\s*)([A-Za-z_][\w]*)\s*:\s*(.*)$/.exec(lines[index]);
88
+ if (!match) {
89
+ index += 1;
90
+ continue;
91
+ }
92
+ const key = match[2];
93
+ const value = match[3].trim().replace(/,$/, '');
94
+ const block = value === '|-' || value === '|' || value === '>';
95
+ if (!block) {
96
+ fields.push([key, yamlValue(value)]);
97
+ index += 1;
98
+ continue;
99
+ }
100
+ const rows = [];
101
+ index += 1;
102
+ while (index < lines.length && /^\s+\S/.test(lines[index])) {
103
+ rows.push(lines[index].trim());
104
+ index += 1;
105
+ }
106
+ fields.push([key, rows.join('\n')]);
107
+ }
108
+ return fields;
109
+ };
110
+
111
+ const readYaml = (lines, start) => {
112
+ const body = [];
113
+ let index = start + 1;
114
+ for (; index < lines.length; index++) {
115
+ if (lines[index].trim() === '...') break;
116
+ body.push(lines[index]);
117
+ }
118
+ return { fields: yamlFields(body), next: index + 1 };
119
+ };
120
+
121
+ const tapName = (line) => {
122
+ const match = /^(?:not )?ok\s+\d+\s+-?\s*(.*)$/.exec(line.trim());
123
+ if (!match) return line.trim();
124
+ return match[1].trim();
125
+ };
126
+
127
+ const applyTapComment = (stats, line) => {
128
+ const match = /^#\s+(\w+)\s+(.+)$/.exec(line.trim());
129
+ if (!match) return;
130
+ const key = match[1];
131
+ const value = match[2].trim();
132
+ if (key === 'tests') stats.tests = Number(value);
133
+ if (key === 'pass') stats.passed = Number(value);
134
+ if (key === 'fail') stats.failed = Number(value);
135
+ if (key === 'skipped' || key === 'skip') stats.skipped = Number(value);
136
+ if (key === 'duration_ms') stats.duration = value;
137
+ };
138
+
139
+ const isTapLine = (line) => {
140
+ const text = line.trim();
141
+ if (text.startsWith('TAP version')) return true;
142
+ if (/^(not )?ok\s+\d+\b/.test(text)) return true;
143
+ if (/^1\.\.\d+/.test(text)) return true;
144
+ if (text.startsWith('Bail out!')) return true;
145
+ return false;
146
+ };
147
+
148
+ const parseTap = (text, root) => {
149
+ const lines = `${text ?? ''}`.split(/\r?\n/);
150
+ let seen = false;
151
+ for (const line of lines) {
152
+ if (isTapLine(line)) seen = true;
153
+ }
154
+ if (!seen) return null;
155
+ const stats = summary();
156
+ const problems = [];
157
+ let index = 0;
158
+ while (index < lines.length) {
159
+ const line = lines[index];
160
+ const trimmed = line.trim();
161
+ if (trimmed.startsWith('# ')) applyTapComment(stats, trimmed);
162
+ if (trimmed.startsWith('Bail out!')) {
163
+ problems.push(problem({ name: 'Bail out!', message: trimmed }));
164
+ }
165
+ const failed = /^not ok\s+\d+\b/.test(trimmed);
166
+ const passed = /^ok\s+\d+\b/.test(trimmed);
167
+ if (!failed && !passed) {
168
+ index += 1;
169
+ continue;
170
+ }
171
+ const name = tapName(trimmed);
172
+ let fields = [];
173
+ if (lines[index + 1] && lines[index + 1].trim() === '---') {
174
+ const yaml = readYaml(lines, index + 1);
175
+ fields = yaml.fields;
176
+ index = yaml.next;
177
+ } else {
178
+ index += 1;
179
+ }
180
+ if (failed) problems.push(problemFromFields(name, fields, root));
181
+ }
182
+ return report('test', { summary: stats, problems });
183
+ };
184
+
185
+ const countStatus = (stats, status) => {
186
+ if (status === 'passed' || status === 'pass') {
187
+ stats.passed = (stats.passed || 0) + 1;
188
+ return;
189
+ }
190
+ if (status === 'failed' || status === 'fail' || status === 'timedOut') {
191
+ stats.failed = (stats.failed || 0) + 1;
192
+ return;
193
+ }
194
+ if (status === 'skipped' || status === 'pending' || status === 'todo') {
195
+ stats.skipped = (stats.skipped || 0) + 1;
196
+ }
197
+ };
198
+
199
+ const nodeEventProblem = (event, root) => {
200
+ const data = event.data || {};
201
+ const details = data.details || {};
202
+ const error = details.error || {};
203
+ const fields = [];
204
+ for (const key of Object.keys(error)) fields.push([key, error[key]]);
205
+ if (details.duration_ms !== undefined) {
206
+ fields.push(['duration_ms', details.duration_ms]);
207
+ }
208
+ const name = data.name || '';
209
+ return problemFromFields(name, fields, root);
210
+ };
211
+
212
+ const parseNodeEvents = (events, root) => {
213
+ const stats = summary();
214
+ const problems = [];
215
+ let seen = false;
216
+ for (const event of events) {
217
+ const type = event && event.type;
218
+ if (typeof type !== 'string' || !type.startsWith('test:')) continue;
219
+ seen = true;
220
+ if (type === 'test:pass') countStatus(stats, 'passed');
221
+ if (type === 'test:fail') {
222
+ countStatus(stats, 'failed');
223
+ problems.push(nodeEventProblem(event, root));
224
+ }
225
+ if (type === 'test:skip' || type === 'test:todo') {
226
+ countStatus(stats, 'skipped');
227
+ }
228
+ }
229
+ if (!seen) return null;
230
+ const skipped = stats.skipped || 0;
231
+ stats.tests = (stats.passed || 0) + (stats.failed || 0) + skipped;
232
+ return report('test', { summary: stats, problems });
233
+ };
234
+
235
+ const parseNodeJsonl = (text, root) => {
236
+ const lines = `${text ?? ''}`
237
+ .split(/\r?\n/)
238
+ .map((line) => line.trim())
239
+ .filter(Boolean);
240
+ if (!lines.length) return null;
241
+ const events = [];
242
+ for (const line of lines) {
243
+ if (line[0] !== '{') return null;
244
+ try {
245
+ events.push(JSON.parse(line));
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ return parseNodeEvents(events, root);
251
+ };
252
+
253
+ const jestTrace = (text, file, root) => {
254
+ const stacks = `${text ?? ''}`
255
+ .split('\n')
256
+ .filter((line) => /^\s*at\s/.test(line));
257
+ const trace = userTrace(stacks.join('\n'), root);
258
+ if (trace) return trace;
259
+ return userTrace(file, root);
260
+ };
261
+
262
+ const jestProblem = (item, file, root) => {
263
+ const messages = item.failureMessages || [];
264
+ const text = messages.join('\n');
265
+ const expected = /Expected:\s*(.*)/.exec(text);
266
+ const received = /Received:\s*(.*)/.exec(text);
267
+ return problem({
268
+ name: item.fullName || item.title || '',
269
+ message: text,
270
+ trace: jestTrace(text, file, root),
271
+ expected: expected ? expected[1] : '',
272
+ actual: received ? received[1] : '',
273
+ duration: item.duration === undefined ? '' : `${item.duration}`,
274
+ });
275
+ };
276
+
277
+ const parseJest = (data, root) => {
278
+ if (!data || !Array.isArray(data.testResults)) return null;
279
+ if (data.numFailedTests === undefined && data.numPassedTests === undefined) {
280
+ return null;
281
+ }
282
+ const stats = summary();
283
+ stats.passed = data.numPassedTests ?? null;
284
+ stats.failed = data.numFailedTests ?? null;
285
+ stats.skipped = data.numPendingTests ?? null;
286
+ if (stats.passed !== null || stats.failed !== null) {
287
+ const skipped = stats.skipped || 0;
288
+ stats.tests = (stats.passed || 0) + (stats.failed || 0) + skipped;
289
+ }
290
+ const problems = [];
291
+ for (const file of data.testResults) {
292
+ const assertions = file.assertionResults || [];
293
+ for (const item of assertions) {
294
+ if (item.status !== 'failed') continue;
295
+ problems.push(jestProblem(item, file.name || '', root));
296
+ }
297
+ }
298
+ return report('test', { summary: stats, problems });
299
+ };
300
+
301
+ const walkPlaywright = (suite, problems, root) => {
302
+ const specs = suite.specs || [];
303
+ for (const spec of specs) {
304
+ const tests = spec.tests || [];
305
+ for (const item of tests) {
306
+ const results = item.results || [];
307
+ for (const result of results) {
308
+ const status = result.status;
309
+ if (status !== 'failed' && status !== 'timedOut') continue;
310
+ const error = result.error || {};
311
+ problems.push(
312
+ problem({
313
+ name: spec.title || '',
314
+ message: error.message || status,
315
+ trace: userTrace(error.stack || '', root),
316
+ duration: result.duration === undefined ? '' : `${result.duration}`,
317
+ }),
318
+ );
319
+ }
320
+ }
321
+ }
322
+ const children = suite.suites || [];
323
+ for (const child of children) walkPlaywright(child, problems, root);
324
+ };
325
+
326
+ const parsePlaywright = (data, root) => {
327
+ if (!data || !Array.isArray(data.suites)) return null;
328
+ const stats = summary();
329
+ const box = data.stats || {};
330
+ if (box.expected !== undefined) stats.passed = box.expected;
331
+ if (box.unexpected !== undefined) stats.failed = box.unexpected;
332
+ if (box.skipped !== undefined) stats.skipped = box.skipped;
333
+ if (box.duration !== undefined) stats.duration = `${box.duration}`;
334
+ const problems = [];
335
+ for (const suite of data.suites) walkPlaywright(suite, problems, root);
336
+ if (stats.passed === null && stats.failed === null && !problems.length) {
337
+ return null;
338
+ }
339
+ return report('test', { summary: stats, problems });
340
+ };
341
+
342
+ const parseTestJson = (data, root) => {
343
+ const jest = parseJest(data, root);
344
+ if (jest) return jest;
345
+ return parsePlaywright(data, root);
346
+ };
347
+
348
+ const specStats = (stats, line) => {
349
+ const match = /^[ℹi]\s+(\w+)\s+(.+)$/.exec(line.trim());
350
+ if (!match) return false;
351
+ const key = match[1];
352
+ const value = match[2].trim();
353
+ if (key === 'tests') stats.tests = Number(value);
354
+ if (key === 'pass') stats.passed = Number(value);
355
+ if (key === 'fail') stats.failed = Number(value);
356
+ if (key === 'skipped') stats.skipped = Number(value);
357
+ if (key === 'duration_ms') stats.duration = value;
358
+ return key === 'tests' || key === 'pass' || key === 'fail';
359
+ };
360
+
361
+ const specHeader = (line) =>
362
+ /^[✖●]\s+(failing|cancelled|todo) tests:?$/.test(line.trim());
363
+
364
+ const failureStart = (line) => {
365
+ const text = line.trim();
366
+ if (specHeader(line)) return false;
367
+ if (text.startsWith('✖') && !/✖\s+\d+\s+problems?\b/.test(text)) return true;
368
+ if (text.startsWith('FAIL ') || text === 'FAIL') return true;
369
+ if (/^not ok\s+\d+\b/.test(text)) return true;
370
+ if (text.startsWith('● ')) return true;
371
+ return false;
372
+ };
373
+
374
+ const failureName = (line) => {
375
+ const text = line
376
+ .trim()
377
+ .replace(/^[✖●]\s*/, '')
378
+ .replace(/^FAIL\s+/, '');
379
+ return text.replace(/\s+\([\d.]+m?s\)$/, '');
380
+ };
381
+
382
+ const readSpecObject = (lines, start) => {
383
+ const body = [];
384
+ let index = start + 1;
385
+ for (; index < lines.length; index++) {
386
+ if (lines[index].trim() === '}') break;
387
+ body.push(lines[index]);
388
+ }
389
+ return { fields: yamlFields(body), next: index + 1 };
390
+ };
391
+
392
+ const pushSpec = (problems, current, root) => {
393
+ if (!current) return;
394
+ const fields = current.fields;
395
+ const item = problemFromFields(current.name, fields, root);
396
+ if (current.message && !item.message) item.message = current.message;
397
+ const trace = userTrace(current.trace.join('\n'), root);
398
+ if (trace) item.trace = trace;
399
+ if (current.expected) item.expected = current.expected;
400
+ if (current.actual) item.actual = current.actual;
401
+ problems.push(item);
402
+ };
403
+
404
+ const takeSpecLine = (current, line) => {
405
+ const text = line.trim();
406
+ if (/^(AssertionError|Error)\b/.test(text)) {
407
+ current.message = text;
408
+ return;
409
+ }
410
+ const expected = /^Expected:\s*(.*)$/.exec(text);
411
+ if (expected) current.expected = expected[1];
412
+ const received = /^Received:\s*(.*)$/.exec(text);
413
+ if (received) current.actual = received[1];
414
+ };
415
+
416
+ const takeSpecFrame = (lines, index, current) => {
417
+ const line = lines[index];
418
+ if (!/^\s*at\s/.test(line)) return 0;
419
+ if (!/\{\s*$/.test(line)) {
420
+ current.trace.push(line);
421
+ return 1;
422
+ }
423
+ const frame = line.replace(/\s*\{\s*$/, '');
424
+ if (frame.trim()) current.trace.push(frame);
425
+ const block = readSpecObject(lines, index);
426
+ current.fields.push(...block.fields);
427
+ return block.next - index;
428
+ };
429
+
430
+ const hasDetail = (item) =>
431
+ item.message ||
432
+ item.trace ||
433
+ item.code ||
434
+ item.operator ||
435
+ item.expected ||
436
+ item.actual;
437
+
438
+ const dropPreviews = (problems) => {
439
+ const named = new Set();
440
+ for (const item of problems) {
441
+ if (item.name && hasDetail(item)) named.add(item.name);
442
+ }
443
+ const kept = [];
444
+ for (const item of problems) {
445
+ if (item.name && named.has(item.name) && !hasDetail(item)) continue;
446
+ kept.push(item);
447
+ }
448
+ return kept;
449
+ };
450
+
451
+ const parseSpec = (text, root) => {
452
+ const lines = `${text ?? ''}`.split(/\r?\n/);
453
+ const stats = summary();
454
+ const problems = [];
455
+ let current = null;
456
+ let marked = false;
457
+ let index = 0;
458
+ while (index < lines.length) {
459
+ const line = lines[index];
460
+ if (specStats(stats, line)) marked = true;
461
+ if (isPassingTest(line)) {
462
+ marked = true;
463
+ index += 1;
464
+ continue;
465
+ }
466
+ if (line.trim() === '{') {
467
+ const block = readSpecObject(lines, index);
468
+ if (current) current.fields.push(...block.fields);
469
+ index = block.next;
470
+ continue;
471
+ }
472
+ if (failureStart(line)) {
473
+ pushSpec(problems, current, root);
474
+ current = {
475
+ name: failureName(line),
476
+ message: '',
477
+ trace: [],
478
+ fields: [],
479
+ expected: '',
480
+ actual: '',
481
+ };
482
+ marked = true;
483
+ index += 1;
484
+ continue;
485
+ }
486
+ if (current) {
487
+ const skip = takeSpecFrame(lines, index, current);
488
+ if (skip) {
489
+ index += skip;
490
+ continue;
491
+ }
492
+ takeSpecLine(current, line);
493
+ }
494
+ index += 1;
495
+ }
496
+ pushSpec(problems, current, root);
497
+ const kept = dropPreviews(problems);
498
+ if (!marked && !kept.length) return null;
499
+ return report('test', { summary: stats, problems: kept });
500
+ };
501
+
502
+ module.exports = {
503
+ parseTap,
504
+ parseNodeJsonl,
505
+ parseTestJson,
506
+ parseSpec,
507
+ };