reslop 0.2.7 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +156 -63
  120. package/lib/session/tasks.js +320 -65
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { isHashObject } = require('metautil');
5
+ const { stripAnsi, visibleWidth } = require('../term/ansi.js');
6
+ const utilities = require('../common/utilities.js');
7
+ const { unquote } = utilities;
8
+
9
+ const isPassingTest = (line) => {
10
+ const plain = stripAnsi(line);
11
+ const text = plain.trim();
12
+ if (!text) return false;
13
+ if (/^\s*[✔✓√] /.test(plain)) return true;
14
+ if (/^ok \d+ /.test(text)) return true;
15
+ return /^\s*PASS\b/.test(plain);
16
+ };
17
+
18
+ const indentStack = (line) => {
19
+ if (!/^\s*at\s/.test(stripAnsi(line))) return line;
20
+ return ` ${line.trimStart()}`;
21
+ };
22
+
23
+ const withoutNodeFrame = (line) => {
24
+ const plain = stripAnsi(line);
25
+ if (!plain.includes('(node:')) return line;
26
+ return /\{\s*$/.test(plain) ? '{' : null;
27
+ };
28
+
29
+ const withSep = (dir) => {
30
+ const sep = dir.includes('\\') ? '\\' : '/';
31
+ if (dir.endsWith(sep)) return dir;
32
+ return `${dir}${sep}`;
33
+ };
34
+
35
+ const stripOne = (text, dir) => {
36
+ if (!dir) return text;
37
+ let next = text.split(withSep(dir)).join('');
38
+ if (next.includes(dir)) next = next.split(dir).join('.');
39
+ return next;
40
+ };
41
+
42
+ const relativize = (text, root) => {
43
+ if (!root) return text;
44
+ const slash = root.replaceAll('\\', '/');
45
+ const back = slash.replaceAll('/', '\\');
46
+ const resolved = path.resolve(root).replaceAll('\\', '/');
47
+ const resolvedBack = resolved.replaceAll('/', '\\');
48
+ let next = text;
49
+ for (const dir of [slash, back, resolved, resolvedBack]) {
50
+ next = stripOne(next, dir);
51
+ }
52
+ return next;
53
+ };
54
+
55
+ const traceLine = (line, root) => {
56
+ const next = withoutNodeFrame(line);
57
+ if (next === null) return null;
58
+ return relativize(indentStack(next), root);
59
+ };
60
+
61
+ const objectEnd = (lines, start) => {
62
+ if (stripAnsi(lines[start]).trim() !== '{') return -1;
63
+ for (let i = start + 1; i < lines.length; i++) {
64
+ if (stripAnsi(lines[i]).trim() === '}') return i;
65
+ }
66
+ return -1;
67
+ };
68
+
69
+ const parseInspect = (lines) => {
70
+ const entries = [];
71
+ for (const line of lines) {
72
+ const plain = stripAnsi(line).trim().replace(/,$/, '');
73
+ const match = /^([A-Za-z_][\w]*)\s*:\s*([\s\S]*)$/.exec(plain);
74
+ if (!match) {
75
+ if (!entries.length) return null;
76
+ const last = entries[entries.length - 1];
77
+ last[1] = `${last[1]}\n${plain}`;
78
+ continue;
79
+ }
80
+ entries.push([match[1], unquote(match[2])]);
81
+ }
82
+ return entries.length ? entries : null;
83
+ };
84
+
85
+ const showValue = (value) => {
86
+ if (value === null) return 'null';
87
+ if (typeof value === 'string') return value;
88
+ if (typeof value === 'number' || typeof value === 'boolean') {
89
+ return `${value}`;
90
+ }
91
+ return JSON.stringify(value);
92
+ };
93
+
94
+ const parseObject = (lines) => {
95
+ try {
96
+ const text = lines.join('\n');
97
+ const value = JSON.parse(text);
98
+ if (!isHashObject(value)) return null;
99
+ const entries = Object.keys(value).map((key) => [
100
+ key,
101
+ showValue(value[key]),
102
+ ]);
103
+ return entries.length ? entries : null;
104
+ } catch {
105
+ return parseInspect(lines.slice(1, -1));
106
+ }
107
+ };
108
+
109
+ const TABLE_KEY = '\x1b[900m';
110
+
111
+ const TABLE_VAL = '\x1b[901m';
112
+
113
+ const formatTable = (entries) => {
114
+ const prepared = [];
115
+ let keyWidth = 0;
116
+ for (const entry of entries) {
117
+ const lines = `${entry[1]}`.split('\n').filter((line) => line.trim());
118
+ if (!lines.length) continue;
119
+ prepared.push([entry[0], lines]);
120
+ keyWidth = Math.max(keyWidth, visibleWidth(entry[0]));
121
+ }
122
+ const rows = [];
123
+ for (const entry of prepared) {
124
+ const lines = entry[1];
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const name = i === 0 ? entry[0] : '';
127
+ const pad = ' '.repeat(Math.max(0, keyWidth - visibleWidth(name)));
128
+ const key = ` ${pad}${name} `;
129
+ const value = ` ${lines[i]} `;
130
+ rows.push(`${TABLE_KEY}${key}${TABLE_VAL} ${value}`);
131
+ }
132
+ }
133
+ return rows;
134
+ };
135
+
136
+ const foldObjects = (lines) => {
137
+ const out = [];
138
+ for (let i = 0; i < lines.length; i++) {
139
+ const end = objectEnd(lines, i);
140
+ const entries = end < 0 ? null : parseObject(lines.slice(i, end + 1));
141
+ if (!entries) {
142
+ out.push(lines[i]);
143
+ continue;
144
+ }
145
+ if (out.length && out[out.length - 1] !== '') out.push('');
146
+ out.push(...formatTable(entries));
147
+ i = end;
148
+ }
149
+ return out;
150
+ };
151
+
152
+ const reduceOutput = (text, root, status) => {
153
+ const kept = [];
154
+ for (const line of `${text ?? ''}`.split(/\r?\n/)) {
155
+ if (isPassingTest(line)) continue;
156
+ const next = traceLine(line, root);
157
+ if (next !== null) kept.push(next);
158
+ }
159
+ const folded = foldObjects(kept);
160
+ while (folded.length && folded[folded.length - 1] === '') folded.pop();
161
+ if (status !== '') folded.push(`exit ${status ?? 'null'}`);
162
+ if (!folded.length) return '';
163
+ return `${folded.join('\n')}\n`;
164
+ };
165
+
166
+ module.exports = {
167
+ isPassingTest,
168
+ relativize,
169
+ traceLine,
170
+ TABLE_KEY,
171
+ TABLE_VAL,
172
+ formatTable,
173
+ reduceOutput,
174
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const { spawnBase } = require('../common/process.js');
5
+
6
+ const signalPid = (pid, signal) => {
7
+ try {
8
+ process.kill(pid, signal);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ };
14
+
15
+ const killTree = (proc) => {
16
+ const pid = proc.pid;
17
+ if (!pid) return;
18
+ if (process.platform === 'win32') {
19
+ const killer = spawn(
20
+ 'taskkill',
21
+ ['/pid', String(pid), '/t', '/f'],
22
+ spawnBase({ stdio: 'ignore' }),
23
+ );
24
+ killer.on('error', () => proc.kill());
25
+ return;
26
+ }
27
+ const group = -pid;
28
+ if (!signalPid(group, 'SIGTERM')) signalPid(pid, 'SIGTERM');
29
+ const timer = setTimeout(() => {
30
+ if (!signalPid(group, 'SIGKILL')) signalPid(pid, 'SIGKILL');
31
+ }, 200);
32
+ timer.unref();
33
+ proc.once('close', () => clearTimeout(timer));
34
+ };
35
+
36
+ const commandEnv = (base = process.env) => {
37
+ const env = { ...base };
38
+ if (!env.NO_COLOR && !env.FORCE_COLOR) env.FORCE_COLOR = '1';
39
+ if (!env.TERM) env.TERM = 'xterm-256color';
40
+ env.RESLOP_OUTPUT = 'raw';
41
+ return env;
42
+ };
43
+
44
+ class RunProcess {
45
+ constructor(onData, onClose) {
46
+ this.onData = onData;
47
+ this.onClose = onClose;
48
+ this.child = null;
49
+ this.text = '';
50
+ this.settled = false;
51
+ this.stopped = false;
52
+ }
53
+
54
+ get closed() {
55
+ return this.stopped || this.settled;
56
+ }
57
+
58
+ show(text) {
59
+ this.text = text;
60
+ this.onData(text);
61
+ }
62
+
63
+ finish(status) {
64
+ if (this.settled) return;
65
+ this.settled = true;
66
+ this.onClose({ status, text: this.text });
67
+ }
68
+
69
+ kill() {
70
+ if (this.closed) return;
71
+ this.stopped = true;
72
+ killTree(this.child);
73
+ }
74
+
75
+ raw() {
76
+ return this.text;
77
+ }
78
+ }
79
+
80
+ class PipeProcess extends RunProcess {
81
+ start(cmd, args, options) {
82
+ try {
83
+ this.child = spawn(cmd, args, options);
84
+ } catch (error) {
85
+ this.text = `${error.message}\n`;
86
+ this.finish(1);
87
+ return this;
88
+ }
89
+ const { child } = this;
90
+ for (const stream of [child.stdout, child.stderr]) {
91
+ if (!stream) continue;
92
+ stream.setEncoding('utf8');
93
+ stream.on('data', (chunk) => this.push(chunk));
94
+ }
95
+ child.on('error', (error) => {
96
+ if (!this.stopped) this.text += `${error.message}\n`;
97
+ this.finish(1);
98
+ });
99
+ child.on('close', (status) => this.finish(status));
100
+ return this;
101
+ }
102
+
103
+ push(chunk) {
104
+ if (!this.closed) this.show(this.text + chunk);
105
+ }
106
+
107
+ closeInput() {
108
+ const stdin = this.child && this.child.stdin;
109
+ if (stdin) stdin.on('error', () => {}).end();
110
+ }
111
+ }
112
+
113
+ module.exports = { commandEnv, RunProcess, PipeProcess };
@@ -2,23 +2,33 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { readJson, listDir, trimText } = require('../common/utilities.js');
6
+ const { isHashObject } = require('metautil');
7
+ const { LOG_DIR } = require('../common/files.js');
5
8
 
6
- const ansi = require('./ansi.js');
7
- const { stripAnsi } = ansi;
8
- const commands = require('./npm-commands.js');
9
- const { isPassingTest } = commands;
9
+ const { stripAnsi } = require('../term/ansi.js');
10
+ const { isPassingTest } = require('./output.js');
11
+ const { nextLogFile } = require('./logs.js');
10
12
 
11
- const LOG_DIR = '.log';
12
13
  const RUNS_REL = `${LOG_DIR}/.runs`;
13
14
  const RUNS_KEEP = 24;
14
15
  const WRITE_MS = 150;
15
16
  const FAILING_TEST = /^\s*(?:not ok \d+|[✖✗×]) /;
16
17
  const LIFECYCLE_EVENT = 'npm_lifecycle_event';
17
18
 
19
+ const logDir = (root) => path.join(root, LOG_DIR);
20
+
18
21
  const runsDir = (root) => path.join(root, RUNS_REL);
19
22
 
20
- const isRunsRel = (rel) =>
21
- rel === LOG_DIR || rel === RUNS_REL || rel.startsWith(`${RUNS_REL}/`);
23
+ const isRunsRel = (rel) => {
24
+ if (rel === LOG_DIR || rel === RUNS_REL || rel.startsWith(`${RUNS_REL}/`)) {
25
+ return true;
26
+ }
27
+ if (!rel.startsWith(`${LOG_DIR}/`)) return false;
28
+ const name = rel.slice(LOG_DIR.length + 1);
29
+ if (name.includes('/')) return false;
30
+ return name.endsWith('.json') || name.endsWith('.log');
31
+ };
22
32
 
23
33
  const commandLabel = (program, args) =>
24
34
  [path.basename(program), ...args].join(' ').trim();
@@ -54,42 +64,37 @@ const pidAlive = (pid) => {
54
64
  };
55
65
 
56
66
  const readRecord = (file) => {
57
- try {
58
- const record = JSON.parse(fs.readFileSync(file, 'utf8'));
59
- if (!record || typeof record !== 'object' || !record.id) return null;
60
- return record;
61
- } catch {
62
- return null;
63
- }
67
+ const record = readJson(file);
68
+ return isHashObject(record) && record.id ? record : null;
64
69
  };
65
70
 
66
- const listRunNames = (root) => {
67
- try {
68
- return fs.readdirSync(runsDir(root)).filter((n) => n.endsWith('.json'));
69
- } catch {
70
- return [];
71
- }
72
- };
71
+ const listJson = (dir) => listDir(dir).filter((name) => name.endsWith('.json'));
72
+
73
+ const isCommandRun = (record) =>
74
+ Boolean(record && record.id && record.progress && !record.cliId);
73
75
 
74
76
  const settleRecord = (record, alive = pidAlive) => {
75
77
  if (record.status !== 'running' || alive(record.pid)) return record;
76
78
  return { ...record, status: 'aborted' };
77
79
  };
78
80
 
79
- const readRuns = (root) => {
80
- const dir = runsDir(root);
81
- const records = [];
82
- for (const name of listRunNames(root)) {
81
+ const pushRuns = (records, dir, names) => {
82
+ for (const name of names) {
83
83
  const record = readRecord(path.join(dir, name));
84
- if (record) records.push(settleRecord(record));
84
+ if (isCommandRun(record)) records.push(settleRecord(record));
85
85
  }
86
+ };
87
+
88
+ const readRuns = (root) => {
89
+ const records = [];
90
+ const dir = logDir(root);
91
+ pushRuns(records, dir, listJson(dir));
92
+ pushRuns(records, runsDir(root), listJson(runsDir(root)));
86
93
  return records.sort((left, right) => right.startedAt - left.startedAt);
87
94
  };
88
95
 
89
- const writeRecord = (root, record) => {
90
- const dir = runsDir(root);
91
- fs.mkdirSync(dir, { recursive: true });
92
- const file = path.join(dir, `${record.id}.json`);
96
+ const writeRecord = (file, record) => {
97
+ fs.mkdirSync(path.dirname(file), { recursive: true });
93
98
  const temp = `${file}.tmp`;
94
99
  const text = JSON.stringify(record);
95
100
  try {
@@ -100,12 +105,41 @@ const writeRecord = (root, record) => {
100
105
  }
101
106
  };
102
107
 
108
+ const removeStem = (dir, jsonName) => {
109
+ const stem = jsonName.replace(/\.json$/, '');
110
+ const suffixes = ['.json', '.log', '.raw', '.raw.log'];
111
+ for (const suffix of suffixes) {
112
+ fs.rmSync(path.join(dir, `${stem}${suffix}`), { force: true });
113
+ }
114
+ };
115
+
116
+ const byStart = (left, right) => {
117
+ const order = left.startedAt - right.startedAt;
118
+ return order || left.name.localeCompare(right.name, 'en');
119
+ };
120
+
121
+ const commandJson = (root) => {
122
+ const dir = logDir(root);
123
+ const found = [];
124
+ for (const name of listJson(dir)) {
125
+ if (/-agent-\d+\.json$/.test(name)) continue;
126
+ const record = readRecord(path.join(dir, name));
127
+ if (!isCommandRun(record)) continue;
128
+ found.push({ name, startedAt: Number(record.startedAt) || 0 });
129
+ }
130
+ return found.sort(byStart).map((entry) => entry.name);
131
+ };
132
+
103
133
  const pruneRuns = (root) => {
104
- const dir = runsDir(root);
105
- const names = listRunNames(root).sort();
134
+ const dir = logDir(root);
135
+ const names = commandJson(root);
106
136
  const extra = names.length - RUNS_KEEP;
107
- for (let i = 0; i < extra; i++) {
108
- fs.rmSync(path.join(dir, names[i]), { force: true });
137
+ for (let i = 0; i < extra; i++) removeStem(dir, names[i]);
138
+ const legacy = runsDir(root);
139
+ const old = listJson(legacy).sort();
140
+ const leftover = old.length - RUNS_KEEP;
141
+ for (let i = 0; i < leftover; i++) {
142
+ fs.rmSync(path.join(legacy, old[i]), { force: true });
109
143
  }
110
144
  };
111
145
 
@@ -164,6 +198,14 @@ class RunRecorder {
164
198
  this.tracker = new Tracker();
165
199
  this.lastWrite = 0;
166
200
  this.closed = false;
201
+ this.slot = Object.hasOwn(options, 'slot') ? options.slot : null;
202
+ if (!Object.hasOwn(options, 'slot')) {
203
+ try {
204
+ this.slot = nextLogFile(root, command);
205
+ } catch {
206
+ this.slot = null;
207
+ }
208
+ }
167
209
  const startedAt = this.clock();
168
210
  const env = options.env || process.env;
169
211
  this.record = {
@@ -171,7 +213,7 @@ class RunRecorder {
171
213
  id: `${startedAt}-${process.pid}`,
172
214
  pid: process.pid,
173
215
  command,
174
- script: `${env[LIFECYCLE_EVENT] ?? ''}`.trim(),
216
+ script: trimText(env[LIFECYCLE_EVENT]),
175
217
  cwd: root,
176
218
  startedAt,
177
219
  endedAt: 0,
@@ -180,6 +222,10 @@ class RunRecorder {
180
222
  progress: { ...this.tracker.progress, expected: 0 },
181
223
  result: null,
182
224
  };
225
+ if (this.slot) {
226
+ this.record.log = this.slot.name;
227
+ this.record.raw = this.slot.rawName;
228
+ }
183
229
  safely(() => {
184
230
  this.record.progress.expected = previousTotal(root, command);
185
231
  pruneRuns(root);
@@ -188,8 +234,9 @@ class RunRecorder {
188
234
  }
189
235
 
190
236
  write() {
237
+ if (!this.slot) return;
191
238
  Object.assign(this.record.progress, this.tracker.progress);
192
- writeRecord(this.root, this.record);
239
+ writeRecord(this.slot.jsonFull, this.record);
193
240
  }
194
241
 
195
242
  feed(chunk) {
@@ -214,6 +261,7 @@ class RunRecorder {
214
261
  }
215
262
 
216
263
  module.exports = {
264
+ logDir,
217
265
  runsDir,
218
266
  isRunsRel,
219
267
  commandLabel,
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const utilities = require('../common/utilities.js');
6
+ const { readJson } = utilities;
7
+ const { MANIFEST } = require('../common/files.js');
8
+
9
+ const SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies'];
10
+
11
+ const readManifest = (root) => {
12
+ const pkg = readJson(path.join(root, MANIFEST));
13
+ return pkg && typeof pkg === 'object' ? pkg : null;
14
+ };
15
+
16
+ const writeManifest = (root, pkg) => {
17
+ const file = path.join(root, MANIFEST);
18
+ fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
19
+ };
20
+
21
+ const scriptEntries = (pkg) => {
22
+ const scripts = pkg.scripts;
23
+ if (!scripts || typeof scripts !== 'object') return [];
24
+ const entries = [];
25
+ for (const name of Object.keys(scripts)) {
26
+ const command = scripts[name];
27
+ if (typeof command !== 'string') continue;
28
+ entries.push({ name, command, kind: 'script' });
29
+ }
30
+ return entries;
31
+ };
32
+
33
+ const binNames = (pkg) => {
34
+ if (!pkg || typeof pkg !== 'object') return [];
35
+ const bin = pkg.bin;
36
+ if (typeof bin === 'string' && bin) {
37
+ const raw = `${pkg.name ?? ''}`;
38
+ const base = raw.startsWith('@') ? raw.split('/')[1] : raw;
39
+ return base ? [base] : [];
40
+ }
41
+ if (!bin || typeof bin !== 'object' || Array.isArray(bin)) return [];
42
+ const isPath = (name) => typeof bin[name] === 'string' && bin[name] !== '';
43
+ return Object.keys(bin).filter(isPath);
44
+ };
45
+
46
+ const declaredNames = (pkg) => {
47
+ const names = [];
48
+ for (const section of SECTIONS) {
49
+ const map = pkg[section];
50
+ if (!map || typeof map !== 'object') continue;
51
+ names.push(...Object.keys(map));
52
+ }
53
+ return names;
54
+ };
55
+
56
+ const depManifest = (root, name) => {
57
+ const parts = name.startsWith('@') ? name.split('/').slice(0, 2) : [name];
58
+ return path.join(root, 'node_modules', ...parts, MANIFEST);
59
+ };
60
+
61
+ const binEntries = (root, pkg, taken) => {
62
+ const entries = [];
63
+ const seen = new Set(taken);
64
+ for (const name of declaredNames(pkg)) {
65
+ const dep = readJson(depManifest(root, name));
66
+ for (const bin of binNames(dep)) {
67
+ if (seen.has(bin)) continue;
68
+ seen.add(bin);
69
+ entries.push({ name: bin, command: bin, kind: 'bin', package: name });
70
+ }
71
+ }
72
+ entries.sort((left, right) => left.name.localeCompare(right.name));
73
+ return entries;
74
+ };
75
+
76
+ const listCommands = (root) => {
77
+ const pkg = readManifest(root);
78
+ if (!pkg) return [];
79
+ const scripts = scriptEntries(pkg);
80
+ const taken = scripts.map((entry) => entry.name);
81
+ return [...scripts, ...binEntries(root, pkg, taken)];
82
+ };
83
+
84
+ const orderedScripts = (pkg) => {
85
+ const scripts = pkg.scripts;
86
+ if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
87
+ return {};
88
+ }
89
+ return scripts;
90
+ };
91
+
92
+ const pickScripts = (scripts, names) => {
93
+ const ordered = {};
94
+ for (const key of names) ordered[key] = scripts[key];
95
+ return ordered;
96
+ };
97
+
98
+ const upsertScript = (pkg, prev, next) => {
99
+ const scripts = orderedScripts(pkg);
100
+ const names = Object.keys(scripts);
101
+ if (!prev || !names.includes(prev)) {
102
+ pkg.scripts = { ...scripts, [next.name]: next.command };
103
+ return;
104
+ }
105
+ const ordered = {};
106
+ for (const key of names) {
107
+ if (key === prev) ordered[next.name] = next.command;
108
+ else if (key !== next.name) ordered[key] = scripts[key];
109
+ }
110
+ pkg.scripts = ordered;
111
+ };
112
+
113
+ const saveScript = (root, prev, next) => {
114
+ const pkg = readManifest(root) ?? {};
115
+ upsertScript(pkg, prev, next);
116
+ writeManifest(root, pkg);
117
+ };
118
+
119
+ const removeScript = (root, name) => {
120
+ const pkg = readManifest(root);
121
+ if (!pkg) return false;
122
+ const scripts = orderedScripts(pkg);
123
+ if (!Object.hasOwn(scripts, name)) return false;
124
+ const names = Object.keys(scripts).filter((key) => key !== name);
125
+ pkg.scripts = pickScripts(scripts, names);
126
+ writeManifest(root, pkg);
127
+ return true;
128
+ };
129
+
130
+ const reorderScript = (root, name, delta) => {
131
+ const pkg = readManifest(root);
132
+ if (!pkg) return false;
133
+ const scripts = orderedScripts(pkg);
134
+ const names = Object.keys(scripts);
135
+ const at = names.indexOf(name);
136
+ const next = at + delta;
137
+ if (at < 0 || next < 0 || next >= names.length) return false;
138
+ names[at] = names[next];
139
+ names[next] = name;
140
+ pkg.scripts = pickScripts(scripts, names);
141
+ writeManifest(root, pkg);
142
+ return true;
143
+ };
144
+
145
+ module.exports = {
146
+ listCommands,
147
+ saveScript,
148
+ removeScript,
149
+ reorderScript,
150
+ };