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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const git = require('../git-worktree.js');
|
|
7
|
+
const { runGitAsync } = git;
|
|
8
|
+
const watch = require('../session/watch.js');
|
|
9
|
+
const { UNKNOWN_PATH } = watch;
|
|
10
|
+
|
|
11
|
+
const ROOT_DIR = '.';
|
|
12
|
+
const NO_EXT = '(none)';
|
|
13
|
+
const MAX_LINES_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const BINARY_PROBE = 8000;
|
|
15
|
+
const CONCURRENCY = 32;
|
|
16
|
+
const HEAT_MS = 4000;
|
|
17
|
+
const RECENT_KEEP = 64;
|
|
18
|
+
const SKIP_WALK = new Set(['.git', 'node_modules']);
|
|
19
|
+
const SKIP_PREFIX = ['.git/', 'node_modules/'];
|
|
20
|
+
|
|
21
|
+
const slash = (rel) => `${rel}`.replaceAll('\\', '/');
|
|
22
|
+
|
|
23
|
+
const folderOf = (rel) => {
|
|
24
|
+
const at = rel.indexOf('/');
|
|
25
|
+
return at < 0 ? ROOT_DIR : rel.slice(0, at);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const extOf = (rel) => path.extname(rel).toLowerCase() || NO_EXT;
|
|
29
|
+
|
|
30
|
+
const statOf = (full) => fs.promises.stat(full).catch(() => null);
|
|
31
|
+
|
|
32
|
+
const countLines = (buffer) => {
|
|
33
|
+
const size = buffer.length;
|
|
34
|
+
if (!size || buffer.subarray(0, BINARY_PROBE).includes(0)) return 0;
|
|
35
|
+
let lines = 0;
|
|
36
|
+
let at = buffer.indexOf(10);
|
|
37
|
+
while (at >= 0) {
|
|
38
|
+
lines += 1;
|
|
39
|
+
at = buffer.indexOf(10, at + 1);
|
|
40
|
+
}
|
|
41
|
+
return buffer[size - 1] === 10 ? lines : lines + 1;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mapLimit = async (list, limit, fn) => {
|
|
45
|
+
const results = new Array(list.length);
|
|
46
|
+
let next = 0;
|
|
47
|
+
const worker = async () => {
|
|
48
|
+
while (next < list.length) {
|
|
49
|
+
const index = next;
|
|
50
|
+
next += 1;
|
|
51
|
+
results[index] = await fn(list[index]);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const count = Math.min(limit, list.length);
|
|
55
|
+
await Promise.all(Array.from({ length: count }, worker));
|
|
56
|
+
return results;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const readLines = async (full, size) => {
|
|
60
|
+
if (size > MAX_LINES_BYTES) return 0;
|
|
61
|
+
try {
|
|
62
|
+
return countLines(await fs.promises.readFile(full));
|
|
63
|
+
} catch {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const readEntry = async (root, rel, prev, at) => {
|
|
69
|
+
const full = path.join(root, rel);
|
|
70
|
+
const stat = await statOf(full);
|
|
71
|
+
if (!stat || !stat.isFile()) return null;
|
|
72
|
+
const { size, mtimeMs } = stat;
|
|
73
|
+
if (prev && prev.mtimeMs === mtimeMs && prev.size === size) return prev;
|
|
74
|
+
const lines = await readLines(full, size);
|
|
75
|
+
const dir = folderOf(rel);
|
|
76
|
+
const ext = extOf(rel);
|
|
77
|
+
return { rel, size, lines, mtimeMs, dir, ext, changedAt: at };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const walkFiles = async (root, dir = '') => {
|
|
81
|
+
const found = [];
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = await fs.promises.readdir(path.join(root, dir), {
|
|
85
|
+
withFileTypes: true,
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (SKIP_WALK.has(entry.name)) continue;
|
|
92
|
+
const rel = dir ? `${dir}/${entry.name}` : entry.name;
|
|
93
|
+
if (entry.isFile()) found.push(rel);
|
|
94
|
+
if (!entry.isDirectory()) continue;
|
|
95
|
+
const inner = await walkFiles(root, rel);
|
|
96
|
+
found.push(...inner);
|
|
97
|
+
}
|
|
98
|
+
return found;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const listProject = async (root) => {
|
|
102
|
+
const args = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
|
|
103
|
+
try {
|
|
104
|
+
const result = await runGitAsync(args, root);
|
|
105
|
+
if (result.status === 0) {
|
|
106
|
+
return result.stdout.split('\0').filter(Boolean).map(slash);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// not a git repository: walk the tree
|
|
110
|
+
}
|
|
111
|
+
return walkFiles(root);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const ignoredAmong = async (root, rels) => {
|
|
115
|
+
if (!rels.length) return new Set();
|
|
116
|
+
try {
|
|
117
|
+
const args = ['check-ignore', '-z', '--', ...rels];
|
|
118
|
+
const result = await runGitAsync(args, root);
|
|
119
|
+
if (result.status !== 0 && result.status !== 1) return new Set();
|
|
120
|
+
return new Set(result.stdout.split('\0').filter(Boolean).map(slash));
|
|
121
|
+
} catch {
|
|
122
|
+
return new Set();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const groupOf = (map, key) => {
|
|
127
|
+
let group = map.get(key);
|
|
128
|
+
if (!group) {
|
|
129
|
+
group = { key, files: 0, bytes: 0, lines: 0 };
|
|
130
|
+
map.set(key, group);
|
|
131
|
+
}
|
|
132
|
+
return group;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const bySize = (left, right) =>
|
|
136
|
+
right.bytes - left.bytes || left.key.localeCompare(right.key, 'en');
|
|
137
|
+
|
|
138
|
+
class FileIndex {
|
|
139
|
+
constructor(root) {
|
|
140
|
+
this.root = root;
|
|
141
|
+
this.entries = new Map();
|
|
142
|
+
this.recent = [];
|
|
143
|
+
this.cached = null;
|
|
144
|
+
this.ready = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
note(entry) {
|
|
148
|
+
if (!entry.changedAt) return;
|
|
149
|
+
this.recent.push({ at: entry.changedAt, dir: entry.dir, ext: entry.ext });
|
|
150
|
+
if (this.recent.length > RECENT_KEEP) this.recent.shift();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
readAll(rels, at) {
|
|
154
|
+
const read = (rel) => readEntry(this.root, rel, this.entries.get(rel), at);
|
|
155
|
+
return mapLimit(rels, CONCURRENCY, read);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async rescan() {
|
|
159
|
+
const rels = await listProject(this.root);
|
|
160
|
+
const entries = await this.readAll(rels, this.ready ? Date.now() : 0);
|
|
161
|
+
const next = new Map();
|
|
162
|
+
let changed = !this.ready;
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (!entry) continue;
|
|
165
|
+
if (this.entries.get(entry.rel) !== entry) {
|
|
166
|
+
changed = true;
|
|
167
|
+
this.note(entry);
|
|
168
|
+
}
|
|
169
|
+
next.set(entry.rel, entry);
|
|
170
|
+
}
|
|
171
|
+
if (next.size !== this.entries.size) changed = true;
|
|
172
|
+
this.entries = next;
|
|
173
|
+
this.ready = true;
|
|
174
|
+
if (changed) this.cached = null;
|
|
175
|
+
return changed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dropPrefix(rel) {
|
|
179
|
+
let dropped = false;
|
|
180
|
+
const head = `${rel}/`;
|
|
181
|
+
for (const key of [...this.entries.keys()]) {
|
|
182
|
+
if (key !== rel && !key.startsWith(head)) continue;
|
|
183
|
+
this.entries.delete(key);
|
|
184
|
+
dropped = true;
|
|
185
|
+
}
|
|
186
|
+
return dropped;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async applyFresh(rels) {
|
|
190
|
+
const unknown = rels.filter((rel) => !this.entries.has(rel));
|
|
191
|
+
const ignored = await ignoredAmong(this.root, unknown);
|
|
192
|
+
const wanted = rels.filter((rel) => !ignored.has(rel));
|
|
193
|
+
const entries = await this.readAll(wanted, Date.now());
|
|
194
|
+
let changed = false;
|
|
195
|
+
for (let i = 0; i < wanted.length; i++) {
|
|
196
|
+
const entry = entries[i];
|
|
197
|
+
if (!entry) {
|
|
198
|
+
changed = this.entries.delete(wanted[i]) || changed;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (this.entries.get(entry.rel) === entry) continue;
|
|
202
|
+
this.entries.set(entry.rel, entry);
|
|
203
|
+
this.note(entry);
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
return changed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async touch(paths) {
|
|
210
|
+
const rels = [];
|
|
211
|
+
for (const item of paths) {
|
|
212
|
+
const rel = slash(item);
|
|
213
|
+
if (rel === UNKNOWN_PATH) return this.rescan();
|
|
214
|
+
if (SKIP_PREFIX.some((prefix) => rel.startsWith(prefix))) continue;
|
|
215
|
+
rels.push(rel);
|
|
216
|
+
}
|
|
217
|
+
if (!this.ready) return this.rescan();
|
|
218
|
+
const probe = (rel) => statOf(path.join(this.root, rel));
|
|
219
|
+
const stats = await mapLimit(rels, CONCURRENCY, probe);
|
|
220
|
+
if (stats.some((stat) => stat && stat.isDirectory())) return this.rescan();
|
|
221
|
+
let changed = false;
|
|
222
|
+
const fresh = [];
|
|
223
|
+
for (let i = 0; i < rels.length; i++) {
|
|
224
|
+
const stat = stats[i];
|
|
225
|
+
if (!stat) changed = this.dropPrefix(rels[i]) || changed;
|
|
226
|
+
else if (stat.isFile()) fresh.push(rels[i]);
|
|
227
|
+
}
|
|
228
|
+
if (fresh.length) {
|
|
229
|
+
const applied = await this.applyFresh(fresh);
|
|
230
|
+
changed = applied || changed;
|
|
231
|
+
}
|
|
232
|
+
if (changed) this.cached = null;
|
|
233
|
+
return changed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
summary() {
|
|
237
|
+
if (this.cached) return this.cached;
|
|
238
|
+
const total = { files: 0, bytes: 0, lines: 0 };
|
|
239
|
+
const dirs = new Map();
|
|
240
|
+
const exts = new Map();
|
|
241
|
+
for (const entry of this.entries.values()) {
|
|
242
|
+
const groups = [
|
|
243
|
+
total,
|
|
244
|
+
groupOf(dirs, entry.dir),
|
|
245
|
+
groupOf(exts, entry.ext),
|
|
246
|
+
];
|
|
247
|
+
for (const group of groups) {
|
|
248
|
+
group.files += 1;
|
|
249
|
+
group.bytes += entry.size;
|
|
250
|
+
group.lines += entry.lines;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.cached = {
|
|
254
|
+
total,
|
|
255
|
+
dirs: [...dirs.values()].sort(bySize),
|
|
256
|
+
exts: [...exts.values()].sort(bySize),
|
|
257
|
+
};
|
|
258
|
+
return this.cached;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
hot(now) {
|
|
262
|
+
const dirs = new Set();
|
|
263
|
+
const exts = new Set();
|
|
264
|
+
for (const item of this.recent) {
|
|
265
|
+
if (now - item.at > HEAT_MS) continue;
|
|
266
|
+
dirs.add(item.dir);
|
|
267
|
+
exts.add(item.ext);
|
|
268
|
+
}
|
|
269
|
+
return { dirs, exts };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
hotUntil() {
|
|
273
|
+
const last = this.recent.at(-1);
|
|
274
|
+
return last ? last.at + HEAT_MS : 0;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { ROOT_DIR, NO_EXT, HEAT_MS, folderOf, extOf, FileIndex };
|