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.
Files changed (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { split, jsonParse } = require('metautil');
6
+
7
+ const parseVersion = (text) => {
8
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(`${text ?? ''}`);
9
+ if (!match) return null;
10
+ const major = Number(match[1]);
11
+ const minor = Number(match[2]);
12
+ const patch = Number(match[3]);
13
+ return { major, minor, patch };
14
+ };
15
+
16
+ const cmpVersion = (left, right) => {
17
+ const major = left.major - right.major;
18
+ const minor = left.minor - right.minor;
19
+ const patch = left.patch - right.patch;
20
+ return major || minor || patch;
21
+ };
22
+
23
+ const toInt = (text) => parseInt(text, 10);
24
+
25
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
26
+
27
+ const resource = (dir, name) => fs.readFileSync(path.join(dir, name), 'utf8');
28
+
29
+ const readText = (file) => {
30
+ try {
31
+ return fs.readFileSync(file, 'utf8');
32
+ } catch {
33
+ return '';
34
+ }
35
+ };
36
+
37
+ const readJson = (file) => jsonParse(readText(file));
38
+
39
+ const writeJson = (file, data) => {
40
+ fs.mkdirSync(path.dirname(file), { recursive: true });
41
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
42
+ };
43
+
44
+ const listDir = (dir) => {
45
+ try {
46
+ return fs.readdirSync(dir);
47
+ } catch {
48
+ return [];
49
+ }
50
+ };
51
+
52
+ const errorMessage = (reason) => {
53
+ if (reason instanceof Error) return reason.message;
54
+ return `${reason ?? ''}`;
55
+ };
56
+
57
+ const trimText = (value) => `${value ?? ''}`.trim();
58
+
59
+ const oneLine = (text, fallback = '') => {
60
+ const body = `${text ?? ''}`.trim();
61
+ const first = split(body, '\n')[0];
62
+ return first || fallback;
63
+ };
64
+
65
+ const withNewline = (text) => {
66
+ const body = `${text ?? ''}`;
67
+ return !body || body.endsWith('\n') ? body : `${body}\n`;
68
+ };
69
+
70
+ const withNotice = (text, notice) => {
71
+ const body = `${text ?? ''}`;
72
+ if (body.endsWith(`${notice}\n`)) return body;
73
+ return `${withNewline(body)}${notice}\n`;
74
+ };
75
+
76
+ const asText = (value) => (typeof value === 'string' ? value : '');
77
+
78
+ const unquote = (value) => {
79
+ const text = `${value ?? ''}`;
80
+ if (text.length < 2) return text;
81
+ const open = text[0];
82
+ const quoted = open === text.at(-1) && (open === '"' || open === '\x27');
83
+ return quoted ? text.slice(1, -1) : text;
84
+ };
85
+
86
+ const pad2 = (n) => `${n}`.padStart(2, '0');
87
+
88
+ const dateStamp = (date) => {
89
+ const month = pad2(date.getMonth() + 1);
90
+ return `${date.getFullYear()}-${month}-${pad2(date.getDate())}`;
91
+ };
92
+
93
+ module.exports = {
94
+ parseVersion,
95
+ cmpVersion,
96
+ toInt,
97
+ clamp,
98
+ resource,
99
+ readText,
100
+ readJson,
101
+ writeJson,
102
+ listDir,
103
+ errorMessage,
104
+ trimText,
105
+ oneLine,
106
+ asText,
107
+ withNewline,
108
+ withNotice,
109
+ unquote,
110
+ pad2,
111
+ dateStamp,
112
+ };
@@ -2,9 +2,9 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { readText } = require('../common/utilities.js');
5
6
 
6
- const worktree = require('../git-worktree.js');
7
- const { runGitAsync } = worktree;
7
+ const { runGit } = require('../git/worktree.js');
8
8
 
9
9
  const UNIT = '\x1f';
10
10
  const RECENT_COUNT = 8;
@@ -29,7 +29,7 @@ const TRACK_BEHIND = /behind (\d+)/;
29
29
 
30
30
  const gitText = async (top, args) => {
31
31
  try {
32
- const result = await runGitAsync(args, top);
32
+ const result = await runGit(args, top);
33
33
  return result.status === 0 ? result.stdout.trim() : '';
34
34
  } catch {
35
35
  return '';
@@ -49,11 +49,10 @@ const parseTrack = (text) => {
49
49
  const raw = `${text ?? ''}`;
50
50
  const ahead = TRACK_AHEAD.exec(raw);
51
51
  const behind = TRACK_BEHIND.exec(raw);
52
- return {
53
- ahead: ahead ? Number(ahead[1]) : 0,
54
- behind: behind ? Number(behind[1]) : 0,
55
- gone: raw.includes('gone'),
56
- };
52
+ const aheadCount = ahead ? Number(ahead[1]) : 0;
53
+ const behindCount = behind ? Number(behind[1]) : 0;
54
+ const gone = raw.includes('gone');
55
+ return { ahead: aheadCount, behind: behindCount, gone };
57
56
  };
58
57
 
59
58
  const parseBranch = (line) => {
@@ -82,13 +81,7 @@ const parseCommit = (line) => {
82
81
  };
83
82
  };
84
83
 
85
- const readNumber = (file) => {
86
- try {
87
- return countOf(fs.readFileSync(file, 'utf8'));
88
- } catch {
89
- return 0;
90
- }
91
- };
84
+ const readNumber = (file) => countOf(readText(file));
92
85
 
93
86
  const readStep = (dir, step, total) => ({
94
87
  step: readNumber(path.join(dir, step)),
@@ -125,4 +118,4 @@ const readGitSummary = async (top) => {
125
118
  };
126
119
  };
127
120
 
128
- module.exports = { readGitSummary, parseTrack };
121
+ module.exports = { gitText, countOf, readGitSummary, parseTrack };
@@ -1,58 +1,33 @@
1
1
  'use strict';
2
2
 
3
- const tree = require('./tree.js');
4
- const { folderOf, extOf } = tree;
5
- const files = require('../files.js');
6
- const { TASK_KINDS, taskKind } = files;
3
+ const path = require('node:path');
4
+ const { trimText } = require('../common/utilities.js');
5
+ const { ageSeconds } = require('../common/format.js');
6
+
7
+ const { folderOf, extOf } = require('./tree.js');
8
+ const { TASK_KINDS, taskKind } = require('../common/files.js');
7
9
 
8
10
  const MINUTE_MS = 60_000;
9
11
  const RUNS_SHOWN = 6;
10
12
  const CHAIN_GAP_MS = 2000;
11
13
  const NPM_NAMED = /^npm (?:run|exec)(?:\s+-s)?\s+(\S+)/;
12
14
  const NPM_ALIAS = /^npm (test|start|stop|restart)(?:\s|$)/;
13
- const AGE_TEXT = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
14
- const AGE_SEC = {
15
- second: 1,
16
- minute: 60,
17
- hour: 3600,
18
- day: 86400,
19
- week: 604800,
20
- month: 2592000,
21
- year: 31536000,
22
- };
23
15
  const STATUS_RANK = { running: 3, failed: 2, aborted: 1, passed: 0 };
24
16
 
25
17
  const namedScript = (command) => {
26
- const text = `${command ?? ''}`.trim();
18
+ const text = trimText(command);
27
19
  const named = NPM_NAMED.exec(text);
28
20
  if (named) return named[1];
29
21
  const alias = NPM_ALIAS.exec(text);
30
22
  return alias ? alias[1] : '';
31
23
  };
32
24
 
33
- const scriptName = (command) => {
34
- const named = namedScript(command);
35
- if (named) return named;
36
- const text = `${command ?? ''}`.trim();
37
- const token = text.split(/\s+/)[0];
38
- const slash = Math.max(token.lastIndexOf('/'), token.lastIndexOf('\\'));
39
- return token.slice(slash + 1) || text;
40
- };
41
-
42
25
  const runName = (record) => {
43
- const command = `${record.command ?? ''}`.trim();
44
- const script = `${record.script ?? ''}`.trim();
26
+ const command = trimText(record.command);
27
+ const script = trimText(record.script);
45
28
  return namedScript(command) || script || command;
46
29
  };
47
30
 
48
- const ageSeconds = (text) => {
49
- const raw = `${text ?? ''}`.trim();
50
- if (/^yesterday$/i.test(raw)) return AGE_SEC.day;
51
- const match = AGE_TEXT.exec(raw);
52
- if (!match) return Number.POSITIVE_INFINITY;
53
- return Number(match[1]) * AGE_SEC[match[2].toLowerCase()];
54
- };
55
-
56
31
  const newerDate = (current, next) => {
57
32
  if (!next) return current;
58
33
  if (!current || ageSeconds(next) < ageSeconds(current)) return next;
@@ -130,17 +105,32 @@ const externalRun = (record) => {
130
105
  };
131
106
  };
132
107
 
108
+ const sessionName = (run) => {
109
+ const named = namedScript(run.label);
110
+ if (named) return named;
111
+ const script = trimText(run.name);
112
+ if (script) return script;
113
+ return trimText(run.command ?? run.label);
114
+ };
115
+
116
+ const progressOf = (run) => {
117
+ const progress = run.progress ?? {};
118
+ return {
119
+ done: progress.done ?? 0,
120
+ failed: progress.failed ?? 0,
121
+ expected: progress.expected ?? 0,
122
+ };
123
+ };
124
+
133
125
  const internalRun = (run) => ({
134
126
  source: 'reslop',
135
- name: scriptName(run.label),
127
+ name: sessionName(run),
136
128
  status: npmRunStatus(run),
137
129
  exit: run.exit,
138
130
  startedAt: run.startedAt,
139
131
  endedAt: run.endedAt,
140
- done: 0,
141
- failed: 0,
142
- expected: 0,
143
- result: null,
132
+ ...progressOf(run),
133
+ result: run.result ?? null,
144
134
  });
145
135
 
146
136
  const rankOf = (status) => STATUS_RANK[status] ?? 0;
@@ -175,6 +165,26 @@ const continuesRun = (prev, next) => {
175
165
  return next.startedAt <= prev.endedAt + CHAIN_GAP_MS;
176
166
  };
177
167
 
168
+ const chainedStep = (prev, next) => {
169
+ if (prev.name !== next.name || prev.command === next.command) return false;
170
+ const end = Number(prev.endedAt) || 0;
171
+ const at = Number(next.startedAt) || 0;
172
+ return end > 0 && at >= end && at <= end + CHAIN_GAP_MS;
173
+ };
174
+
175
+ const foldSteps = (runs) => {
176
+ const ordered = [...runs].sort(
177
+ (left, right) => (left.startedAt || 0) - (right.startedAt || 0),
178
+ );
179
+ const groups = [];
180
+ for (const run of ordered) {
181
+ const prev = groups.at(-1);
182
+ if (prev && chainedStep(prev, run)) groups[groups.length - 1] = run;
183
+ else groups.push(run);
184
+ }
185
+ return groups.reverse();
186
+ };
187
+
178
188
  const absorbRun = (prev, next) => {
179
189
  const open = isOpen(prev) || isOpen(next);
180
190
  if (rankOf(next.status) >= rankOf(prev.status)) {
@@ -201,25 +211,55 @@ const collapseRuns = (list) => {
201
211
  return groups.sort((left, right) => right.startedAt - left.startedAt);
202
212
  };
203
213
 
204
- const mergeRuns = (records, npmRun) => {
205
- const list = records.map(externalRun);
206
- if (npmRun) list.push(internalRun(npmRun));
207
- return collapseRuns(list).slice(0, RUNS_SHOWN);
214
+ const overlaps = (row, run) => {
215
+ if (row.name !== sessionName(run)) return false;
216
+ const start = Number(run.startedAt) || 0;
217
+ const end = Number(run.endedAt) || start;
218
+ const at = Number(row.startedAt) || 0;
219
+ const low = start - CHAIN_GAP_MS;
220
+ const high = Math.max(start, end) + CHAIN_GAP_MS;
221
+ return at >= low && at <= high;
222
+ };
223
+
224
+ const applySession = (row, run) => {
225
+ if (!run.running) return;
226
+ const next = internalRun(run);
227
+ row.status = next.status;
228
+ row.exit = next.exit;
229
+ row.done = next.done;
230
+ row.failed = next.failed;
231
+ row.expected = Math.max(row.expected, next.expected);
232
+ row.endedAt = 0;
233
+ if (next.result) row.result = next.result;
234
+ };
235
+
236
+ const mergeRuns = (records, npmRuns) => {
237
+ const sessions = Array.isArray(npmRuns) ? npmRuns : [];
238
+ const list = collapseRuns(records.map(externalRun));
239
+ for (const run of sessions) {
240
+ const row = list.find((item) => overlaps(item, run));
241
+ if (row) applySession(row, run);
242
+ else list.push(internalRun(run));
243
+ }
244
+ list.sort((left, right) => right.startedAt - left.startedAt);
245
+ return list.slice(0, RUNS_SHOWN);
208
246
  };
209
247
 
210
248
  const taskKindCounts = (store) => {
211
249
  const kinds = TASK_KINDS.map((kind) => ({
212
250
  id: kind.id,
213
- title: kind.title,
251
+ title: kind.short,
214
252
  done: 0,
215
253
  total: 0,
216
254
  }));
217
255
  const byId = new Map(kinds.map((row) => [row.id, row]));
218
256
  let done = 0;
219
257
  let total = 0;
220
- if (!store) return { kinds, done, total };
258
+ const file = store && store.reviewPath ? store.reviewPath : '';
259
+ const plan = file ? path.basename(file) : '';
260
+ if (!store) return { kinds, done, total, plan };
221
261
  for (const task of store.tasks) {
222
- if (!`${task.text ?? ''}`.trim()) continue;
262
+ if (!trimText(task.text)) continue;
223
263
  const row = byId.get(taskKind(task));
224
264
  row.total += 1;
225
265
  total += 1;
@@ -227,7 +267,7 @@ const taskKindCounts = (store) => {
227
267
  row.done += 1;
228
268
  done += 1;
229
269
  }
230
- return { kinds, done, total };
270
+ return { kinds, done, total, plan };
231
271
  };
232
272
 
233
273
  const versionFields = (pkg, outdatedMap) => {
@@ -341,16 +381,17 @@ const buildModel = (input) => ({
341
381
  commits: commitsModel(input),
342
382
  branches: branchesModel(input),
343
383
  npm: npmModel(input),
344
- runs: mergeRuns(input.runs, input.npmRun),
384
+ runs: mergeRuns(input.runs, input.npmRuns),
345
385
  tasks: tasksModel(input),
386
+ agents: input.agents ?? { ready: true, installed: 0, total: 0, items: [] },
346
387
  });
347
388
 
348
389
  module.exports = {
349
390
  MINUTE_MS,
350
391
  minuteOf,
351
392
  groupChanges,
393
+ foldSteps,
352
394
  mergeRuns,
353
- scriptName,
354
395
  runName,
355
396
  npmModel,
356
397
  buildModel,
@@ -2,11 +2,10 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { jsonParse } = require('metautil');
5
6
 
6
- const deps = require('../deps.js');
7
- const { MANIFEST } = deps;
8
- const watch = require('../session/watch.js');
9
- const { MODULES_STAMP } = watch;
7
+ const { MANIFEST } = require('../deps/deps.js');
8
+ const { MODULES_STAMP } = require('./watch.js');
10
9
 
11
10
  const MODULES = 'node_modules';
12
11
  const MODULE_PREFIX = `${MODULES}/`;
@@ -15,7 +14,8 @@ const TOP_PACKAGES = 8;
15
14
 
16
15
  const readJson = async (file) => {
17
16
  try {
18
- return JSON.parse(await fs.promises.readFile(file, 'utf8'));
17
+ const text = await fs.promises.readFile(file, 'utf8');
18
+ return jsonParse(text);
19
19
  } catch {
20
20
  return null;
21
21
  }
@@ -54,10 +54,12 @@ const packageName = (root, file) => {
54
54
  const rankedPackages = (totals) => {
55
55
  const list = [];
56
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
- );
57
+ const bySize = (left, right) => {
58
+ const byBytes = right.bytes - left.bytes;
59
+ const byName = left.name.localeCompare(right.name, 'en');
60
+ return byBytes || byName;
61
+ };
62
+ return list.sort(bySize);
61
63
  };
62
64
 
63
65
  const DIRECT_SECTIONS = [
@@ -205,12 +207,13 @@ const measureModules = async (root) => {
205
207
  const worker = async () => {
206
208
  while (queue.length) {
207
209
  const dir = queue.pop();
208
- let entries;
210
+ let entries = null;
209
211
  try {
210
212
  entries = await fs.promises.readdir(dir, { withFileTypes: true });
211
213
  } catch {
212
- continue;
214
+ // unreadable directory
213
215
  }
216
+ if (!entries) continue;
214
217
  const files = [];
215
218
  for (const entry of entries) {
216
219
  const full = path.join(dir, entry.name);
@@ -245,12 +248,13 @@ const installedCount = async (top, lock) => {
245
248
  }
246
249
  return count;
247
250
  }
248
- let entries;
251
+ let entries = null;
249
252
  try {
250
253
  entries = await fs.promises.readdir(path.join(top, MODULES));
251
254
  } catch {
252
- return 0;
255
+ // missing modules
253
256
  }
257
+ if (!entries) return 0;
254
258
  let count = 0;
255
259
  for (const name of entries) {
256
260
  if (name.startsWith('.')) continue;
@@ -2,11 +2,10 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { posixRel } = require('../common/files.js');
5
6
 
6
- const git = require('../git-worktree.js');
7
- const { runGitAsync } = git;
8
- const watch = require('../session/watch.js');
9
- const { UNKNOWN_PATH } = watch;
7
+ const { runGit } = require('../git/worktree.js');
8
+ const { UNKNOWN_PATH } = require('./watch.js');
10
9
 
11
10
  const ROOT_DIR = '.';
12
11
  const NO_EXT = '(none)';
@@ -15,11 +14,9 @@ const BINARY_PROBE = 8000;
15
14
  const CONCURRENCY = 32;
16
15
  const HEAT_MS = 4000;
17
16
  const RECENT_KEEP = 64;
18
- const SKIP_WALK = new Set(['.git', 'node_modules']);
17
+ const SKIP_WALK = ['.git', 'node_modules'];
19
18
  const SKIP_PREFIX = ['.git/', 'node_modules/'];
20
19
 
21
- const slash = (rel) => `${rel}`.replaceAll('\\', '/');
22
-
23
20
  const folderOf = (rel) => {
24
21
  const at = rel.indexOf('/');
25
22
  return at < 0 ? ROOT_DIR : rel.slice(0, at);
@@ -79,16 +76,17 @@ const readEntry = async (root, rel, prev, at) => {
79
76
 
80
77
  const walkFiles = async (root, dir = '') => {
81
78
  const found = [];
82
- let entries;
79
+ let entries = null;
83
80
  try {
84
- entries = await fs.promises.readdir(path.join(root, dir), {
85
- withFileTypes: true,
86
- });
81
+ const dirPath = path.join(root, dir);
82
+ const types = { withFileTypes: true };
83
+ entries = await fs.promises.readdir(dirPath, types);
87
84
  } catch {
88
- return found;
85
+ // unreadable directory
89
86
  }
87
+ if (!entries) return found;
90
88
  for (const entry of entries) {
91
- if (SKIP_WALK.has(entry.name)) continue;
89
+ if (SKIP_WALK.includes(entry.name)) continue;
92
90
  const rel = dir ? `${dir}/${entry.name}` : entry.name;
93
91
  if (entry.isFile()) found.push(rel);
94
92
  if (!entry.isDirectory()) continue;
@@ -101,9 +99,9 @@ const walkFiles = async (root, dir = '') => {
101
99
  const listProject = async (root) => {
102
100
  const args = ['ls-files', '-z', '--cached', '--others', '--exclude-standard'];
103
101
  try {
104
- const result = await runGitAsync(args, root);
102
+ const result = await runGit(args, root);
105
103
  if (result.status === 0) {
106
- return result.stdout.split('\0').filter(Boolean).map(slash);
104
+ return result.stdout.split('\0').filter(Boolean).map(posixRel);
107
105
  }
108
106
  } catch {
109
107
  // not a git repository: walk the tree
@@ -115,9 +113,9 @@ const ignoredAmong = async (root, rels) => {
115
113
  if (!rels.length) return new Set();
116
114
  try {
117
115
  const args = ['check-ignore', '-z', '--', ...rels];
118
- const result = await runGitAsync(args, root);
116
+ const result = await runGit(args, root);
119
117
  if (result.status !== 0 && result.status !== 1) return new Set();
120
- return new Set(result.stdout.split('\0').filter(Boolean).map(slash));
118
+ return new Set(result.stdout.split('\0').filter(Boolean).map(posixRel));
121
119
  } catch {
122
120
  return new Set();
123
121
  }
@@ -132,8 +130,11 @@ const groupOf = (map, key) => {
132
130
  return group;
133
131
  };
134
132
 
135
- const bySize = (left, right) =>
136
- right.bytes - left.bytes || left.key.localeCompare(right.key, 'en');
133
+ const bySize = (left, right) => {
134
+ const byBytes = right.bytes - left.bytes;
135
+ const byKey = left.key.localeCompare(right.key, 'en');
136
+ return byBytes || byKey;
137
+ };
137
138
 
138
139
  class FileIndex {
139
140
  constructor(root) {
@@ -209,7 +210,7 @@ class FileIndex {
209
210
  async touch(paths) {
210
211
  const rels = [];
211
212
  for (const item of paths) {
212
- const rel = slash(item);
213
+ const rel = posixRel(item);
213
214
  if (rel === UNKNOWN_PATH) return this.rescan();
214
215
  if (SKIP_PREFIX.some((prefix) => rel.startsWith(prefix))) continue;
215
216
  rels.push(rel);
@@ -239,22 +240,18 @@ class FileIndex {
239
240
  const dirs = new Map();
240
241
  const exts = new Map();
241
242
  for (const entry of this.entries.values()) {
242
- const groups = [
243
- total,
244
- groupOf(dirs, entry.dir),
245
- groupOf(exts, entry.ext),
246
- ];
243
+ const byDir = groupOf(dirs, entry.dir);
244
+ const byExt = groupOf(exts, entry.ext);
245
+ const groups = [total, byDir, byExt];
247
246
  for (const group of groups) {
248
247
  group.files += 1;
249
248
  group.bytes += entry.size;
250
249
  group.lines += entry.lines;
251
250
  }
252
251
  }
253
- this.cached = {
254
- total,
255
- dirs: [...dirs.values()].sort(bySize),
256
- exts: [...exts.values()].sort(bySize),
257
- };
252
+ const dirList = [...dirs.values()].sort(bySize);
253
+ const extList = [...exts.values()].sort(bySize);
254
+ this.cached = { total, dirs: dirList, exts: extList };
258
255
  return this.cached;
259
256
  }
260
257