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,455 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const tree = require('../dashboard/tree.js');
6
+ const { FileIndex, HEAT_MS } = tree;
7
+ const watch = require('./watch.js');
8
+ const { UNKNOWN_PATH } = watch;
9
+ const workspace = require('../workspace.js');
10
+ const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
11
+ const npmInfo = require('../dashboard/npm.js');
12
+ const { readNpmSummary } = npmInfo;
13
+ const gitDeps = require('../git-deps.js');
14
+ const { collectReportAsync } = gitDeps;
15
+
16
+ const TICK_ID = 'repos';
17
+ const HEAT_TIMER = 'repos-heat';
18
+
19
+ const freshBag = () => ({
20
+ files: [],
21
+ git: false,
22
+ runs: false,
23
+ tasks: false,
24
+ npm: false,
25
+ activity: false,
26
+ });
27
+
28
+ const fullBag = () => ({
29
+ files: [UNKNOWN_PATH],
30
+ git: true,
31
+ runs: true,
32
+ tasks: true,
33
+ npm: true,
34
+ activity: false,
35
+ });
36
+
37
+ const mergeBag = (left, right) => {
38
+ const base = left ?? freshBag();
39
+ return {
40
+ files: [...base.files, ...right.files],
41
+ git: base.git || right.git,
42
+ runs: base.runs || right.runs,
43
+ tasks: base.tasks || right.tasks,
44
+ npm: base.npm || right.npm,
45
+ activity: base.activity || right.activity,
46
+ };
47
+ };
48
+
49
+ const blankRow = (repo) => ({
50
+ name: repo.name,
51
+ dir: repo.dir,
52
+ index: new FileIndex(repo.dir),
53
+ bytes: 0,
54
+ exts: [],
55
+ tasks: 0,
56
+ tasksDone: 0,
57
+ commits: 0,
58
+ diff: emptyDiff(),
59
+ run: null,
60
+ activityAt: 0,
61
+ filesReady: false,
62
+ commitsReady: false,
63
+ diffReady: false,
64
+ tasksReady: false,
65
+ runsReady: false,
66
+ npm: null,
67
+ npmReady: false,
68
+ pending: null,
69
+ running: false,
70
+ });
71
+
72
+ const readFiles = async (row, bag) => {
73
+ if (!bag.files.length) return;
74
+ const unknown = bag.files.includes(UNKNOWN_PATH);
75
+ if (unknown || !row.index.ready) await row.index.rescan();
76
+ else await row.index.touch(bag.files);
77
+ if (!row.index.ready) return;
78
+ const summary = row.index.summary();
79
+ row.bytes = summary.total.bytes;
80
+ row.exts = summary.exts.slice(0, 3);
81
+ row.filesReady = true;
82
+ };
83
+
84
+ const readCommits = async (row) => {
85
+ row.commits = await countCommits(row.dir);
86
+ row.commitsReady = true;
87
+ };
88
+
89
+ const readDiff = async (row) => {
90
+ row.diff = await readDiffStat(row.dir);
91
+ row.diffReady = true;
92
+ };
93
+
94
+ const readTasks = (row) => {
95
+ const stats = taskStats(row.dir);
96
+ row.tasksDone = stats.done;
97
+ row.tasks = stats.total;
98
+ row.tasksReady = true;
99
+ };
100
+
101
+ const readRuns = (row) => {
102
+ row.run = pickRun(row.dir);
103
+ row.runsReady = true;
104
+ };
105
+
106
+ const reportCount = async (dir, kind) => {
107
+ const report = await collectReportAsync(dir, kind);
108
+ return report ? report.size : 0;
109
+ };
110
+
111
+ const readNpm = async (row) => {
112
+ const summary = await readNpmSummary(row.dir, row.npm);
113
+ const [audit, outdated] = await Promise.all([
114
+ reportCount(row.dir, 'audit'),
115
+ reportCount(row.dir, 'outdated'),
116
+ ]);
117
+ row.npm = { ...summary, audit, outdated };
118
+ row.npmReady = true;
119
+ };
120
+
121
+ const toTile = (row) => ({
122
+ id: row.name,
123
+ name: row.name,
124
+ bytes: row.bytes,
125
+ exts: row.exts,
126
+ tasks: row.tasks,
127
+ tasksDone: row.tasksDone,
128
+ commits: row.commits,
129
+ diff: row.diff,
130
+ run: row.run,
131
+ activityAt: row.activityAt,
132
+ filesReady: row.filesReady,
133
+ commitsReady: row.commitsReady,
134
+ diffReady: row.diffReady,
135
+ tasksReady: row.tasksReady,
136
+ runsReady: row.runsReady,
137
+ npm: row.npm,
138
+ npmReady: row.npmReady,
139
+ });
140
+
141
+ const rowBusy = (row) =>
142
+ !row.filesReady ||
143
+ !row.commitsReady ||
144
+ !row.diffReady ||
145
+ !row.tasksReady ||
146
+ !row.runsReady ||
147
+ !row.npmReady;
148
+
149
+ const classifyRest = (rest) => {
150
+ const bag = freshBag();
151
+ if (!rest) {
152
+ bag.files.push(UNKNOWN_PATH);
153
+ bag.git = true;
154
+ bag.runs = true;
155
+ bag.tasks = true;
156
+ bag.npm = true;
157
+ bag.activity = true;
158
+ return bag;
159
+ }
160
+ if (
161
+ rest === 'package.json' ||
162
+ rest === 'package-lock.json' ||
163
+ rest === 'node_modules/.package-lock.json'
164
+ ) {
165
+ bag.npm = true;
166
+ }
167
+ if (rest === '.git' || rest.startsWith('.git/')) {
168
+ bag.git = true;
169
+ return bag;
170
+ }
171
+ if (rest === '.log' || rest.startsWith('.log/')) {
172
+ bag.runs = true;
173
+ return bag;
174
+ }
175
+ if (rest === '.review' || rest.startsWith('.review/')) {
176
+ bag.tasks = true;
177
+ return bag;
178
+ }
179
+ bag.files.push(rest);
180
+ bag.activity = true;
181
+ return bag;
182
+ };
183
+
184
+ class Workspace {
185
+ constructor(ui) {
186
+ this.ui = ui;
187
+ this.epoch = 0;
188
+ this.reset();
189
+ }
190
+
191
+ reset() {
192
+ this.stop();
193
+ this.rows = [];
194
+ this.byName = new Map();
195
+ this.cursor = 0;
196
+ this.screen = {
197
+ now: 0,
198
+ frame: 0,
199
+ cursor: 0,
200
+ cols: 1,
201
+ tiles: [],
202
+ };
203
+ }
204
+
205
+ seed() {
206
+ const info = this.ui.workspaceInfo;
207
+ const repos = info ? info.repos : [];
208
+ this.rows = repos.map(blankRow);
209
+ this.byName = new Map(this.rows.map((row) => [row.name, row]));
210
+ if (this.cursor >= this.rows.length) this.cursor = 0;
211
+ }
212
+
213
+ stale(epoch) {
214
+ return epoch !== this.epoch || !this.active;
215
+ }
216
+
217
+ publish(epoch) {
218
+ if (this.stale(epoch)) return;
219
+ this.afterDraw();
220
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
221
+ }
222
+
223
+ async settle(epoch, read) {
224
+ await read();
225
+ this.publish(epoch);
226
+ }
227
+
228
+ async apply(row, bag, epoch) {
229
+ if (bag.activity) row.activityAt = Date.now();
230
+ const jobs = [];
231
+ if (bag.files.length) {
232
+ jobs.push(this.settle(epoch, () => readFiles(row, bag)));
233
+ }
234
+ if (bag.git) {
235
+ jobs.push(this.settle(epoch, () => readCommits(row)));
236
+ jobs.push(this.settle(epoch, () => readDiff(row)));
237
+ }
238
+ if (bag.tasks) jobs.push(this.settle(epoch, () => readTasks(row)));
239
+ if (bag.runs) jobs.push(this.settle(epoch, () => readRuns(row)));
240
+ if (bag.npm) jobs.push(this.settle(epoch, () => readNpm(row)));
241
+ await Promise.all(jobs);
242
+ }
243
+
244
+ async pump(row) {
245
+ row.running = true;
246
+ while (row.pending && this.active) {
247
+ const bag = row.pending;
248
+ const epoch = this.epoch;
249
+ row.pending = null;
250
+ try {
251
+ await this.apply(row, bag, epoch);
252
+ } catch {
253
+ // keep the previous snapshot until the next event
254
+ }
255
+ if (this.stale(epoch)) break;
256
+ this.afterDraw();
257
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
258
+ }
259
+ row.running = false;
260
+ if (row.pending && this.active) {
261
+ void this.pump(row);
262
+ return;
263
+ }
264
+ this.afterDraw();
265
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
266
+ }
267
+
268
+ kick(row, bag) {
269
+ if (!row || !this.active) return;
270
+ if (bag.activity) row.activityAt = Date.now();
271
+ row.pending = mergeBag(row.pending, bag);
272
+ if (row.running) return;
273
+ void this.pump(row);
274
+ }
275
+
276
+ requestAll() {
277
+ for (const row of this.rows) this.kick(row, fullBag());
278
+ }
279
+
280
+ notePaths(paths) {
281
+ if (!this.active) return;
282
+ for (const rel of paths) {
283
+ if (rel === UNKNOWN_PATH) return void this.requestAll();
284
+ }
285
+ const found = new Map();
286
+ for (const rel of paths) {
287
+ const slash = rel.indexOf('/');
288
+ const name = slash < 0 ? rel : rel.slice(0, slash);
289
+ const rest = slash < 0 ? '' : rel.slice(slash + 1);
290
+ const row = this.byName.get(name);
291
+ if (!row) continue;
292
+ found.set(name, mergeBag(found.get(name), classifyRest(rest)));
293
+ }
294
+ for (const [name, bag] of found) this.kick(this.byName.get(name), bag);
295
+ if (found.size) {
296
+ this.afterDraw();
297
+ if (this.ui.uiOpen) this.ui.paint();
298
+ }
299
+ }
300
+
301
+ start() {
302
+ if (this.active) return;
303
+ this.active = true;
304
+ if (!this.rows.length) this.seed();
305
+ this.requestAll();
306
+ }
307
+
308
+ stop() {
309
+ this.epoch += 1;
310
+ this.active = false;
311
+ this.cancelTimers();
312
+ }
313
+
314
+ cancelTimers() {
315
+ const { progress, term } = this.ui;
316
+ if (!progress || !term) return;
317
+ progress.stop(TICK_ID);
318
+ term.cancel(HEAT_TIMER);
319
+ }
320
+
321
+ hotUntil() {
322
+ let until = 0;
323
+ for (const row of this.rows) {
324
+ until = Math.max(until, row.activityAt + HEAT_MS);
325
+ }
326
+ return until;
327
+ }
328
+
329
+ afterDraw() {
330
+ if (!this.active) return;
331
+ const visible = this.ui.uiOpen && this.ui.nav.pane === 'repos';
332
+ if (!visible) return void this.cancelTimers();
333
+ const now = Date.now();
334
+ const until = this.hotUntil();
335
+ const hot = now < until;
336
+ const running = this.rows.some((row) => row.run?.status === 'running');
337
+ const loading = this.rows.some((row) => rowBusy(row));
338
+ const { progress, term } = this.ui;
339
+ if (running || hot || loading) progress.start(TICK_ID);
340
+ else progress.stop(TICK_ID);
341
+ if (!hot) return;
342
+ term.later(
343
+ HEAT_TIMER,
344
+ () => {
345
+ if (this.ui.nav.pane === 'repos') this.ui.paint();
346
+ },
347
+ until - now + 20,
348
+ );
349
+ }
350
+
351
+ view() {
352
+ const screen = this.screen;
353
+ screen.now = Date.now();
354
+ screen.frame = this.ui.progressFrame;
355
+ screen.cursor = this.cursor;
356
+ screen.tiles = this.rows.map(toTile);
357
+ return screen;
358
+ }
359
+
360
+ move(dx, dy) {
361
+ const count = this.rows.length;
362
+ if (!count) return;
363
+ const cols = Math.max(1, this.screen.cols || 1);
364
+ const next = this.cursor + dx + dy * cols;
365
+ if (next < 0 || next >= count) return;
366
+ this.cursor = next;
367
+ if (this.ui.uiOpen) this.ui.paint();
368
+ }
369
+
370
+ enter(name) {
371
+ const row = this.byName.get(name);
372
+ const ui = this.ui;
373
+ if (!row || ui.mode !== 'review') return Promise.resolve();
374
+ ui.flushReview();
375
+ this.stop();
376
+ ui.lifecycle.stopWatch();
377
+ ui.dashboard.stop();
378
+ ui.loader.reset();
379
+ ui.cwd = row.dir;
380
+ ui.paths = [];
381
+ ui.repoName = row.name;
382
+ ui.branch = '';
383
+ ui.rev = null;
384
+ ui.revShort = '';
385
+ ui.change = null;
386
+ ui.dashboardHome = true;
387
+ ui.collection.reset();
388
+ ui.review.reset();
389
+ ui.nav.pane = 'dashboard';
390
+ ui.nav.tasksOpen = false;
391
+ ui.nav.scroll = 0;
392
+ ui.nav.clearSelection();
393
+ ui.status = '';
394
+ try {
395
+ ui.ensureRepo();
396
+ } catch (error) {
397
+ ui.status = error instanceof Error ? error.message : `${error}`;
398
+ return this.show();
399
+ }
400
+ const pending = ui.openLoad();
401
+ if (ui.uiOpen) {
402
+ ui.lifecycle.startWatch();
403
+ ui.dashboard.start();
404
+ ui.paint();
405
+ }
406
+ return pending ?? Promise.resolve();
407
+ }
408
+
409
+ openSelected() {
410
+ const row = this.rows[this.cursor];
411
+ if (!row) return Promise.resolve();
412
+ return this.enter(row.name);
413
+ }
414
+
415
+ openAt(id) {
416
+ const index = this.rows.findIndex((row) => row.name === id);
417
+ if (index < 0) return Promise.resolve();
418
+ this.cursor = index;
419
+ return this.openSelected();
420
+ }
421
+
422
+ show() {
423
+ const ui = this.ui;
424
+ if (!ui.workspaceRoot) return Promise.resolve();
425
+ ui.flushReview();
426
+ ui.dashboard.stop();
427
+ ui.lifecycle.stopWatch();
428
+ ui.loader.reset();
429
+ ui.cwd = ui.workspaceRoot;
430
+ ui.top = ui.workspaceRoot;
431
+ ui.paths = [];
432
+ ui.repoName = path.basename(ui.workspaceRoot);
433
+ ui.branch = '';
434
+ ui.rev = null;
435
+ ui.revShort = '';
436
+ ui.change = null;
437
+ ui.dashboardHome = false;
438
+ ui.collection.reset();
439
+ ui.review.reset();
440
+ ui.nav.pane = 'repos';
441
+ ui.nav.tasksOpen = false;
442
+ ui.nav.scroll = 0;
443
+ ui.nav.clearSelection();
444
+ ui.status = '';
445
+ ui.loader.didLoad = true;
446
+ this.start();
447
+ if (ui.uiOpen) {
448
+ ui.lifecycle.startWatch();
449
+ ui.paint();
450
+ }
451
+ return Promise.resolve();
452
+ }
453
+ }
454
+
455
+ module.exports = { Workspace, classifyRest };