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
@@ -0,0 +1,27 @@
1
+ {
2
+ "label": "Project Tasks: Feature requests, Improvements, Bug reports, Refactoring, Research, Security",
3
+ "kinds": [
4
+ {
5
+ "id": "features",
6
+ "title": "Feature requests and Enhancements",
7
+ "short": "Features"
8
+ },
9
+ { "id": "improvements", "title": "Improvements", "short": "Improvements" },
10
+ { "id": "bugs", "title": "Bug Reports and Fixes", "short": "Bug Reports" },
11
+ {
12
+ "id": "debt",
13
+ "title": "Refactoring and Technical debt",
14
+ "short": "Refactoring"
15
+ },
16
+ {
17
+ "id": "research",
18
+ "title": "Research and Experiments",
19
+ "short": "Research"
20
+ },
21
+ {
22
+ "id": "security",
23
+ "title": "Security and Compliance",
24
+ "short": "Security"
25
+ }
26
+ ]
27
+ }
@@ -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,57 +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');
7
6
 
7
+ const { folderOf, extOf } = require('./tree.js');
8
+ const { TASK_KINDS, taskKind } = require('../common/files.js');
9
+
10
+ const MINUTE_MS = 60_000;
8
11
  const RUNS_SHOWN = 6;
9
12
  const CHAIN_GAP_MS = 2000;
10
13
  const NPM_NAMED = /^npm (?:run|exec)(?:\s+-s)?\s+(\S+)/;
11
14
  const NPM_ALIAS = /^npm (test|start|stop|restart)(?:\s|$)/;
12
- const AGE_TEXT = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
13
- const AGE_SEC = {
14
- second: 1,
15
- minute: 60,
16
- hour: 3600,
17
- day: 86400,
18
- week: 604800,
19
- month: 2592000,
20
- year: 31536000,
21
- };
22
15
  const STATUS_RANK = { running: 3, failed: 2, aborted: 1, passed: 0 };
23
16
 
24
17
  const namedScript = (command) => {
25
- const text = `${command ?? ''}`.trim();
18
+ const text = trimText(command);
26
19
  const named = NPM_NAMED.exec(text);
27
20
  if (named) return named[1];
28
21
  const alias = NPM_ALIAS.exec(text);
29
22
  return alias ? alias[1] : '';
30
23
  };
31
24
 
32
- const scriptName = (command) => {
33
- const named = namedScript(command);
34
- if (named) return named;
35
- const text = `${command ?? ''}`.trim();
36
- const token = text.split(/\s+/)[0];
37
- const slash = Math.max(token.lastIndexOf('/'), token.lastIndexOf('\\'));
38
- return token.slice(slash + 1) || text;
39
- };
40
-
41
25
  const runName = (record) => {
42
- const command = `${record.command ?? ''}`.trim();
43
- const script = `${record.script ?? ''}`.trim();
26
+ const command = trimText(record.command);
27
+ const script = trimText(record.script);
44
28
  return namedScript(command) || script || command;
45
29
  };
46
30
 
47
- const ageSeconds = (text) => {
48
- const raw = `${text ?? ''}`.trim();
49
- if (/^yesterday$/i.test(raw)) return AGE_SEC.day;
50
- const match = AGE_TEXT.exec(raw);
51
- if (!match) return Number.POSITIVE_INFINITY;
52
- return Number(match[1]) * AGE_SEC[match[2].toLowerCase()];
53
- };
54
-
55
31
  const newerDate = (current, next) => {
56
32
  if (!next) return current;
57
33
  if (!current || ageSeconds(next) < ageSeconds(current)) return next;
@@ -129,17 +105,32 @@ const externalRun = (record) => {
129
105
  };
130
106
  };
131
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
+
132
125
  const internalRun = (run) => ({
133
126
  source: 'reslop',
134
- name: scriptName(run.label),
127
+ name: sessionName(run),
135
128
  status: npmRunStatus(run),
136
129
  exit: run.exit,
137
130
  startedAt: run.startedAt,
138
131
  endedAt: run.endedAt,
139
- done: 0,
140
- failed: 0,
141
- expected: 0,
142
- result: null,
132
+ ...progressOf(run),
133
+ result: run.result ?? null,
143
134
  });
144
135
 
145
136
  const rankOf = (status) => STATUS_RANK[status] ?? 0;
@@ -174,6 +165,26 @@ const continuesRun = (prev, next) => {
174
165
  return next.startedAt <= prev.endedAt + CHAIN_GAP_MS;
175
166
  };
176
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
+
177
188
  const absorbRun = (prev, next) => {
178
189
  const open = isOpen(prev) || isOpen(next);
179
190
  if (rankOf(next.status) >= rankOf(prev.status)) {
@@ -200,25 +211,55 @@ const collapseRuns = (list) => {
200
211
  return groups.sort((left, right) => right.startedAt - left.startedAt);
201
212
  };
202
213
 
203
- const mergeRuns = (records, npmRun) => {
204
- const list = records.map(externalRun);
205
- if (npmRun) list.push(internalRun(npmRun));
206
- 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);
207
246
  };
208
247
 
209
248
  const taskKindCounts = (store) => {
210
249
  const kinds = TASK_KINDS.map((kind) => ({
211
250
  id: kind.id,
212
- title: kind.title,
251
+ title: kind.short,
213
252
  done: 0,
214
253
  total: 0,
215
254
  }));
216
255
  const byId = new Map(kinds.map((row) => [row.id, row]));
217
256
  let done = 0;
218
257
  let total = 0;
219
- 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 };
220
261
  for (const task of store.tasks) {
221
- if (!`${task.text ?? ''}`.trim()) continue;
262
+ if (!trimText(task.text)) continue;
222
263
  const row = byId.get(taskKind(task));
223
264
  row.total += 1;
224
265
  total += 1;
@@ -226,7 +267,7 @@ const taskKindCounts = (store) => {
226
267
  row.done += 1;
227
268
  done += 1;
228
269
  }
229
- return { kinds, done, total };
270
+ return { kinds, done, total, plan };
230
271
  };
231
272
 
232
273
  const versionFields = (pkg, outdatedMap) => {
@@ -237,12 +278,25 @@ const versionFields = (pkg, outdatedMap) => {
237
278
  return { current, wanted: hit.wanted, latest: hit.latest };
238
279
  };
239
280
 
281
+ const auditFields = (pkg, auditMap) => {
282
+ const finding = auditMap ? auditMap.get(pkg.name) : null;
283
+ return {
284
+ audit: Boolean(finding),
285
+ fix: finding && finding.fix ? finding.fix : '',
286
+ range: finding && finding.range ? finding.range : '',
287
+ severity: finding && finding.severity ? finding.severity : '',
288
+ title: finding && finding.title ? finding.title : '',
289
+ };
290
+ };
291
+
240
292
  const modulesModel = (modules, auditMap, outdatedMap) => {
241
293
  const packages = modules.packages.map((pkg) => ({
242
294
  name: pkg.name,
243
295
  bytes: pkg.bytes,
244
296
  dev: pkg.dev,
245
- audit: Boolean(auditMap && auditMap.get(pkg.name)),
297
+ transitive: Boolean(pkg.transitive),
298
+ chain: pkg.chain || '',
299
+ ...auditFields(pkg, auditMap),
246
300
  ...versionFields(pkg, outdatedMap),
247
301
  }));
248
302
  return { ...modules, packages };
@@ -266,6 +320,8 @@ const npmModel = (input) => {
266
320
  };
267
321
  };
268
322
 
323
+ const minuteOf = (at) => Math.floor((at || 0) / MINUTE_MS);
324
+
269
325
  const filesModel = (input) => {
270
326
  const index = input.index;
271
327
  const ready = Boolean(index && index.ready);
@@ -288,7 +344,7 @@ const diffsModel = (input) => {
288
344
  dirs: groups.dirs,
289
345
  exts: groups.exts,
290
346
  files: input.entries.length,
291
- samples: input.samples,
347
+ activity: input.activity ?? { minutes: [], live: false },
292
348
  delta: input.diffDelta,
293
349
  };
294
350
  };
@@ -325,8 +381,18 @@ const buildModel = (input) => ({
325
381
  commits: commitsModel(input),
326
382
  branches: branchesModel(input),
327
383
  npm: npmModel(input),
328
- runs: mergeRuns(input.runs, input.npmRun),
384
+ runs: mergeRuns(input.runs, input.npmRuns),
329
385
  tasks: tasksModel(input),
386
+ agents: input.agents ?? { ready: true, installed: 0, total: 0, items: [] },
330
387
  });
331
388
 
332
- module.exports = { groupChanges, mergeRuns, scriptName, runName, buildModel };
389
+ module.exports = {
390
+ MINUTE_MS,
391
+ minuteOf,
392
+ groupChanges,
393
+ foldSteps,
394
+ mergeRuns,
395
+ runName,
396
+ npmModel,
397
+ buildModel,
398
+ };
@@ -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,13 +54,36 @@ 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);
63
+ };
64
+
65
+ const DIRECT_SECTIONS = [
66
+ 'dependencies',
67
+ 'devDependencies',
68
+ 'optionalDependencies',
69
+ 'peerDependencies',
70
+ ];
71
+ const CHILD_SECTIONS = [
72
+ 'dependencies',
73
+ 'optionalDependencies',
74
+ 'peerDependencies',
75
+ ];
76
+ const CHAIN_MARK = ' 🢒 ';
77
+
78
+ const directNames = (pkg) => {
79
+ const names = new Set();
80
+ for (const section of DIRECT_SECTIONS) {
81
+ for (const name of declaredNames(pkg, section)) names.add(name);
82
+ }
83
+ return names;
61
84
  };
62
85
 
63
- const devOnlyNames = (pkg, lock) => {
86
+ const devNames = (pkg) => {
64
87
  const keep = new Set([
65
88
  ...declaredNames(pkg, 'dependencies'),
66
89
  ...declaredNames(pkg, 'optionalDependencies'),
@@ -69,16 +92,84 @@ const devOnlyNames = (pkg, lock) => {
69
92
  for (const name of declaredNames(pkg, 'devDependencies')) {
70
93
  if (!keep.has(name)) names.add(name);
71
94
  }
95
+ return names;
96
+ };
97
+
98
+ const entryName = (key) => {
99
+ const at = key.lastIndexOf(MODULE_PREFIX);
100
+ if (at < 0) return '';
101
+ const rest = key.slice(at + MODULE_PREFIX.length);
102
+ if (rest.startsWith('@')) {
103
+ const parts = rest.split('/');
104
+ if (!parts[1]) return '';
105
+ return `${parts[0]}/${parts[1]}`;
106
+ }
107
+ return rest.split('/')[0] || '';
108
+ };
109
+
110
+ const childNames = (entry) => {
111
+ const names = [];
112
+ if (!entry || typeof entry !== 'object') return names;
113
+ for (const section of CHILD_SECTIONS) {
114
+ const map = entry[section];
115
+ if (!map || typeof map !== 'object') continue;
116
+ for (const name of Object.keys(map)) {
117
+ if (!names.includes(name)) names.push(name);
118
+ }
119
+ }
120
+ return names;
121
+ };
122
+
123
+ const dependencyChildren = (lock) => {
124
+ const children = new Map();
72
125
  const packages = lockPackages(lock);
73
- if (!packages) return names;
126
+ if (!packages) return children;
74
127
  for (const key of Object.keys(packages)) {
75
- const entry = packages[key];
76
- if (!entry || !entry.dev || !key.startsWith(MODULE_PREFIX)) continue;
77
- const name = key.slice(MODULE_PREFIX.length);
78
- if (!name || name.includes('/node_modules/') || keep.has(name)) continue;
79
- names.add(name);
128
+ if (!key) continue;
129
+ const name = entryName(key);
130
+ if (!name) continue;
131
+ const list = children.get(name) ?? [];
132
+ for (const child of childNames(packages[key])) {
133
+ if (!list.includes(child)) list.push(child);
134
+ }
135
+ if (list.length) children.set(name, list);
80
136
  }
81
- return names;
137
+ return children;
138
+ };
139
+
140
+ const parentChain = (name, declared, children) => {
141
+ if (declared.has(name)) return '';
142
+ const prev = new Map();
143
+ const queue = [];
144
+ const roots = [...declared].sort();
145
+ for (const root of roots) {
146
+ if (prev.has(root)) continue;
147
+ prev.set(root, '');
148
+ queue.push(root);
149
+ }
150
+ let head = 0;
151
+ while (head < queue.length) {
152
+ const at = queue[head];
153
+ head += 1;
154
+ const kids = children.get(at);
155
+ if (!kids) continue;
156
+ for (const kid of kids) {
157
+ if (prev.has(kid)) continue;
158
+ prev.set(kid, at);
159
+ if (kid !== name) {
160
+ queue.push(kid);
161
+ continue;
162
+ }
163
+ const path = [];
164
+ let cursor = at;
165
+ while (cursor) {
166
+ path.push(cursor);
167
+ cursor = prev.get(cursor) || '';
168
+ }
169
+ return path.join(CHAIN_MARK);
170
+ }
171
+ }
172
+ return '';
82
173
  };
83
174
 
84
175
  const lockVersion = (lock, name) => {
@@ -89,16 +180,24 @@ const lockVersion = (lock, name) => {
89
180
  };
90
181
 
91
182
  const topPackages = (ranked, pkg, lock) => {
92
- const devNames = devOnlyNames(pkg, lock);
93
- const rows = ranked.map((item) => ({
94
- name: item.name,
95
- bytes: item.bytes,
96
- dev: devNames.has(item.name),
97
- version: lockVersion(lock, item.name),
98
- }));
99
- const runtime = rows.filter((row) => !row.dev);
100
- const devs = rows.filter((row) => row.dev);
101
- return [...runtime, ...devs].slice(0, TOP_PACKAGES);
183
+ const declared = directNames(pkg);
184
+ const devs = devNames(pkg);
185
+ const children = dependencyChildren(lock);
186
+ const rows = ranked.map((item) => {
187
+ const direct = declared.has(item.name);
188
+ return {
189
+ name: item.name,
190
+ bytes: item.bytes,
191
+ dev: direct && devs.has(item.name),
192
+ transitive: !direct,
193
+ chain: parentChain(item.name, declared, children),
194
+ version: lockVersion(lock, item.name),
195
+ };
196
+ });
197
+ const runtime = rows.filter((row) => !row.dev && !row.transitive);
198
+ const development = rows.filter((row) => row.dev);
199
+ const nested = rows.filter((row) => row.transitive);
200
+ return [...runtime, ...development, ...nested];
102
201
  };
103
202
 
104
203
  const measureModules = async (root) => {
@@ -108,12 +207,13 @@ const measureModules = async (root) => {
108
207
  const worker = async () => {
109
208
  while (queue.length) {
110
209
  const dir = queue.pop();
111
- let entries;
210
+ let entries = null;
112
211
  try {
113
212
  entries = await fs.promises.readdir(dir, { withFileTypes: true });
114
213
  } catch {
115
- continue;
214
+ // unreadable directory
116
215
  }
216
+ if (!entries) continue;
117
217
  const files = [];
118
218
  for (const entry of entries) {
119
219
  const full = path.join(dir, entry.name);
@@ -148,12 +248,13 @@ const installedCount = async (top, lock) => {
148
248
  }
149
249
  return count;
150
250
  }
151
- let entries;
251
+ let entries = null;
152
252
  try {
153
253
  entries = await fs.promises.readdir(path.join(top, MODULES));
154
254
  } catch {
155
- return 0;
255
+ // missing modules
156
256
  }
257
+ if (!entries) return 0;
157
258
  let count = 0;
158
259
  for (const name of entries) {
159
260
  if (name.startsWith('.')) continue;
@@ -190,4 +291,4 @@ const readNpmSummary = async (top, previous) => {
190
291
  };
191
292
  };
192
293
 
193
- module.exports = { readNpmSummary };
294
+ module.exports = { TOP_PACKAGES, readNpmSummary };