reslop 0.2.5 → 0.2.7

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -6,31 +6,26 @@ const model = require('./report-model.js');
6
6
  const testParse = require('./report-test.js');
7
7
  const toolParse = require('./report-tools.js');
8
8
 
9
- const { problem, report, groupReport } = model;
10
- const { reduceStderr, shortTail, envelope } = model;
9
+ const { problem, report, userTrace, groupReport, envelope } = model;
11
10
  const { parseTap, parseNodeJsonl, parseTestJson, parseSpec } = testParse;
12
- const { parseToolJson, parseToolText } = toolParse;
11
+ const { parseToolJson, parseToolText, stderrRest } = toolParse;
13
12
 
14
- const KNOWN = ['test', 'tsc', 'eslint', 'prettier', 'audit', 'outdated', 'ls'];
13
+ const COUNTS = ['tests', 'passed', 'failed', 'skipped', 'errors', 'warnings'];
15
14
 
16
15
  const toolHint = (program, args) => {
17
- const name = `${program ?? ''}`.split(/[/\\]/).pop();
18
- const list = args || [];
19
- const joined = [name, ...list].join(' ');
20
- if (name === 'eslint') return 'eslint';
21
- if (name === 'tsc') return 'tsc';
22
- if (name === 'prettier') return 'prettier';
16
+ const name = program.split(/[/\\]/).pop();
17
+ const joined = [name, ...args].join(' ');
18
+ if (name === 'eslint' || name === 'tsc' || name === 'prettier') return name;
23
19
  if (/\baudit\b/.test(joined)) return 'audit';
24
20
  if (/\boutdated\b/.test(joined)) return 'outdated';
25
- if (name === 'npm' || name === 'pnpm' || name === 'npm.cmd') {
26
- if (/\b(ls|list)\b/.test(joined)) return 'ls';
27
- }
28
- if (name === 'node' && (args || []).includes('--test')) return 'test';
21
+ const isNpm = name === 'npm' || name === 'pnpm' || name === 'npm.cmd';
22
+ if (isNpm && /\b(ls|list)\b/.test(joined)) return 'ls';
23
+ if (name === 'node' && args.includes('--test')) return 'test';
29
24
  return '';
30
25
  };
31
26
 
32
27
  const parseJson = (text) => {
33
- const trimmed = `${text ?? ''}`.trim();
28
+ const trimmed = text.trim();
34
29
  if (!trimmed || (trimmed[0] !== '{' && trimmed[0] !== '[')) return null;
35
30
  try {
36
31
  return JSON.parse(trimmed);
@@ -46,40 +41,64 @@ const parseJsonReports = (text, root) => {
46
41
  if (!data) return null;
47
42
  const test = parseTestJson(data, root);
48
43
  if (test) return [test];
49
- const tools = parseToolJson(data, root);
50
- if (tools) return tools;
51
- return [];
44
+ return parseToolJson(data, root) || [];
52
45
  };
53
46
 
54
47
  const parseReports = (text, root) => {
55
48
  const json = parseJsonReports(text, root);
56
49
  if (json) return json;
57
- const reports = [];
58
50
  const tap = parseTap(text, root);
59
- if (tap) reports.push(tap);
60
- const tools = parseToolText(text, root);
61
- for (const item of tools) reports.push(item);
62
- if (!tap) {
63
- const spec = parseSpec(text, root);
64
- if (spec) reports.push(spec);
65
- }
66
- return reports;
51
+ const spec = tap ? null : parseSpec(text, root);
52
+ const reports = [tap, ...parseToolText(text, root), spec];
53
+ return reports.filter(Boolean);
67
54
  };
68
55
 
56
+ const shortTail = (text) => text.trim().split(/\r?\n/).slice(-20).join('\n');
57
+
69
58
  const withHint = (reports, text, exit, hint) => {
70
- if (reports.length || !`${text ?? ''}`.trim() || exit === 0) return reports;
71
- const tool = KNOWN.includes(hint) ? hint : 'test';
72
- return [report(tool, { problems: [problem({ message: shortTail(text) })] })];
59
+ if (reports.length || !text.trim() || exit === 0) return reports;
60
+ const fallback = problem({ message: shortTail(text) });
61
+ return [report(hint || 'test', { problems: [fallback] })];
73
62
  };
74
63
 
75
- const buildDocument = (stdout, stderr, root, exit, hint) => {
76
- const text = stripAnsi(stdout);
77
- const reports = withHint(parseReports(text, root), text, exit, hint);
78
- const grouped = new Array(reports.length);
79
- for (let i = 0; i < reports.length; i++) {
80
- grouped[i] = groupReport(reports[i]);
64
+ const mergeSummary = (left, right) => {
65
+ const stats = { ...left };
66
+ for (const key of COUNTS) {
67
+ if (right[key] === null) continue;
68
+ stats[key] = (stats[key] ?? 0) + right[key];
69
+ }
70
+ if (!stats.duration && right.duration) stats.duration = right.duration;
71
+ return stats;
72
+ };
73
+
74
+ const mergeReports = (left, right) => {
75
+ const merged = left.slice();
76
+ for (const item of right) {
77
+ const at = merged.findIndex((entry) => entry.tool === item.tool);
78
+ if (at < 0) {
79
+ merged.push(item);
80
+ continue;
81
+ }
82
+ const prev = merged[at];
83
+ merged[at] = report(item.tool, {
84
+ summary: mergeSummary(prev.summary, item.summary),
85
+ problems: prev.problems.concat(item.problems),
86
+ });
81
87
  }
82
- return envelope(grouped, exit, reduceStderr(stderr, root));
88
+ return merged;
89
+ };
90
+
91
+ const buildDocument = (stdout, stderr, root, exit, hint) => {
92
+ const out = stripAnsi(stdout);
93
+ const err = stripAnsi(stderr);
94
+ const reports = mergeReports(
95
+ parseReports(out, root),
96
+ parseReports(err, root),
97
+ );
98
+ const text = out.trim() ? out : err;
99
+ const grouped = withHint(reports, text, exit, hint).map(groupReport);
100
+ const rest = stderrRest(err, grouped);
101
+ return envelope(grouped, exit, userTrace(rest, root));
83
102
  };
84
103
 
85
104
  module.exports = {
@@ -1,78 +1,111 @@
1
1
  'use strict';
2
2
 
3
+ const utilities = require('./utilities.js');
4
+ const { unquote } = utilities;
5
+
3
6
  const NOTICE_HEAD =
4
7
  'reslop: reduced output; passing results are omitted ' +
5
8
  'and similar problems are grouped';
6
9
 
7
- const NOTICE = [
8
- NOTICE_HEAD,
9
- 'If reduced output hides information needed for debugging,',
10
- 'rerun the same command with RESLOP_OUTPUT=raw.',
11
- ].join('\n');
10
+ const notice = (rawFile) => {
11
+ const target = rawFile
12
+ ? `the raw log \`.log/${rawFile}\``
13
+ : 'the matching raw log under `.log/`';
14
+ const hint = `Agent: if this report hides a needed detail, read ${target}.`;
15
+ return `${NOTICE_HEAD}\n${hint}`;
16
+ };
12
17
 
13
- const SUMMARY_ROWS = 6;
14
- const PROBLEM_FIELDS = 7;
18
+ const addRow = (rows, key, value) => {
19
+ if (value === null || value === undefined || value === '') return;
20
+ rows.push([key, `${value}`]);
21
+ };
15
22
 
16
- const writeRow = (rows, index, key, value) => {
17
- if (value === null || value === undefined || value === '') return index;
18
- rows[index] = [key, `${value}`];
19
- return index + 1;
23
+ const isComparison = (line) => {
24
+ const match = /^(.*) (?:!==|===|!=|==) (\S.*)$/.exec(line.trim());
25
+ return !!match && match[1].trim() !== '';
20
26
  };
21
27
 
22
- const summaryRows = (stats) => {
23
- const rows = new Array(SUMMARY_ROWS);
24
- let index = 0;
25
- index = writeRow(rows, index, 'passed', stats.passed);
26
- index = writeRow(rows, index, 'failed', stats.failed);
27
- index = writeRow(rows, index, 'skipped', stats.skipped);
28
- index = writeRow(rows, index, 'errors', stats.errors);
29
- index = writeRow(rows, index, 'warnings', stats.warnings);
30
- index = writeRow(rows, index, 'duration', stats.duration);
31
- rows.length = index;
32
- return rows;
28
+ const isDiffHeader = (line) => {
29
+ const text = line.trim().replace(/^[+-]\s*/, '');
30
+ return /^actual\s+[+-]?\s*expected$/.test(text);
33
31
  };
34
32
 
35
- const problemRows = (item) => {
36
- const extra = item.extra;
37
- let size = PROBLEM_FIELDS + extra.length;
38
- if (item.count > 1) size += 1;
39
- if (item.omitted > 0) size += 1;
40
- const rows = new Array(size);
41
- let index = 0;
42
- index = writeRow(rows, index, 'severity', item.severity);
43
- index = writeRow(rows, index, 'duration', item.duration);
44
- index = writeRow(rows, index, 'code', item.code);
45
- index = writeRow(rows, index, 'operator', item.operator);
46
- index = writeRow(rows, index, 'expected', item.expected);
47
- index = writeRow(rows, index, 'actual', item.actual);
48
- index = writeRow(rows, index, 'message', item.message);
49
- for (let i = 0; i < extra.length; i++) {
50
- const pair = extra[i];
51
- index = writeRow(rows, index, pair[0], pair[1]);
33
+ const isMarkedDiff = (line) => /^[+-](?:\s|$)/.test(line.trim());
34
+
35
+ const sameValue = (line, value) => {
36
+ if (!value) return false;
37
+ const text = line.trim();
38
+ return text === value || unquote(text) === value;
39
+ };
40
+
41
+ const startsDiff = (line, item) =>
42
+ isComparison(line) ||
43
+ isDiffHeader(line) ||
44
+ isMarkedDiff(line) ||
45
+ sameValue(line, item.expected) ||
46
+ sameValue(line, item.actual);
47
+
48
+ const displayMessage = (item) => {
49
+ const values = item.expected !== '' || item.actual !== '';
50
+ const lines = [];
51
+ for (const line of `${item.message}`.split('\n')) {
52
+ if (line.trim() === '') continue;
53
+ if (values && startsDiff(line, item)) break;
54
+ lines.push(line);
52
55
  }
53
- if (item.count > 1) index = writeRow(rows, index, 'count', item.count);
54
- if (item.omitted > 0) {
55
- index = writeRow(rows, index, 'omitted', item.omitted);
56
+ if (lines.length && lines[0].trimEnd().endsWith(':')) {
57
+ lines[0] = lines[0].trimEnd().slice(0, -1);
56
58
  }
57
- rows.length = index;
59
+ if (lines.length && lines[0].trim() === '') lines.shift();
60
+ return lines.join('\n');
61
+ };
62
+
63
+ const summaryRows = (stats) => {
64
+ const rows = [];
65
+ addRow(rows, 'passed', stats.passed);
66
+ addRow(rows, 'failed', stats.failed);
67
+ addRow(rows, 'skipped', stats.skipped);
68
+ addRow(rows, 'errors', stats.errors);
69
+ addRow(rows, 'warnings', stats.warnings);
70
+ addRow(rows, 'duration', stats.duration);
58
71
  return rows;
59
72
  };
60
73
 
61
- const markdownValue = (value) => {
62
- const text = `${value}`;
63
- if (!text.includes('\n')) return text;
64
- return `\n${text}`;
74
+ const problemRows = (item) => {
75
+ const rows = [];
76
+ if (item.severity !== 'error') addRow(rows, 'severity', item.severity);
77
+ addRow(rows, 'duration', item.duration);
78
+ addRow(rows, 'code', item.code);
79
+ addRow(rows, 'operator', item.operator);
80
+ addRow(rows, 'expected', item.expected);
81
+ addRow(rows, 'actual', item.actual);
82
+ for (const pair of item.extra) addRow(rows, pair[0], pair[1]);
83
+ if (item.count > 1) addRow(rows, 'count', item.count);
84
+ if (item.omitted > 0) addRow(rows, 'omitted', item.omitted);
85
+ return rows;
65
86
  };
66
87
 
88
+ const prefixLines = (prefix, text) =>
89
+ `${text}`
90
+ .replace(/\n+$/, '')
91
+ .split('\n')
92
+ .map((line) => `${prefix}${line}`);
93
+
94
+ const quoteLines = (text) => prefixLines('│ ', text);
95
+
67
96
  const markdownProblem = (item) => {
68
97
  const title = item.name || item.message || item.code || 'problem';
69
98
  const lines = ['', `## ${title}`, ''];
70
99
  for (const row of problemRows(item)) {
71
- lines.push(`- ${row[0]}: ${markdownValue(row[1])}`);
100
+ const value = row[1].includes('\n') ? `\n${row[1]}` : row[1];
101
+ lines.push(`- ${row[0]}: ${value}`);
72
102
  }
73
- for (const trace of item.traces) {
74
- if (!trace) continue;
75
- lines.push(`- trace:\n${trace}`);
103
+ const message = displayMessage(item);
104
+ if (message) lines.push('', ...prefixLines('> ', message), '');
105
+ if (item.trace) lines.push(`- trace:\n${item.trace}`);
106
+ if (item.related.length) {
107
+ lines.push('- related:');
108
+ for (const row of item.related) lines.push(` - ${row}`);
76
109
  }
77
110
  return lines;
78
111
  };
@@ -82,23 +115,33 @@ const markdownReport = (item) => {
82
115
  for (const row of summaryRows(item.summary)) {
83
116
  lines.push(`${row[0]}: ${row[1]}`);
84
117
  }
85
- for (const problem of item.problems) {
86
- lines.push(...markdownProblem(problem));
87
- }
118
+ for (const entry of item.problems) lines.push(...markdownProblem(entry));
88
119
  return lines;
89
120
  };
90
121
 
91
- const renderMarkdown = (doc) => {
92
- const lines = [NOTICE, '', `exit: ${doc.exit}`];
122
+ const renderDocument = (doc, rawFile) => {
123
+ const lines = [notice(rawFile)];
93
124
  for (const item of doc.reports) lines.push(...markdownReport(item));
94
125
  if (doc.stderr) lines.push('', '# stderr', '', doc.stderr);
126
+ lines.push('', `exit: ${doc.exit}`);
95
127
  return `${lines.join('\n').replace(/\n+$/, '')}\n`;
96
128
  };
97
129
 
98
- const renderDocument = (doc) => renderMarkdown(doc);
130
+ const loggedText = (text, rawFile) => {
131
+ const body = `${text ?? ''}`;
132
+ if (body.startsWith(NOTICE_HEAD)) return body;
133
+ const head = notice(rawFile);
134
+ if (!body.trim()) return `${head}\n`;
135
+ return `${head}\n\n${body}`;
136
+ };
99
137
 
100
138
  module.exports = {
101
- NOTICE,
102
- renderMarkdown,
139
+ NOTICE_HEAD,
140
+ notice,
141
+ summaryRows,
142
+ problemRows,
143
+ displayMessage,
144
+ quoteLines,
145
+ loggedText,
103
146
  renderDocument,
104
147
  };
package/lib/report-run.js CHANGED
@@ -7,20 +7,16 @@ const { spawn } = childProcess;
7
7
  const utilities = require('./utilities.js');
8
8
  const { spawnBase } = utilities;
9
9
  const npmCommands = require('./npm-commands.js');
10
- const { writeLog } = npmCommands;
10
+ const { nextLogFile, saveLogs } = npmCommands;
11
11
  const reportParse = require('./report-parse.js');
12
12
  const { toolHint, buildDocument } = reportParse;
13
13
  const reportRender = require('./report-render.js');
14
14
  const { renderDocument } = reportRender;
15
+ const runs = require('./runs.js');
16
+ const { RunRecorder, commandLabel, summarizeDocument } = runs;
15
17
 
16
- const OUTPUTS = new Set(['md', 'raw']);
17
-
18
- const outputMode = (env) => {
19
- const value = env.RESLOP_OUTPUT;
20
- if (value) return OUTPUTS.has(value) ? value : '';
21
- return 'md';
22
- };
23
-
18
+ const OUTPUTS = ['md', 'raw'];
19
+ const PIPE = ['ignore', 'pipe', 'pipe'];
24
20
  const CMD_SAFE = /^[\w./:=@+-]+$/;
25
21
 
26
22
  const quoteCmdArg = (arg) => {
@@ -35,34 +31,18 @@ const winCommand = (program, args) => {
35
31
  return `"${parts.join(' ')}"`;
36
32
  };
37
33
 
38
- const spawnCommand = (program, args, options) => {
39
- const { cwd, env, stdio } = options;
40
- const base = { cwd, env, stdio };
34
+ const spawnCommand = (command, options) => {
35
+ const { program, args } = command;
41
36
  if (process.platform !== 'win32') {
42
- return spawn(program, args, spawnBase(base));
37
+ return spawn(program, args, spawnBase(options));
43
38
  }
44
- const command = winCommand(program, args);
45
39
  const shell = process.env.comspec || 'cmd.exe';
46
- return spawn(
47
- shell,
48
- ['/d', '/s', '/c', command],
49
- spawnBase({ ...base, windowsVerbatimArguments: true }),
50
- );
40
+ const line = winCommand(program, args);
41
+ const opts = spawnBase({ ...options, windowsVerbatimArguments: true });
42
+ return spawn(shell, ['/d', '/s', '/c', line], opts);
51
43
  };
52
44
 
53
- const waitStatus = (child) =>
54
- new Promise((resolve) => {
55
- let settled = false;
56
- const finish = (status) => {
57
- if (settled) return;
58
- settled = true;
59
- resolve(status);
60
- };
61
- child.on('error', () => finish(1));
62
- child.on('close', (status) => finish(status === null ? 1 : status));
63
- });
64
-
65
- const collect = (child) =>
45
+ const collect = (child, onOut, onErr) =>
66
46
  new Promise((resolve) => {
67
47
  let out = '';
68
48
  let err = '';
@@ -76,89 +56,119 @@ const collect = (child) =>
76
56
  child.stdout.setEncoding('utf8');
77
57
  child.stdout.on('data', (chunk) => {
78
58
  out += chunk;
59
+ if (onOut) onOut(chunk);
79
60
  });
80
61
  }
81
62
  if (child.stderr) {
82
63
  child.stderr.setEncoding('utf8');
83
64
  child.stderr.on('data', (chunk) => {
84
65
  err += chunk;
66
+ if (onErr) onErr(chunk);
85
67
  });
86
68
  }
87
69
  child.on('error', (error) => {
88
70
  err += `${error.message}\n`;
89
71
  finish(1);
90
72
  });
91
- child.on('close', (status) => finish(status === null ? 1 : status));
73
+ child.on('close', (status) => finish(status ?? 1));
92
74
  });
93
75
 
94
- const commandArgs = (argv) => {
95
- if (argv[0] !== 't' || argv[1] !== '--' || !argv[2]) return null;
96
- return { program: argv[2], args: argv.slice(3) };
76
+ const beginRun = (cwd, command) =>
77
+ new RunRecorder(cwd, commandLabel(command.program, command.args));
78
+
79
+ const documentOf = (command, result, cwd) => {
80
+ const hint = toolHint(command.program, command.args);
81
+ const { stdout, stderr, status } = result;
82
+ return buildDocument(stdout, stderr, cwd, status, hint);
97
83
  };
98
84
 
99
- const usage = (stderr) => {
100
- stderr.write('reslop: missing command\n');
101
- stderr.write('Usage: reslop t -- <program> [args]\n');
102
- return 1;
85
+ const rawStreams = (stdout, stderr) => {
86
+ if (!stderr.trim()) return stdout;
87
+ const lead = stdout.endsWith('\n') || !stdout ? stdout : `${stdout}\n`;
88
+ return `${lead}# stderr\n${stderr}`;
103
89
  };
104
90
 
105
- const runInherit = async (spawnFn, program, args, options) => {
106
- let child;
91
+ const runInherit = async (command, cwd, env) => {
107
92
  try {
108
- child = spawnFn(program, args, { ...options, stdio: 'inherit' });
93
+ const child = spawnCommand(command, { cwd, env, stdio: 'inherit' });
94
+ return (await collect(child)).status;
109
95
  } catch {
110
96
  return 1;
111
97
  }
112
- return waitStatus(child);
113
98
  };
114
99
 
115
- const runCapture = async (proc, deps = {}) => {
116
- const env = proc.env || {};
117
- const argv = proc.argv.slice(2);
118
- const command = commandArgs(argv);
119
- if (!command) return usage(proc.stderr);
120
- const mode = outputMode(env);
121
- if (!mode) {
122
- proc.stderr.write(`reslop: unknown RESLOP_OUTPUT ${env.RESLOP_OUTPUT}\n`);
100
+ const runRaw = async (proc, command, cwd, env) => {
101
+ const recorder = beginRun(cwd, command);
102
+ let child;
103
+ try {
104
+ child = spawnCommand(command, { cwd, env, stdio: PIPE });
105
+ } catch {
106
+ recorder.finish(1);
123
107
  return 1;
124
108
  }
125
- const cwd = deps.cwd ? deps.cwd : proc.cwd();
126
- const spawnFn = deps.spawn || spawnCommand;
127
- const capture = env.RESLOP_CAPTURE === '1';
128
- if (mode === 'raw' || capture) {
129
- return runInherit(spawnFn, command.program, command.args, { cwd, env });
130
- }
131
- const childEnv = { ...env, RESLOP_CAPTURE: '1' };
109
+ const onOut = (chunk) => {
110
+ proc.stdout.write(chunk);
111
+ recorder.feed(chunk);
112
+ };
113
+ const onErr = (chunk) => proc.stderr.write(chunk);
114
+ const result = await collect(child, onOut, onErr);
115
+ const doc = documentOf(command, result, cwd);
116
+ recorder.finish(result.status, summarizeDocument(doc));
117
+ return result.status;
118
+ };
119
+
120
+ const runReport = async (proc, command, cwd, env) => {
121
+ const recorder = beginRun(cwd, command);
122
+ const fail = (error) => proc.stderr.write(`reslop: ${error.message}\n`);
132
123
  let child;
133
124
  try {
134
- child = spawnFn(command.program, command.args, {
135
- cwd,
136
- env: childEnv,
137
- stdio: ['ignore', 'pipe', 'pipe'],
138
- });
125
+ const childEnv = { ...env, RESLOP_CAPTURE: '1' };
126
+ child = spawnCommand(command, { cwd, env: childEnv, stdio: PIPE });
139
127
  } catch (error) {
140
- proc.stderr.write(`reslop: ${error.message}\n`);
128
+ fail(error);
129
+ recorder.finish(1);
141
130
  return 1;
142
131
  }
143
- const result = await collect(child);
144
- const hint = toolHint(command.program, command.args);
145
- const doc = buildDocument(
146
- result.stdout,
147
- result.stderr,
148
- cwd,
149
- result.status,
150
- hint,
151
- );
152
- const text = renderDocument(doc);
132
+ const result = await collect(child, (chunk) => recorder.feed(chunk));
133
+ const doc = documentOf(command, result, cwd);
134
+ let slot = null;
135
+ try {
136
+ slot = nextLogFile(cwd, path.basename(command.program));
137
+ } catch (error) {
138
+ fail(error);
139
+ }
140
+ const text = renderDocument(doc, slot ? slot.rawName : '');
141
+ recorder.finish(result.status, summarizeDocument(doc));
153
142
  proc.stdout.write(text);
143
+ if (!slot) return result.status;
154
144
  try {
155
- writeLog(cwd, path.basename(command.program), text);
145
+ await saveLogs(slot, text, rawStreams(result.stdout, result.stderr));
156
146
  } catch (error) {
157
- proc.stderr.write(`reslop: ${error.message}\n`);
147
+ fail(error);
158
148
  }
159
149
  return result.status;
160
150
  };
161
151
 
152
+ const runCapture = async (proc, deps = {}) => {
153
+ const argv = proc.argv.slice(2);
154
+ if (argv[1] !== '--' || !argv[2]) {
155
+ proc.stderr.write('reslop: missing command\n');
156
+ proc.stderr.write('Usage: reslop t -- <program> [args]\n');
157
+ return 1;
158
+ }
159
+ const command = { program: argv[2], args: argv.slice(3) };
160
+ const env = proc.env ?? {};
161
+ const mode = env.RESLOP_OUTPUT || 'md';
162
+ if (!OUTPUTS.includes(mode)) {
163
+ proc.stderr.write(`reslop: unknown RESLOP_OUTPUT ${env.RESLOP_OUTPUT}\n`);
164
+ return 1;
165
+ }
166
+ const cwd = deps.cwd ?? proc.cwd();
167
+ if (env.RESLOP_CAPTURE === '1') return runInherit(command, cwd, env);
168
+ if (mode === 'raw') return runRaw(proc, command, cwd, env);
169
+ return runReport(proc, command, cwd, env);
170
+ };
171
+
162
172
  module.exports = {
163
173
  winCommand,
164
174
  runCapture,