reslop 0.2.5 → 0.2.6
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 +12 -1
- package/LICENSE +1 -1
- package/README.md +11 -11
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -8
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +39 -47
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +197 -402
- package/lib/deps.js +113 -240
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +76 -144
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +79 -178
- package/lib/editor.js +52 -69
- package/lib/files.js +71 -89
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +147 -272
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +155 -141
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +14 -14
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +19 -25
- package/lib/highlight/txt.js +7 -10
- package/lib/highlight.js +17 -46
- package/lib/keys.js +49 -82
- package/lib/manifest.js +67 -126
- package/lib/npm-commands.js +120 -169
- package/lib/remote.js +140 -190
- package/lib/render/branches.js +63 -61
- package/lib/render/chrome.js +220 -333
- package/lib/render/commits.js +160 -361
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -258
- package/lib/render/files.js +40 -85
- package/lib/render/frame.js +78 -117
- package/lib/render/notes.js +139 -210
- package/lib/render/npm.js +213 -177
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +72 -174
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -31
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -37
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -77
- package/lib/report-test.js +221 -99
- package/lib/report-tools.js +96 -154
- package/lib/review-merge.js +33 -41
- package/lib/review.js +68 -129
- package/lib/runs.js +225 -0
- package/lib/select.js +53 -91
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +61 -43
- package/lib/session/branches.js +166 -222
- package/lib/session/change-actions.js +49 -119
- package/lib/session/commit.js +274 -355
- package/lib/session/compose.js +556 -714
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +162 -170
- package/lib/session/files-pane.js +76 -44
- package/lib/session/items.js +6 -17
- package/lib/session/lifecycle.js +393 -466
- package/lib/session/load.js +1 -14
- package/lib/session/navigation.js +2 -2
- package/lib/session/npm.js +403 -420
- package/lib/session/ops.js +5 -13
- package/lib/session/pointer.js +16 -14
- package/lib/session/progress.js +39 -30
- package/lib/session/review.js +1 -3
- package/lib/session/tasks.js +276 -235
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -132
- package/lib/session/view.js +47 -81
- package/lib/session/watch.js +196 -212
- package/lib/session.js +81 -161
- package/lib/update.js +44 -111
- package/lib/utilities.js +46 -88
- package/lib/wrap.js +29 -55
- package/package.json +1 -1
package/lib/review.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const files = require('./files.js');
|
|
7
|
-
const { TASKS_FILE, REVIEW_DIR } = files;
|
|
7
|
+
const { TASKS_FILE, REVIEW_DIR, TASK_KINDS, taskKind, taskTitle } = files;
|
|
8
8
|
const reviewMerge = require('./review-merge.js');
|
|
9
9
|
const { captureBaseline, mergeReview } = reviewMerge;
|
|
10
10
|
const utilities = require('./utilities.js');
|
|
@@ -42,6 +42,17 @@ const hasNotes = (store) => {
|
|
|
42
42
|
return counts.feedback > 0 || counts.tasks > 0 || counts.code > 0;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
const toNote = (note, done = false) => ({
|
|
46
|
+
file: note.file,
|
|
47
|
+
oldStart: note.oldStart ?? 0,
|
|
48
|
+
newStart: note.newStart ?? 0,
|
|
49
|
+
blockId: note.blockId ?? 0,
|
|
50
|
+
origin: note.origin ?? '',
|
|
51
|
+
header: note.header ?? '',
|
|
52
|
+
text: note.text ?? '',
|
|
53
|
+
done: note.done ?? done,
|
|
54
|
+
});
|
|
55
|
+
|
|
45
56
|
const putNote = (store, notes, key, note, clear) => {
|
|
46
57
|
const prev = notes.get(key);
|
|
47
58
|
if (clear) {
|
|
@@ -50,18 +61,8 @@ const putNote = (store, notes, key, note, clear) => {
|
|
|
50
61
|
store.dirty = true;
|
|
51
62
|
return;
|
|
52
63
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
notes.set(key, {
|
|
56
|
-
file: note.file,
|
|
57
|
-
oldStart: note.oldStart ?? 0,
|
|
58
|
-
newStart: note.newStart ?? 0,
|
|
59
|
-
blockId: note.blockId ?? 0,
|
|
60
|
-
origin: note.origin ?? '',
|
|
61
|
-
header: note.header ?? '',
|
|
62
|
-
text,
|
|
63
|
-
done: note.done ?? prev?.done ?? false,
|
|
64
|
-
});
|
|
64
|
+
if (prev && prev.text === (note.text ?? '')) return;
|
|
65
|
+
notes.set(key, toNote(note, prev?.done));
|
|
65
66
|
store.dirty = true;
|
|
66
67
|
};
|
|
67
68
|
|
|
@@ -74,27 +75,19 @@ const setCode = (store, key, note) => {
|
|
|
74
75
|
putNote(store, store.code, key, note, Boolean(note.clear));
|
|
75
76
|
};
|
|
76
77
|
|
|
77
|
-
const addTask = (store, file, text = '') => {
|
|
78
|
+
const addTask = (store, file, text = '', done = false, kind = 'backlog') => {
|
|
78
79
|
const id = store.nextTaskId;
|
|
79
80
|
store.nextTaskId += 1;
|
|
80
|
-
const todo = { id, file, text, done:
|
|
81
|
+
const todo = { id, file, text, done, kind: taskKind({ kind }) };
|
|
81
82
|
store.tasks.push(todo);
|
|
82
83
|
store.dirty = true;
|
|
83
84
|
return todo;
|
|
84
85
|
};
|
|
85
86
|
|
|
86
87
|
const removeTask = (store, id) => {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (todo.id === id) {
|
|
91
|
-
found = true;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
next.push(todo);
|
|
95
|
-
}
|
|
96
|
-
if (!found) return false;
|
|
97
|
-
store.tasks = next;
|
|
88
|
+
const tasks = store.tasks.filter((todo) => todo.id !== id);
|
|
89
|
+
if (tasks.length === store.tasks.length) return false;
|
|
90
|
+
store.tasks = tasks;
|
|
98
91
|
store.dirty = true;
|
|
99
92
|
return true;
|
|
100
93
|
};
|
|
@@ -102,20 +95,16 @@ const removeTask = (store, id) => {
|
|
|
102
95
|
const setTaskText = (store, id, text) => {
|
|
103
96
|
const i = store.tasks.findIndex((todo) => todo.id === id);
|
|
104
97
|
if (i < 0) return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (prev.text === text) return;
|
|
109
|
-
store.tasks[i] = { id: prev.id, file: prev.file, text, done: prev.done };
|
|
98
|
+
if (!text.trim()) return void removeTask(store, id);
|
|
99
|
+
if (store.tasks[i].text === text) return;
|
|
100
|
+
store.tasks[i] = { ...store.tasks[i], text };
|
|
110
101
|
store.dirty = true;
|
|
111
102
|
};
|
|
112
103
|
|
|
113
104
|
const setTaskDone = (store, id, done) => {
|
|
114
105
|
const i = store.tasks.findIndex((todo) => todo.id === id);
|
|
115
|
-
if (i < 0) return;
|
|
116
|
-
|
|
117
|
-
if (prev.done === done) return;
|
|
118
|
-
store.tasks[i] = { id: prev.id, file: prev.file, text: prev.text, done };
|
|
106
|
+
if (i < 0 || store.tasks[i].done === done) return;
|
|
107
|
+
store.tasks[i] = { ...store.tasks[i], done };
|
|
119
108
|
store.dirty = true;
|
|
120
109
|
};
|
|
121
110
|
|
|
@@ -140,21 +129,11 @@ const applyImportedNotes = (store, imported) => {
|
|
|
140
129
|
const prev = store.feedback.get(key);
|
|
141
130
|
const text = prev ? `${prev.text}\n\n${note.text}` : note.text;
|
|
142
131
|
const done = prev ? prev.done && !!note.done : !!note.done;
|
|
143
|
-
setFeedback(store, key, {
|
|
144
|
-
file: note.file,
|
|
145
|
-
oldStart: note.oldStart ?? 0,
|
|
146
|
-
newStart: note.newStart ?? 0,
|
|
147
|
-
blockId: note.blockId ?? 0,
|
|
148
|
-
origin: note.origin ?? '',
|
|
149
|
-
header: note.header ?? '',
|
|
150
|
-
text,
|
|
151
|
-
done,
|
|
152
|
-
});
|
|
132
|
+
setFeedback(store, key, { ...note, text, done });
|
|
153
133
|
}
|
|
154
134
|
const todos = imported.tasks ?? imported.todos ?? [];
|
|
155
135
|
for (const todo of todos) {
|
|
156
|
-
|
|
157
|
-
if (todo.done) added.done = true;
|
|
136
|
+
addTask(store, todo.file || '', todo.text ?? '', !!todo.done);
|
|
158
137
|
}
|
|
159
138
|
};
|
|
160
139
|
|
|
@@ -222,6 +201,13 @@ const SECTION_LINE = {
|
|
|
222
201
|
};
|
|
223
202
|
const BACKLOG_HEAD = 'Backlog';
|
|
224
203
|
const LEGACY_BACKLOG = 'TODOs';
|
|
204
|
+
const TASK_TITLES = new Map([
|
|
205
|
+
[BACKLOG_HEAD, 'backlog'],
|
|
206
|
+
[LEGACY_BACKLOG, 'backlog'],
|
|
207
|
+
['Issues', 'issues'],
|
|
208
|
+
['Bug Reports', 'bugs'],
|
|
209
|
+
['Feature Requests', 'features'],
|
|
210
|
+
]);
|
|
225
211
|
|
|
226
212
|
const AGENT_INSTRUCTIONS = [
|
|
227
213
|
'- Execute reviews with `status` `ready, partial, editing`',
|
|
@@ -238,10 +224,8 @@ const AGENT_INSTRUCTIONS = [
|
|
|
238
224
|
const toInt = (text) => parseInt(text, 10);
|
|
239
225
|
|
|
240
226
|
const canonicalStatus = (raw) => {
|
|
241
|
-
if (!raw) return '';
|
|
242
227
|
const name = raw === 'pending' ? 'ready' : raw;
|
|
243
|
-
|
|
244
|
-
return '';
|
|
228
|
+
return REVIEW_STATUSES.includes(name) ? name : '';
|
|
245
229
|
};
|
|
246
230
|
|
|
247
231
|
const parseFrontmatterStatus = (text) => {
|
|
@@ -261,44 +245,15 @@ const parseFrontmatterStatus = (text) => {
|
|
|
261
245
|
|
|
262
246
|
const isDoneMark = (mark) => mark === 'x' || mark === 'X';
|
|
263
247
|
|
|
264
|
-
const pushTodo = (builder, file, text, done) => {
|
|
265
|
-
const id = builder.nextTaskId;
|
|
266
|
-
builder.nextTaskId += 1;
|
|
267
|
-
builder.tasks.push({ id, file, text, done });
|
|
268
|
-
};
|
|
269
|
-
|
|
270
248
|
const checkboxLine = (text, done) => {
|
|
271
249
|
const flat = text.trim().replace(/\n/g, ' ');
|
|
272
250
|
const mark = done ? 'x' : ' ';
|
|
273
251
|
return `- [${mark}] ${flat}`;
|
|
274
252
|
};
|
|
275
253
|
|
|
276
|
-
const
|
|
277
|
-
const next =
|
|
278
|
-
|
|
279
|
-
oldStart: note.oldStart ?? 0,
|
|
280
|
-
newStart: note.newStart ?? 0,
|
|
281
|
-
blockId: note.blockId ?? 0,
|
|
282
|
-
origin: note.origin ?? '',
|
|
283
|
-
header: note.header ?? '',
|
|
284
|
-
text: note.text,
|
|
285
|
-
done: note.done ?? false,
|
|
286
|
-
};
|
|
287
|
-
builder.feedback.set(key || feedbackKey(next), next);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const storeCode = (builder, note, key) => {
|
|
291
|
-
const next = {
|
|
292
|
-
file: note.file,
|
|
293
|
-
oldStart: note.oldStart ?? 0,
|
|
294
|
-
newStart: note.newStart ?? 0,
|
|
295
|
-
blockId: note.blockId ?? 0,
|
|
296
|
-
origin: note.origin ?? '',
|
|
297
|
-
header: note.header ?? '',
|
|
298
|
-
text: note.text,
|
|
299
|
-
done: note.done ?? false,
|
|
300
|
-
};
|
|
301
|
-
builder.code.set(key || feedbackKey(next), next);
|
|
254
|
+
const storeNote = (notes, note, key) => {
|
|
255
|
+
const next = toNote(note);
|
|
256
|
+
notes.set(key || feedbackKey(next), next);
|
|
302
257
|
};
|
|
303
258
|
|
|
304
259
|
const feedbackLine = (note) => {
|
|
@@ -345,11 +300,17 @@ const fileGroup = (groups, file) => {
|
|
|
345
300
|
return group;
|
|
346
301
|
};
|
|
347
302
|
|
|
303
|
+
const TASK_SECTION = 'task:';
|
|
304
|
+
|
|
305
|
+
const taskSection = (id) => `${TASK_SECTION}${id}`;
|
|
306
|
+
|
|
348
307
|
const groupReviewNotes = (notes) => {
|
|
349
308
|
const groups = new Map();
|
|
350
|
-
for (const
|
|
351
|
-
|
|
352
|
-
|
|
309
|
+
for (const kind of TASK_KINDS) {
|
|
310
|
+
for (const todo of notes.tasks) {
|
|
311
|
+
if (!todo.text.trim() || taskKind(todo) !== kind.id) continue;
|
|
312
|
+
fileGroup(groups, taskSection(kind.id)).tasks.push(todo);
|
|
313
|
+
}
|
|
353
314
|
}
|
|
354
315
|
for (const key of notes.feedback.keys()) {
|
|
355
316
|
const note = notes.feedback.get(key);
|
|
@@ -400,7 +361,10 @@ const emitCodeItem = (lines, note) => {
|
|
|
400
361
|
lines.push(fence);
|
|
401
362
|
};
|
|
402
363
|
|
|
403
|
-
const sectionTitle = (file) =>
|
|
364
|
+
const sectionTitle = (file) => {
|
|
365
|
+
if (!file.startsWith(TASK_SECTION)) return file;
|
|
366
|
+
return taskTitle(file.slice(TASK_SECTION.length));
|
|
367
|
+
};
|
|
404
368
|
|
|
405
369
|
const backlogFile = (name) => {
|
|
406
370
|
if (name === BACKLOG_HEAD || name === LEGACY_BACKLOG) return TASKS_FILE;
|
|
@@ -452,7 +416,7 @@ const parseFeedbackHeading = (line) => {
|
|
|
452
416
|
|
|
453
417
|
const flushPendingFeedback = (builder, pending) => {
|
|
454
418
|
if (!pending || !(pending.text ?? '').trim()) return null;
|
|
455
|
-
|
|
419
|
+
storeNote(builder.feedback, pending, pending.key);
|
|
456
420
|
return null;
|
|
457
421
|
};
|
|
458
422
|
|
|
@@ -460,7 +424,10 @@ const headingFile = (line) => {
|
|
|
460
424
|
const quoted = FILE_QUOTE.exec(line);
|
|
461
425
|
if (quoted) return backlogFile(quoted[1]);
|
|
462
426
|
if (line.startsWith('## ') && line !== '## Agent instructions') {
|
|
463
|
-
|
|
427
|
+
const name = line.slice(3);
|
|
428
|
+
const kind = TASK_TITLES.get(name);
|
|
429
|
+
if (kind) return taskSection(kind);
|
|
430
|
+
return name;
|
|
464
431
|
}
|
|
465
432
|
return null;
|
|
466
433
|
};
|
|
@@ -479,33 +446,23 @@ const startFeedbackPending = (heading, file) => ({
|
|
|
479
446
|
const applyFeedMeta = (pending, raw) => {
|
|
480
447
|
const loc = parseLoc(raw);
|
|
481
448
|
if (!loc) return pending;
|
|
482
|
-
pending
|
|
483
|
-
pending.file = loc.file;
|
|
484
|
-
pending.oldStart = loc.oldStart;
|
|
485
|
-
pending.newStart = loc.newStart;
|
|
486
|
-
pending.blockId = loc.blockId;
|
|
487
|
-
pending.key = feedbackKey(loc);
|
|
488
|
-
return pending;
|
|
449
|
+
return Object.assign(pending, loc, { key: feedbackKey(loc) });
|
|
489
450
|
};
|
|
490
451
|
|
|
491
452
|
const applyCheckItem = (builder, file, section, pending, textItem, done) => {
|
|
453
|
+
if (file.startsWith(TASK_SECTION)) {
|
|
454
|
+
const kind = file.slice(TASK_SECTION.length);
|
|
455
|
+
addTask(builder, TASKS_FILE, textItem, done, kind);
|
|
456
|
+
return pending;
|
|
457
|
+
}
|
|
492
458
|
if (section === 'todo' && file) {
|
|
493
|
-
|
|
459
|
+
addTask(builder, file, textItem, done, 'backlog');
|
|
494
460
|
return pending;
|
|
495
461
|
}
|
|
496
462
|
const parsed = parseFeedSuffix(textItem);
|
|
497
463
|
if (parsed) {
|
|
498
464
|
flushPendingFeedback(builder, pending);
|
|
499
|
-
|
|
500
|
-
file: parsed.file || file,
|
|
501
|
-
oldStart: parsed.oldStart,
|
|
502
|
-
newStart: parsed.newStart,
|
|
503
|
-
blockId: parsed.blockId,
|
|
504
|
-
origin: '',
|
|
505
|
-
header: '',
|
|
506
|
-
text: parsed.text,
|
|
507
|
-
done,
|
|
508
|
-
});
|
|
465
|
+
storeNote(builder.feedback, { ...parsed, file: parsed.file || file, done });
|
|
509
466
|
return null;
|
|
510
467
|
}
|
|
511
468
|
if (pending) {
|
|
@@ -514,7 +471,7 @@ const applyCheckItem = (builder, file, section, pending, textItem, done) => {
|
|
|
514
471
|
return flushPendingFeedback(builder, pending);
|
|
515
472
|
}
|
|
516
473
|
if (file && section !== 'feedback') {
|
|
517
|
-
|
|
474
|
+
addTask(builder, file, textItem, done, 'backlog');
|
|
518
475
|
}
|
|
519
476
|
return pending;
|
|
520
477
|
};
|
|
@@ -553,29 +510,11 @@ const startCodePending = (line) => {
|
|
|
553
510
|
if (!match) return null;
|
|
554
511
|
const loc = parseLoc(match[2]);
|
|
555
512
|
if (!loc) return null;
|
|
556
|
-
return {
|
|
557
|
-
done: isDoneMark(match[1]),
|
|
558
|
-
origin: loc.origin,
|
|
559
|
-
file: loc.file,
|
|
560
|
-
oldStart: loc.oldStart,
|
|
561
|
-
newStart: loc.newStart,
|
|
562
|
-
blockId: loc.blockId,
|
|
563
|
-
open: '',
|
|
564
|
-
lines: [],
|
|
565
|
-
};
|
|
513
|
+
return { done: isDoneMark(match[1]), ...loc, open: '', lines: [] };
|
|
566
514
|
};
|
|
567
515
|
|
|
568
516
|
const storeCodeNote = (builder, pending, text) => {
|
|
569
|
-
|
|
570
|
-
file: pending.file,
|
|
571
|
-
oldStart: pending.oldStart,
|
|
572
|
-
newStart: pending.newStart,
|
|
573
|
-
blockId: pending.blockId,
|
|
574
|
-
origin: pending.origin,
|
|
575
|
-
header: '',
|
|
576
|
-
text,
|
|
577
|
-
done: pending.done,
|
|
578
|
-
});
|
|
517
|
+
storeNote(builder.code, { ...pending, text });
|
|
579
518
|
};
|
|
580
519
|
|
|
581
520
|
const flushPendingCode = (builder, pending) => {
|
|
@@ -653,8 +592,7 @@ const allocateReviewPath = (dir, date, existingNames) => {
|
|
|
653
592
|
if (n > max) max = n;
|
|
654
593
|
}
|
|
655
594
|
const next = max + 1;
|
|
656
|
-
|
|
657
|
-
return path.join(dir, REVIEW_DIR, file);
|
|
595
|
+
return path.join(dir, REVIEW_DIR, `${stamp}-${pad2(next)}.md`);
|
|
658
596
|
};
|
|
659
597
|
|
|
660
598
|
const listReviewNames = (dir) => {
|
|
@@ -790,6 +728,7 @@ module.exports = {
|
|
|
790
728
|
setFeedback,
|
|
791
729
|
setCode,
|
|
792
730
|
addTask,
|
|
731
|
+
taskKind,
|
|
793
732
|
removeTask,
|
|
794
733
|
setTaskText,
|
|
795
734
|
setTaskDone,
|
package/lib/runs.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const ansi = require('./ansi.js');
|
|
7
|
+
const { stripAnsi } = ansi;
|
|
8
|
+
const commands = require('./npm-commands.js');
|
|
9
|
+
const { isPassingTest } = commands;
|
|
10
|
+
|
|
11
|
+
const LOG_DIR = '.log';
|
|
12
|
+
const RUNS_REL = `${LOG_DIR}/.runs`;
|
|
13
|
+
const RUNS_KEEP = 24;
|
|
14
|
+
const WRITE_MS = 150;
|
|
15
|
+
const FAILING_TEST = /^\s*(?:not ok \d+|[✖✗×]) /;
|
|
16
|
+
const LIFECYCLE_EVENT = 'npm_lifecycle_event';
|
|
17
|
+
|
|
18
|
+
const runsDir = (root) => path.join(root, RUNS_REL);
|
|
19
|
+
|
|
20
|
+
const isRunsRel = (rel) =>
|
|
21
|
+
rel === LOG_DIR || rel === RUNS_REL || rel.startsWith(`${RUNS_REL}/`);
|
|
22
|
+
|
|
23
|
+
const commandLabel = (program, args) =>
|
|
24
|
+
[path.basename(program), ...args].join(' ').trim();
|
|
25
|
+
|
|
26
|
+
const isFailingTest = (line) => FAILING_TEST.test(stripAnsi(line));
|
|
27
|
+
|
|
28
|
+
class Tracker {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.progress = { done: 0, failed: 0, lines: 0 };
|
|
31
|
+
this.carry = '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
feed(chunk) {
|
|
35
|
+
const parts = `${this.carry}${chunk}`.split('\n');
|
|
36
|
+
this.carry = parts.pop();
|
|
37
|
+
const progress = this.progress;
|
|
38
|
+
for (const line of parts) {
|
|
39
|
+
progress.lines += 1;
|
|
40
|
+
if (isPassingTest(line)) progress.done += 1;
|
|
41
|
+
else if (isFailingTest(line)) progress.failed += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pidAlive = (pid) => {
|
|
47
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return error.code === 'EPERM';
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
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
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const listRunNames = (root) => {
|
|
67
|
+
try {
|
|
68
|
+
return fs.readdirSync(runsDir(root)).filter((n) => n.endsWith('.json'));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const settleRecord = (record, alive = pidAlive) => {
|
|
75
|
+
if (record.status !== 'running' || alive(record.pid)) return record;
|
|
76
|
+
return { ...record, status: 'aborted' };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const readRuns = (root) => {
|
|
80
|
+
const dir = runsDir(root);
|
|
81
|
+
const records = [];
|
|
82
|
+
for (const name of listRunNames(root)) {
|
|
83
|
+
const record = readRecord(path.join(dir, name));
|
|
84
|
+
if (record) records.push(settleRecord(record));
|
|
85
|
+
}
|
|
86
|
+
return records.sort((left, right) => right.startedAt - left.startedAt);
|
|
87
|
+
};
|
|
88
|
+
|
|
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`);
|
|
93
|
+
const temp = `${file}.tmp`;
|
|
94
|
+
const text = JSON.stringify(record);
|
|
95
|
+
try {
|
|
96
|
+
fs.writeFileSync(temp, text);
|
|
97
|
+
fs.renameSync(temp, file);
|
|
98
|
+
} catch {
|
|
99
|
+
fs.writeFileSync(file, text);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const pruneRuns = (root) => {
|
|
104
|
+
const dir = runsDir(root);
|
|
105
|
+
const names = listRunNames(root).sort();
|
|
106
|
+
const extra = names.length - RUNS_KEEP;
|
|
107
|
+
for (let i = 0; i < extra; i++) {
|
|
108
|
+
fs.rmSync(path.join(dir, names[i]), { force: true });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const previousTotal = (root, command) => {
|
|
113
|
+
for (const record of readRuns(root)) {
|
|
114
|
+
if (record.command !== command) continue;
|
|
115
|
+
if (record.status === 'running' || record.status === 'aborted') continue;
|
|
116
|
+
const progress = record.progress ?? {};
|
|
117
|
+
const total = (progress.done ?? 0) + (progress.failed ?? 0);
|
|
118
|
+
if (total > 0) return total;
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const sumField = (reports, key) => {
|
|
124
|
+
let total = null;
|
|
125
|
+
for (const item of reports) {
|
|
126
|
+
const value = item.summary[key];
|
|
127
|
+
if (typeof value !== 'number') continue;
|
|
128
|
+
total = (total ?? 0) + value;
|
|
129
|
+
}
|
|
130
|
+
return total;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const summarizeDocument = (doc) => {
|
|
134
|
+
if (!doc || !Array.isArray(doc.reports)) return null;
|
|
135
|
+
const reports = doc.reports;
|
|
136
|
+
const durations = reports.map((item) => item.summary.duration);
|
|
137
|
+
let problems = 0;
|
|
138
|
+
for (const item of reports) problems += item.problems.length;
|
|
139
|
+
return {
|
|
140
|
+
tools: reports.map((item) => item.tool),
|
|
141
|
+
tests: sumField(reports, 'tests'),
|
|
142
|
+
passed: sumField(reports, 'passed'),
|
|
143
|
+
failed: sumField(reports, 'failed'),
|
|
144
|
+
skipped: sumField(reports, 'skipped'),
|
|
145
|
+
errors: sumField(reports, 'errors'),
|
|
146
|
+
warnings: sumField(reports, 'warnings'),
|
|
147
|
+
duration: durations.find(Boolean) ?? '',
|
|
148
|
+
problems,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const safely = (fn) => {
|
|
153
|
+
try {
|
|
154
|
+
fn();
|
|
155
|
+
} catch {
|
|
156
|
+
// run records are best effort
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
class RunRecorder {
|
|
161
|
+
constructor(root, command, options = {}) {
|
|
162
|
+
this.root = root;
|
|
163
|
+
this.clock = options.now ?? Date.now;
|
|
164
|
+
this.tracker = new Tracker();
|
|
165
|
+
this.lastWrite = 0;
|
|
166
|
+
this.closed = false;
|
|
167
|
+
const startedAt = this.clock();
|
|
168
|
+
const env = options.env || process.env;
|
|
169
|
+
this.record = {
|
|
170
|
+
v: 1,
|
|
171
|
+
id: `${startedAt}-${process.pid}`,
|
|
172
|
+
pid: process.pid,
|
|
173
|
+
command,
|
|
174
|
+
script: `${env[LIFECYCLE_EVENT] ?? ''}`.trim(),
|
|
175
|
+
cwd: root,
|
|
176
|
+
startedAt,
|
|
177
|
+
endedAt: 0,
|
|
178
|
+
status: 'running',
|
|
179
|
+
exit: null,
|
|
180
|
+
progress: { ...this.tracker.progress, expected: 0 },
|
|
181
|
+
result: null,
|
|
182
|
+
};
|
|
183
|
+
safely(() => {
|
|
184
|
+
this.record.progress.expected = previousTotal(root, command);
|
|
185
|
+
pruneRuns(root);
|
|
186
|
+
this.write();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
write() {
|
|
191
|
+
Object.assign(this.record.progress, this.tracker.progress);
|
|
192
|
+
writeRecord(this.root, this.record);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
feed(chunk) {
|
|
196
|
+
if (this.closed) return;
|
|
197
|
+
this.tracker.feed(chunk);
|
|
198
|
+
const now = this.clock();
|
|
199
|
+
if (now - this.lastWrite < WRITE_MS) return;
|
|
200
|
+
this.lastWrite = now;
|
|
201
|
+
safely(() => this.write());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
finish(status, result = null) {
|
|
205
|
+
if (this.closed) return;
|
|
206
|
+
this.closed = true;
|
|
207
|
+
const record = this.record;
|
|
208
|
+
record.endedAt = this.clock();
|
|
209
|
+
record.exit = status;
|
|
210
|
+
record.status = status === 0 ? 'passed' : 'failed';
|
|
211
|
+
record.result = result;
|
|
212
|
+
safely(() => this.write());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
runsDir,
|
|
218
|
+
isRunsRel,
|
|
219
|
+
commandLabel,
|
|
220
|
+
Tracker,
|
|
221
|
+
settleRecord,
|
|
222
|
+
readRuns,
|
|
223
|
+
summarizeDocument,
|
|
224
|
+
RunRecorder,
|
|
225
|
+
};
|