reslop 0.2.5 → 0.2.7

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -0,0 +1,290 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const deps = require('../deps.js');
7
+ const { MANIFEST } = deps;
8
+ const watch = require('../session/watch.js');
9
+ const { MODULES_STAMP } = watch;
10
+
11
+ const MODULES = 'node_modules';
12
+ const MODULE_PREFIX = `${MODULES}/`;
13
+ const WORKERS = 8;
14
+ const TOP_PACKAGES = 8;
15
+
16
+ const readJson = async (file) => {
17
+ try {
18
+ return JSON.parse(await fs.promises.readFile(file, 'utf8'));
19
+ } catch {
20
+ return null;
21
+ }
22
+ };
23
+
24
+ const declaredNames = (pkg, section) => {
25
+ const map = pkg && pkg[section];
26
+ if (!map || typeof map !== 'object') return [];
27
+ return Object.keys(map);
28
+ };
29
+
30
+ const lockPackages = (lock) => {
31
+ const packages = lock && lock.packages;
32
+ return packages && typeof packages === 'object' ? packages : null;
33
+ };
34
+
35
+ const modulesStamp = async (top) => {
36
+ try {
37
+ const stat = await fs.promises.stat(path.join(top, MODULES_STAMP));
38
+ return `${stat.mtimeMs}:${stat.size}`;
39
+ } catch {
40
+ return '';
41
+ }
42
+ };
43
+
44
+ const packageName = (root, file) => {
45
+ const parts = path.relative(root, file).split(path.sep);
46
+ const head = parts[0];
47
+ if (!head || head.startsWith('.')) return '';
48
+ if (!head.startsWith('@')) return head;
49
+ const name = parts[1];
50
+ if (!name || name.startsWith('.')) return '';
51
+ return `${head}/${name}`;
52
+ };
53
+
54
+ const rankedPackages = (totals) => {
55
+ const list = [];
56
+ for (const [name, bytes] of totals) list.push({ name, bytes });
57
+ return list.sort(
58
+ (left, right) =>
59
+ right.bytes - left.bytes || left.name.localeCompare(right.name, 'en'),
60
+ );
61
+ };
62
+
63
+ const DIRECT_SECTIONS = [
64
+ 'dependencies',
65
+ 'devDependencies',
66
+ 'optionalDependencies',
67
+ 'peerDependencies',
68
+ ];
69
+ const CHILD_SECTIONS = [
70
+ 'dependencies',
71
+ 'optionalDependencies',
72
+ 'peerDependencies',
73
+ ];
74
+ const CHAIN_MARK = ' 🢒 ';
75
+
76
+ const directNames = (pkg) => {
77
+ const names = new Set();
78
+ for (const section of DIRECT_SECTIONS) {
79
+ for (const name of declaredNames(pkg, section)) names.add(name);
80
+ }
81
+ return names;
82
+ };
83
+
84
+ const devNames = (pkg) => {
85
+ const keep = new Set([
86
+ ...declaredNames(pkg, 'dependencies'),
87
+ ...declaredNames(pkg, 'optionalDependencies'),
88
+ ]);
89
+ const names = new Set();
90
+ for (const name of declaredNames(pkg, 'devDependencies')) {
91
+ if (!keep.has(name)) names.add(name);
92
+ }
93
+ return names;
94
+ };
95
+
96
+ const entryName = (key) => {
97
+ const at = key.lastIndexOf(MODULE_PREFIX);
98
+ if (at < 0) return '';
99
+ const rest = key.slice(at + MODULE_PREFIX.length);
100
+ if (rest.startsWith('@')) {
101
+ const parts = rest.split('/');
102
+ if (!parts[1]) return '';
103
+ return `${parts[0]}/${parts[1]}`;
104
+ }
105
+ return rest.split('/')[0] || '';
106
+ };
107
+
108
+ const childNames = (entry) => {
109
+ const names = [];
110
+ if (!entry || typeof entry !== 'object') return names;
111
+ for (const section of CHILD_SECTIONS) {
112
+ const map = entry[section];
113
+ if (!map || typeof map !== 'object') continue;
114
+ for (const name of Object.keys(map)) {
115
+ if (!names.includes(name)) names.push(name);
116
+ }
117
+ }
118
+ return names;
119
+ };
120
+
121
+ const dependencyChildren = (lock) => {
122
+ const children = new Map();
123
+ const packages = lockPackages(lock);
124
+ if (!packages) return children;
125
+ for (const key of Object.keys(packages)) {
126
+ if (!key) continue;
127
+ const name = entryName(key);
128
+ if (!name) continue;
129
+ const list = children.get(name) ?? [];
130
+ for (const child of childNames(packages[key])) {
131
+ if (!list.includes(child)) list.push(child);
132
+ }
133
+ if (list.length) children.set(name, list);
134
+ }
135
+ return children;
136
+ };
137
+
138
+ const parentChain = (name, declared, children) => {
139
+ if (declared.has(name)) return '';
140
+ const prev = new Map();
141
+ const queue = [];
142
+ const roots = [...declared].sort();
143
+ for (const root of roots) {
144
+ if (prev.has(root)) continue;
145
+ prev.set(root, '');
146
+ queue.push(root);
147
+ }
148
+ let head = 0;
149
+ while (head < queue.length) {
150
+ const at = queue[head];
151
+ head += 1;
152
+ const kids = children.get(at);
153
+ if (!kids) continue;
154
+ for (const kid of kids) {
155
+ if (prev.has(kid)) continue;
156
+ prev.set(kid, at);
157
+ if (kid !== name) {
158
+ queue.push(kid);
159
+ continue;
160
+ }
161
+ const path = [];
162
+ let cursor = at;
163
+ while (cursor) {
164
+ path.push(cursor);
165
+ cursor = prev.get(cursor) || '';
166
+ }
167
+ return path.join(CHAIN_MARK);
168
+ }
169
+ }
170
+ return '';
171
+ };
172
+
173
+ const lockVersion = (lock, name) => {
174
+ const packages = lockPackages(lock);
175
+ const entry = packages && packages[`${MODULE_PREFIX}${name}`];
176
+ if (!entry || typeof entry.version !== 'string') return '';
177
+ return entry.version;
178
+ };
179
+
180
+ const topPackages = (ranked, pkg, lock) => {
181
+ const declared = directNames(pkg);
182
+ const devs = devNames(pkg);
183
+ const children = dependencyChildren(lock);
184
+ const rows = ranked.map((item) => {
185
+ const direct = declared.has(item.name);
186
+ return {
187
+ name: item.name,
188
+ bytes: item.bytes,
189
+ dev: direct && devs.has(item.name),
190
+ transitive: !direct,
191
+ chain: parentChain(item.name, declared, children),
192
+ version: lockVersion(lock, item.name),
193
+ };
194
+ });
195
+ const runtime = rows.filter((row) => !row.dev && !row.transitive);
196
+ const development = rows.filter((row) => row.dev);
197
+ const nested = rows.filter((row) => row.transitive);
198
+ return [...runtime, ...development, ...nested];
199
+ };
200
+
201
+ const measureModules = async (root) => {
202
+ let total = 0;
203
+ const totals = new Map();
204
+ const queue = [root];
205
+ const worker = async () => {
206
+ while (queue.length) {
207
+ const dir = queue.pop();
208
+ let entries;
209
+ try {
210
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
211
+ } catch {
212
+ continue;
213
+ }
214
+ const files = [];
215
+ for (const entry of entries) {
216
+ const full = path.join(dir, entry.name);
217
+ if (entry.isDirectory()) queue.push(full);
218
+ else if (entry.isFile()) files.push(full);
219
+ }
220
+ const sizes = await Promise.all(
221
+ files.map((file) =>
222
+ fs.promises.lstat(file).then(
223
+ (stat) => ({ name: packageName(root, file), bytes: stat.size }),
224
+ () => ({ name: '', bytes: 0 }),
225
+ ),
226
+ ),
227
+ );
228
+ for (const item of sizes) {
229
+ total += item.bytes;
230
+ if (!item.name || !item.bytes) continue;
231
+ totals.set(item.name, (totals.get(item.name) ?? 0) + item.bytes);
232
+ }
233
+ }
234
+ };
235
+ await Promise.all(Array.from({ length: WORKERS }, worker));
236
+ return { bytes: total, ranked: rankedPackages(totals) };
237
+ };
238
+
239
+ const installedCount = async (top, lock) => {
240
+ const packages = lockPackages(lock);
241
+ if (packages) {
242
+ let count = 0;
243
+ for (const key of Object.keys(packages)) {
244
+ if (key && !packages[key].link) count += 1;
245
+ }
246
+ return count;
247
+ }
248
+ let entries;
249
+ try {
250
+ entries = await fs.promises.readdir(path.join(top, MODULES));
251
+ } catch {
252
+ return 0;
253
+ }
254
+ let count = 0;
255
+ for (const name of entries) {
256
+ if (name.startsWith('.')) continue;
257
+ if (!name.startsWith('@')) {
258
+ count += 1;
259
+ continue;
260
+ }
261
+ const scoped = await fs.promises.readdir(path.join(top, MODULES, name));
262
+ count += scoped.length;
263
+ }
264
+ return count;
265
+ };
266
+
267
+ const readModules = async (top, lock, previous) => {
268
+ const stamp = await modulesStamp(top);
269
+ if (previous && stamp && previous.stamp === stamp) return previous;
270
+ const [count, measured] = await Promise.all([
271
+ installedCount(top, lock),
272
+ measureModules(path.join(top, MODULES)),
273
+ ]);
274
+ return { stamp, count, bytes: measured.bytes, ranked: measured.ranked };
275
+ };
276
+
277
+ const readNpmSummary = async (top, previous) => {
278
+ const pkg = await readJson(path.join(top, MANIFEST));
279
+ const lock = await readJson(path.join(top, MODULES_STAMP));
280
+ const cached = previous ? previous.modules : null;
281
+ const modules = await readModules(top, lock, cached);
282
+ return {
283
+ hasManifest: Boolean(pkg),
284
+ deps: declaredNames(pkg, 'dependencies').length,
285
+ dev: declaredNames(pkg, 'devDependencies').length,
286
+ modules: { ...modules, packages: topPackages(modules.ranked, pkg, lock) },
287
+ };
288
+ };
289
+
290
+ module.exports = { TOP_PACKAGES, readNpmSummary };
@@ -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 };