reslop 0.2.4 → 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 +25 -1
- package/LICENSE +1 -1
- package/README.md +12 -13
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -12
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +48 -18
- 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 +196 -404
- package/lib/deps.js +240 -399
- 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 +138 -141
- 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 +90 -206
- package/lib/editor.js +98 -55
- package/lib/files.js +86 -103
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +182 -344
- 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 +157 -132
- 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 +16 -26
- 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 +22 -35
- package/lib/highlight/txt.js +9 -22
- package/lib/highlight.js +17 -46
- package/lib/keys.js +62 -85
- package/lib/manifest.js +67 -130
- package/lib/npm-commands.js +119 -189
- package/lib/remote.js +141 -197
- package/lib/render/branches.js +66 -73
- package/lib/render/chrome.js +264 -323
- package/lib/render/commits.js +187 -500
- 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 -228
- package/lib/render/files.js +45 -99
- package/lib/render/frame.js +129 -183
- package/lib/render/notes.js +144 -160
- package/lib/render/npm.js +234 -112
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +131 -170
- 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 -28
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -38
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -78
- package/lib/report-test.js +256 -141
- package/lib/report-tools.js +96 -161
- package/lib/review-merge.js +114 -152
- package/lib/review.js +115 -189
- package/lib/runs.js +225 -0
- package/lib/select.js +89 -75
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +82 -63
- package/lib/session/branches.js +193 -247
- package/lib/session/change-actions.js +171 -214
- package/lib/session/commit.js +280 -342
- package/lib/session/compose.js +559 -674
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +164 -157
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +85 -53
- package/lib/session/items.js +73 -104
- package/lib/session/lifecycle.js +403 -541
- package/lib/session/load.js +48 -68
- package/lib/session/navigation.js +96 -87
- package/lib/session/npm.js +409 -345
- package/lib/session/ops.js +196 -63
- package/lib/session/pointer.js +23 -15
- package/lib/session/progress.js +33 -21
- package/lib/session/review.js +33 -54
- package/lib/session/tasks.js +333 -0
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -136
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +52 -83
- package/lib/session/watch.js +208 -204
- package/lib/session.js +112 -237
- package/lib/update.js +45 -116
- package/lib/utilities.js +67 -89
- package/lib/wrap.js +33 -44
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/lib/review.js
CHANGED
|
@@ -4,29 +4,31 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const files = require('./files.js');
|
|
7
|
-
const {
|
|
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
|
+
const utilities = require('./utilities.js');
|
|
11
|
+
const { pad2, dateStamp } = utilities;
|
|
10
12
|
|
|
11
13
|
const createStore = (reviewPath, templates = []) => ({
|
|
12
14
|
reviewPath,
|
|
13
15
|
dirty: false,
|
|
14
|
-
|
|
16
|
+
nextTaskId: 1,
|
|
15
17
|
feedback: new Map(),
|
|
16
18
|
code: new Map(),
|
|
17
|
-
|
|
19
|
+
tasks: [],
|
|
18
20
|
templates: [...templates],
|
|
19
21
|
status: 'editing',
|
|
20
22
|
baseline: null,
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
const noteCounts = (store) => {
|
|
24
|
-
const counts = { feedback: 0,
|
|
26
|
+
const counts = { feedback: 0, tasks: 0, tasksDone: 0, code: 0 };
|
|
25
27
|
if (!store) return counts;
|
|
26
|
-
for (const
|
|
27
|
-
if (!
|
|
28
|
-
counts.
|
|
29
|
-
if (
|
|
28
|
+
for (const task of store.tasks) {
|
|
29
|
+
if (!task.text.trim()) continue;
|
|
30
|
+
counts.tasks += 1;
|
|
31
|
+
if (task.done) counts.tasksDone += 1;
|
|
30
32
|
}
|
|
31
33
|
for (const note of store.feedback.values()) {
|
|
32
34
|
if (note.text.trim()) counts.feedback += 1;
|
|
@@ -37,98 +39,72 @@ const noteCounts = (store) => {
|
|
|
37
39
|
|
|
38
40
|
const hasNotes = (store) => {
|
|
39
41
|
const counts = noteCounts(store);
|
|
40
|
-
return counts.feedback > 0 || counts.
|
|
41
|
-
};
|
|
42
|
+
return counts.feedback > 0 || counts.tasks > 0 || counts.code > 0;
|
|
43
|
+
};
|
|
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
|
+
});
|
|
42
55
|
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const prev = store.feedback.get(key);
|
|
47
|
-
if (!trimmed) {
|
|
56
|
+
const putNote = (store, notes, key, note, clear) => {
|
|
57
|
+
const prev = notes.get(key);
|
|
58
|
+
if (clear) {
|
|
48
59
|
if (!prev) return;
|
|
49
|
-
|
|
60
|
+
notes.delete(key);
|
|
50
61
|
store.dirty = true;
|
|
51
62
|
return;
|
|
52
63
|
}
|
|
53
|
-
if (prev && prev.text === text) return;
|
|
54
|
-
|
|
55
|
-
file: note.file,
|
|
56
|
-
oldStart: note.oldStart ?? 0,
|
|
57
|
-
newStart: note.newStart ?? 0,
|
|
58
|
-
blockId: note.blockId ?? 0,
|
|
59
|
-
origin: note.origin ?? '',
|
|
60
|
-
header: note.header ?? '',
|
|
61
|
-
text,
|
|
62
|
-
done: note.done ?? prev?.done ?? false,
|
|
63
|
-
});
|
|
64
|
+
if (prev && prev.text === (note.text ?? '')) return;
|
|
65
|
+
notes.set(key, toNote(note, prev?.done));
|
|
64
66
|
store.dirty = true;
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
const setFeedback = (store, key, note) => {
|
|
70
|
+
const clear = !(note.text ?? '').trim();
|
|
71
|
+
putNote(store, store.feedback, key, note, clear);
|
|
72
|
+
};
|
|
73
|
+
|
|
67
74
|
const setCode = (store, key, note) => {
|
|
68
|
-
|
|
69
|
-
if (note.clear) {
|
|
70
|
-
if (!prev) return;
|
|
71
|
-
store.code.delete(key);
|
|
72
|
-
store.dirty = true;
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const text = note.text ?? '';
|
|
76
|
-
if (prev && prev.text === text) return;
|
|
77
|
-
store.code.set(key, {
|
|
78
|
-
file: note.file,
|
|
79
|
-
oldStart: note.oldStart ?? 0,
|
|
80
|
-
newStart: note.newStart ?? 0,
|
|
81
|
-
blockId: note.blockId ?? 0,
|
|
82
|
-
origin: note.origin ?? '',
|
|
83
|
-
header: note.header ?? '',
|
|
84
|
-
text,
|
|
85
|
-
done: note.done ?? prev?.done ?? false,
|
|
86
|
-
});
|
|
87
|
-
store.dirty = true;
|
|
75
|
+
putNote(store, store.code, key, note, Boolean(note.clear));
|
|
88
76
|
};
|
|
89
77
|
|
|
90
|
-
const
|
|
91
|
-
const id = store.
|
|
92
|
-
store.
|
|
93
|
-
const todo = { id, file, text, done:
|
|
94
|
-
store.
|
|
78
|
+
const addTask = (store, file, text = '', done = false, kind = 'backlog') => {
|
|
79
|
+
const id = store.nextTaskId;
|
|
80
|
+
store.nextTaskId += 1;
|
|
81
|
+
const todo = { id, file, text, done, kind: taskKind({ kind }) };
|
|
82
|
+
store.tasks.push(todo);
|
|
95
83
|
store.dirty = true;
|
|
96
84
|
return todo;
|
|
97
85
|
};
|
|
98
86
|
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (todo.id === id) {
|
|
104
|
-
found = true;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
next.push(todo);
|
|
108
|
-
}
|
|
109
|
-
if (!found) return false;
|
|
110
|
-
store.todos = next;
|
|
87
|
+
const removeTask = (store, id) => {
|
|
88
|
+
const tasks = store.tasks.filter((todo) => todo.id !== id);
|
|
89
|
+
if (tasks.length === store.tasks.length) return false;
|
|
90
|
+
store.tasks = tasks;
|
|
111
91
|
store.dirty = true;
|
|
112
92
|
return true;
|
|
113
93
|
};
|
|
114
94
|
|
|
115
|
-
const
|
|
116
|
-
const i = store.
|
|
95
|
+
const setTaskText = (store, id, text) => {
|
|
96
|
+
const i = store.tasks.findIndex((todo) => todo.id === id);
|
|
117
97
|
if (i < 0) return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (prev.text === text) return;
|
|
122
|
-
store.todos[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 };
|
|
123
101
|
store.dirty = true;
|
|
124
102
|
};
|
|
125
103
|
|
|
126
|
-
const
|
|
127
|
-
const i = store.
|
|
128
|
-
if (i < 0) return;
|
|
129
|
-
|
|
130
|
-
if (prev.done === done) return;
|
|
131
|
-
store.todos[i] = { id: prev.id, file: prev.file, text: prev.text, done };
|
|
104
|
+
const setTaskDone = (store, id, done) => {
|
|
105
|
+
const i = store.tasks.findIndex((todo) => todo.id === id);
|
|
106
|
+
if (i < 0 || store.tasks[i].done === done) return;
|
|
107
|
+
store.tasks[i] = { ...store.tasks[i], done };
|
|
132
108
|
store.dirty = true;
|
|
133
109
|
};
|
|
134
110
|
|
|
@@ -153,21 +129,11 @@ const applyImportedNotes = (store, imported) => {
|
|
|
153
129
|
const prev = store.feedback.get(key);
|
|
154
130
|
const text = prev ? `${prev.text}\n\n${note.text}` : note.text;
|
|
155
131
|
const done = prev ? prev.done && !!note.done : !!note.done;
|
|
156
|
-
setFeedback(store, key, {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
newStart: note.newStart ?? 0,
|
|
160
|
-
blockId: note.blockId ?? 0,
|
|
161
|
-
origin: note.origin ?? '',
|
|
162
|
-
header: note.header ?? '',
|
|
163
|
-
text,
|
|
164
|
-
done,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
const todos = imported.todos ?? [];
|
|
132
|
+
setFeedback(store, key, { ...note, text, done });
|
|
133
|
+
}
|
|
134
|
+
const todos = imported.tasks ?? imported.todos ?? [];
|
|
168
135
|
for (const todo of todos) {
|
|
169
|
-
|
|
170
|
-
if (todo.done) added.done = true;
|
|
136
|
+
addTask(store, todo.file || '', todo.text ?? '', !!todo.done);
|
|
171
137
|
}
|
|
172
138
|
};
|
|
173
139
|
|
|
@@ -233,6 +199,15 @@ const SECTION_LINE = {
|
|
|
233
199
|
'### Todo': 'todo',
|
|
234
200
|
'### Feedback': 'feedback',
|
|
235
201
|
};
|
|
202
|
+
const BACKLOG_HEAD = 'Backlog';
|
|
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
|
+
]);
|
|
236
211
|
|
|
237
212
|
const AGENT_INSTRUCTIONS = [
|
|
238
213
|
'- Execute reviews with `status` `ready, partial, editing`',
|
|
@@ -249,10 +224,8 @@ const AGENT_INSTRUCTIONS = [
|
|
|
249
224
|
const toInt = (text) => parseInt(text, 10);
|
|
250
225
|
|
|
251
226
|
const canonicalStatus = (raw) => {
|
|
252
|
-
if (!raw) return '';
|
|
253
227
|
const name = raw === 'pending' ? 'ready' : raw;
|
|
254
|
-
|
|
255
|
-
return '';
|
|
228
|
+
return REVIEW_STATUSES.includes(name) ? name : '';
|
|
256
229
|
};
|
|
257
230
|
|
|
258
231
|
const parseFrontmatterStatus = (text) => {
|
|
@@ -272,44 +245,15 @@ const parseFrontmatterStatus = (text) => {
|
|
|
272
245
|
|
|
273
246
|
const isDoneMark = (mark) => mark === 'x' || mark === 'X';
|
|
274
247
|
|
|
275
|
-
const pushTodo = (builder, file, text, done) => {
|
|
276
|
-
const id = builder.nextTodoId;
|
|
277
|
-
builder.nextTodoId += 1;
|
|
278
|
-
builder.todos.push({ id, file, text, done });
|
|
279
|
-
};
|
|
280
|
-
|
|
281
248
|
const checkboxLine = (text, done) => {
|
|
282
249
|
const flat = text.trim().replace(/\n/g, ' ');
|
|
283
250
|
const mark = done ? 'x' : ' ';
|
|
284
251
|
return `- [${mark}] ${flat}`;
|
|
285
252
|
};
|
|
286
253
|
|
|
287
|
-
const
|
|
288
|
-
const next =
|
|
289
|
-
|
|
290
|
-
oldStart: note.oldStart ?? 0,
|
|
291
|
-
newStart: note.newStart ?? 0,
|
|
292
|
-
blockId: note.blockId ?? 0,
|
|
293
|
-
origin: note.origin ?? '',
|
|
294
|
-
header: note.header ?? '',
|
|
295
|
-
text: note.text,
|
|
296
|
-
done: note.done ?? false,
|
|
297
|
-
};
|
|
298
|
-
builder.feedback.set(key || feedbackKey(next), next);
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const storeCode = (builder, note, key) => {
|
|
302
|
-
const next = {
|
|
303
|
-
file: note.file,
|
|
304
|
-
oldStart: note.oldStart ?? 0,
|
|
305
|
-
newStart: note.newStart ?? 0,
|
|
306
|
-
blockId: note.blockId ?? 0,
|
|
307
|
-
origin: note.origin ?? '',
|
|
308
|
-
header: note.header ?? '',
|
|
309
|
-
text: note.text,
|
|
310
|
-
done: note.done ?? false,
|
|
311
|
-
};
|
|
312
|
-
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);
|
|
313
257
|
};
|
|
314
258
|
|
|
315
259
|
const feedbackLine = (note) => {
|
|
@@ -350,17 +294,23 @@ const parseFeedSuffix = (textItem) => {
|
|
|
350
294
|
const fileGroup = (groups, file) => {
|
|
351
295
|
let group = groups.get(file);
|
|
352
296
|
if (!group) {
|
|
353
|
-
group = {
|
|
297
|
+
group = { tasks: [], feedback: [], code: [] };
|
|
354
298
|
groups.set(file, group);
|
|
355
299
|
}
|
|
356
300
|
return group;
|
|
357
301
|
};
|
|
358
302
|
|
|
303
|
+
const TASK_SECTION = 'task:';
|
|
304
|
+
|
|
305
|
+
const taskSection = (id) => `${TASK_SECTION}${id}`;
|
|
306
|
+
|
|
359
307
|
const groupReviewNotes = (notes) => {
|
|
360
308
|
const groups = new Map();
|
|
361
|
-
for (const
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
}
|
|
364
314
|
}
|
|
365
315
|
for (const key of notes.feedback.keys()) {
|
|
366
316
|
const note = notes.feedback.get(key);
|
|
@@ -411,12 +361,22 @@ const emitCodeItem = (lines, note) => {
|
|
|
411
361
|
lines.push(fence);
|
|
412
362
|
};
|
|
413
363
|
|
|
364
|
+
const sectionTitle = (file) => {
|
|
365
|
+
if (!file.startsWith(TASK_SECTION)) return file;
|
|
366
|
+
return taskTitle(file.slice(TASK_SECTION.length));
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const backlogFile = (name) => {
|
|
370
|
+
if (name === BACKLOG_HEAD || name === LEGACY_BACKLOG) return TASKS_FILE;
|
|
371
|
+
return name;
|
|
372
|
+
};
|
|
373
|
+
|
|
414
374
|
const emitReviewGroups = (lines, groups) => {
|
|
415
375
|
for (const file of groups.keys()) {
|
|
416
376
|
const group = groups.get(file);
|
|
417
|
-
lines.push(`## ${file}`);
|
|
377
|
+
lines.push(`## ${sectionTitle(file)}`);
|
|
418
378
|
lines.push('');
|
|
419
|
-
for (const todo of group.
|
|
379
|
+
for (const todo of group.tasks) {
|
|
420
380
|
lines.push(checkboxLine(todo.text, todo.done));
|
|
421
381
|
}
|
|
422
382
|
for (const item of group.feedback) {
|
|
@@ -456,15 +416,18 @@ const parseFeedbackHeading = (line) => {
|
|
|
456
416
|
|
|
457
417
|
const flushPendingFeedback = (builder, pending) => {
|
|
458
418
|
if (!pending || !(pending.text ?? '').trim()) return null;
|
|
459
|
-
|
|
419
|
+
storeNote(builder.feedback, pending, pending.key);
|
|
460
420
|
return null;
|
|
461
421
|
};
|
|
462
422
|
|
|
463
423
|
const headingFile = (line) => {
|
|
464
424
|
const quoted = FILE_QUOTE.exec(line);
|
|
465
|
-
if (quoted) return quoted[1];
|
|
425
|
+
if (quoted) return backlogFile(quoted[1]);
|
|
466
426
|
if (line.startsWith('## ') && line !== '## Agent instructions') {
|
|
467
|
-
|
|
427
|
+
const name = line.slice(3);
|
|
428
|
+
const kind = TASK_TITLES.get(name);
|
|
429
|
+
if (kind) return taskSection(kind);
|
|
430
|
+
return name;
|
|
468
431
|
}
|
|
469
432
|
return null;
|
|
470
433
|
};
|
|
@@ -483,33 +446,23 @@ const startFeedbackPending = (heading, file) => ({
|
|
|
483
446
|
const applyFeedMeta = (pending, raw) => {
|
|
484
447
|
const loc = parseLoc(raw);
|
|
485
448
|
if (!loc) return pending;
|
|
486
|
-
pending
|
|
487
|
-
pending.file = loc.file;
|
|
488
|
-
pending.oldStart = loc.oldStart;
|
|
489
|
-
pending.newStart = loc.newStart;
|
|
490
|
-
pending.blockId = loc.blockId;
|
|
491
|
-
pending.key = feedbackKey(loc);
|
|
492
|
-
return pending;
|
|
449
|
+
return Object.assign(pending, loc, { key: feedbackKey(loc) });
|
|
493
450
|
};
|
|
494
451
|
|
|
495
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
|
+
}
|
|
496
458
|
if (section === 'todo' && file) {
|
|
497
|
-
|
|
459
|
+
addTask(builder, file, textItem, done, 'backlog');
|
|
498
460
|
return pending;
|
|
499
461
|
}
|
|
500
462
|
const parsed = parseFeedSuffix(textItem);
|
|
501
463
|
if (parsed) {
|
|
502
464
|
flushPendingFeedback(builder, pending);
|
|
503
|
-
|
|
504
|
-
file: parsed.file || file,
|
|
505
|
-
oldStart: parsed.oldStart,
|
|
506
|
-
newStart: parsed.newStart,
|
|
507
|
-
blockId: parsed.blockId,
|
|
508
|
-
origin: '',
|
|
509
|
-
header: '',
|
|
510
|
-
text: parsed.text,
|
|
511
|
-
done,
|
|
512
|
-
});
|
|
465
|
+
storeNote(builder.feedback, { ...parsed, file: parsed.file || file, done });
|
|
513
466
|
return null;
|
|
514
467
|
}
|
|
515
468
|
if (pending) {
|
|
@@ -518,7 +471,7 @@ const applyCheckItem = (builder, file, section, pending, textItem, done) => {
|
|
|
518
471
|
return flushPendingFeedback(builder, pending);
|
|
519
472
|
}
|
|
520
473
|
if (file && section !== 'feedback') {
|
|
521
|
-
|
|
474
|
+
addTask(builder, file, textItem, done, 'backlog');
|
|
522
475
|
}
|
|
523
476
|
return pending;
|
|
524
477
|
};
|
|
@@ -557,29 +510,11 @@ const startCodePending = (line) => {
|
|
|
557
510
|
if (!match) return null;
|
|
558
511
|
const loc = parseLoc(match[2]);
|
|
559
512
|
if (!loc) return null;
|
|
560
|
-
return {
|
|
561
|
-
done: isDoneMark(match[1]),
|
|
562
|
-
origin: loc.origin,
|
|
563
|
-
file: loc.file,
|
|
564
|
-
oldStart: loc.oldStart,
|
|
565
|
-
newStart: loc.newStart,
|
|
566
|
-
blockId: loc.blockId,
|
|
567
|
-
open: '',
|
|
568
|
-
lines: [],
|
|
569
|
-
};
|
|
513
|
+
return { done: isDoneMark(match[1]), ...loc, open: '', lines: [] };
|
|
570
514
|
};
|
|
571
515
|
|
|
572
516
|
const storeCodeNote = (builder, pending, text) => {
|
|
573
|
-
|
|
574
|
-
file: pending.file,
|
|
575
|
-
oldStart: pending.oldStart,
|
|
576
|
-
newStart: pending.newStart,
|
|
577
|
-
blockId: pending.blockId,
|
|
578
|
-
origin: pending.origin,
|
|
579
|
-
header: '',
|
|
580
|
-
text,
|
|
581
|
-
done: pending.done,
|
|
582
|
-
});
|
|
517
|
+
storeNote(builder.code, { ...pending, text });
|
|
583
518
|
};
|
|
584
519
|
|
|
585
520
|
const flushPendingCode = (builder, pending) => {
|
|
@@ -646,15 +581,6 @@ const TEMPLATES_FILE = 'templates.json';
|
|
|
646
581
|
const AUTOSAVE_MS = 3000;
|
|
647
582
|
const REVIEW_NAME = /^(\d{4}-\d{2}-\d{2})-(\d+)\.md$/;
|
|
648
583
|
|
|
649
|
-
const pad2 = (n) => `${n}`.padStart(2, '0');
|
|
650
|
-
|
|
651
|
-
const dateStamp = (date) => {
|
|
652
|
-
const y = date.getFullYear();
|
|
653
|
-
const month = pad2(date.getMonth() + 1);
|
|
654
|
-
const day = pad2(date.getDate());
|
|
655
|
-
return `${y}-${month}-${day}`;
|
|
656
|
-
};
|
|
657
|
-
|
|
658
584
|
const allocateReviewPath = (dir, date, existingNames) => {
|
|
659
585
|
const stamp = dateStamp(date);
|
|
660
586
|
let max = -1;
|
|
@@ -666,8 +592,7 @@ const allocateReviewPath = (dir, date, existingNames) => {
|
|
|
666
592
|
if (n > max) max = n;
|
|
667
593
|
}
|
|
668
594
|
const next = max + 1;
|
|
669
|
-
|
|
670
|
-
return path.join(dir, REVIEW_DIR, file);
|
|
595
|
+
return path.join(dir, REVIEW_DIR, `${stamp}-${pad2(next)}.md`);
|
|
671
596
|
};
|
|
672
597
|
|
|
673
598
|
const listReviewNames = (dir) => {
|
|
@@ -802,10 +727,11 @@ module.exports = {
|
|
|
802
727
|
rememberTemplate,
|
|
803
728
|
setFeedback,
|
|
804
729
|
setCode,
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
730
|
+
addTask,
|
|
731
|
+
taskKind,
|
|
732
|
+
removeTask,
|
|
733
|
+
setTaskText,
|
|
734
|
+
setTaskDone,
|
|
809
735
|
checkLabel,
|
|
810
736
|
feedbackKey,
|
|
811
737
|
applyImportedNotes,
|
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
|
+
};
|