reslop 0.3.0 → 0.3.2
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 +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/select.js +2 -1
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/render/repos.js
CHANGED
|
@@ -9,6 +9,7 @@ const { paintTile, composeBands, tileHits, pageFill } = tiles;
|
|
|
9
9
|
const { segmentsWidth, clipSegments, blinkTone } = tiles;
|
|
10
10
|
const { size } = require('../common/format.js');
|
|
11
11
|
const { isHot } = require('./table.js');
|
|
12
|
+
const { paintNameEdit } = require('./name-edit.js');
|
|
12
13
|
|
|
13
14
|
const REPO_MIN_H = 6;
|
|
14
15
|
const MARGIN_MIN_H = 12;
|
|
@@ -40,11 +41,24 @@ const fitAside = (left, right, width) => {
|
|
|
40
41
|
return [...clipped, seg(' '.repeat(gap)), ...right];
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
// A linked worktree hangs under its main one, the branch tells them apart
|
|
45
|
+
const branchSegs = (row) => {
|
|
46
|
+
if (!row.branch && !row.head) return [];
|
|
47
|
+
const label = row.branch || `(${row.head})`;
|
|
48
|
+
const segs = [seg(` ⎇ ${label}`, 'sha')];
|
|
49
|
+
if (row.dirty) segs.push(seg('*', 'warn', true));
|
|
50
|
+
if (row.locked) segs.push(seg(' locked', 'muted'));
|
|
51
|
+
return segs;
|
|
52
|
+
};
|
|
53
|
+
|
|
43
54
|
const titleLine = (row, ctx, selected, width) => {
|
|
44
55
|
const hot = isHot(row.activityAt, ctx.now);
|
|
45
56
|
const tone = hot ? blinkTone(ctx.frame) : 'text';
|
|
46
|
-
const left = [
|
|
57
|
+
const left = [];
|
|
58
|
+
if (row.linked) left.push(seg('↳ ', 'muted'));
|
|
59
|
+
left.push(seg(row.name, tone, selected || hot));
|
|
47
60
|
if (hot) left.push(seg(' ●', tone, true));
|
|
61
|
+
left.push(...branchSegs(row));
|
|
48
62
|
const right = [];
|
|
49
63
|
if (rowPending(row)) right.push(spin(ctx.frame), seg(' '));
|
|
50
64
|
const label = row.filesReady ? size(row.bytes) : '…';
|
|
@@ -90,7 +104,7 @@ const resultNote = (run) => {
|
|
|
90
104
|
return [];
|
|
91
105
|
};
|
|
92
106
|
|
|
93
|
-
const
|
|
107
|
+
const runState = (row, ctx) => {
|
|
94
108
|
if (!row.runsReady) return [seg('run: ', 'muted'), spin(ctx.frame)];
|
|
95
109
|
const run = row.run;
|
|
96
110
|
if (!run) return [seg('run: ', 'muted'), seg('idle', 'muted')];
|
|
@@ -113,6 +127,17 @@ const runLine = (row, ctx) => {
|
|
|
113
127
|
];
|
|
114
128
|
};
|
|
115
129
|
|
|
130
|
+
// The agents working in the directory go on the line of the run
|
|
131
|
+
const agentsNote = (row) => {
|
|
132
|
+
const count = row.agents ?? 0;
|
|
133
|
+
return [
|
|
134
|
+
seg(' agents active: ', 'muted'),
|
|
135
|
+
seg(`${count}`, count ? 'warn' : 'muted', count > 0),
|
|
136
|
+
];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const runLine = (row, ctx) => [...runState(row, ctx), ...agentsNote(row)];
|
|
140
|
+
|
|
116
141
|
const npmLine = (row, ctx) => {
|
|
117
142
|
if (!row.npmReady) return [seg('deps: ', 'muted'), spin(ctx.frame)];
|
|
118
143
|
const npm = row.npm;
|
|
@@ -191,14 +216,29 @@ const paintRepos = (model, bands, color) => {
|
|
|
191
216
|
return painted;
|
|
192
217
|
};
|
|
193
218
|
|
|
219
|
+
// One row of tiles above another list, paged sideways
|
|
220
|
+
const paintTreeBand = (model, width, height, color) => {
|
|
221
|
+
const grid = equalGrid(width, height, model.tiles.length, 34, height);
|
|
222
|
+
const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
|
|
223
|
+
const shown = model.tiles.slice(page.start, page.end);
|
|
224
|
+
const bands = placeEqualTiles(shown, grid);
|
|
225
|
+
const painted = paintRepos(model, bands, color);
|
|
226
|
+
const rows = composeBands(bands, painted, width, height, color);
|
|
227
|
+
return { rows, bands, cols: grid.cols };
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// The name of a new worktree is typed in a row above the tiles
|
|
194
231
|
const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
|
|
195
232
|
const model = view.repos;
|
|
196
233
|
if (!model || !model.tiles.length) {
|
|
197
234
|
const blank = paintBodyFill(width, color, 'files');
|
|
198
235
|
return paneResult({ body: new Array(bodyH).fill(blank) });
|
|
199
236
|
}
|
|
200
|
-
const
|
|
201
|
-
const
|
|
237
|
+
const naming = view.compose?.kind === 'worktree';
|
|
238
|
+
const edit = naming ? paintNameEdit(view.compose, width, color) : null;
|
|
239
|
+
const lead = edit ? 1 : 0;
|
|
240
|
+
const margin = bodyH - lead >= MARGIN_MIN_H ? 1 : 0;
|
|
241
|
+
const height = Math.max(1, bodyH - lead - margin * 2);
|
|
202
242
|
const grid = equalGrid(width, height, model.tiles.length, 34, REPO_MIN_H);
|
|
203
243
|
const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
|
|
204
244
|
const shown = model.tiles.slice(page.start, page.end);
|
|
@@ -206,9 +246,13 @@ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
|
|
|
206
246
|
const painted = paintRepos(model, bands, color);
|
|
207
247
|
const rows = composeBands(bands, painted, width, height, color);
|
|
208
248
|
const pad = new Array(margin).fill(pageFill(width, color));
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
249
|
+
const head = edit ? [edit.row] : [];
|
|
250
|
+
const body = [...head, ...pad, ...rows, ...pad];
|
|
251
|
+
const fileHits = tileHits(bands, headerLines + lead + margin);
|
|
252
|
+
const typing = edit ? { cursor: { ...edit.cursor, row: 0 } } : {};
|
|
253
|
+
const editHits = edit ? edit.edits.map((span) => ({ ...span, row: 0 })) : [];
|
|
254
|
+
const result = paneResult({ body, fileHits, editHits, ...typing });
|
|
255
|
+
return { ...result, gridCols: grid.cols };
|
|
212
256
|
};
|
|
213
257
|
|
|
214
|
-
module.exports = { paintBodyRepos };
|
|
258
|
+
module.exports = { paintBodyRepos, paintTreeBand };
|
package/lib/render/status.js
CHANGED
|
@@ -9,6 +9,7 @@ const { npmStatus } = require('./dashboard/blocks.js');
|
|
|
9
9
|
const { menuBoxWidth, dropBox, menuFaces } = require('./menu.js');
|
|
10
10
|
const field = require('./field.js');
|
|
11
11
|
const { paintPlanFace, paintImportStatus, paintFindStatus } = field;
|
|
12
|
+
const { fieldHit } = field;
|
|
12
13
|
const { fileTotals } = files;
|
|
13
14
|
const { CONFIRM, confirmPrefix, promptFromChoices } = actions;
|
|
14
15
|
const { THEME, paint, visibleWidth } = ansi;
|
|
@@ -267,8 +268,8 @@ const paintTasksLeft = (view, text, color) => {
|
|
|
267
268
|
const before = text.slice(0, place.at);
|
|
268
269
|
const after = text.slice(place.at + place.face.length);
|
|
269
270
|
const searching = Boolean(view.planOpen && view.planQuery);
|
|
270
|
-
const comboFg = searching ? THEME.
|
|
271
|
-
const comboBg =
|
|
271
|
+
const comboFg = searching ? THEME.warnFg : THEME.shaFg;
|
|
272
|
+
const comboBg = THEME.chromeBg;
|
|
272
273
|
const branch = paintBranchHead(before, view.branch, color);
|
|
273
274
|
const head = branch ?? paintStatusLeft(before, color);
|
|
274
275
|
const face = paintPlanFace(view, place.face, comboFg, comboBg, color);
|
|
@@ -293,7 +294,10 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
293
294
|
const leftGap = ' '.repeat(layout.padLeft);
|
|
294
295
|
const rightGap = ' '.repeat(layout.padRight);
|
|
295
296
|
if (!color) {
|
|
296
|
-
|
|
297
|
+
const left = view.planOpen
|
|
298
|
+
? paintTasksLeft(view, layout.clipped, false)
|
|
299
|
+
: layout.clipped;
|
|
300
|
+
return left + leftGap + layout.msg + rightGap + layout.right;
|
|
297
301
|
}
|
|
298
302
|
const bgRgb = THEME.chromeBg;
|
|
299
303
|
const head = statusHead(view, layout.clipped, color);
|
|
@@ -323,6 +327,19 @@ const statusPlanHit = (view, width, y) => {
|
|
|
323
327
|
return { id: 'plan', y, x0, x1: x0 + visibleWidth(place.face) };
|
|
324
328
|
};
|
|
325
329
|
|
|
330
|
+
const statusEditHits = (view, width, y) => {
|
|
331
|
+
const editor = view.import?.editor ?? view.find?.editor;
|
|
332
|
+
if (editor) {
|
|
333
|
+
const x = view.import ? 5 : 2;
|
|
334
|
+
return [fieldHit(editor, x, Math.max(0, width - x), y)];
|
|
335
|
+
}
|
|
336
|
+
if (!view.planOpen || !view.planEditor) return [];
|
|
337
|
+
const layout = statusLayout(view, view.status ?? '', width);
|
|
338
|
+
const place = planFace(view, layout.clipped);
|
|
339
|
+
if (!place) return [];
|
|
340
|
+
return [fieldHit(view.planEditor, place.x, visibleWidth(place.face), y)];
|
|
341
|
+
};
|
|
342
|
+
|
|
326
343
|
const planMenu = (view, width, color, statusY) => {
|
|
327
344
|
if (view.pane !== 'tasks' || !view.planOpen || statusY < 3) return null;
|
|
328
345
|
const names = view.planNames ?? [];
|
|
@@ -333,7 +350,7 @@ const planMenu = (view, width, color, statusY) => {
|
|
|
333
350
|
const room = Math.max(1, statusY - 2);
|
|
334
351
|
const box = dropBox(names, view.planCursor, view.planScroll, room);
|
|
335
352
|
const boxW = menuBoxWidth(names, visibleWidth(place.face), width - x);
|
|
336
|
-
const faces = menuFaces(names, box, boxW, color);
|
|
353
|
+
const faces = menuFaces(names, box, boxW, color, view.planQuery);
|
|
337
354
|
const lines = [];
|
|
338
355
|
const hits = [];
|
|
339
356
|
for (let i = 0; i < faces.length; i++) {
|
|
@@ -366,5 +383,6 @@ module.exports = {
|
|
|
366
383
|
statusChoiceHits,
|
|
367
384
|
statusBranchHit,
|
|
368
385
|
statusPlanHit,
|
|
386
|
+
statusEditHits,
|
|
369
387
|
planMenu,
|
|
370
388
|
};
|
package/lib/report/render.js
CHANGED
|
@@ -3,15 +3,12 @@
|
|
|
3
3
|
const { unquote } = require('../common/utilities.js');
|
|
4
4
|
const { formatTable } = require('../runs/output.js');
|
|
5
5
|
|
|
6
|
-
const NOTICE_HEAD =
|
|
7
|
-
'reslop: reduced output; passing results are omitted ' +
|
|
8
|
-
'and similar problems are grouped';
|
|
6
|
+
const NOTICE_HEAD = 'Reduced output;';
|
|
9
7
|
|
|
10
8
|
const notice = (rawFile) => {
|
|
11
|
-
let target = '
|
|
12
|
-
if (rawFile) target =
|
|
13
|
-
|
|
14
|
-
return `${NOTICE_HEAD}\n${hint}`;
|
|
9
|
+
let target = 'under `.log/`';
|
|
10
|
+
if (rawFile) target = `\`.log/${rawFile}\``;
|
|
11
|
+
return `${NOTICE_HEAD} the raw log if needed ${target}.`;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
14
|
const addRow = (rows, key, value) => {
|
package/lib/report/run.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const { spawn } = require('node:child_process');
|
|
5
4
|
|
|
6
|
-
const { spawnBase } = require('../common/process.js');
|
|
5
|
+
const { spawnBase, spawnCmd, winCommand } = require('../common/process.js');
|
|
7
6
|
const { withNewline } = require('../common/utilities.js');
|
|
8
7
|
const { nextLogFile, saveLogs } = require('../runs/logs.js');
|
|
9
8
|
const { toolHint, buildDocument } = require('./parse.js');
|
|
@@ -16,29 +15,12 @@ const {
|
|
|
16
15
|
|
|
17
16
|
const OUTPUTS = ['md', 'raw'];
|
|
18
17
|
const PIPE = ['ignore', 'pipe', 'pipe'];
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const quoteCmdArg = (arg) => {
|
|
22
|
-
const text = `${arg}`;
|
|
23
|
-
if (text && CMD_SAFE.test(text)) return text;
|
|
24
|
-
const escaped = text.replaceAll('"', '""').replaceAll('%', '"%"');
|
|
25
|
-
return `"${escaped}"`;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const winCommand = (program, args) => {
|
|
29
|
-
const parts = [program, ...args].map(quoteCmdArg);
|
|
30
|
-
return `"${parts.join(' ')}"`;
|
|
31
|
-
};
|
|
18
|
+
const EXIT_DRAIN_MS = 100;
|
|
32
19
|
|
|
33
20
|
const spawnCommand = (command, options) => {
|
|
34
21
|
const { program, args } = command;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
const shell = process.env.comspec || 'cmd.exe';
|
|
39
|
-
const line = winCommand(program, args);
|
|
40
|
-
const opts = spawnBase({ ...options, windowsVerbatimArguments: true });
|
|
41
|
-
return spawn(shell, ['/d', '/s', '/c', line], opts);
|
|
22
|
+
const shell = process.platform === 'win32';
|
|
23
|
+
return spawnCmd(program, args, spawnBase({ ...options, shell }));
|
|
42
24
|
};
|
|
43
25
|
|
|
44
26
|
const collect = (child, onOut, onErr) =>
|
|
@@ -46,9 +28,15 @@ const collect = (child, onOut, onErr) =>
|
|
|
46
28
|
let out = '';
|
|
47
29
|
let err = '';
|
|
48
30
|
let settled = false;
|
|
49
|
-
|
|
31
|
+
let exitTimer = null;
|
|
32
|
+
const finish = (status, release = false) => {
|
|
50
33
|
if (settled) return;
|
|
51
34
|
settled = true;
|
|
35
|
+
clearTimeout(exitTimer);
|
|
36
|
+
if (release) {
|
|
37
|
+
if (child.stdout) child.stdout.destroy();
|
|
38
|
+
if (child.stderr) child.stderr.destroy();
|
|
39
|
+
}
|
|
52
40
|
resolve({ status, stdout: out, stderr: err });
|
|
53
41
|
};
|
|
54
42
|
if (child.stdout) {
|
|
@@ -69,6 +57,10 @@ const collect = (child, onOut, onErr) =>
|
|
|
69
57
|
err += `${error.message}\n`;
|
|
70
58
|
finish(1);
|
|
71
59
|
});
|
|
60
|
+
child.on('exit', (status) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
exitTimer = setTimeout(() => finish(status ?? 1, true), EXIT_DRAIN_MS);
|
|
63
|
+
});
|
|
72
64
|
child.on('close', (status) => finish(status ?? 1));
|
|
73
65
|
});
|
|
74
66
|
|
|
@@ -182,6 +174,7 @@ const runCapture = async (proc, deps = {}) => {
|
|
|
182
174
|
};
|
|
183
175
|
|
|
184
176
|
module.exports = {
|
|
177
|
+
collect,
|
|
185
178
|
winCommand,
|
|
186
179
|
runCapture,
|
|
187
180
|
};
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
1
|
+
Work through unchecked items.
|
|
2
|
+
|
|
3
|
+
- Run reviews with status `editing` or `partial`
|
|
4
|
+
- Do not start or change reviews with status `done`
|
|
5
|
+
- After finishing an item, mark it `[x]`
|
|
6
|
+
- Mark `[x]` only after the code changes are done and tests pass
|
|
7
|
+
- For `code` items, replace the added lines at the given location
|
|
8
|
+
- Set `status` to `partial` if any items remain, or `done` if all are `[x]`
|
|
7
9
|
- Do not redo items already marked `[x]`
|
|
8
|
-
- Run
|
|
9
|
-
- Run
|
|
10
|
+
- Run `npm t` for lint and tests that do not start processes
|
|
11
|
+
- Run tests that start processes with `node --test <files>`
|
|
12
|
+
- An item prefixed with `F<number>:` gets its own commit in this repo
|
|
13
|
+
- Leave items without that prefix unstaged, do not commit or stage them
|
|
14
|
+
- Do not add `Authored-by` or `Co-authored-by` unless the issue asks for it
|
package/lib/review/format.js
CHANGED
|
@@ -58,10 +58,6 @@ const TASK_TITLES = Object.fromEntries([
|
|
|
58
58
|
...TASK_KINDS.map((kind) => [kind.title, kind.id]),
|
|
59
59
|
]);
|
|
60
60
|
|
|
61
|
-
const AGENT_BRIEF =
|
|
62
|
-
'Follow the agent instructions. Work through unchecked items. ' +
|
|
63
|
-
'Mark [x] only near exit after tests pass.';
|
|
64
|
-
|
|
65
61
|
const AGENT_INSTRUCTIONS = resource(__dirname, 'agent-instructions.md').trim();
|
|
66
62
|
|
|
67
63
|
const canonicalStatus = (raw) => {
|
|
@@ -179,8 +175,6 @@ const reviewHeader = (notes) => {
|
|
|
179
175
|
'',
|
|
180
176
|
'## Agent instructions',
|
|
181
177
|
'',
|
|
182
|
-
AGENT_BRIEF,
|
|
183
|
-
'',
|
|
184
178
|
AGENT_INSTRUCTIONS,
|
|
185
179
|
'',
|
|
186
180
|
];
|
package/lib/runs/process.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spawn } = require('node:child_process');
|
|
4
|
-
const { spawnBase } = require('../common/process.js');
|
|
4
|
+
const { spawnBase, spawnCmd } = require('../common/process.js');
|
|
5
5
|
|
|
6
6
|
const signalPid = (pid, signal) => {
|
|
7
7
|
try {
|
|
@@ -80,7 +80,7 @@ class RunProcess {
|
|
|
80
80
|
class PipeProcess extends RunProcess {
|
|
81
81
|
start(cmd, args, options) {
|
|
82
82
|
try {
|
|
83
|
-
this.child =
|
|
83
|
+
this.child = spawnCmd(cmd, args, options);
|
|
84
84
|
} catch (error) {
|
|
85
85
|
this.text = `${error.message}\n`;
|
|
86
86
|
this.finish(1);
|
|
@@ -110,4 +110,4 @@ class PipeProcess extends RunProcess {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
module.exports = { commandEnv, RunProcess, PipeProcess };
|
|
113
|
+
module.exports = { killTree, commandEnv, RunProcess, PipeProcess };
|
package/lib/runs/runs.js
CHANGED
|
@@ -13,6 +13,8 @@ const { nextLogFile } = require('./logs.js');
|
|
|
13
13
|
const RUNS_REL = `${LOG_DIR}/.runs`;
|
|
14
14
|
const RUNS_KEEP = 24;
|
|
15
15
|
const WRITE_MS = 150;
|
|
16
|
+
const HEARTBEAT_MS = 1000;
|
|
17
|
+
const HEARTBEAT_STALE_MS = 5000;
|
|
16
18
|
const FAILING_TEST = /^\s*(?:not ok \d+|[✖✗×]) /;
|
|
17
19
|
const LIFECYCLE_EVENT = 'npm_lifecycle_event';
|
|
18
20
|
|
|
@@ -73,9 +75,19 @@ const listJson = (dir) => listDir(dir).filter((name) => name.endsWith('.json'));
|
|
|
73
75
|
const isCommandRun = (record) =>
|
|
74
76
|
Boolean(record && record.id && record.progress && !record.cliId);
|
|
75
77
|
|
|
76
|
-
const settleRecord = (record, alive = pidAlive) => {
|
|
77
|
-
if (record.status !== 'running'
|
|
78
|
-
|
|
78
|
+
const settleRecord = (record, alive = pidAlive, now = Date.now) => {
|
|
79
|
+
if (record.status !== 'running') return record;
|
|
80
|
+
const activeAt = Number(record.activeAt);
|
|
81
|
+
const current = Number(now());
|
|
82
|
+
const fresh =
|
|
83
|
+
record.v >= 2 &&
|
|
84
|
+
Number.isFinite(activeAt) &&
|
|
85
|
+
Number.isFinite(current) &&
|
|
86
|
+
current - activeAt <= HEARTBEAT_STALE_MS;
|
|
87
|
+
if (fresh && alive(record.pid)) return record;
|
|
88
|
+
const endedAt =
|
|
89
|
+
Number(record.endedAt) || activeAt || Number(record.startedAt) || 0;
|
|
90
|
+
return { ...record, status: 'aborted', endedAt };
|
|
79
91
|
};
|
|
80
92
|
|
|
81
93
|
const pushRuns = (records, dir, names) => {
|
|
@@ -198,6 +210,7 @@ class RunRecorder {
|
|
|
198
210
|
this.tracker = new Tracker();
|
|
199
211
|
this.lastWrite = 0;
|
|
200
212
|
this.closed = false;
|
|
213
|
+
this.heartbeat = null;
|
|
201
214
|
this.slot = Object.hasOwn(options, 'slot') ? options.slot : null;
|
|
202
215
|
if (!Object.hasOwn(options, 'slot')) {
|
|
203
216
|
try {
|
|
@@ -209,7 +222,7 @@ class RunRecorder {
|
|
|
209
222
|
const startedAt = this.clock();
|
|
210
223
|
const env = options.env || process.env;
|
|
211
224
|
this.record = {
|
|
212
|
-
v:
|
|
225
|
+
v: 2,
|
|
213
226
|
id: `${startedAt}-${process.pid}`,
|
|
214
227
|
pid: process.pid,
|
|
215
228
|
command,
|
|
@@ -217,6 +230,7 @@ class RunRecorder {
|
|
|
217
230
|
cwd: root,
|
|
218
231
|
startedAt,
|
|
219
232
|
endedAt: 0,
|
|
233
|
+
activeAt: startedAt,
|
|
220
234
|
status: 'running',
|
|
221
235
|
exit: null,
|
|
222
236
|
progress: { ...this.tracker.progress, expected: 0 },
|
|
@@ -231,10 +245,17 @@ class RunRecorder {
|
|
|
231
245
|
pruneRuns(root);
|
|
232
246
|
this.write();
|
|
233
247
|
});
|
|
248
|
+
if (this.slot) {
|
|
249
|
+
this.heartbeat = setInterval(() => {
|
|
250
|
+
safely(() => this.write());
|
|
251
|
+
}, HEARTBEAT_MS);
|
|
252
|
+
this.heartbeat.unref();
|
|
253
|
+
}
|
|
234
254
|
}
|
|
235
255
|
|
|
236
256
|
write() {
|
|
237
257
|
if (!this.slot) return;
|
|
258
|
+
if (!this.closed) this.record.activeAt = this.clock();
|
|
238
259
|
Object.assign(this.record.progress, this.tracker.progress);
|
|
239
260
|
writeRecord(this.slot.jsonFull, this.record);
|
|
240
261
|
}
|
|
@@ -251,6 +272,8 @@ class RunRecorder {
|
|
|
251
272
|
finish(status, result = null) {
|
|
252
273
|
if (this.closed) return;
|
|
253
274
|
this.closed = true;
|
|
275
|
+
clearInterval(this.heartbeat);
|
|
276
|
+
this.heartbeat = null;
|
|
254
277
|
const record = this.record;
|
|
255
278
|
record.endedAt = this.clock();
|
|
256
279
|
record.exit = status;
|