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,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const files = require('../common/files.js');
|
|
4
|
+
const { ISSUE_FILE } = files;
|
|
5
|
+
const { TASK_KINDS, taskKind } = files;
|
|
6
|
+
const { upsertTemplate } = require('./templates.js');
|
|
7
|
+
const { trimText } = require('../common/utilities.js');
|
|
8
|
+
|
|
9
|
+
const noteCounts = (store) => {
|
|
10
|
+
const counts = { feedback: 0, tasks: 0, tasksDone: 0, code: 0 };
|
|
11
|
+
if (!store) return counts;
|
|
12
|
+
for (const task of store.tasks) {
|
|
13
|
+
if (!task.text.trim()) continue;
|
|
14
|
+
counts.tasks += 1;
|
|
15
|
+
if (task.done) counts.tasksDone += 1;
|
|
16
|
+
}
|
|
17
|
+
for (const note of store.feedback.values()) {
|
|
18
|
+
if (note.text.trim()) counts.feedback += 1;
|
|
19
|
+
}
|
|
20
|
+
counts.code = store.code.size;
|
|
21
|
+
return counts;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const reviewProgress = (store) => {
|
|
25
|
+
let done = 0;
|
|
26
|
+
let total = 0;
|
|
27
|
+
if (!store) return { done, total };
|
|
28
|
+
for (const task of store.tasks) {
|
|
29
|
+
if (!trimText(task.text)) continue;
|
|
30
|
+
total += 1;
|
|
31
|
+
if (task.done) done += 1;
|
|
32
|
+
}
|
|
33
|
+
for (const note of store.feedback.values()) {
|
|
34
|
+
total += 1;
|
|
35
|
+
if (note.done) done += 1;
|
|
36
|
+
}
|
|
37
|
+
for (const note of store.code.values()) {
|
|
38
|
+
total += 1;
|
|
39
|
+
if (note.done) done += 1;
|
|
40
|
+
}
|
|
41
|
+
return { done, total };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const hasNotes = (store) => {
|
|
45
|
+
const counts = noteCounts(store);
|
|
46
|
+
return counts.feedback > 0 || counts.tasks > 0 || counts.code > 0;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const toNote = (note, done = false) => ({
|
|
50
|
+
file: note.file,
|
|
51
|
+
oldStart: note.oldStart ?? 0,
|
|
52
|
+
newStart: note.newStart ?? 0,
|
|
53
|
+
blockId: note.blockId ?? 0,
|
|
54
|
+
origin: note.origin ?? '',
|
|
55
|
+
header: note.header ?? '',
|
|
56
|
+
text: note.text ?? '',
|
|
57
|
+
done: note.done ?? done,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const kindOrder = () => TASK_KINDS.map((kind) => kind.id);
|
|
61
|
+
|
|
62
|
+
const feedbackSpots = (entries, file) => {
|
|
63
|
+
const spots = [];
|
|
64
|
+
for (let i = 0; i < entries.length; i++) {
|
|
65
|
+
const note = entries[i][1];
|
|
66
|
+
if ((note.file ?? '') !== file) continue;
|
|
67
|
+
if (!trimText(note.text)) continue;
|
|
68
|
+
spots.push(i);
|
|
69
|
+
}
|
|
70
|
+
return spots;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const checkLabel = (text, done) => {
|
|
74
|
+
const mark = done ? 'x' : ' ';
|
|
75
|
+
return `[${mark}] ${text}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const feedbackKey = (note) => {
|
|
79
|
+
const file = note.file ?? '';
|
|
80
|
+
const oldStart = note.oldStart ?? 0;
|
|
81
|
+
const newStart = note.newStart ?? 0;
|
|
82
|
+
const block = note.blockId ?? 0;
|
|
83
|
+
return `${file}:${oldStart}:${newStart}:${block}`;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const flatTaskText = (text) => trimText(text).replace(/\n/g, ' ');
|
|
87
|
+
|
|
88
|
+
const knownTaskText = (store) => {
|
|
89
|
+
const known = new Set();
|
|
90
|
+
for (const task of store.tasks) known.add(flatTaskText(task.text));
|
|
91
|
+
return known;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
class ReviewStore {
|
|
95
|
+
constructor(reviewPath, templates = []) {
|
|
96
|
+
this.reviewPath = reviewPath;
|
|
97
|
+
this.dirty = false;
|
|
98
|
+
this.nextTaskId = 1;
|
|
99
|
+
this.feedback = new Map();
|
|
100
|
+
this.code = new Map();
|
|
101
|
+
this.tasks = [];
|
|
102
|
+
this.templates = [...templates];
|
|
103
|
+
this.status = 'editing';
|
|
104
|
+
this.baseline = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
reopen() {
|
|
108
|
+
if (this.status === 'done') this.status = 'editing';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
putNote(notes, key, note, clear) {
|
|
112
|
+
const prev = notes.get(key);
|
|
113
|
+
if (clear) {
|
|
114
|
+
if (!prev) return;
|
|
115
|
+
notes.delete(key);
|
|
116
|
+
this.dirty = true;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (prev && prev.text === (note.text ?? '')) return;
|
|
120
|
+
notes.set(key, toNote(note, prev?.done));
|
|
121
|
+
this.dirty = true;
|
|
122
|
+
this.reopen();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setFeedback(key, note) {
|
|
126
|
+
const clear = !(note.text ?? '').trim();
|
|
127
|
+
this.putNote(this.feedback, key, note, clear);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setCode(key, note) {
|
|
131
|
+
this.putNote(this.code, key, note, Boolean(note.clear));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
addTask(file, text = '', done = false, kind = 'features') {
|
|
135
|
+
const id = this.nextTaskId;
|
|
136
|
+
this.nextTaskId += 1;
|
|
137
|
+
const todo = { id, file, text, done, kind: taskKind({ kind }) };
|
|
138
|
+
this.tasks.push(todo);
|
|
139
|
+
this.dirty = true;
|
|
140
|
+
if (!done) this.reopen();
|
|
141
|
+
return todo;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
removeTask(id) {
|
|
145
|
+
const tasks = this.tasks.filter((todo) => todo.id !== id);
|
|
146
|
+
if (tasks.length === this.tasks.length) return false;
|
|
147
|
+
this.tasks = tasks;
|
|
148
|
+
this.dirty = true;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setTaskText(id, text) {
|
|
153
|
+
const i = this.tasks.findIndex((todo) => todo.id === id);
|
|
154
|
+
if (i < 0) return;
|
|
155
|
+
if (!text.trim()) return void this.removeTask(id);
|
|
156
|
+
if (this.tasks[i].text === text) return;
|
|
157
|
+
this.tasks[i] = { ...this.tasks[i], text };
|
|
158
|
+
this.dirty = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setTaskDone(id, done) {
|
|
162
|
+
const i = this.tasks.findIndex((todo) => todo.id === id);
|
|
163
|
+
if (i < 0 || this.tasks[i].done === done) return;
|
|
164
|
+
this.tasks[i] = { ...this.tasks[i], done };
|
|
165
|
+
this.dirty = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setFeedbackDone(key, done) {
|
|
169
|
+
const note = this.feedback.get(key);
|
|
170
|
+
if (!note || note.done === done) return;
|
|
171
|
+
this.feedback.set(key, { ...note, done });
|
|
172
|
+
this.dirty = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
moveTask(id, delta) {
|
|
176
|
+
if (!delta) return false;
|
|
177
|
+
const tasks = this.tasks;
|
|
178
|
+
const index = tasks.findIndex((task) => task.id === id);
|
|
179
|
+
if (index < 0) return false;
|
|
180
|
+
const task = tasks[index];
|
|
181
|
+
const kind = taskKind(task);
|
|
182
|
+
const order = kindOrder();
|
|
183
|
+
const same = [];
|
|
184
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
185
|
+
if (taskKind(tasks[i]) === kind) same.push(i);
|
|
186
|
+
}
|
|
187
|
+
const at = same.indexOf(index);
|
|
188
|
+
const neighbor = same[at + delta];
|
|
189
|
+
if (neighbor !== undefined) {
|
|
190
|
+
const next = tasks.slice();
|
|
191
|
+
const swap = next[index];
|
|
192
|
+
next[index] = next[neighbor];
|
|
193
|
+
next[neighbor] = swap;
|
|
194
|
+
this.tasks = next;
|
|
195
|
+
this.dirty = true;
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
const dest = order[order.indexOf(kind) + delta];
|
|
199
|
+
if (!dest) return false;
|
|
200
|
+
const rest = [];
|
|
201
|
+
for (const item of tasks) {
|
|
202
|
+
if (item.id !== id) rest.push(item);
|
|
203
|
+
}
|
|
204
|
+
let insertAt = rest.length;
|
|
205
|
+
if (delta > 0) {
|
|
206
|
+
const first = rest.findIndex((item) => taskKind(item) === dest);
|
|
207
|
+
if (first >= 0) insertAt = first;
|
|
208
|
+
} else {
|
|
209
|
+
let last = -1;
|
|
210
|
+
for (let i = 0; i < rest.length; i++) {
|
|
211
|
+
if (taskKind(rest[i]) === dest) last = i;
|
|
212
|
+
}
|
|
213
|
+
insertAt = last + 1;
|
|
214
|
+
}
|
|
215
|
+
rest.splice(insertAt, 0, { ...task, kind: dest });
|
|
216
|
+
this.tasks = rest;
|
|
217
|
+
this.dirty = true;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
moveFeedback(key, delta) {
|
|
222
|
+
if (!delta) return false;
|
|
223
|
+
const note = this.feedback.get(key);
|
|
224
|
+
if (!note) return false;
|
|
225
|
+
const entries = [...this.feedback];
|
|
226
|
+
const spots = feedbackSpots(entries, note.file ?? '');
|
|
227
|
+
const at = spots.findIndex((i) => entries[i][0] === key);
|
|
228
|
+
if (at < 0) return false;
|
|
229
|
+
const neighbor = spots[at + delta];
|
|
230
|
+
if (neighbor === undefined) return false;
|
|
231
|
+
const next = entries.slice();
|
|
232
|
+
const swap = next[spots[at]];
|
|
233
|
+
next[spots[at]] = next[neighbor];
|
|
234
|
+
next[neighbor] = swap;
|
|
235
|
+
this.feedback = new Map(next);
|
|
236
|
+
this.dirty = true;
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
applyImportedNotes(imported) {
|
|
241
|
+
if (!imported) return;
|
|
242
|
+
const feedbacks = imported.feedback ?? [];
|
|
243
|
+
for (const note of feedbacks) {
|
|
244
|
+
const key = feedbackKey(note);
|
|
245
|
+
const prev = this.feedback.get(key);
|
|
246
|
+
const text = prev ? `${prev.text}\n\n${note.text}` : note.text;
|
|
247
|
+
const done = prev ? prev.done && !!note.done : !!note.done;
|
|
248
|
+
this.setFeedback(key, { ...note, text, done });
|
|
249
|
+
}
|
|
250
|
+
const todos = imported.tasks ?? imported.todos ?? [];
|
|
251
|
+
const known = knownTaskText(this);
|
|
252
|
+
for (const todo of todos) {
|
|
253
|
+
const text = todo.text ?? '';
|
|
254
|
+
const file = todo.file || '';
|
|
255
|
+
const same = file === ISSUE_FILE && known.has(flatTaskText(text));
|
|
256
|
+
if (same) continue;
|
|
257
|
+
this.addTask(file, text, !!todo.done);
|
|
258
|
+
if (file === ISSUE_FILE) known.add(flatTaskText(text));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
rememberTemplate(prevText, nextText) {
|
|
263
|
+
const trimmed = nextText.trim();
|
|
264
|
+
if (!trimmed) return;
|
|
265
|
+
const prev = (prevText ?? '').trim();
|
|
266
|
+
if (prev === trimmed) return;
|
|
267
|
+
this.templates = upsertTemplate(this.templates, trimmed);
|
|
268
|
+
this.dirty = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
ReviewStore,
|
|
274
|
+
noteCounts,
|
|
275
|
+
reviewProgress,
|
|
276
|
+
hasNotes,
|
|
277
|
+
toNote,
|
|
278
|
+
checkLabel,
|
|
279
|
+
feedbackKey,
|
|
280
|
+
flatTaskText,
|
|
281
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const files = require('../common/files.js');
|
|
6
|
+
const { REVIEW_DIR } = files;
|
|
7
|
+
const { toInt } = require('../common/utilities.js');
|
|
8
|
+
|
|
9
|
+
const TEMPLATE_SHOW = 5;
|
|
10
|
+
|
|
11
|
+
const rankedTemplates = (templates) => {
|
|
12
|
+
const ranked = [...templates];
|
|
13
|
+
ranked.sort((a, b) => {
|
|
14
|
+
const byCount = b.count - a.count;
|
|
15
|
+
if (byCount) return byCount;
|
|
16
|
+
return a.text.localeCompare(b.text);
|
|
17
|
+
});
|
|
18
|
+
return ranked;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const prefixTemplates = (templates, prefix) => {
|
|
22
|
+
if (!prefix) return templates;
|
|
23
|
+
return templates.filter((entry) => entry.text.startsWith(prefix));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const upsertTemplate = (templates, text) => {
|
|
27
|
+
const trimmed = text.trim();
|
|
28
|
+
if (!trimmed) return templates;
|
|
29
|
+
const next = [...templates];
|
|
30
|
+
const i = next.findIndex((entry) => entry.text === trimmed);
|
|
31
|
+
if (i < 0) {
|
|
32
|
+
next.push({ text: trimmed, count: 1 });
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
const prev = next[i];
|
|
36
|
+
next[i] = { text: prev.text, count: prev.count + 1 };
|
|
37
|
+
return next;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const TEMPLATES_FILE = '.templates';
|
|
41
|
+
|
|
42
|
+
const LEGACY_TEMPLATES = 'templates.json';
|
|
43
|
+
|
|
44
|
+
const templateEntries = (data) => {
|
|
45
|
+
if (!Array.isArray(data)) return [];
|
|
46
|
+
const templates = [];
|
|
47
|
+
for (const entry of data) {
|
|
48
|
+
if (!entry || typeof entry.text !== 'string') continue;
|
|
49
|
+
const count = toInt(entry.count);
|
|
50
|
+
templates.push({
|
|
51
|
+
text: entry.text,
|
|
52
|
+
count: Number.isFinite(count) ? count : 1,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return templates;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const readTemplateFile = (file) => {
|
|
59
|
+
try {
|
|
60
|
+
return templateEntries(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error && error.code === 'ENOENT') return null;
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const loadTemplates = (dir) => {
|
|
68
|
+
const folder = path.join(dir, REVIEW_DIR);
|
|
69
|
+
const current = readTemplateFile(path.join(folder, TEMPLATES_FILE));
|
|
70
|
+
if (current) return current;
|
|
71
|
+
const legacy = readTemplateFile(path.join(folder, LEGACY_TEMPLATES));
|
|
72
|
+
return legacy ?? [];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
TEMPLATE_SHOW,
|
|
77
|
+
rankedTemplates,
|
|
78
|
+
prefixTemplates,
|
|
79
|
+
upsertTemplate,
|
|
80
|
+
TEMPLATES_FILE,
|
|
81
|
+
LEGACY_TEMPLATES,
|
|
82
|
+
loadTemplates,
|
|
83
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { npmBin, npmOpts } = require('../common/process.js');
|
|
4
|
+
const { commandEnv, PipeProcess } = require('./process.js');
|
|
5
|
+
|
|
6
|
+
const startNpm = (cwd, entry, onData, onClose) => {
|
|
7
|
+
const isBin = entry.kind === 'bin';
|
|
8
|
+
const args = isBin ? ['exec', '--', entry.name] : ['run', entry.name];
|
|
9
|
+
const detached = process.platform !== 'win32';
|
|
10
|
+
const options = npmOpts({ cwd, env: commandEnv(), detached });
|
|
11
|
+
return new PipeProcess(onData, onClose).start(npmBin(), args, options);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = { startNpm };
|
package/lib/runs/logs.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { stripAnsi } = require('../term/ansi.js');
|
|
6
|
+
const utilities = require('../common/utilities.js');
|
|
7
|
+
const { pad2 } = utilities;
|
|
8
|
+
const { readJson } = utilities;
|
|
9
|
+
const { dateStamp, readText } = utilities;
|
|
10
|
+
const { isHashObject } = require('metautil');
|
|
11
|
+
const { LOG_DIR } = require('../common/files.js');
|
|
12
|
+
|
|
13
|
+
const safeName = (name) => `${name}`.replace(/[^A-Za-z0-9._-]+/g, '-');
|
|
14
|
+
|
|
15
|
+
const logFileName = (command, date, index) => {
|
|
16
|
+
const safe = safeName(command);
|
|
17
|
+
const n = pad2(index);
|
|
18
|
+
return `${date}-${safe}-${n}.log`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const LOG_STALE_DAYS = 5;
|
|
22
|
+
|
|
23
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
const LOG_DATE = /^(\d{4})-(\d{2})-(\d{2})-/;
|
|
26
|
+
|
|
27
|
+
const logDay = (name) => {
|
|
28
|
+
const match = LOG_DATE.exec(name);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
const year = Number(match[1]);
|
|
31
|
+
const month = Number(match[2]);
|
|
32
|
+
const day = Number(match[3]);
|
|
33
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
34
|
+
return Date.UTC(year, month - 1, day) / DAY_MS;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const todayIndex = (now) =>
|
|
38
|
+
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / DAY_MS;
|
|
39
|
+
|
|
40
|
+
const isStaleLog = (name, now) => {
|
|
41
|
+
const day = logDay(name);
|
|
42
|
+
if (day === null) return false;
|
|
43
|
+
return todayIndex(now) - day > LOG_STALE_DAYS;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const readLogNames = (dir) => {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readdirSync(dir);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.code === 'ENOENT') return [];
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const staleLogFiles = (root, now = new Date()) => {
|
|
56
|
+
const dir = path.join(root, LOG_DIR);
|
|
57
|
+
const files = [];
|
|
58
|
+
let bytes = 0;
|
|
59
|
+
for (const name of readLogNames(dir)) {
|
|
60
|
+
if (!/\.(?:log|raw|json)$/.test(name) || !isStaleLog(name, now)) continue;
|
|
61
|
+
const full = path.join(dir, name);
|
|
62
|
+
let stat = null;
|
|
63
|
+
try {
|
|
64
|
+
stat = fs.statSync(full);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code !== 'ENOENT') throw error;
|
|
67
|
+
}
|
|
68
|
+
if (!stat || !stat.isFile()) continue;
|
|
69
|
+
bytes += stat.size;
|
|
70
|
+
files.push(full);
|
|
71
|
+
}
|
|
72
|
+
return { bytes, files };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const removeStaleLogs = (root, now = new Date()) => {
|
|
76
|
+
const found = staleLogFiles(root, now);
|
|
77
|
+
for (const full of found.files) fs.rmSync(full, { force: true });
|
|
78
|
+
return found.bytes;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const logSlot = (dir, name) => {
|
|
82
|
+
const stem = name.replace(/\.log$/, '');
|
|
83
|
+
const rawName = `${stem}.raw`;
|
|
84
|
+
const jsonName = `${stem}.json`;
|
|
85
|
+
const full = path.join(dir, name);
|
|
86
|
+
const rawFull = path.join(dir, rawName);
|
|
87
|
+
const jsonFull = path.join(dir, jsonName);
|
|
88
|
+
const legacy = path.join(dir, `${stem}.raw.log`);
|
|
89
|
+
return { name, full, dir, rawName, rawFull, jsonName, jsonFull, legacy };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const slotTaken = (slot) => {
|
|
93
|
+
const log = fs.existsSync(slot.full);
|
|
94
|
+
const raw = fs.existsSync(slot.rawFull);
|
|
95
|
+
const json = fs.existsSync(slot.jsonFull);
|
|
96
|
+
const legacy = fs.existsSync(slot.legacy);
|
|
97
|
+
return log || raw || json || legacy;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const nextLogFile = (root, command, now = new Date()) => {
|
|
101
|
+
const date = dateStamp(now);
|
|
102
|
+
const dir = path.join(root, LOG_DIR);
|
|
103
|
+
let slot = null;
|
|
104
|
+
for (let index = 1; index <= 100; index++) {
|
|
105
|
+
slot = logSlot(dir, logFileName(command, date, index));
|
|
106
|
+
if (!slotTaken(slot)) break;
|
|
107
|
+
}
|
|
108
|
+
return slot;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const logText = (text) =>
|
|
112
|
+
stripAnsi(text)
|
|
113
|
+
.split('\n')
|
|
114
|
+
.map((line) => line.trimEnd())
|
|
115
|
+
.join('\n');
|
|
116
|
+
|
|
117
|
+
const RUN_MARK = 'npm-run';
|
|
118
|
+
|
|
119
|
+
const LOG_FILE = /^(\d{4}-\d{2}-\d{2})-(.+)-(\d{2})\.log$/;
|
|
120
|
+
|
|
121
|
+
const AGENT_LOG = /-agent-\d{2}\.log$/;
|
|
122
|
+
|
|
123
|
+
const fileName = (name) => {
|
|
124
|
+
const file = `${name ?? ''}`;
|
|
125
|
+
if (!file || file.includes('/') || file.includes('\\')) return '';
|
|
126
|
+
return file;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const runMeta = (run, slot) => ({
|
|
130
|
+
reslop: RUN_MARK,
|
|
131
|
+
name: run.name,
|
|
132
|
+
kind: run.kind === 'bin' ? 'bin' : 'script',
|
|
133
|
+
command: `${run.command ?? ''}`,
|
|
134
|
+
status: run.status === 'stopped' ? 'stopped' : 'exited',
|
|
135
|
+
exit: run.status === 'stopped' ? '' : run.exit,
|
|
136
|
+
startedAt: Number(run.startedAt) || 0,
|
|
137
|
+
endedAt: Number(run.endedAt) || 0,
|
|
138
|
+
log: slot.name,
|
|
139
|
+
raw: slot.rawName,
|
|
140
|
+
result: run.result ?? null,
|
|
141
|
+
progress: run.progress ?? null,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const saveLogs = async (slot, reduced, raw, run) => {
|
|
145
|
+
await fs.promises.mkdir(slot.dir, { recursive: true });
|
|
146
|
+
const writes = [
|
|
147
|
+
fs.promises.writeFile(slot.full, logText(reduced)),
|
|
148
|
+
fs.promises.writeFile(slot.rawFull, logText(raw)),
|
|
149
|
+
];
|
|
150
|
+
if (run) {
|
|
151
|
+
const body = `${JSON.stringify(runMeta(run, slot), null, 2)}\n`;
|
|
152
|
+
writes.push(fs.promises.writeFile(slot.jsonFull, body));
|
|
153
|
+
}
|
|
154
|
+
await Promise.all(writes);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const readObject = (file) => {
|
|
158
|
+
const data = readJson(file);
|
|
159
|
+
return isHashObject(data) ? data : null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const storedExit = (data) => {
|
|
163
|
+
if (data.status === 'stopped') return '';
|
|
164
|
+
if (typeof data.exit === 'number' && Number.isInteger(data.exit)) {
|
|
165
|
+
return data.exit;
|
|
166
|
+
}
|
|
167
|
+
if (typeof data.exit === 'string' && /^-?\d+$/.test(data.exit)) {
|
|
168
|
+
return Number(data.exit);
|
|
169
|
+
}
|
|
170
|
+
return '';
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const savedFromRecord = (data) => {
|
|
174
|
+
if (!data || data.reslop !== RUN_MARK) return null;
|
|
175
|
+
const name = `${data.name ?? ''}`;
|
|
176
|
+
const logName = fileName(data.log);
|
|
177
|
+
if (!name || !logName) return null;
|
|
178
|
+
const startedAt = Number(data.startedAt) || 0;
|
|
179
|
+
const endedAt = Number(data.endedAt) || startedAt;
|
|
180
|
+
return {
|
|
181
|
+
name,
|
|
182
|
+
kind: data.kind === 'bin' ? 'bin' : 'script',
|
|
183
|
+
command: `${data.command ?? ''}`,
|
|
184
|
+
status: data.status === 'stopped' ? 'stopped' : 'exited',
|
|
185
|
+
exit: storedExit(data),
|
|
186
|
+
startedAt,
|
|
187
|
+
endedAt,
|
|
188
|
+
logName,
|
|
189
|
+
rawName: fileName(data.raw),
|
|
190
|
+
result: data.result ?? null,
|
|
191
|
+
progress: data.progress ?? null,
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const exitOf = (text) => {
|
|
196
|
+
const lines = `${text ?? ''}`.split('\n');
|
|
197
|
+
while (lines.length && lines.at(-1) === '') lines.pop();
|
|
198
|
+
const last = lines.at(-1) ?? '';
|
|
199
|
+
if (last === 'terminated') return { status: 'stopped', exit: '' };
|
|
200
|
+
const match = /^exit (.+)$/.exec(last);
|
|
201
|
+
if (!match) return { status: 'exited', exit: '' };
|
|
202
|
+
if (match[1] === 'null') return { status: 'stopped', exit: '' };
|
|
203
|
+
const code = Number(match[1]);
|
|
204
|
+
if (!Number.isInteger(code)) return { status: 'exited', exit: '' };
|
|
205
|
+
return { status: 'exited', exit: code };
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const commandFor = (commands, safe) => {
|
|
209
|
+
const hits = [];
|
|
210
|
+
for (const entry of commands) {
|
|
211
|
+
if (safeName(entry.name) === safe) hits.push(entry);
|
|
212
|
+
}
|
|
213
|
+
for (const entry of hits) {
|
|
214
|
+
if (entry.name === safe && entry.kind === 'script') return entry;
|
|
215
|
+
}
|
|
216
|
+
for (const entry of hits) {
|
|
217
|
+
if (entry.kind === 'script') return entry;
|
|
218
|
+
}
|
|
219
|
+
return hits[0] || null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const legacyRun = (dir, name, names, commands) => {
|
|
223
|
+
const match = LOG_FILE.exec(name);
|
|
224
|
+
if (!match) return null;
|
|
225
|
+
const stem = name.replace(/\.log$/, '');
|
|
226
|
+
const linked = commandFor(commands, match[2]);
|
|
227
|
+
const text = readText(path.join(dir, name));
|
|
228
|
+
const outcome = exitOf(text);
|
|
229
|
+
let endedAt = 0;
|
|
230
|
+
try {
|
|
231
|
+
endedAt = fs.statSync(path.join(dir, name)).mtimeMs;
|
|
232
|
+
} catch {
|
|
233
|
+
// missing log
|
|
234
|
+
}
|
|
235
|
+
const rawFile = `${stem}.raw`;
|
|
236
|
+
return {
|
|
237
|
+
name: linked ? linked.name : match[2],
|
|
238
|
+
kind: linked ? linked.kind : 'script',
|
|
239
|
+
command: linked ? `${linked.command ?? ''}` : '',
|
|
240
|
+
status: outcome.status,
|
|
241
|
+
exit: outcome.exit,
|
|
242
|
+
startedAt: endedAt,
|
|
243
|
+
endedAt,
|
|
244
|
+
logName: name,
|
|
245
|
+
rawName: names.includes(rawFile) ? rawFile : '',
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const readSavedRuns = (root, commands = []) => {
|
|
250
|
+
const dir = path.join(root, LOG_DIR);
|
|
251
|
+
const names = readLogNames(dir);
|
|
252
|
+
const runs = [];
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
for (const name of names) {
|
|
255
|
+
if (!name.endsWith('.json')) continue;
|
|
256
|
+
const saved = savedFromRecord(readObject(path.join(dir, name)));
|
|
257
|
+
if (!saved || seen.has(saved.logName)) continue;
|
|
258
|
+
seen.add(saved.logName);
|
|
259
|
+
runs.push(saved);
|
|
260
|
+
}
|
|
261
|
+
for (const name of names) {
|
|
262
|
+
if (!LOG_FILE.test(name) || AGENT_LOG.test(name) || seen.has(name)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const stem = name.replace(/\.log$/, '');
|
|
266
|
+
if (names.includes(`${stem}.json`)) continue;
|
|
267
|
+
const saved = legacyRun(dir, name, names, commands);
|
|
268
|
+
if (!saved) continue;
|
|
269
|
+
seen.add(saved.logName);
|
|
270
|
+
runs.push(saved);
|
|
271
|
+
}
|
|
272
|
+
runs.sort((left, right) => {
|
|
273
|
+
const at = left.startedAt - right.startedAt;
|
|
274
|
+
if (at) return at;
|
|
275
|
+
return left.logName.localeCompare(right.logName);
|
|
276
|
+
});
|
|
277
|
+
return runs;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const readLogPair = (root, logName, rawName) => {
|
|
281
|
+
const dir = path.join(root, LOG_DIR);
|
|
282
|
+
const log = fileName(logName);
|
|
283
|
+
const raw = fileName(rawName);
|
|
284
|
+
return {
|
|
285
|
+
output: log ? readText(path.join(dir, log)) : '',
|
|
286
|
+
raw: raw ? readText(path.join(dir, raw)) : '',
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
logFileName,
|
|
292
|
+
staleLogFiles,
|
|
293
|
+
removeStaleLogs,
|
|
294
|
+
nextLogFile,
|
|
295
|
+
logText,
|
|
296
|
+
saveLogs,
|
|
297
|
+
readSavedRuns,
|
|
298
|
+
readLogPair,
|
|
299
|
+
};
|