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.
- package/CHANGELOG.md +17 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +504 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +16 -0
- package/lib/agents/models.js +460 -0
- package/lib/api.js +15 -12
- package/lib/cli.js +43 -68
- package/lib/{files.js → common/files.js} +48 -55
- package/lib/{find.js → common/find.js} +1 -1
- package/lib/common/format.js +101 -0
- package/lib/{utilities.js → common/process.js} +7 -95
- package/lib/common/tasks.json +27 -0
- package/lib/common/utilities.js +112 -0
- package/lib/dashboard/git.js +9 -16
- package/lib/dashboard/model.js +90 -49
- package/lib/dashboard/npm.js +17 -13
- package/lib/dashboard/tree.js +27 -30
- package/lib/{session → dashboard}/watch.js +36 -31
- package/lib/{workspace.js → dashboard/workspace.js} +23 -38
- package/lib/{deps.js → deps/deps.js} +31 -45
- package/lib/{deps-diff.js → deps/diff.js} +35 -25
- package/lib/{manifest.js → deps/manifest.js} +47 -30
- package/lib/diff/align.js +1 -2
- package/lib/diff/diff.js +2 -2
- package/lib/diff/display.js +29 -5
- package/lib/diff/inline.js +10 -14
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +39 -3
- package/lib/diff/unit.js +45 -16
- package/lib/{git-branches.js → git/branches.js} +53 -48
- package/lib/{git-deps.js → git/deps.js} +123 -96
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +475 -0
- package/lib/highlight/bash.js +1 -2
- package/lib/highlight/csv.js +1 -2
- package/lib/highlight/dot.js +1 -2
- package/lib/{highlight.js → highlight/highlight.js} +42 -11
- package/lib/highlight/html.js +20 -18
- package/lib/highlight/json.js +1 -2
- package/lib/highlight/log.js +1 -2
- package/lib/highlight/txt.js +1 -2
- package/lib/{session → input}/actions.js +111 -48
- package/lib/{editor.js → input/editor.js} +7 -8
- package/lib/{keys.js → input/keys.js} +15 -43
- package/lib/input/line-edit.js +77 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +640 -0
- package/lib/render/branches.js +4 -5
- package/lib/render/commits.js +54 -64
- package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
- package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
- package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
- package/lib/render/diff.js +166 -45
- package/lib/render/field.js +131 -0
- package/lib/render/files.js +9 -14
- package/lib/render/footer.js +383 -0
- package/lib/render/frame.js +123 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +319 -0
- package/lib/render/menu.js +60 -0
- package/lib/render/notes.js +16 -11
- package/lib/render/npm.js +322 -196
- package/lib/render/packages.js +9 -13
- package/lib/render/primitives.js +101 -15
- package/lib/render/render.js +7 -11
- package/lib/render/repos.js +13 -25
- package/lib/render/status.js +370 -0
- package/lib/render/status.json +85 -0
- package/lib/render/{dash-table.js → table.js} +13 -60
- package/lib/render/tiles.js +109 -48
- package/lib/render/unit.js +32 -13
- package/lib/{report-model.js → report/model.js} +4 -6
- package/lib/{report-parse.js → report/parse.js} +9 -14
- package/lib/{report-render.js → report/render.js} +46 -14
- package/lib/{report-run.js → report/run.js} +42 -30
- package/lib/{report-test.js → report/test.js} +56 -55
- package/lib/{report-tools.js → report/tools.js} +24 -33
- package/lib/review/agent-instructions.md +9 -0
- package/lib/review/format.js +426 -0
- package/lib/{review-merge.js → review/merge.js} +9 -8
- package/lib/review/review.js +190 -0
- package/lib/review/store.js +281 -0
- package/lib/review/templates.js +83 -0
- package/lib/runs/commands.js +14 -0
- package/lib/runs/logs.js +299 -0
- package/lib/runs/output.js +174 -0
- package/lib/runs/process.js +113 -0
- package/lib/{runs.js → runs/runs.js} +84 -36
- package/lib/runs/scripts.js +150 -0
- package/lib/session/agents/agents.js +958 -0
- package/lib/session/agents/job.js +320 -0
- package/lib/session/agents/jobs.js +355 -0
- package/lib/session/agents/options.js +153 -0
- package/lib/session/agents/plans.js +83 -0
- package/lib/session/branches.js +33 -39
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +91 -52
- package/lib/session/compose.js +181 -145
- package/lib/session/dashboard.js +23 -22
- package/lib/session/dispatch.js +85 -21
- package/lib/session/files-pane.js +47 -26
- package/lib/session/find.js +16 -29
- package/lib/session/import.js +82 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +75 -72
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +6 -3
- package/lib/session/npm/history.js +249 -0
- package/lib/session/npm/npm.js +815 -0
- package/lib/session/{packages.js → npm/packages.js} +21 -14
- package/lib/session/ops.js +125 -34
- package/lib/session/pointer.js +202 -10
- package/lib/session/prefs.js +112 -0
- package/lib/session/reads.js +109 -0
- package/lib/session/review.js +28 -10
- package/lib/session/run-panel.js +40 -0
- package/lib/{session.js → session/session.js} +156 -63
- package/lib/session/tasks.js +320 -65
- package/lib/session/templates.js +1 -1
- package/lib/session/terminal.js +94 -12
- package/lib/session/unit.js +35 -30
- package/lib/session/update.js +194 -6
- package/lib/session/view.js +65 -65
- package/lib/session/workspace.js +39 -78
- package/lib/{capabilities.js → source/capabilities.js} +3 -12
- package/lib/{github.js → source/github.js} +102 -32
- package/lib/{gitlab.js → source/gitlab.js} +87 -10
- package/lib/{remote.js → source/remote.js} +144 -40
- package/lib/source/review-threads.graphql +20 -0
- package/lib/source/source.js +74 -0
- package/lib/term/ansi.js +396 -0
- package/lib/{clipboard.js → term/clipboard.js} +3 -6
- package/lib/{select.js → term/select.js} +2 -2
- package/lib/term/themes/dark.json +122 -0
- package/lib/term/themes/light.json +122 -0
- package/lib/term/wide.txt +21 -0
- package/lib/{wrap.js → term/wrap.js} +4 -4
- package/package.json +2 -2
- package/lib/ansi.js +0 -545
- package/lib/detect.js +0 -34
- package/lib/diff/edit.js +0 -33
- package/lib/git-worktree.js +0 -455
- package/lib/git.js +0 -79
- package/lib/npm-commands.js +0 -522
- package/lib/render/chrome.js +0 -806
- package/lib/render/present.js +0 -19
- package/lib/render/report.js +0 -43
- package/lib/render/search.js +0 -25
- package/lib/review.js +0 -745
- package/lib/session/accessors.js +0 -26
- package/lib/session/edit-click.js +0 -57
- package/lib/session/npm.js +0 -452
- package/lib/session/progress.js +0 -41
- package/lib/source.js +0 -36
- package/lib/update.js +0 -192
- /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
|
|
7
|
-
const {
|
|
8
|
-
const
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
80
|
-
const
|
|
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 = (
|
|
90
|
-
|
|
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 =
|
|
105
|
-
const names =
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
+
};
|