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.
- package/CHANGELOG.md +27 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +504 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +16 -0
- package/lib/agents/models.js +460 -0
- package/lib/api.js +15 -12
- package/lib/cli.js +61 -66
- package/lib/{files.js → common/files.js} +51 -52
- package/lib/common/find.js +61 -0
- package/lib/common/format.js +101 -0
- package/lib/{utilities.js → common/process.js} +7 -95
- package/lib/common/tasks.json +27 -0
- package/lib/common/utilities.js +112 -0
- package/lib/dashboard/git.js +9 -16
- package/lib/dashboard/model.js +117 -51
- package/lib/dashboard/npm.js +133 -32
- package/lib/dashboard/tree.js +27 -30
- package/lib/{session → dashboard}/watch.js +41 -31
- package/lib/dashboard/workspace.js +151 -0
- package/lib/{deps.js → deps/deps.js} +31 -45
- package/lib/{deps-diff.js → deps/diff.js} +35 -25
- package/lib/{manifest.js → deps/manifest.js} +47 -30
- package/lib/diff/align.js +1 -2
- package/lib/diff/diff.js +2 -2
- package/lib/diff/display.js +29 -5
- package/lib/diff/inline.js +10 -14
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +39 -3
- package/lib/diff/unit.js +45 -16
- package/lib/{git-branches.js → git/branches.js} +53 -48
- package/lib/{git-deps.js → git/deps.js} +123 -95
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +475 -0
- package/lib/highlight/bash.js +1 -2
- package/lib/highlight/csv.js +1 -2
- package/lib/highlight/dot.js +1 -2
- package/lib/{highlight.js → highlight/highlight.js} +42 -11
- package/lib/highlight/html.js +20 -18
- package/lib/highlight/json.js +1 -2
- package/lib/highlight/log.js +1 -2
- package/lib/highlight/txt.js +1 -2
- package/lib/{session → input}/actions.js +240 -71
- package/lib/{editor.js → input/editor.js} +7 -8
- package/lib/{keys.js → input/keys.js} +36 -39
- package/lib/input/line-edit.js +77 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +640 -0
- package/lib/render/branches.js +4 -5
- package/lib/render/commits.js +54 -64
- package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
- package/lib/render/dashboard/blocks.js +490 -0
- package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
- package/lib/render/diff.js +166 -45
- package/lib/render/field.js +131 -0
- package/lib/render/files.js +22 -8
- package/lib/render/footer.js +383 -0
- package/lib/render/frame.js +127 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +319 -0
- package/lib/render/menu.js +60 -0
- package/lib/render/notes.js +29 -13
- package/lib/render/npm.js +322 -196
- package/lib/render/packages.js +89 -0
- package/lib/render/primitives.js +109 -16
- package/lib/render/render.js +7 -11
- package/lib/render/repos.js +214 -0
- package/lib/render/status.js +370 -0
- package/lib/render/status.json +85 -0
- package/lib/render/{dash-table.js → table.js} +19 -61
- package/lib/render/tiles.js +203 -47
- package/lib/render/unit.js +32 -13
- package/lib/{report-model.js → report/model.js} +4 -6
- package/lib/{report-parse.js → report/parse.js} +9 -14
- package/lib/{report-render.js → report/render.js} +46 -14
- package/lib/{report-run.js → report/run.js} +42 -30
- package/lib/{report-test.js → report/test.js} +56 -55
- package/lib/{report-tools.js → report/tools.js} +24 -33
- package/lib/review/agent-instructions.md +9 -0
- package/lib/review/format.js +426 -0
- package/lib/{review-merge.js → review/merge.js} +9 -8
- package/lib/review/review.js +190 -0
- package/lib/review/store.js +281 -0
- package/lib/review/templates.js +83 -0
- package/lib/runs/commands.js +14 -0
- package/lib/runs/logs.js +299 -0
- package/lib/runs/output.js +174 -0
- package/lib/runs/process.js +113 -0
- package/lib/{runs.js → runs/runs.js} +84 -36
- package/lib/runs/scripts.js +150 -0
- package/lib/session/agents/agents.js +958 -0
- package/lib/session/agents/job.js +320 -0
- package/lib/session/agents/jobs.js +355 -0
- package/lib/session/agents/options.js +153 -0
- package/lib/session/agents/plans.js +83 -0
- package/lib/session/branches.js +36 -44
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +122 -56
- package/lib/session/compose.js +197 -146
- package/lib/session/dashboard.js +183 -37
- package/lib/session/dispatch.js +120 -22
- package/lib/session/files-pane.js +60 -24
- package/lib/session/find.js +81 -0
- package/lib/session/import.js +82 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +96 -66
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +8 -3
- package/lib/session/npm/history.js +249 -0
- package/lib/session/npm/npm.js +815 -0
- package/lib/session/npm/packages.js +150 -0
- package/lib/session/ops.js +125 -34
- package/lib/session/pointer.js +216 -10
- package/lib/session/prefs.js +112 -0
- package/lib/session/reads.js +109 -0
- package/lib/session/review.js +28 -10
- package/lib/session/run-panel.js +40 -0
- package/lib/{session.js → session/session.js} +166 -59
- package/lib/session/tasks.js +321 -66
- package/lib/session/templates.js +1 -1
- package/lib/session/terminal.js +94 -12
- package/lib/session/unit.js +35 -30
- package/lib/session/update.js +194 -6
- package/lib/session/view.js +72 -59
- package/lib/session/workspace.js +416 -0
- package/lib/{capabilities.js → source/capabilities.js} +3 -12
- package/lib/{github.js → source/github.js} +102 -32
- package/lib/{gitlab.js → source/gitlab.js} +87 -10
- package/lib/{remote.js → source/remote.js} +144 -40
- package/lib/source/review-threads.graphql +20 -0
- package/lib/source/source.js +74 -0
- package/lib/term/ansi.js +396 -0
- package/lib/{clipboard.js → term/clipboard.js} +3 -6
- package/lib/{select.js → term/select.js} +2 -2
- package/lib/term/themes/dark.json +122 -0
- package/lib/term/themes/light.json +122 -0
- package/lib/term/wide.txt +21 -0
- package/lib/{wrap.js → term/wrap.js} +4 -4
- package/package.json +2 -2
- package/lib/ansi.js +0 -505
- package/lib/detect.js +0 -34
- package/lib/diff/edit.js +0 -33
- package/lib/git-worktree.js +0 -437
- package/lib/git.js +0 -79
- package/lib/npm-commands.js +0 -522
- package/lib/render/chrome.js +0 -628
- package/lib/render/dash-blocks.js +0 -245
- package/lib/render/present.js +0 -19
- package/lib/render/report.js +0 -43
- package/lib/review.js +0 -744
- package/lib/session/accessors.js +0 -26
- package/lib/session/edit-click.js +0 -57
- package/lib/session/npm.js +0 -452
- package/lib/session/progress.js +0 -41
- package/lib/source.js +0 -36
- package/lib/update.js +0 -192
- /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
package/lib/dashboard/tree.js
CHANGED
|
@@ -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
|
|
7
|
-
const {
|
|
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 =
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
85
|
+
// unreadable directory
|
|
89
86
|
}
|
|
87
|
+
if (!entries) return found;
|
|
90
88
|
for (const entry of entries) {
|
|
91
|
-
if (SKIP_WALK.
|
|
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
|
|
102
|
+
const result = await runGit(args, root);
|
|
105
103
|
if (result.status === 0) {
|
|
106
|
-
return result.stdout.split('\0').filter(Boolean).map(
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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',
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
if (
|
|
31
|
-
|
|
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((
|
|
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 =
|
|
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
|
-
|
|
64
|
-
|
|
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 =
|
|
71
|
-
return rel ===
|
|
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,
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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 ===
|
|
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(
|
|
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
|
|
6
|
-
const {
|
|
7
|
-
const
|
|
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('./
|
|
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',
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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,
|
|
456
|
-
const unusedNames = unusedChangeNames(summary.changes,
|
|
457
|
-
const exportEntries = loadExportEntries(unusedNames,
|
|
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,
|
|
461
|
-
changes = markChanges(changes,
|
|
462
|
-
changes = markChanges(changes,
|
|
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);
|