reslop 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  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 +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  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} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -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 +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -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/dashboard/blocks.js +490 -0
  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 +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  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} +19 -61
  72. package/lib/render/tiles.js +203 -47
  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} +46 -14
  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 +9 -0
  81. package/lib/review/format.js +426 -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 +958 -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 +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -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} +166 -59
  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 +72 -59
  126. package/lib/session/workspace.js +416 -0
  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} +2 -2
  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 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  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
@@ -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
 
@@ -3,14 +3,15 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const runs = require('../runs.js');
6
+ const runs = require('../runs/runs.js');
7
7
  const { isRunsRel } = runs;
8
+ const { REVIEW_DIR, posixRel } = require('../common/files.js');
8
9
 
9
10
  const DEBOUNCE_MS = 200;
10
11
  const UNKNOWN_PATH = '.';
11
12
  const MODULES_STAMP = 'node_modules/.package-lock.json';
12
13
 
13
- const SKIP_DIRS = ['node_modules', '.review', 'coverage', 'dist', '.cache'];
14
+ const SKIP_DIRS = ['node_modules', REVIEW_DIR, 'coverage', 'dist', '.cache'];
14
15
 
15
16
  const GIT_WATCH_NAMES = [
16
17
  'HEAD',
@@ -23,32 +24,31 @@ const GIT_WATCH_NAMES = [
23
24
  'rebase-apply',
24
25
  ];
25
26
 
26
- const canWatchRecursive = (
27
- platform = process.platform,
28
- version = process.versions.node,
29
- ) => {
30
- if (platform === 'win32' || platform === 'darwin') return true;
31
- if (platform !== 'linux') return false;
32
- const [major, minor] = `${version}`.split('.').map(Number);
27
+ const canWatchRecursive = (platform, version) => {
28
+ const host = platform ?? process.platform;
29
+ const node = version ?? process.versions.node;
30
+ if (host === 'win32' || host === 'darwin') return true;
31
+ if (host !== 'linux') return false;
32
+ const [major, minor] = `${node}`.split('.').map(Number);
33
33
  return major > 19 || (major === 19 && minor >= 1);
34
34
  };
35
35
 
36
36
  const IGNORED_SUFFIXES = ['.lock', '~', '.swp', '.swo'];
37
37
 
38
- const ignoredName = (name) =>
39
- IGNORED_SUFFIXES.some((suffix) => name.endsWith(suffix)) ||
40
- name.startsWith('.#') ||
41
- name === '.DS_Store';
38
+ const ignoredName = (name) => {
39
+ const suffix = IGNORED_SUFFIXES.some((item) => name.endsWith(item));
40
+ const backup = name.startsWith('.#');
41
+ const store = name === '.DS_Store';
42
+ return suffix || backup || store;
43
+ };
42
44
 
43
45
  const ignoredGitRel = ([, name = '', sub]) => {
44
46
  if (name === 'logs' && sub === 'HEAD') return false;
45
47
  return name !== '' && !GIT_WATCH_NAMES.includes(name);
46
48
  };
47
49
 
48
- const slashRel = (rel) => `${rel}`.replaceAll('\\', '/');
49
-
50
50
  const ignoredRel = (rel) => {
51
- const norm = slashRel(rel);
51
+ const norm = posixRel(rel);
52
52
  if (!norm || norm === '.') return false;
53
53
  if (norm === MODULES_STAMP) return false;
54
54
  const parts = norm.split('/');
@@ -60,15 +60,16 @@ const ignoredRel = (rel) => {
60
60
  const skipDirName = (name) => name === '.git' || SKIP_DIRS.includes(name);
61
61
 
62
62
  const reviewMarkdown = (rel) => {
63
- if (!rel.startsWith('.review/')) return false;
64
- const rest = rel.slice('.review/'.length);
63
+ const prefix = `${REVIEW_DIR}/`;
64
+ if (!rel.startsWith(prefix)) return false;
65
+ const rest = rel.slice(prefix.length);
65
66
  if (!rest || rest.includes('/')) return false;
66
67
  return rest.endsWith('.md');
67
68
  };
68
69
 
69
70
  const isReviewDir = (root, dir) => {
70
- const rel = slashRel(path.relative(root, dir));
71
- return rel === '.review' || rel.startsWith('.review/');
71
+ const rel = posixRel(path.relative(root, dir));
72
+ return rel === REVIEW_DIR || rel.startsWith(`${REVIEW_DIR}/`);
72
73
  };
73
74
 
74
75
  const hasFile = (full) => {
@@ -84,13 +85,13 @@ const isBareReviewName = (root, dir, rel) => {
84
85
  if (!rel || rel.includes('/')) return false;
85
86
  const base = path.basename(rel);
86
87
  if (hasFile(path.join(dir, base))) return false;
87
- return hasFile(path.join(root, '.review', base));
88
+ return hasFile(path.join(root, REVIEW_DIR, base));
88
89
  };
89
90
 
90
91
  const isReviewEvent = (root, dir, name) => {
91
92
  if (isReviewDir(root, dir)) return true;
92
93
  if (!name) return false;
93
- const rel = slashRel(path.relative(root, path.join(dir, name)));
94
+ const rel = posixRel(path.relative(root, path.join(dir, name)));
94
95
  if (reviewMarkdown(rel)) return true;
95
96
  return isBareReviewName(root, dir, rel);
96
97
  };
@@ -98,7 +99,7 @@ const isReviewEvent = (root, dir, name) => {
98
99
  const isOwnDirEvent = (dir, name) => {
99
100
  if (!name) return false;
100
101
  const base = path.basename(dir);
101
- const slash = slashRel(name);
102
+ const slash = posixRel(name);
102
103
  const bare = slash.startsWith('./') ? slash.slice(2) : slash;
103
104
  const trimmed = bare.endsWith('/') ? bare.slice(0, -1) : bare;
104
105
  if (trimmed === base) {
@@ -136,6 +137,7 @@ class DiskWatcher {
136
137
  this.onChange = options.onChange;
137
138
  this.onReview = options.onReview;
138
139
  this.onPaths = options.onPaths;
140
+ this.matchReview = options.matchReview ?? null;
139
141
  this.recursive = options.recursive ?? canWatchRecursive();
140
142
  this.watchers = [];
141
143
  this.watched = new Set();
@@ -152,8 +154,9 @@ class DiskWatcher {
152
154
  this.watchOne(gitDir);
153
155
  this.watchOne(path.join(gitDir, 'refs'));
154
156
  this.watchOne(path.join(gitDir, 'logs'));
157
+ this.watchOne(path.resolve(this.root, runs.logDir('.')));
155
158
  this.watchOne(path.resolve(this.root, runs.runsDir('.')));
156
- this.watchOne(path.resolve(this.root, '.review'));
159
+ this.watchOne(path.resolve(this.root, REVIEW_DIR));
157
160
  }
158
161
 
159
162
  schedule(key, fire) {
@@ -210,9 +213,11 @@ class DiskWatcher {
210
213
  onEvent(dir, eventType, filename) {
211
214
  if (this.closed) return;
212
215
  const name = filename ? `${filename}` : '';
213
- const rel = name
214
- ? slashRel(path.relative(this.root, path.join(dir, name)))
215
- : '';
216
+ let rel = '';
217
+ if (name) {
218
+ const full = path.join(dir, name);
219
+ rel = posixRel(path.relative(this.root, full));
220
+ }
216
221
  const wanted = this.files.has(rel);
217
222
  if (isRunsRel(rel)) {
218
223
  this.pending.add(rel);
@@ -227,6 +232,10 @@ class DiskWatcher {
227
232
  this.schedule('review', this.onReview);
228
233
  if (!wanted) return;
229
234
  }
235
+ if (rel && this.matchReview && this.matchReview(rel)) {
236
+ this.pending.add(rel);
237
+ return void this.schedule('paths', () => this.flushPaths());
238
+ }
230
239
  if (!name) return void this.scheduleUnnamed();
231
240
  if (!wanted && ignoredRel(rel)) return;
232
241
  if (isOwnDirEvent(dir, name)) return;
@@ -255,12 +264,13 @@ class DiskWatcher {
255
264
  watchTree(dir) {
256
265
  this.watchOne(dir, this.recursive);
257
266
  if (this.recursive) return;
258
- let entries;
267
+ let entries = null;
259
268
  try {
260
269
  entries = fs.readdirSync(dir, { withFileTypes: true });
261
270
  } catch {
262
- return;
271
+ // unreadable directory
263
272
  }
273
+ if (!entries) return;
264
274
  for (const entry of entries) {
265
275
  if (!entry.isDirectory() || skipDirName(entry.name)) continue;
266
276
  this.watchTree(path.join(dir, entry.name));
@@ -275,7 +285,7 @@ class DiskWatcher {
275
285
  return;
276
286
  }
277
287
  const name = path.basename(full);
278
- if (name === '.review') this.watchOne(full);
288
+ if (name === REVIEW_DIR) this.watchOne(full);
279
289
  else if (!skipDirName(name)) this.watchTree(full);
280
290
  }
281
291
 
@@ -294,7 +304,7 @@ class DiskWatcher {
294
304
  for (const rel of rels) {
295
305
  if (!rel) continue;
296
306
  const full = path.resolve(this.root, rel);
297
- this.files.add(slashRel(path.relative(this.root, full)));
307
+ this.files.add(posixRel(path.relative(this.root, full)));
298
308
  this.watchOne(path.dirname(full));
299
309
  }
300
310
  }
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { REVIEW_DIR } = require('../common/files.js');
7
+ const review = require('../review/review.js');
8
+ const { listReviewNames, latestReviewName, loadReview, noteCounts } = review;
9
+ const { countUntrackedAdded } = require('../git/worktree.js');
10
+ const { gitText, countOf } = require('./git.js');
11
+ const { runName } = require('./model.js');
12
+ const { readRuns } = require('../runs/runs.js');
13
+
14
+ const emptyDiff = () => ({
15
+ stagedAdded: 0,
16
+ unstagedAdded: 0,
17
+ stagedRemoved: 0,
18
+ unstagedRemoved: 0,
19
+ });
20
+
21
+ const isRepoDir = (dir) => {
22
+ try {
23
+ fs.accessSync(path.join(dir, '.git'));
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ };
29
+
30
+ const listChildRepos = (dir) => {
31
+ let entries = null;
32
+ try {
33
+ entries = fs.readdirSync(dir, { withFileTypes: true });
34
+ } catch {
35
+ // unreadable directory
36
+ }
37
+ if (!entries) return [];
38
+ const repos = [];
39
+ for (const entry of entries) {
40
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
41
+ const full = path.join(dir, entry.name);
42
+ if (!isRepoDir(full)) continue;
43
+ repos.push({ name: entry.name, dir: full });
44
+ }
45
+ repos.sort((left, right) => left.name.localeCompare(right.name, 'en'));
46
+ return repos;
47
+ };
48
+
49
+ const workspaceAt = (dir) => {
50
+ let stat = null;
51
+ try {
52
+ stat = fs.statSync(dir);
53
+ } catch {
54
+ // missing path
55
+ }
56
+ if (!stat) return null;
57
+ if (!stat.isDirectory() || isRepoDir(dir)) return null;
58
+ const repos = listChildRepos(dir);
59
+ if (!repos.length) return null;
60
+ return { root: path.resolve(dir), repos };
61
+ };
62
+
63
+ const workspaceFromArgs = (cwd, paths) => {
64
+ if (paths.length > 1) return null;
65
+ const dir = paths.length ? path.resolve(cwd, paths[0]) : cwd;
66
+ if (paths.length) {
67
+ try {
68
+ if (!fs.statSync(dir).isDirectory()) return null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ return workspaceAt(dir);
74
+ };
75
+
76
+ const sumNumstat = (text) => {
77
+ let added = 0;
78
+ let removed = 0;
79
+ for (const line of `${text ?? ''}`.split('\n')) {
80
+ if (!line) continue;
81
+ const parts = line.split('\t');
82
+ const add = Number(parts[0]);
83
+ const del = Number(parts[1]);
84
+ if (Number.isFinite(add)) added += add;
85
+ if (Number.isFinite(del)) removed += del;
86
+ }
87
+ return { added, removed };
88
+ };
89
+
90
+ const readNumstat = async (dir, cached) => {
91
+ const stagedArgs = ['diff', '--cached', '--numstat'];
92
+ const openArgs = ['diff', '--numstat'];
93
+ const args = cached ? stagedArgs : openArgs;
94
+ return sumNumstat(await gitText(dir, args));
95
+ };
96
+
97
+ const readDiffStat = async (dir) => {
98
+ const [staged, open, untracked] = await Promise.all([
99
+ readNumstat(dir, true),
100
+ readNumstat(dir, false),
101
+ countUntrackedAdded(dir),
102
+ ]);
103
+ return {
104
+ stagedAdded: staged.added,
105
+ unstagedAdded: open.added + untracked,
106
+ stagedRemoved: staged.removed,
107
+ unstagedRemoved: open.removed,
108
+ };
109
+ };
110
+
111
+ const countCommits = async (dir) =>
112
+ countOf(await gitText(dir, ['rev-list', '--count', 'HEAD']));
113
+
114
+ const taskStats = (dir) => {
115
+ const latest = latestReviewName(listReviewNames(dir));
116
+ if (!latest) return { done: 0, total: 0 };
117
+ try {
118
+ const file = path.join(dir, REVIEW_DIR, latest);
119
+ const counts = noteCounts(loadReview(file, []));
120
+ return { done: counts.tasksDone, total: counts.tasks };
121
+ } catch {
122
+ return { done: 0, total: 0 };
123
+ }
124
+ };
125
+
126
+ const pickRun = (dir) => {
127
+ const list = readRuns(dir);
128
+ const live = list.find((row) => row.status === 'running');
129
+ const record = live ?? list[0] ?? null;
130
+ if (!record) return null;
131
+ const progress = record.progress ?? {};
132
+ return {
133
+ name: runName(record),
134
+ status: record.status,
135
+ done: progress.done ?? 0,
136
+ failed: progress.failed ?? 0,
137
+ expected: progress.expected ?? 0,
138
+ result: record.result ?? null,
139
+ };
140
+ };
141
+
142
+ module.exports = {
143
+ emptyDiff,
144
+ workspaceAt,
145
+ workspaceFromArgs,
146
+ sumNumstat,
147
+ readDiffStat,
148
+ countCommits,
149
+ taskStats,
150
+ pickRun,
151
+ };
@@ -2,18 +2,15 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const metautil = require('metautil');
6
- const { jsonParse, isHashObject } = metautil;
7
- const files = require('./files.js');
8
- const { itemPath } = files;
9
- const utilities = require('./utilities.js');
10
- const { asText } = utilities;
5
+ const { jsonParse, isHashObject } = require('metautil');
6
+ const { itemPath, REVIEW_DIR } = require('../common/files.js');
7
+ const { asText, readText, readJson } = require('../common/utilities.js');
11
8
  const manifest = require('./manifest.js');
12
9
  const { MANIFEST, LOCKFILE, SECTION_KEYS, isObject } = manifest;
13
10
  const { depFileMeta, relOf, listFilePath, readSections } = manifest;
14
11
  const { lockEntries, findDeclared, applyWantedRange } = manifest;
15
12
  const { proposedLockVersion, proposedWanted } = manifest;
16
- const depDiff = require('./deps-diff.js');
13
+ const depDiff = require('./diff.js');
17
14
  const { summarizeGroup, collectGroups, takeManifestItems } = depDiff;
18
15
  const { foldedDepNames, stripFoldedDepItem } = depDiff;
19
16
  const { makeDepItems, stubFileItem } = depDiff;
@@ -26,17 +23,9 @@ const SPEC_PATTERNS = [
26
23
  /import\(\s*['"]([^'"]+)['"]\s*\)/g,
27
24
  ];
28
25
  const SCRIPT_RUNNERS = ['npx', 'npm', 'node'];
29
- const SKIP_WALK = ['node_modules', '.git', '.review', 'coverage', 'dist'];
26
+ const SKIP_WALK = ['node_modules', '.git', REVIEW_DIR, 'coverage', 'dist'];
30
27
  const SEVERITY_RANK = { critical: 4, high: 3, moderate: 2, low: 1, info: 0 };
31
28
 
32
- const readText = (file) => {
33
- try {
34
- return fs.readFileSync(file, 'utf8');
35
- } catch {
36
- return '';
37
- }
38
- };
39
-
40
29
  const toPackageName = (spec) => {
41
30
  if (!spec || spec.startsWith('.') || spec.startsWith('/')) return '';
42
31
  if (spec.startsWith('node:')) return '';
@@ -102,12 +91,13 @@ const pkgExportsLibrary = (pkg) => {
102
91
  };
103
92
 
104
93
  const walkUsedNames = (used, dir) => {
105
- let entries;
94
+ let entries = null;
106
95
  try {
107
96
  entries = fs.readdirSync(dir, { withFileTypes: true });
108
97
  } catch {
109
- return;
98
+ // unreadable directory
110
99
  }
100
+ if (!entries) return;
111
101
  for (const entry of entries) {
112
102
  if (entry.isSymbolicLink()) continue;
113
103
  const full = path.join(dir, entry.name);
@@ -115,7 +105,7 @@ const walkUsedNames = (used, dir) => {
115
105
  const skip = SKIP_WALK.includes(entry.name) || entry.name.startsWith('.');
116
106
  if (!skip) walkUsedNames(used, full);
117
107
  } else if (entry.name === MANIFEST) {
118
- collectUsedFromScripts(used, jsonParse(readText(full))?.scripts);
108
+ collectUsedFromScripts(used, readJson(full)?.scripts);
119
109
  } else if (SOURCE_EXT.includes(path.extname(entry.name))) {
120
110
  collectUsedFromText(used, readText(full));
121
111
  }
@@ -160,7 +150,7 @@ const collectExportEntries = (root, pkgDir, names) => {
160
150
  };
161
151
 
162
152
  const collectExportMeta = (root) => {
163
- const pkg = jsonParse(readText(path.join(root, MANIFEST)));
153
+ const pkg = readJson(path.join(root, MANIFEST));
164
154
  const maps = readSections(pkg);
165
155
  const names = SECTION_KEYS.flatMap((key) => [...maps[key].keys()]);
166
156
  return collectExportEntries(root, '.', names);
@@ -288,8 +278,11 @@ const markChanges = (changes, report, kind) => {
288
278
 
289
279
  const depExports = (exportEntries, name) => exportEntries?.get(name) === true;
290
280
 
291
- const isAddedDep = (change) =>
292
- change.action === 'added' && change.section !== 'resolved';
281
+ const isAddedDep = (change) => {
282
+ const added = change.action === 'added';
283
+ const declared = change.section !== 'resolved';
284
+ return added && declared;
285
+ };
293
286
 
294
287
  const unusedChangeNames = (changes, used) => {
295
288
  if (!used || !used.size) return [];
@@ -330,15 +323,10 @@ const unusedDeclaredChanges = (pkg, used, exportEntries) => {
330
323
  taken.add(name);
331
324
  const action = 'removed';
332
325
  const to = '';
333
- changes.push({
334
- name,
335
- section,
336
- action,
337
- from,
338
- to,
339
- propose: true,
340
- unused: true,
341
- });
326
+ const propose = true;
327
+ const unused = true;
328
+ const change = { name, section, action, from, to, propose, unused };
329
+ changes.push(change);
342
330
  }
343
331
  return changes;
344
332
  };
@@ -438,28 +426,26 @@ const placeFolded = (items, { folded, synthetic, leftoverNames }) => {
438
426
  return spliceAtSlots(out, synthetic, slot);
439
427
  };
440
428
 
441
- const foldDepItems = (
442
- items,
443
- readSides = () => ({ oldText: '', newText: '' }),
444
- usedNames = null,
445
- audit = null,
446
- outdated = null,
447
- extra = {},
448
- ) => {
429
+ const foldDepItems = (items, readSides, usedNames, audit, outdated, extra) => {
430
+ const read = readSides ?? (() => ({ oldText: '', newText: '' }));
431
+ const used = usedNames ?? null;
432
+ const auditMap = audit ?? null;
433
+ const outdatedMap = outdated ?? null;
434
+ const options = extra ?? {};
449
435
  const groups = collectGroups(items);
450
436
  if (!groups.size) return items;
451
437
  const folded = new Set();
452
438
  const synthetic = new Map();
453
439
  const leftoverNames = new Map();
454
440
  for (const [key, group] of groups) {
455
- const summary = summarizeGroup(group, readSides);
456
- const unusedNames = unusedChangeNames(summary.changes, usedNames);
457
- const exportEntries = loadExportEntries(unusedNames, extra, group.dir);
441
+ const summary = summarizeGroup(group, read);
442
+ const unusedNames = unusedChangeNames(summary.changes, used);
443
+ const exportEntries = loadExportEntries(unusedNames, options, group.dir);
458
444
  const { pkg } = summary;
459
445
  let changes = summary.changes;
460
- changes = markUnusedChanges(changes, usedNames, pkg, exportEntries);
461
- changes = markChanges(changes, audit, 'audit');
462
- changes = markChanges(changes, outdated, 'outdated');
446
+ changes = markUnusedChanges(changes, used, pkg, exportEntries);
447
+ changes = markChanges(changes, auditMap, 'audit');
448
+ changes = markChanges(changes, outdatedMap, 'outdated');
463
449
  const next = { ...summary, changes };
464
450
  leftoverNames.set(key, foldedDepNames(changes));
465
451
  const taken = takeManifestItems(next);