reslop 0.2.7 → 0.3.1
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 +30 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +573 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +17 -0
- package/lib/agents/models.js +467 -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} +151 -95
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +504 -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 +114 -48
- package/lib/{editor.js → input/editor.js} +16 -8
- package/lib/{keys.js → input/keys.js} +24 -45
- package/lib/input/line-edit.js +78 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +642 -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 +385 -0
- package/lib/render/frame.js +123 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +320 -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} +48 -19
- 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 +14 -0
- package/lib/review/format.js +420 -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 +976 -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 -40
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +91 -52
- package/lib/session/compose.js +189 -147
- package/lib/session/dashboard.js +23 -22
- package/lib/session/dispatch.js +86 -21
- package/lib/session/files-pane.js +47 -26
- package/lib/session/find.js +16 -29
- package/lib/session/import.js +93 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +78 -72
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +6 -3
- package/lib/session/npm/history.js +250 -0
- package/lib/session/npm/npm.js +836 -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} +158 -63
- package/lib/session/tasks.js +321 -66
- 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} +4 -3
- 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,420 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const files = require('../common/files.js');
|
|
5
|
+
const { TASKS_FILE, TASK_KINDS, taskKind, taskTitle } = files;
|
|
6
|
+
const { captureBaseline } = require('./merge.js');
|
|
7
|
+
const { toInt, resource } = require('../common/utilities.js');
|
|
8
|
+
const storePart = require('./store.js');
|
|
9
|
+
const { flatTaskText, toNote, feedbackKey } = storePart;
|
|
10
|
+
const { ReviewStore } = storePart;
|
|
11
|
+
|
|
12
|
+
const REVIEW_STATUSES = ['editing', 'partial', 'done'];
|
|
13
|
+
|
|
14
|
+
const EDITING_ALIASES = ['pending', 'ready'];
|
|
15
|
+
|
|
16
|
+
const FEED_HEAD = /^### Feedback `([^`]+)` \+(\d+) \((.*)\)\s*$/;
|
|
17
|
+
|
|
18
|
+
const FEED_BLOCK = /^(.*), block (\d+)$/;
|
|
19
|
+
|
|
20
|
+
const FEED_BLOCK_ONLY = /^block (\d+)$/;
|
|
21
|
+
|
|
22
|
+
const FEED_META = /^<!-- reslop:(.*) -->\s*$/;
|
|
23
|
+
|
|
24
|
+
const FEED_SUFFIX = /^(.*) - (.+):(\d+):(\d+):(\d+)$/;
|
|
25
|
+
|
|
26
|
+
const CHECK_ITEM = /^- \[([ xX])\] (.*)$/;
|
|
27
|
+
|
|
28
|
+
const CODE_HEAD = /^- \[([ xX])\] code `([^`]+)`\s*$/;
|
|
29
|
+
|
|
30
|
+
const FENCE_LINE = /^(`{3,})\s*$/;
|
|
31
|
+
|
|
32
|
+
const FILE_QUOTE = /^> (.+)$/;
|
|
33
|
+
|
|
34
|
+
const STATUS_LINE = /^status:\s*(\S+)\s*$/;
|
|
35
|
+
|
|
36
|
+
const GIT_ORIGINS = ['unstaged', 'staged', 'untracked', 'commit', 'pr'];
|
|
37
|
+
|
|
38
|
+
const SECTION_LINE = {
|
|
39
|
+
'### Todo': 'todo',
|
|
40
|
+
'### Feedback': 'feedback',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const BACKLOG_HEAD = 'Backlog';
|
|
44
|
+
|
|
45
|
+
const LEGACY_BACKLOG = 'TODOs';
|
|
46
|
+
|
|
47
|
+
const TASK_TITLES = Object.fromEntries([
|
|
48
|
+
[BACKLOG_HEAD, 'debt'],
|
|
49
|
+
[LEGACY_BACKLOG, 'debt'],
|
|
50
|
+
['Issues', 'debt'],
|
|
51
|
+
['Bug Reports', 'bugs'],
|
|
52
|
+
['Bug reports', 'bugs'],
|
|
53
|
+
['Feature Requests', 'features'],
|
|
54
|
+
['Feature requests', 'features'],
|
|
55
|
+
['Technical debt', 'debt'],
|
|
56
|
+
['Research', 'research'],
|
|
57
|
+
['Security', 'security'],
|
|
58
|
+
...TASK_KINDS.map((kind) => [kind.title, kind.id]),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const AGENT_INSTRUCTIONS = resource(__dirname, 'agent-instructions.md').trim();
|
|
62
|
+
|
|
63
|
+
const canonicalStatus = (raw) => {
|
|
64
|
+
const name = EDITING_ALIASES.includes(raw) ? 'editing' : raw;
|
|
65
|
+
return REVIEW_STATUSES.includes(name) ? name : '';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const frontmatterStatus = (text) => {
|
|
69
|
+
const body = `${text ?? ''}`;
|
|
70
|
+
if (!body.startsWith('---')) return '';
|
|
71
|
+
const nl = body.indexOf('\n');
|
|
72
|
+
if (nl < 0) return '';
|
|
73
|
+
const rest = body.slice(nl + 1);
|
|
74
|
+
const end = rest.indexOf('\n---');
|
|
75
|
+
if (end < 0) return '';
|
|
76
|
+
for (const line of rest.slice(0, end).split('\n')) {
|
|
77
|
+
const match = STATUS_LINE.exec(line);
|
|
78
|
+
if (match) return match[1];
|
|
79
|
+
}
|
|
80
|
+
return '';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const parseFrontmatterStatus = (text) =>
|
|
84
|
+
canonicalStatus(frontmatterStatus(text));
|
|
85
|
+
|
|
86
|
+
const isDoneMark = (mark) => mark === 'x' || mark === 'X';
|
|
87
|
+
|
|
88
|
+
const checkboxLine = (text, done) => {
|
|
89
|
+
const mark = done ? 'x' : ' ';
|
|
90
|
+
return `- [${mark}] ${flatTaskText(text)}`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const storeNote = (notes, note, key) => {
|
|
94
|
+
const next = toNote(note);
|
|
95
|
+
notes.set(key || feedbackKey(next), next);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const feedbackLine = (note) => {
|
|
99
|
+
const loc = feedbackKey(note);
|
|
100
|
+
return `${checkboxLine(note.text, note.done)} - ${loc}`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const parseLoc = (raw) => {
|
|
104
|
+
const parts = raw.split(':');
|
|
105
|
+
const n = parts.length;
|
|
106
|
+
if (n < 4) return null;
|
|
107
|
+
const blockId = toInt(parts[n - 1]);
|
|
108
|
+
const newStart = toInt(parts[n - 2]);
|
|
109
|
+
const oldStart = toInt(parts[n - 3]);
|
|
110
|
+
if (!Number.isFinite(blockId + newStart + oldStart)) return null;
|
|
111
|
+
let fileParts = parts.slice(0, n - 3);
|
|
112
|
+
let origin = '';
|
|
113
|
+
if (fileParts.length > 1 && GIT_ORIGINS.includes(fileParts[0])) {
|
|
114
|
+
origin = fileParts[0];
|
|
115
|
+
fileParts = fileParts.slice(1);
|
|
116
|
+
}
|
|
117
|
+
if (!fileParts.length) return null;
|
|
118
|
+
const file = fileParts.join(':');
|
|
119
|
+
return { origin, file, oldStart, newStart, blockId };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const parseFeedSuffix = (textItem) => {
|
|
123
|
+
const match = FEED_SUFFIX.exec(textItem);
|
|
124
|
+
if (!match) return null;
|
|
125
|
+
const text = match[1];
|
|
126
|
+
const file = match[2];
|
|
127
|
+
const oldStart = toInt(match[3]);
|
|
128
|
+
const newStart = toInt(match[4]);
|
|
129
|
+
const blockId = toInt(match[5]);
|
|
130
|
+
return { text, file, oldStart, newStart, blockId };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const fileGroup = (groups, file) => {
|
|
134
|
+
let group = groups.get(file);
|
|
135
|
+
if (!group) {
|
|
136
|
+
group = { tasks: [], feedback: [], code: [] };
|
|
137
|
+
groups.set(file, group);
|
|
138
|
+
}
|
|
139
|
+
return group;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const TASK_SECTION = 'task:';
|
|
143
|
+
|
|
144
|
+
const taskSection = (id) => `${TASK_SECTION}${id}`;
|
|
145
|
+
|
|
146
|
+
const groupReviewNotes = (notes) => {
|
|
147
|
+
const groups = new Map();
|
|
148
|
+
for (const kind of TASK_KINDS) {
|
|
149
|
+
for (const todo of notes.tasks) {
|
|
150
|
+
if (!todo.text.trim() || taskKind(todo) !== kind.id) continue;
|
|
151
|
+
fileGroup(groups, taskSection(kind.id)).tasks.push(todo);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const key of notes.feedback.keys()) {
|
|
155
|
+
const note = notes.feedback.get(key);
|
|
156
|
+
if (!note.text.trim()) continue;
|
|
157
|
+
fileGroup(groups, note.file).feedback.push({ key, note });
|
|
158
|
+
}
|
|
159
|
+
for (const key of notes.code.keys()) {
|
|
160
|
+
const note = notes.code.get(key);
|
|
161
|
+
fileGroup(groups, note.file).code.push({ key, note });
|
|
162
|
+
}
|
|
163
|
+
return groups;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const reviewHeader = (notes) => {
|
|
167
|
+
const name = path.basename(notes.reviewPath, '.md');
|
|
168
|
+
const status = canonicalStatus(notes.status) || 'editing';
|
|
169
|
+
return [
|
|
170
|
+
'---',
|
|
171
|
+
`status: ${status}`,
|
|
172
|
+
'---',
|
|
173
|
+
'',
|
|
174
|
+
`# reslop review ${name}`,
|
|
175
|
+
'',
|
|
176
|
+
'## Agent instructions',
|
|
177
|
+
'',
|
|
178
|
+
AGENT_INSTRUCTIONS,
|
|
179
|
+
'',
|
|
180
|
+
];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const fenceFor = (text) => {
|
|
184
|
+
let n = 3;
|
|
185
|
+
for (const line of text.split('\n')) {
|
|
186
|
+
const match = /^(`{3,})/.exec(line);
|
|
187
|
+
if (!match) continue;
|
|
188
|
+
if (match[1].length >= n) n = match[1].length + 1;
|
|
189
|
+
}
|
|
190
|
+
return '`'.repeat(n);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const emitCodeItem = (lines, note) => {
|
|
194
|
+
const loc = feedbackKey(note);
|
|
195
|
+
const mark = note.done ? 'x' : ' ';
|
|
196
|
+
const fence = fenceFor(note.text);
|
|
197
|
+
lines.push(`- [${mark}] code \`${loc}\``);
|
|
198
|
+
lines.push(fence);
|
|
199
|
+
if (note.text !== '') lines.push(note.text);
|
|
200
|
+
lines.push(fence);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const sectionTitle = (file) => {
|
|
204
|
+
if (!file.startsWith(TASK_SECTION)) return file;
|
|
205
|
+
return taskTitle(file.slice(TASK_SECTION.length));
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const backlogFile = (name) => {
|
|
209
|
+
if (name === BACKLOG_HEAD || name === LEGACY_BACKLOG) return TASKS_FILE;
|
|
210
|
+
return name;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const emitReviewGroups = (lines, groups) => {
|
|
214
|
+
for (const file of groups.keys()) {
|
|
215
|
+
const group = groups.get(file);
|
|
216
|
+
lines.push(`## ${sectionTitle(file)}`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
for (const todo of group.tasks) {
|
|
219
|
+
lines.push(checkboxLine(todo.text, todo.done));
|
|
220
|
+
}
|
|
221
|
+
for (const item of group.feedback) {
|
|
222
|
+
lines.push(feedbackLine(item.note));
|
|
223
|
+
}
|
|
224
|
+
for (const item of group.code) {
|
|
225
|
+
emitCodeItem(lines, item.note);
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const serializeReview = (notes) => {
|
|
232
|
+
const lines = reviewHeader(notes);
|
|
233
|
+
emitReviewGroups(lines, groupReviewNotes(notes));
|
|
234
|
+
return `${lines.join('\n')}\n`;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const parseFeedbackHeading = (line) => {
|
|
238
|
+
const match = FEED_HEAD.exec(line);
|
|
239
|
+
if (!match) return null;
|
|
240
|
+
const file = match[1];
|
|
241
|
+
const newStart = toInt(match[2]);
|
|
242
|
+
const rest = match[3];
|
|
243
|
+
const only = FEED_BLOCK_ONLY.exec(rest);
|
|
244
|
+
if (only) {
|
|
245
|
+
return { file, newStart, header: '', blockId: toInt(only[1]) };
|
|
246
|
+
}
|
|
247
|
+
const withHead = FEED_BLOCK.exec(rest);
|
|
248
|
+
if (!withHead) {
|
|
249
|
+
return { file, newStart, header: rest, blockId: 0 };
|
|
250
|
+
}
|
|
251
|
+
const header = withHead[1];
|
|
252
|
+
const blockId = toInt(withHead[2]);
|
|
253
|
+
return { file, newStart, header, blockId };
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const headingFile = (line) => {
|
|
257
|
+
const quoted = FILE_QUOTE.exec(line);
|
|
258
|
+
if (quoted) return backlogFile(quoted[1]);
|
|
259
|
+
if (line.startsWith('## ') && line !== '## Agent instructions') {
|
|
260
|
+
const name = line.slice(3);
|
|
261
|
+
const kind = TASK_TITLES[name];
|
|
262
|
+
if (kind) return taskSection(kind);
|
|
263
|
+
return name;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const startFeedbackPending = (heading, file) => ({
|
|
269
|
+
file: heading.file || file,
|
|
270
|
+
newStart: heading.newStart,
|
|
271
|
+
header: heading.header,
|
|
272
|
+
blockId: heading.blockId,
|
|
273
|
+
oldStart: heading.newStart,
|
|
274
|
+
origin: '',
|
|
275
|
+
key: '',
|
|
276
|
+
text: '',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const applyFeedMeta = (pending, raw) => {
|
|
280
|
+
const loc = parseLoc(raw);
|
|
281
|
+
if (!loc) return pending;
|
|
282
|
+
return Object.assign(pending, loc, { key: feedbackKey(loc) });
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const startCodePending = (line) => {
|
|
286
|
+
const match = CODE_HEAD.exec(line);
|
|
287
|
+
if (!match) return null;
|
|
288
|
+
const loc = parseLoc(match[2]);
|
|
289
|
+
if (!loc) return null;
|
|
290
|
+
return { done: isDoneMark(match[1]), ...loc, open: '', lines: [] };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
class ReviewParser {
|
|
294
|
+
constructor(store) {
|
|
295
|
+
this.store = store;
|
|
296
|
+
this.file = '';
|
|
297
|
+
this.section = '';
|
|
298
|
+
this.pending = null;
|
|
299
|
+
this.code = null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
flushFeedback() {
|
|
303
|
+
const pending = this.pending;
|
|
304
|
+
this.pending = null;
|
|
305
|
+
if (!pending || !(pending.text ?? '').trim()) return;
|
|
306
|
+
storeNote(this.store.feedback, pending, pending.key);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
closeCode(text) {
|
|
310
|
+
storeNote(this.store.code, { ...this.code, text });
|
|
311
|
+
this.code = null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
takeCheckItem(text, done) {
|
|
315
|
+
const { store, file, section, pending } = this;
|
|
316
|
+
if (file.startsWith(TASK_SECTION)) {
|
|
317
|
+
const kind = file.slice(TASK_SECTION.length);
|
|
318
|
+
store.addTask(TASKS_FILE, text, done, kind);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (section === 'todo' && file) {
|
|
322
|
+
store.addTask(file, text, done, 'backlog');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const parsed = parseFeedSuffix(text);
|
|
326
|
+
if (parsed) {
|
|
327
|
+
this.flushFeedback();
|
|
328
|
+
storeNote(store.feedback, { ...parsed, file: parsed.file || file, done });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (pending) {
|
|
332
|
+
pending.text = text;
|
|
333
|
+
pending.done = done;
|
|
334
|
+
this.flushFeedback();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (file && section !== 'feedback') {
|
|
338
|
+
store.addTask(file, text, done, 'backlog');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
takeReviewLine(line) {
|
|
343
|
+
const nextFile = headingFile(line);
|
|
344
|
+
if (nextFile !== null) {
|
|
345
|
+
this.flushFeedback();
|
|
346
|
+
this.file = nextFile;
|
|
347
|
+
this.section = '';
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const nextSection = SECTION_LINE[line];
|
|
351
|
+
if (nextSection) {
|
|
352
|
+
this.flushFeedback();
|
|
353
|
+
this.section = nextSection;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const heading = parseFeedbackHeading(line);
|
|
357
|
+
if (heading) {
|
|
358
|
+
this.flushFeedback();
|
|
359
|
+
this.pending = startFeedbackPending(heading, this.file);
|
|
360
|
+
this.section = 'feedback';
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const meta = FEED_META.exec(line);
|
|
364
|
+
if (meta && this.pending) {
|
|
365
|
+
applyFeedMeta(this.pending, meta[1]);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const item = CHECK_ITEM.exec(line);
|
|
369
|
+
if (item) this.takeCheckItem(item[2], isDoneMark(item[1]));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
takeCodeLine(line) {
|
|
373
|
+
const code = this.code;
|
|
374
|
+
const fence = FENCE_LINE.exec(line);
|
|
375
|
+
if (!code.open) {
|
|
376
|
+
if (fence) code.open = fence[1];
|
|
377
|
+
else if (line.trim()) this.closeCode('');
|
|
378
|
+
return this.code !== null;
|
|
379
|
+
}
|
|
380
|
+
const closes = fence && fence[1].length >= code.open.length;
|
|
381
|
+
if (closes) this.closeCode(code.lines.join('\n'));
|
|
382
|
+
else code.lines.push(line);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
takeLine(line) {
|
|
387
|
+
if (this.code && this.takeCodeLine(line)) return;
|
|
388
|
+
const started = startCodePending(line);
|
|
389
|
+
if (started) {
|
|
390
|
+
this.flushFeedback();
|
|
391
|
+
this.code = started;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.takeReviewLine(line);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
finish() {
|
|
398
|
+
const code = this.code;
|
|
399
|
+
if (code) this.closeCode(code.open ? code.lines.join('\n') : '');
|
|
400
|
+
this.flushFeedback();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const parseReview = (text, reviewPath, templates = []) => {
|
|
405
|
+
const store = new ReviewStore(reviewPath, templates);
|
|
406
|
+
const parser = new ReviewParser(store);
|
|
407
|
+
for (const line of text.split('\n')) parser.takeLine(line);
|
|
408
|
+
parser.finish();
|
|
409
|
+
store.status = parseFrontmatterStatus(text) || 'editing';
|
|
410
|
+
store.dirty = false;
|
|
411
|
+
captureBaseline(store);
|
|
412
|
+
return store;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
module.exports = {
|
|
416
|
+
frontmatterStatus,
|
|
417
|
+
parseFrontmatterStatus,
|
|
418
|
+
serializeReview,
|
|
419
|
+
parseReview,
|
|
420
|
+
};
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
const flatText = (text) => `${text ?? ''}`.trim().replace(/\n/g, ' ');
|
|
3
|
+
const { TASKS_FILE, taskKind } = require('../common/files.js');
|
|
4
|
+
const { flatTaskText } = require('./store.js');
|
|
7
5
|
|
|
8
6
|
const snapTodo = (todo) => ({
|
|
9
7
|
file: todo.file ?? '',
|
|
10
|
-
text:
|
|
8
|
+
text: flatTaskText(todo.text),
|
|
11
9
|
done: todo.done === true,
|
|
12
10
|
kind: taskKind(todo),
|
|
13
11
|
});
|
|
@@ -53,7 +51,7 @@ const withDone = (todo, done) => {
|
|
|
53
51
|
return { ...todo, done };
|
|
54
52
|
};
|
|
55
53
|
|
|
56
|
-
const liveText = (todo) =>
|
|
54
|
+
const liveText = (todo) => flatTaskText(todo.text);
|
|
57
55
|
|
|
58
56
|
const snapText = (snap) => snap.text;
|
|
59
57
|
|
|
@@ -182,8 +180,11 @@ class TodoMerge {
|
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
const sameNote = (left, right) =>
|
|
186
|
-
|
|
183
|
+
const sameNote = (left, right) => {
|
|
184
|
+
const sameText = left.text === right.text;
|
|
185
|
+
const sameDone = !!left.done === !!right.done;
|
|
186
|
+
return sameText && sameDone;
|
|
187
|
+
};
|
|
187
188
|
|
|
188
189
|
const cloneNote = (note, done) => {
|
|
189
190
|
if (!!note.done === done) return note;
|
|
@@ -0,0 +1,190 @@
|
|
|
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, taskKind } = files;
|
|
7
|
+
const { captureBaseline, mergeReview } = require('./merge.js');
|
|
8
|
+
const {
|
|
9
|
+
pad2,
|
|
10
|
+
dateStamp,
|
|
11
|
+
toInt,
|
|
12
|
+
readText,
|
|
13
|
+
writeJson,
|
|
14
|
+
listDir,
|
|
15
|
+
} = require('../common/utilities.js');
|
|
16
|
+
const formatPart = require('./format.js');
|
|
17
|
+
const { parseFrontmatterStatus, parseReview, serializeReview } = formatPart;
|
|
18
|
+
const { frontmatterStatus } = formatPart;
|
|
19
|
+
const storePart = require('./store.js');
|
|
20
|
+
const { hasNotes, ReviewStore, noteCounts, reviewProgress } = storePart;
|
|
21
|
+
const { checkLabel, feedbackKey } = storePart;
|
|
22
|
+
const templatesPart = require('./templates.js');
|
|
23
|
+
const { TEMPLATES_FILE, LEGACY_TEMPLATES, TEMPLATE_SHOW, rankedTemplates } =
|
|
24
|
+
templatesPart;
|
|
25
|
+
const { prefixTemplates, upsertTemplate, loadTemplates } = templatesPart;
|
|
26
|
+
|
|
27
|
+
const AUTOSAVE_MS = 3000;
|
|
28
|
+
|
|
29
|
+
const REVIEW_NAME = /^(\d{4}-\d{2}-\d{2})-(\d+)\.md$/;
|
|
30
|
+
|
|
31
|
+
const allocateReviewPath = (dir, date, existingNames) => {
|
|
32
|
+
const stamp = dateStamp(date);
|
|
33
|
+
let max = -1;
|
|
34
|
+
for (const name of existingNames) {
|
|
35
|
+
const match = REVIEW_NAME.exec(name);
|
|
36
|
+
if (!match) continue;
|
|
37
|
+
if (match[1] !== stamp) continue;
|
|
38
|
+
const n = toInt(match[2]);
|
|
39
|
+
if (n > max) max = n;
|
|
40
|
+
}
|
|
41
|
+
const next = max + 1;
|
|
42
|
+
return path.join(dir, REVIEW_DIR, `${stamp}-${pad2(next)}.md`);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const listReviewNames = (dir) => listDir(path.join(dir, REVIEW_DIR));
|
|
46
|
+
|
|
47
|
+
const compareReviewNames = (left, right) => {
|
|
48
|
+
const a = REVIEW_NAME.exec(left);
|
|
49
|
+
const b = REVIEW_NAME.exec(right);
|
|
50
|
+
if (!a && !b) return 0;
|
|
51
|
+
if (!a) return -1;
|
|
52
|
+
if (!b) return 1;
|
|
53
|
+
if (a[1] < b[1]) return -1;
|
|
54
|
+
if (a[1] > b[1]) return 1;
|
|
55
|
+
return toInt(a[2]) - toInt(b[2]);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const latestReviewName = (names) => {
|
|
59
|
+
let best = '';
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
if (!REVIEW_NAME.test(name)) continue;
|
|
62
|
+
if (!best || compareReviewNames(name, best) > 0) best = name;
|
|
63
|
+
}
|
|
64
|
+
return best;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const readEntries = (folder) => {
|
|
68
|
+
try {
|
|
69
|
+
return fs.readdirSync(folder, { withFileTypes: true });
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const planFiles = (dir) => {
|
|
76
|
+
const reviews = [];
|
|
77
|
+
const notes = [];
|
|
78
|
+
for (const entry of readEntries(path.join(dir, REVIEW_DIR))) {
|
|
79
|
+
const name = entry.name;
|
|
80
|
+
if (!entry.isFile() || !name.endsWith('.md')) continue;
|
|
81
|
+
if (REVIEW_NAME.test(name)) reviews.push(name);
|
|
82
|
+
else notes.push(name);
|
|
83
|
+
}
|
|
84
|
+
return { reviews, notes };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const planFileNames = (dir) => {
|
|
88
|
+
const { reviews, notes } = planFiles(dir);
|
|
89
|
+
reviews.sort((left, right) => compareReviewNames(right, left));
|
|
90
|
+
return reviews.concat(notes.sort());
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const orderedPlanNames = (dir) => {
|
|
94
|
+
const { reviews, notes } = planFiles(dir);
|
|
95
|
+
reviews.sort(compareReviewNames);
|
|
96
|
+
notes.sort();
|
|
97
|
+
return notes.concat(reviews);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const readReviewStatus = (reviewPath) =>
|
|
101
|
+
parseFrontmatterStatus(readText(reviewPath));
|
|
102
|
+
|
|
103
|
+
const resolveReviewPath = (dir, date, existingNames, extra = {}) => {
|
|
104
|
+
if (!extra.forceNew) {
|
|
105
|
+
const latest = latestReviewName(existingNames);
|
|
106
|
+
if (latest) {
|
|
107
|
+
const reviewPath = path.join(dir, REVIEW_DIR, latest);
|
|
108
|
+
if (readReviewStatus(reviewPath) === 'editing') {
|
|
109
|
+
return { reviewPath, resume: true };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
reviewPath: allocateReviewPath(dir, date, existingNames),
|
|
115
|
+
resume: false,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const loadReview = (reviewPath, templates) => {
|
|
120
|
+
const raw = fs.readFileSync(reviewPath, 'utf8');
|
|
121
|
+
return parseReview(raw, reviewPath, templates);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const readReviewText = (reviewPath) => {
|
|
125
|
+
try {
|
|
126
|
+
return fs.readFileSync(reviewPath, 'utf8');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT') return '';
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const mergeDiskReview = (notes) => {
|
|
134
|
+
const text = readReviewText(notes.reviewPath);
|
|
135
|
+
if (!text.startsWith('---')) return;
|
|
136
|
+
const disk = parseReview(text, notes.reviewPath, notes.templates);
|
|
137
|
+
mergeReview(notes, disk);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const flushReview = (notes, force = false) => {
|
|
141
|
+
if (!notes || !notes.dirty) return false;
|
|
142
|
+
mergeDiskReview(notes);
|
|
143
|
+
if (!hasNotes(notes) && !force) {
|
|
144
|
+
notes.dirty = false;
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const folder = path.dirname(notes.reviewPath);
|
|
148
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
149
|
+
fs.writeFileSync(notes.reviewPath, serializeReview(notes), 'utf8');
|
|
150
|
+
const top = path.dirname(folder);
|
|
151
|
+
const templatesFile = path.join(top, REVIEW_DIR, TEMPLATES_FILE);
|
|
152
|
+
writeJson(templatesFile, notes.templates);
|
|
153
|
+
const legacyFile = path.join(top, REVIEW_DIR, LEGACY_TEMPLATES);
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(legacyFile);
|
|
156
|
+
} catch {
|
|
157
|
+
// ignore when the old templates file is already gone
|
|
158
|
+
}
|
|
159
|
+
captureBaseline(notes);
|
|
160
|
+
notes.dirty = false;
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
TEMPLATE_SHOW,
|
|
166
|
+
AUTOSAVE_MS,
|
|
167
|
+
allocateReviewPath,
|
|
168
|
+
listReviewNames,
|
|
169
|
+
latestReviewName,
|
|
170
|
+
planFileNames,
|
|
171
|
+
orderedPlanNames,
|
|
172
|
+
parseFrontmatterStatus,
|
|
173
|
+
resolveReviewPath,
|
|
174
|
+
rankedTemplates,
|
|
175
|
+
prefixTemplates,
|
|
176
|
+
upsertTemplate,
|
|
177
|
+
ReviewStore,
|
|
178
|
+
noteCounts,
|
|
179
|
+
reviewProgress,
|
|
180
|
+
frontmatterStatus,
|
|
181
|
+
hasNotes,
|
|
182
|
+
taskKind,
|
|
183
|
+
checkLabel,
|
|
184
|
+
feedbackKey,
|
|
185
|
+
serializeReview,
|
|
186
|
+
parseReview,
|
|
187
|
+
loadReview,
|
|
188
|
+
loadTemplates,
|
|
189
|
+
flushReview,
|
|
190
|
+
};
|