reslop 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const worktree = require('../git-worktree.js');
7
+ const { runGitAsync } = worktree;
8
+
9
+ const UNIT = '\x1f';
10
+ const RECENT_COUNT = 8;
11
+ const BRANCH_FORMAT = [
12
+ '%(HEAD)',
13
+ '%(refname:short)',
14
+ '%(objectname:short)',
15
+ '%(committerdate:unix)',
16
+ '%(upstream:short)',
17
+ '%(upstream:track)',
18
+ '%(subject)',
19
+ ].join('%09');
20
+ const LAST_FORMAT = ['%H', '%h', '%ct', '%s'].join('%x1f');
21
+ const PROBES = {
22
+ total: ['rev-list', '--count', 'HEAD'],
23
+ recent: ['log', `--max-count=${RECENT_COUNT}`, `--format=${LAST_FORMAT}`],
24
+ refs: ['for-each-ref', `--format=${BRANCH_FORMAT}`, 'refs/heads'],
25
+ dir: ['rev-parse', '--absolute-git-dir'],
26
+ };
27
+ const TRACK_AHEAD = /ahead (\d+)/;
28
+ const TRACK_BEHIND = /behind (\d+)/;
29
+
30
+ const gitText = async (top, args) => {
31
+ try {
32
+ const result = await runGitAsync(args, top);
33
+ return result.status === 0 ? result.stdout.trim() : '';
34
+ } catch {
35
+ return '';
36
+ }
37
+ };
38
+
39
+ const countOf = (text) => {
40
+ const value = parseInt(text, 10);
41
+ return Number.isFinite(value) ? value : 0;
42
+ };
43
+
44
+ const msOf = (seconds) => (Number.isFinite(seconds) ? seconds * 1000 : 0);
45
+
46
+ const parseLines = (text, parse) => text.split('\n').map(parse).filter(Boolean);
47
+
48
+ const parseTrack = (text) => {
49
+ const raw = `${text ?? ''}`;
50
+ const ahead = TRACK_AHEAD.exec(raw);
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
+ };
57
+ };
58
+
59
+ const parseBranch = (line) => {
60
+ const parts = line.split('\t');
61
+ const name = (parts[1] ?? '').trim();
62
+ if (!name) return null;
63
+ return {
64
+ name,
65
+ current: parts[0].includes('*'),
66
+ sha: (parts[2] ?? '').trim(),
67
+ at: msOf(Number(parts[3])),
68
+ upstream: (parts[4] ?? '').trim(),
69
+ ...parseTrack(parts[5]),
70
+ subject: parts.slice(6).join('\t').trim(),
71
+ };
72
+ };
73
+
74
+ const parseCommit = (line) => {
75
+ const parts = line.split(UNIT);
76
+ if (parts.length < 4 || !parts[0]) return null;
77
+ return {
78
+ sha: parts[0],
79
+ short: parts[1],
80
+ at: msOf(Number(parts[2])),
81
+ subject: parts.slice(3).join(UNIT).trim(),
82
+ };
83
+ };
84
+
85
+ const readNumber = (file) => {
86
+ try {
87
+ return countOf(fs.readFileSync(file, 'utf8'));
88
+ } catch {
89
+ return 0;
90
+ }
91
+ };
92
+
93
+ const readStep = (dir, step, total) => ({
94
+ step: readNumber(path.join(dir, step)),
95
+ total: readNumber(path.join(dir, total)),
96
+ });
97
+
98
+ const rebaseState = (gitDir) => {
99
+ if (!gitDir) return null;
100
+ const merge = path.join(gitDir, 'rebase-merge');
101
+ if (fs.existsSync(merge)) return readStep(merge, 'msgnum', 'end');
102
+ const apply = path.join(gitDir, 'rebase-apply');
103
+ if (fs.existsSync(apply)) return readStep(apply, 'next', 'last');
104
+ return null;
105
+ };
106
+
107
+ const readGitSummary = async (top) => {
108
+ const names = Object.keys(PROBES);
109
+ const read = (name) => gitText(top, PROBES[name]);
110
+ const texts = await Promise.all(names.map(read));
111
+ const probes = {};
112
+ for (let i = 0; i < names.length; i++) probes[names[i]] = texts[i];
113
+ const branches = parseLines(probes.refs, parseBranch);
114
+ const current = branches.find((entry) => entry.current);
115
+ const recent = parseLines(probes.recent, parseCommit);
116
+ return {
117
+ branch: current ? current.name : '',
118
+ rebase: rebaseState(probes.dir),
119
+ commits: {
120
+ total: countOf(probes.total),
121
+ last: recent[0] ?? null,
122
+ recent,
123
+ },
124
+ branches,
125
+ };
126
+ };
127
+
128
+ module.exports = { readGitSummary, parseTrack };
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ const tree = require('./tree.js');
4
+ const { folderOf, extOf } = tree;
5
+ const files = require('../files.js');
6
+ const { TASK_KINDS, taskKind } = files;
7
+
8
+ const MINUTE_MS = 60_000;
9
+ const RUNS_SHOWN = 6;
10
+ const CHAIN_GAP_MS = 2000;
11
+ const NPM_NAMED = /^npm (?:run|exec)(?:\s+-s)?\s+(\S+)/;
12
+ const NPM_ALIAS = /^npm (test|start|stop|restart)(?:\s|$)/;
13
+ const AGE_TEXT = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
14
+ const AGE_SEC = {
15
+ second: 1,
16
+ minute: 60,
17
+ hour: 3600,
18
+ day: 86400,
19
+ week: 604800,
20
+ month: 2592000,
21
+ year: 31536000,
22
+ };
23
+ const STATUS_RANK = { running: 3, failed: 2, aborted: 1, passed: 0 };
24
+
25
+ const namedScript = (command) => {
26
+ const text = `${command ?? ''}`.trim();
27
+ const named = NPM_NAMED.exec(text);
28
+ if (named) return named[1];
29
+ const alias = NPM_ALIAS.exec(text);
30
+ return alias ? alias[1] : '';
31
+ };
32
+
33
+ const scriptName = (command) => {
34
+ const named = namedScript(command);
35
+ if (named) return named;
36
+ const text = `${command ?? ''}`.trim();
37
+ const token = text.split(/\s+/)[0];
38
+ const slash = Math.max(token.lastIndexOf('/'), token.lastIndexOf('\\'));
39
+ return token.slice(slash + 1) || text;
40
+ };
41
+
42
+ const runName = (record) => {
43
+ const command = `${record.command ?? ''}`.trim();
44
+ const script = `${record.script ?? ''}`.trim();
45
+ return namedScript(command) || script || command;
46
+ };
47
+
48
+ const ageSeconds = (text) => {
49
+ const raw = `${text ?? ''}`.trim();
50
+ if (/^yesterday$/i.test(raw)) return AGE_SEC.day;
51
+ const match = AGE_TEXT.exec(raw);
52
+ if (!match) return Number.POSITIVE_INFINITY;
53
+ return Number(match[1]) * AGE_SEC[match[2].toLowerCase()];
54
+ };
55
+
56
+ const newerDate = (current, next) => {
57
+ if (!next) return current;
58
+ if (!current || ageSeconds(next) < ageSeconds(current)) return next;
59
+ return current;
60
+ };
61
+
62
+ const groupOf = (map, key) => {
63
+ let group = map.get(key);
64
+ if (!group) {
65
+ group = {
66
+ key,
67
+ added: 0,
68
+ removed: 0,
69
+ stagedAdded: 0,
70
+ stagedRemoved: 0,
71
+ staged: 0,
72
+ remaining: 0,
73
+ date: '',
74
+ };
75
+ map.set(key, group);
76
+ }
77
+ return group;
78
+ };
79
+
80
+ const byChange = (left, right) => {
81
+ const lhs = left.added + left.removed;
82
+ const rhs = right.added + right.removed;
83
+ return rhs - lhs || left.key.localeCompare(right.key, 'en');
84
+ };
85
+
86
+ const groupChanges = (entries) => {
87
+ const dirs = new Map();
88
+ const exts = new Map();
89
+ for (const entry of entries) {
90
+ const groups = [
91
+ groupOf(dirs, folderOf(entry.path)),
92
+ groupOf(exts, extOf(entry.path)),
93
+ ];
94
+ for (const group of groups) {
95
+ group.added += entry.added ?? 0;
96
+ group.removed += entry.removed ?? 0;
97
+ group.stagedAdded += entry.stagedAdded ?? 0;
98
+ group.stagedRemoved += entry.stagedRemoved ?? 0;
99
+ group.staged += entry.staged ?? 0;
100
+ group.remaining += entry.remaining ?? 0;
101
+ group.date = newerDate(group.date, entry.date);
102
+ }
103
+ }
104
+ return {
105
+ dirs: [...dirs.values()].sort(byChange),
106
+ exts: [...exts.values()].sort(byChange),
107
+ };
108
+ };
109
+
110
+ const npmRunStatus = (run) => {
111
+ if (run.running) return 'running';
112
+ if (run.exit === 0) return 'passed';
113
+ if (run.exit === '' || run.exit === null) return 'aborted';
114
+ return 'failed';
115
+ };
116
+
117
+ const externalRun = (record) => {
118
+ const progress = record.progress ?? {};
119
+ return {
120
+ source: 'reslop t',
121
+ name: runName(record),
122
+ status: record.status,
123
+ exit: record.exit,
124
+ startedAt: record.startedAt,
125
+ endedAt: record.endedAt,
126
+ done: progress.done ?? 0,
127
+ failed: progress.failed ?? 0,
128
+ expected: progress.expected ?? 0,
129
+ result: record.result ?? null,
130
+ };
131
+ };
132
+
133
+ const internalRun = (run) => ({
134
+ source: 'reslop',
135
+ name: scriptName(run.label),
136
+ status: npmRunStatus(run),
137
+ exit: run.exit,
138
+ startedAt: run.startedAt,
139
+ endedAt: run.endedAt,
140
+ done: 0,
141
+ failed: 0,
142
+ expected: 0,
143
+ result: null,
144
+ });
145
+
146
+ const rankOf = (status) => STATUS_RANK[status] ?? 0;
147
+
148
+ const isOpen = (run) => run.status === 'running' || !run.endedAt;
149
+
150
+ const addNum = (left, right) => {
151
+ if (typeof left !== 'number') return typeof right === 'number' ? right : null;
152
+ return typeof right === 'number' ? left + right : left;
153
+ };
154
+
155
+ const mergeResult = (left, right) => {
156
+ if (!left || !right) return left || right;
157
+ const tools = new Set(left.tools);
158
+ for (const tool of right.tools ?? []) tools.add(tool);
159
+ return {
160
+ tools: [...tools],
161
+ tests: addNum(left.tests, right.tests),
162
+ passed: addNum(left.passed, right.passed),
163
+ failed: addNum(left.failed, right.failed),
164
+ skipped: addNum(left.skipped, right.skipped),
165
+ errors: addNum(left.errors, right.errors),
166
+ warnings: addNum(left.warnings, right.warnings),
167
+ duration: left.duration || right.duration || '',
168
+ problems: addNum(left.problems, right.problems),
169
+ };
170
+ };
171
+
172
+ const continuesRun = (prev, next) => {
173
+ if (prev.name !== next.name) return false;
174
+ if (isOpen(prev)) return next.startedAt >= prev.startedAt;
175
+ return next.startedAt <= prev.endedAt + CHAIN_GAP_MS;
176
+ };
177
+
178
+ const absorbRun = (prev, next) => {
179
+ const open = isOpen(prev) || isOpen(next);
180
+ if (rankOf(next.status) >= rankOf(prev.status)) {
181
+ prev.exit = next.exit;
182
+ prev.status = next.status;
183
+ }
184
+ prev.endedAt = open ? 0 : Math.max(prev.endedAt, next.endedAt);
185
+ prev.done += next.done;
186
+ prev.failed += next.failed;
187
+ prev.expected = Math.max(prev.expected, next.expected);
188
+ prev.result = mergeResult(prev.result, next.result);
189
+ };
190
+
191
+ const collapseRuns = (list) => {
192
+ const ordered = [...list].sort(
193
+ (left, right) => left.startedAt - right.startedAt,
194
+ );
195
+ const groups = [];
196
+ for (const run of ordered) {
197
+ const prev = groups.at(-1);
198
+ if (prev && continuesRun(prev, run)) absorbRun(prev, run);
199
+ else groups.push({ ...run });
200
+ }
201
+ return groups.sort((left, right) => right.startedAt - left.startedAt);
202
+ };
203
+
204
+ const mergeRuns = (records, npmRun) => {
205
+ const list = records.map(externalRun);
206
+ if (npmRun) list.push(internalRun(npmRun));
207
+ return collapseRuns(list).slice(0, RUNS_SHOWN);
208
+ };
209
+
210
+ const taskKindCounts = (store) => {
211
+ const kinds = TASK_KINDS.map((kind) => ({
212
+ id: kind.id,
213
+ title: kind.title,
214
+ done: 0,
215
+ total: 0,
216
+ }));
217
+ const byId = new Map(kinds.map((row) => [row.id, row]));
218
+ let done = 0;
219
+ let total = 0;
220
+ if (!store) return { kinds, done, total };
221
+ for (const task of store.tasks) {
222
+ if (!`${task.text ?? ''}`.trim()) continue;
223
+ const row = byId.get(taskKind(task));
224
+ row.total += 1;
225
+ total += 1;
226
+ if (!task.done) continue;
227
+ row.done += 1;
228
+ done += 1;
229
+ }
230
+ return { kinds, done, total };
231
+ };
232
+
233
+ const versionFields = (pkg, outdatedMap) => {
234
+ const hit = outdatedMap ? outdatedMap.get(pkg.name) : null;
235
+ const current = (hit && hit.current) || pkg.version;
236
+ if (!outdatedMap) return { current, wanted: '', latest: '' };
237
+ if (!hit) return { current, wanted: current, latest: current };
238
+ return { current, wanted: hit.wanted, latest: hit.latest };
239
+ };
240
+
241
+ const auditFields = (pkg, auditMap) => {
242
+ const finding = auditMap ? auditMap.get(pkg.name) : null;
243
+ return {
244
+ audit: Boolean(finding),
245
+ fix: finding && finding.fix ? finding.fix : '',
246
+ range: finding && finding.range ? finding.range : '',
247
+ severity: finding && finding.severity ? finding.severity : '',
248
+ title: finding && finding.title ? finding.title : '',
249
+ };
250
+ };
251
+
252
+ const modulesModel = (modules, auditMap, outdatedMap) => {
253
+ const packages = modules.packages.map((pkg) => ({
254
+ name: pkg.name,
255
+ bytes: pkg.bytes,
256
+ dev: pkg.dev,
257
+ transitive: Boolean(pkg.transitive),
258
+ chain: pkg.chain || '',
259
+ ...auditFields(pkg, auditMap),
260
+ ...versionFields(pkg, outdatedMap),
261
+ }));
262
+ return { ...modules, packages };
263
+ };
264
+
265
+ const npmModel = (input) => {
266
+ const info = input.npm;
267
+ const extras = input.npmExtras;
268
+ const auditMap = extras ? extras.auditMap : null;
269
+ const outdatedMap = extras ? extras.outdatedMap : null;
270
+ const npmRun = input.npmRun;
271
+ return {
272
+ ready: Boolean(info),
273
+ hasManifest: info ? info.hasManifest : false,
274
+ deps: info ? info.deps : 0,
275
+ dev: info ? info.dev : 0,
276
+ modules: info ? modulesModel(info.modules, auditMap, outdatedMap) : null,
277
+ audit: auditMap ? auditMap.size : null,
278
+ outdated: outdatedMap ? outdatedMap.size : null,
279
+ running: npmRun && npmRun.running ? npmRun.label : '',
280
+ };
281
+ };
282
+
283
+ const minuteOf = (at) => Math.floor((at || 0) / MINUTE_MS);
284
+
285
+ const filesModel = (input) => {
286
+ const index = input.index;
287
+ const ready = Boolean(index && index.ready);
288
+ const summary = ready ? index.summary() : null;
289
+ const cold = { dirs: new Set(), exts: new Set() };
290
+ return {
291
+ ready,
292
+ total: summary ? summary.total : { files: 0, bytes: 0, lines: 0 },
293
+ dirs: summary ? summary.dirs : [],
294
+ exts: summary ? summary.exts : [],
295
+ hot: index ? index.hot(input.now) : cold,
296
+ delta: input.fileDelta,
297
+ };
298
+ };
299
+
300
+ const diffsModel = (input) => {
301
+ const groups = groupChanges(input.entries);
302
+ return {
303
+ totals: input.totals,
304
+ dirs: groups.dirs,
305
+ exts: groups.exts,
306
+ files: input.entries.length,
307
+ activity: input.activity ?? { minutes: [], live: false },
308
+ delta: input.diffDelta,
309
+ };
310
+ };
311
+
312
+ const commitsModel = (input) => {
313
+ const git = input.git;
314
+ return {
315
+ ready: Boolean(git),
316
+ rebase: git ? git.rebase : null,
317
+ info: git ? git.commits : null,
318
+ changedAt: input.marks.commit,
319
+ };
320
+ };
321
+
322
+ const branchesModel = (input) => {
323
+ const git = input.git;
324
+ return {
325
+ ready: Boolean(git),
326
+ list: git ? git.branches : [],
327
+ rebase: git ? git.rebase : null,
328
+ switches: input.switches,
329
+ hot: input.branchAt,
330
+ };
331
+ };
332
+
333
+ const tasksModel = (input) => taskKindCounts(input.store);
334
+
335
+ const buildModel = (input) => ({
336
+ now: input.now,
337
+ frame: input.frame,
338
+ busy: input.busy,
339
+ files: filesModel(input),
340
+ diffs: diffsModel(input),
341
+ commits: commitsModel(input),
342
+ branches: branchesModel(input),
343
+ npm: npmModel(input),
344
+ runs: mergeRuns(input.runs, input.npmRun),
345
+ tasks: tasksModel(input),
346
+ });
347
+
348
+ module.exports = {
349
+ MINUTE_MS,
350
+ minuteOf,
351
+ groupChanges,
352
+ mergeRuns,
353
+ scriptName,
354
+ runName,
355
+ npmModel,
356
+ buildModel,
357
+ };