reslop 0.2.7 → 0.3.1

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 +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  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 +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  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} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -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 +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -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/{dash-blocks.js → dashboard/blocks.js} +131 -137
  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 +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  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} +13 -60
  72. package/lib/render/tiles.js +109 -48
  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} +48 -19
  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 +14 -0
  81. package/lib/review/format.js +420 -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 +976 -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 +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -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} +158 -63
  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 +65 -65
  126. package/lib/session/workspace.js +39 -78
  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} +4 -3
  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 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  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
@@ -3,20 +3,16 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const utilities = require('../utilities.js');
7
- const { isAbort } = utilities;
8
- const sessionLoad = require('./load.js');
9
- const { tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
10
- const watch = require('./watch.js');
11
- const { DiskWatcher, DEBOUNCE_MS } = watch;
12
- const files = require('../files.js');
13
- const { itemPath, relativeAge } = files;
14
- const deps = require('../deps.js');
15
- const { MANIFEST, LOCKFILE, depFileMeta } = deps;
16
- const review = require('../review.js');
17
- const { hasNotes, loadReview } = review;
18
- const reviewMerge = require('../review-merge.js');
19
- const { mergeReview } = reviewMerge;
6
+ const { isAbort } = require('../common/process.js');
7
+ const { tagLoaded, fetchSnapshot, fetchExtras } = require('./load.js');
8
+ const { DiskWatcher, DEBOUNCE_MS } = require('../dashboard/watch.js');
9
+ const { itemPath, REVIEW_DIR } = require('../common/files.js');
10
+ const { relativeAge } = require('../common/format.js');
11
+ const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps/deps.js');
12
+ const { hasNotes, loadReview } = require('../review/review.js');
13
+ const { setTheme } = require('../term/ansi.js');
14
+ const { readUiPrefs } = require('./prefs.js');
15
+ const { mergeReview } = require('../review/merge.js');
20
16
 
21
17
  const WATCH_IGNORE_MS = 500;
22
18
 
@@ -147,8 +143,9 @@ class Lifecycle {
147
143
  syncUnitEditor() {
148
144
  const ui = this.ui;
149
145
  const rel = ui.nav.reviewPath;
150
- if (ui.nav.pane !== 'unit' || !rel || !ui.repo.fileText) return;
151
- ui.composer.applyDiskText(ui.repo.fileText(this.root, rel, ui.rev));
146
+ if (ui.nav.pane !== 'unit' || !rel) return;
147
+ const text = ui.reads.cached(rel);
148
+ if (text !== null) ui.composer.applyDiskText(text);
152
149
  }
153
150
 
154
151
  syncItemWatches() {
@@ -161,6 +158,30 @@ class Lifecycle {
161
158
  this.diskWatcher.watchFiles(rels);
162
159
  }
163
160
 
161
+ applyPrefs() {
162
+ const ui = this.ui;
163
+ const saved = readUiPrefs(ui.top);
164
+ ui.lineNumbers = saved.lineNumbers;
165
+ ui.layout = saved.layout;
166
+ ui.commits.commitView = saved.commitView;
167
+ setTheme(saved.theme);
168
+ }
169
+
170
+ applySource(loaded) {
171
+ const ui = this.ui;
172
+ if (loaded.repositoryUrl !== undefined) {
173
+ ui.repositoryUrl = loaded.repositoryUrl || '';
174
+ }
175
+ if (loaded.rev) ui.rev = loaded.rev;
176
+ if (loaded.revShort) ui.revShort = loaded.revShort;
177
+ else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
178
+ if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
179
+ if (loaded.change) ui.change = loaded.change;
180
+ if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
181
+ const repository = ui.change ? ui.change.repository : '';
182
+ if (!ui.repoName && repository) ui.repoName = repository;
183
+ }
184
+
164
185
  applyLoaded(loaded) {
165
186
  const ui = this.ui;
166
187
  if (ui.nav.pane === 'repos') return false;
@@ -169,21 +190,20 @@ class Lifecycle {
169
190
  if (!loader.isCurrent(loaded.generation, ui.done)) return false;
170
191
  }
171
192
  const wasTasks = nav.tasksOpen === true;
193
+ const firstLoad = !loader.didLoad;
172
194
  const here = ui.current();
173
195
  ui.top = loaded.top ?? ui.cwd;
196
+ this.applyPrefs();
174
197
  collection.replace(loaded.items);
175
198
  stampItemDates(ui.top, collection.items);
176
- if (loaded.rev) ui.rev = loaded.rev;
177
- if (loaded.revShort) ui.revShort = loaded.revShort;
178
- else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
179
- if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
180
- if (loaded.change) ui.change = loaded.change;
181
- if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
182
- const repository = ui.change ? ui.change.repository : '';
183
- if (!ui.repoName && repository) ui.repoName = repository;
199
+ this.applySource(loaded);
184
200
  if (!ui.review.store) ui.initReview();
185
201
  ui.review.applyImported(loaded.imported);
186
202
  this.restoreAfterLoad(here, wasTasks);
203
+ const issue = loaded.change && loaded.change.source === 'issue';
204
+ if (firstLoad && issue && !collection.items.length) {
205
+ ui.composer.tasks.openTasksPage();
206
+ }
187
207
  loader.didLoad = true;
188
208
  ui.updater.maybePrompt();
189
209
  loader.pendingExtras = loaded.pending === true;
@@ -193,10 +213,9 @@ class Lifecycle {
193
213
  return true;
194
214
  }
195
215
 
196
- load() {
197
- const ui = this.ui;
198
- const loaded = ui.repo.load(ui.cwd, ui.paths, this.loadOptions());
199
- this.applyLoaded(loaded);
216
+ async load() {
217
+ await this.openLoad();
218
+ if (this.ui.loadError) throw this.ui.loadError;
200
219
  }
201
220
 
202
221
  npmStamp() {
@@ -246,13 +265,18 @@ class Lifecycle {
246
265
  paths: ui.paths,
247
266
  signal: ui.loader.signal,
248
267
  });
249
- let next;
250
- try {
251
- next = await fetchExtras(ui.repo, loaded, extra);
252
- } catch (error) {
253
- if (ui.loader.isCurrent(gen)) this.failOpen(error);
254
- return;
255
- }
268
+ const takeExtras = async () => {
269
+ try {
270
+ const value = await fetchExtras(ui.repo, loaded, extra);
271
+ return { value };
272
+ } catch (error) {
273
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
274
+ return null;
275
+ }
276
+ };
277
+ const outcome = await takeExtras();
278
+ if (!outcome) return;
279
+ const next = outcome.value;
256
280
  if (!this.isWanted(gen)) return;
257
281
  if (next) {
258
282
  this.applyLoaded(tagLoaded(next, gen));
@@ -284,7 +308,7 @@ class Lifecycle {
284
308
 
285
309
  afterSnapshot(kind, extra = {}) {
286
310
  const nav = this.ui.nav;
287
- if (nav.pane === 'commits') this.ui.commits.refresh();
311
+ if (nav.pane === 'commits') this.ui.commits.reload();
288
312
  if (kind !== 'open' && kind !== 'watch') {
289
313
  nav.scroll = 0;
290
314
  nav.clearSelection();
@@ -299,15 +323,18 @@ class Lifecycle {
299
323
  ui.ops.setBusy('loading');
300
324
  ui.paint();
301
325
  }
302
- let loaded;
326
+ let loaded = null;
327
+ const side = ui.reads.read();
303
328
  try {
304
329
  const opts = this.snapshotOpts({ signal: ui.loader.signal });
305
330
  loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
306
331
  } catch (error) {
307
332
  if (ui.loader.isCurrent(gen)) this.failOpen(error);
308
- return;
309
333
  }
334
+ if (!loaded) return;
335
+ const found = await side;
310
336
  if (!this.isWanted(gen)) return;
337
+ ui.reads.apply(found);
311
338
  this.applyLoaded(tagLoaded(loaded, gen));
312
339
  this.afterSnapshot(kind, extra);
313
340
  if (loaded.pending === true || this.npmExtrasStale()) {
@@ -335,7 +362,8 @@ class Lifecycle {
335
362
  if (loader.didLoad && !loader.pendingExtras) {
336
363
  loader.promise = Promise.resolve();
337
364
  } else {
338
- loader.promise = this.runSnapshot(loader.bump(), 'open');
365
+ const run = this.runSnapshot(loader.bump(), 'open');
366
+ loader.promise = this.ui.ops.track(run);
339
367
  }
340
368
  return loader.promise;
341
369
  }
@@ -345,32 +373,15 @@ class Lifecycle {
345
373
  this.ui.loader.didLoad = true;
346
374
  return;
347
375
  }
348
- this.ui.ensureRepo();
349
- await this.openLoad();
350
- if (this.ui.loadError) throw this.ui.loadError;
376
+ await this.ui.ensureRepo();
377
+ await this.load();
351
378
  }
352
379
 
353
380
  refreshFromRepo(extra = {}) {
354
381
  const ui = this.ui;
355
382
  ui.loader.promise = null;
356
383
  const gen = ui.loader.bump();
357
- const kind = kindFromExtra(extra);
358
- if (ui.uiOpen && ui.repo.loadAsync) {
359
- return void this.runSnapshot(gen, kind, extra);
360
- }
361
- const loaded = ui.repo.load(ui.cwd, ui.paths, this.snapshotOpts(extra));
362
- this.applyLoaded(tagLoaded(loaded, gen));
363
- this.afterSnapshot(kind, extra);
364
- if (loaded.pending === true || this.npmExtrasStale()) {
365
- ui.ops.setBusy('checking npm');
366
- return void this.followExtras(loaded, gen, kind);
367
- }
368
- if (kind === 'keep') return;
369
- this.finishIfEmpty(kind);
370
- if (extra.doneStatus && !ui.done && !ui.loader.pendingExtras) {
371
- ui.status = extra.doneStatus;
372
- }
373
- if (kind === 'watch') ui.paint();
384
+ ui.ops.track(this.runSnapshot(gen, kindFromExtra(extra), extra));
374
385
  }
375
386
 
376
387
  reloadAfterChange(afterLoad) {
@@ -385,11 +396,7 @@ class Lifecycle {
385
396
  ui.syncFileCursor();
386
397
  if (afterLoad) afterLoad();
387
398
  };
388
- if (ui.uiOpen && ui.repo.loadAsync) {
389
- return void this.refreshFromRepo({ afterLoad: restore });
390
- }
391
- this.refreshFromRepo();
392
- restore();
399
+ this.refreshFromRepo({ afterLoad: restore });
393
400
  }
394
401
 
395
402
  onReload() {
@@ -455,12 +462,13 @@ class Lifecycle {
455
462
  return;
456
463
  }
457
464
  const store = this.ui.review.store;
458
- let disk;
465
+ let disk = null;
459
466
  try {
460
467
  disk = loadReview(store.reviewPath, store.templates);
461
468
  } catch {
462
- return;
469
+ // unreadable review
463
470
  }
471
+ if (!disk) return;
464
472
  mergeReview(store, disk);
465
473
  this.reviewStamp = stamp;
466
474
  this.ui.paint();
@@ -484,12 +492,10 @@ class Lifecycle {
484
492
  const repos = info ? info.repos : [];
485
493
  const paths = ['.'];
486
494
  for (const repo of repos) paths.push(path.join(repo.name, '.git'));
487
- this.diskWatcher = new DiskWatcher({
488
- root,
489
- paths,
490
- matchReview: (rel) => rel.includes('/.review/'),
491
- onPaths: (rels) => ui.workspace.notePaths(rels),
492
- });
495
+ const matchReview = (rel) => rel.includes(`/${REVIEW_DIR}/`);
496
+ const onPaths = (rels) => ui.workspace.notePaths(rels);
497
+ const options = { root, paths, matchReview, onPaths };
498
+ this.diskWatcher = new DiskWatcher(options);
493
499
  return;
494
500
  }
495
501
  const root = this.root;
@@ -40,10 +40,8 @@ class LoadCoordinator {
40
40
 
41
41
  const tagLoaded = (loaded, gen) => ({ ...loaded, generation: gen });
42
42
 
43
- const fetchSnapshot = async (repo, cwd, paths, options) => {
44
- if (repo.loadAsync) return repo.loadAsync(cwd, paths, options);
45
- return repo.load(cwd, paths, options);
46
- };
43
+ const fetchSnapshot = async (repo, cwd, paths, options) =>
44
+ repo.load(cwd, paths, options);
47
45
 
48
46
  const fetchExtras = async (repo, loaded, extra) => {
49
47
  if (!repo.loadExtras) return null;
@@ -1,9 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const files = require('../files.js');
3
+ const files = require('../common/files.js');
4
4
  const { listPath, isTaskItem, isTasksEntry, TASKS_FILE } = files;
5
- const actions = require('./actions.js');
6
- const { LIST_PANES } = actions;
5
+ const { LIST_PANES } = require('../input/actions.js');
7
6
 
8
7
  const freshListScroll = () =>
9
8
  Object.fromEntries(LIST_PANES.map((pane) => [pane, 0]));
@@ -21,6 +20,7 @@ class Navigation {
21
20
  this.commitCursor = 0;
22
21
  this.npmCursor = 0;
23
22
  this.packagesCursor = 0;
23
+ this.agentCursor = 0;
24
24
  this.scroll = 0;
25
25
  this.listScrollByPane = freshListScroll();
26
26
  this.reviewPath = null;
@@ -29,10 +29,12 @@ class Navigation {
29
29
  this.selection = null;
30
30
  this.mouseAnchor = null;
31
31
  this.pendingClick = null;
32
+ this.scrollDrag = null;
32
33
  this.lastClick = null;
33
34
  this.tasksOpen = false;
34
35
  this.tasksFocus = 0;
35
36
  this.find = null;
37
+ this.import = null;
36
38
  }
37
39
 
38
40
  clampIndex(length) {
@@ -98,6 +100,7 @@ class Navigation {
98
100
  this.selection = null;
99
101
  this.mouseAnchor = null;
100
102
  this.pendingClick = null;
103
+ this.scrollDrag = null;
101
104
  }
102
105
 
103
106
  get listScroll() {
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ const { stripAnsi } = require('../../term/ansi.js');
4
+ const { buildDocument } = require('../../report/parse.js');
5
+ const report = require('../../report/render.js');
6
+ const { renderDocument, renderReport } = report;
7
+ const runStore = require('../../runs/runs.js');
8
+ const { Tracker, summarizeDocument } = runStore;
9
+ const { runName } = require('../../dashboard/model.js');
10
+ const { reduceOutput } = require('../../runs/output.js');
11
+ const { trimText, pad2 } = require('../../common/utilities.js');
12
+
13
+ const TESTS_RE = /^(?:TAP version \d+|\s*(?:not )?ok\b|[ℹi]\s+tests\s+\d+)/m;
14
+
15
+ const filteredOutput = (raw, root, status, rawFile) => {
16
+ const text = stripAnsi(`${raw ?? ''}`);
17
+ if (status === '' || !TESTS_RE.test(text)) {
18
+ const output = reduceOutput(raw, root, status);
19
+ return { output, log: output, result: null };
20
+ }
21
+ const doc = buildDocument(raw, '', root, status, 'test');
22
+ const result = summarizeDocument(doc);
23
+ const output = renderReport(doc);
24
+ const log = renderDocument(doc, rawFile);
25
+ return { output, log, result };
26
+ };
27
+
28
+ const runLabel = (entry) =>
29
+ entry.kind === 'bin' ? `npm exec ${entry.name}` : `npm run ${entry.name}`;
30
+
31
+ const historyMark = (run) => {
32
+ if (run.status === 'running') return 'run';
33
+ if (run.status === 'stopped') return 'stop';
34
+ if (run.exit === 0 || run.exit === '0') return 'pass';
35
+ return 'fail';
36
+ };
37
+
38
+ const historyCounts = (run) => {
39
+ const result = run.result;
40
+ if (result && typeof result.tests === 'number') {
41
+ const fail = result.failed ?? 0;
42
+ const ok = result.passed ?? Math.max(0, result.tests - fail);
43
+ return { done: result.tests, ok, fail, total: result.tests };
44
+ }
45
+ const progress = run.progress;
46
+ if (!progress) return null;
47
+ const ok = progress.done ?? 0;
48
+ const fail = progress.failed ?? 0;
49
+ if (!ok && !fail && !(progress.expected > 0)) return null;
50
+ const total = progress.expected > 0 ? progress.expected : null;
51
+ return { done: ok + fail, ok, fail, total };
52
+ };
53
+
54
+ const countText = (value) => (typeof value === 'number' ? `${value}` : '');
55
+
56
+ const historyExit = (run, counts) => {
57
+ if (counts || run.status !== 'exited') return '';
58
+ if (run.exit === '' || run.exit === null || run.exit === undefined) return '';
59
+ return `exit ${run.exit}`;
60
+ };
61
+
62
+ const earlierTotal = (list, run) => {
63
+ let best = 0;
64
+ let at = -1;
65
+ for (const item of list) {
66
+ if (item === run || item.name !== run.name) continue;
67
+ if (item.status === 'running') continue;
68
+ const counts = historyCounts(item);
69
+ if (!counts || typeof counts.total !== 'number') continue;
70
+ const started = item.startedAt || 0;
71
+ if (started < at) continue;
72
+ at = started;
73
+ best = counts.total;
74
+ }
75
+ return best;
76
+ };
77
+
78
+ const noteTests = (run, list, text) => {
79
+ const plain = stripAnsi(`${text ?? ''}`);
80
+ if (!TESTS_RE.test(plain)) return;
81
+ const tracker = new Tracker();
82
+ tracker.feed(plain);
83
+ if (!Object.hasOwn(run, 'expected')) run.expected = earlierTotal(list, run);
84
+ run.progress = {
85
+ done: tracker.progress.done,
86
+ failed: tracker.progress.failed,
87
+ expected: run.expected,
88
+ };
89
+ };
90
+
91
+ const RUN_KEEP = 50;
92
+
93
+ const LOG_GAP_MS = 2000;
94
+
95
+ const RESLOP_RUN = /^reslop(?:\.js)?\s+t\s+--\s+/;
96
+
97
+ const RESLOP_T = /reslop(?:\.js)?\s+t\s+--/;
98
+
99
+ const launchedReslop = (run) => {
100
+ if (run.externalId) return false;
101
+ const command = `${run.command ?? ''}`;
102
+ return RESLOP_T.test(command);
103
+ };
104
+
105
+ const historyName = (run) => {
106
+ const script = trimText(run.name);
107
+ if (script) return script;
108
+ return trimText(run.command).replace(RESLOP_RUN, '');
109
+ };
110
+
111
+ const sameWindow = (left, right) => {
112
+ const start = Number(left.startedAt) || 0;
113
+ const end = Number(left.endedAt) || start;
114
+ const at = Number(right.startedAt) || 0;
115
+ const done = Number(right.endedAt) || at;
116
+ return done >= start - LOG_GAP_MS && at <= end + LOG_GAP_MS;
117
+ };
118
+
119
+ const logBase = (name) => {
120
+ const file = `${name ?? ''}`;
121
+ if (!file || file.includes('/') || file.includes('\\')) return '';
122
+ return file;
123
+ };
124
+
125
+ const externalStatus = (record) => {
126
+ if (record.status === 'running') return 'running';
127
+ if (record.status === 'aborted') return 'stopped';
128
+ return 'exited';
129
+ };
130
+
131
+ const externalExit = (record) => {
132
+ if (record.status === 'running' || record.status === 'aborted') return '';
133
+ if (record.exit === null || record.exit === undefined) return '';
134
+ return record.exit;
135
+ };
136
+
137
+ const progressCount = (progress, key) => {
138
+ if (!progress || typeof progress[key] !== 'number') return 0;
139
+ return progress[key];
140
+ };
141
+
142
+ const progressSame = (left, right) => {
143
+ const done = progressCount(left, 'done') === progressCount(right, 'done');
144
+ const leftFailed = progressCount(left, 'failed');
145
+ const rightFailed = progressCount(right, 'failed');
146
+ const failed = leftFailed === rightFailed;
147
+ const leftExpected = progressCount(left, 'expected');
148
+ const rightExpected = progressCount(right, 'expected');
149
+ const expected = leftExpected === rightExpected;
150
+ return done && failed && expected;
151
+ };
152
+
153
+ const resultSame = (left, right) => {
154
+ if (!left && !right) return true;
155
+ if (!left || !right) return false;
156
+ return (
157
+ left.tests === right.tests &&
158
+ left.passed === right.passed &&
159
+ left.failed === right.failed &&
160
+ left.errors === right.errors
161
+ );
162
+ };
163
+
164
+ const applyExternal = (run, record) => {
165
+ const status = externalStatus(record);
166
+ const exit = externalExit(record);
167
+ const endedAt = Number(record.endedAt) || 0;
168
+ const progress = record.progress ?? null;
169
+ const result = record.result ?? null;
170
+ const same =
171
+ run.status === status &&
172
+ run.exit === exit &&
173
+ (run.endedAt || 0) === endedAt &&
174
+ progressSame(run.progress, progress) &&
175
+ resultSame(run.result, result);
176
+ if (same) return false;
177
+ run.status = status;
178
+ run.exit = exit;
179
+ run.endedAt = endedAt;
180
+ run.progress = progress;
181
+ run.result = result;
182
+ return true;
183
+ };
184
+
185
+ const recordedItem = (record) => {
186
+ const script = trimText(record.script);
187
+ const command = trimText(record.command);
188
+ const name = runName({ command, script }).replace(RESLOP_RUN, '');
189
+ return {
190
+ name,
191
+ kind: script ? 'script' : 'command',
192
+ script,
193
+ command,
194
+ status: externalStatus(record),
195
+ exit: externalExit(record),
196
+ startedAt: Number(record.startedAt) || 0,
197
+ endedAt: Number(record.endedAt) || 0,
198
+ logName: logBase(record.log),
199
+ rawName: logBase(record.raw),
200
+ externalId: `${record.id ?? ''}`,
201
+ result: record.result ?? null,
202
+ progress: record.progress ?? null,
203
+ };
204
+ };
205
+
206
+ const whenLabel = (ms) => {
207
+ if (!ms) return '';
208
+ const date = new Date(ms);
209
+ const hh = pad2(date.getHours());
210
+ const mm = pad2(date.getMinutes());
211
+ const time = `${hh}:${mm}`;
212
+ const today = new Date();
213
+ const sameDay =
214
+ date.getFullYear() === today.getFullYear() &&
215
+ date.getMonth() === today.getMonth() &&
216
+ date.getDate() === today.getDate();
217
+ if (sameDay) return time;
218
+ const month = pad2(date.getMonth() + 1);
219
+ const day = pad2(date.getDate());
220
+ return `${month}-${day} ${time}`;
221
+ };
222
+
223
+ const elapsedText = (run, now) => {
224
+ const end = run.endedAt || now;
225
+ const ms = Math.max(0, end - (run.startedAt || end));
226
+ const sec = Math.round(ms / 1000);
227
+ if (sec < 60) return `${sec}s`;
228
+ const min = Math.floor(sec / 60);
229
+ const rest = sec % 60;
230
+ return `${min}m${pad2(rest)}s`;
231
+ };
232
+
233
+ module.exports = {
234
+ filteredOutput,
235
+ runLabel,
236
+ historyMark,
237
+ historyCounts,
238
+ countText,
239
+ historyExit,
240
+ noteTests,
241
+ RUN_KEEP,
242
+ launchedReslop,
243
+ historyName,
244
+ sameWindow,
245
+ logBase,
246
+ applyExternal,
247
+ recordedItem,
248
+ whenLabel,
249
+ elapsedText,
250
+ };