reslop 0.2.7 → 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 +17 -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 +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} +123 -96
  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 +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  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/{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 +383 -0
  59. package/lib/render/frame.js +123 -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 +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} +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 +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -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} +156 -63
  120. package/lib/session/tasks.js +320 -65
  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} +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 -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,27 @@ 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.rev) ui.rev = loaded.rev;
173
+ if (loaded.revShort) ui.revShort = loaded.revShort;
174
+ else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
175
+ if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
176
+ if (loaded.change) ui.change = loaded.change;
177
+ if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
178
+ const repository = ui.change ? ui.change.repository : '';
179
+ if (!ui.repoName && repository) ui.repoName = repository;
180
+ }
181
+
164
182
  applyLoaded(loaded) {
165
183
  const ui = this.ui;
166
184
  if (ui.nav.pane === 'repos') return false;
@@ -169,21 +187,20 @@ class Lifecycle {
169
187
  if (!loader.isCurrent(loaded.generation, ui.done)) return false;
170
188
  }
171
189
  const wasTasks = nav.tasksOpen === true;
190
+ const firstLoad = !loader.didLoad;
172
191
  const here = ui.current();
173
192
  ui.top = loaded.top ?? ui.cwd;
193
+ this.applyPrefs();
174
194
  collection.replace(loaded.items);
175
195
  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;
196
+ this.applySource(loaded);
184
197
  if (!ui.review.store) ui.initReview();
185
198
  ui.review.applyImported(loaded.imported);
186
199
  this.restoreAfterLoad(here, wasTasks);
200
+ const issue = loaded.change && loaded.change.source === 'issue';
201
+ if (firstLoad && issue && !collection.items.length) {
202
+ ui.composer.tasks.openTasksPage();
203
+ }
187
204
  loader.didLoad = true;
188
205
  ui.updater.maybePrompt();
189
206
  loader.pendingExtras = loaded.pending === true;
@@ -193,10 +210,9 @@ class Lifecycle {
193
210
  return true;
194
211
  }
195
212
 
196
- load() {
197
- const ui = this.ui;
198
- const loaded = ui.repo.load(ui.cwd, ui.paths, this.loadOptions());
199
- this.applyLoaded(loaded);
213
+ async load() {
214
+ await this.openLoad();
215
+ if (this.ui.loadError) throw this.ui.loadError;
200
216
  }
201
217
 
202
218
  npmStamp() {
@@ -246,13 +262,18 @@ class Lifecycle {
246
262
  paths: ui.paths,
247
263
  signal: ui.loader.signal,
248
264
  });
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
- }
265
+ const takeExtras = async () => {
266
+ try {
267
+ const value = await fetchExtras(ui.repo, loaded, extra);
268
+ return { value };
269
+ } catch (error) {
270
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
271
+ return null;
272
+ }
273
+ };
274
+ const outcome = await takeExtras();
275
+ if (!outcome) return;
276
+ const next = outcome.value;
256
277
  if (!this.isWanted(gen)) return;
257
278
  if (next) {
258
279
  this.applyLoaded(tagLoaded(next, gen));
@@ -284,7 +305,7 @@ class Lifecycle {
284
305
 
285
306
  afterSnapshot(kind, extra = {}) {
286
307
  const nav = this.ui.nav;
287
- if (nav.pane === 'commits') this.ui.commits.refresh();
308
+ if (nav.pane === 'commits') this.ui.commits.reload();
288
309
  if (kind !== 'open' && kind !== 'watch') {
289
310
  nav.scroll = 0;
290
311
  nav.clearSelection();
@@ -299,15 +320,18 @@ class Lifecycle {
299
320
  ui.ops.setBusy('loading');
300
321
  ui.paint();
301
322
  }
302
- let loaded;
323
+ let loaded = null;
324
+ const side = ui.reads.read();
303
325
  try {
304
326
  const opts = this.snapshotOpts({ signal: ui.loader.signal });
305
327
  loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
306
328
  } catch (error) {
307
329
  if (ui.loader.isCurrent(gen)) this.failOpen(error);
308
- return;
309
330
  }
331
+ if (!loaded) return;
332
+ const found = await side;
310
333
  if (!this.isWanted(gen)) return;
334
+ ui.reads.apply(found);
311
335
  this.applyLoaded(tagLoaded(loaded, gen));
312
336
  this.afterSnapshot(kind, extra);
313
337
  if (loaded.pending === true || this.npmExtrasStale()) {
@@ -335,7 +359,8 @@ class Lifecycle {
335
359
  if (loader.didLoad && !loader.pendingExtras) {
336
360
  loader.promise = Promise.resolve();
337
361
  } else {
338
- loader.promise = this.runSnapshot(loader.bump(), 'open');
362
+ const run = this.runSnapshot(loader.bump(), 'open');
363
+ loader.promise = this.ui.ops.track(run);
339
364
  }
340
365
  return loader.promise;
341
366
  }
@@ -345,32 +370,15 @@ class Lifecycle {
345
370
  this.ui.loader.didLoad = true;
346
371
  return;
347
372
  }
348
- this.ui.ensureRepo();
349
- await this.openLoad();
350
- if (this.ui.loadError) throw this.ui.loadError;
373
+ await this.ui.ensureRepo();
374
+ await this.load();
351
375
  }
352
376
 
353
377
  refreshFromRepo(extra = {}) {
354
378
  const ui = this.ui;
355
379
  ui.loader.promise = null;
356
380
  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();
381
+ ui.ops.track(this.runSnapshot(gen, kindFromExtra(extra), extra));
374
382
  }
375
383
 
376
384
  reloadAfterChange(afterLoad) {
@@ -385,11 +393,7 @@ class Lifecycle {
385
393
  ui.syncFileCursor();
386
394
  if (afterLoad) afterLoad();
387
395
  };
388
- if (ui.uiOpen && ui.repo.loadAsync) {
389
- return void this.refreshFromRepo({ afterLoad: restore });
390
- }
391
- this.refreshFromRepo();
392
- restore();
396
+ this.refreshFromRepo({ afterLoad: restore });
393
397
  }
394
398
 
395
399
  onReload() {
@@ -455,12 +459,13 @@ class Lifecycle {
455
459
  return;
456
460
  }
457
461
  const store = this.ui.review.store;
458
- let disk;
462
+ let disk = null;
459
463
  try {
460
464
  disk = loadReview(store.reviewPath, store.templates);
461
465
  } catch {
462
- return;
466
+ // unreadable review
463
467
  }
468
+ if (!disk) return;
464
469
  mergeReview(store, disk);
465
470
  this.reviewStamp = stamp;
466
471
  this.ui.paint();
@@ -484,12 +489,10 @@ class Lifecycle {
484
489
  const repos = info ? info.repos : [];
485
490
  const paths = ['.'];
486
491
  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
- });
492
+ const matchReview = (rel) => rel.includes(`/${REVIEW_DIR}/`);
493
+ const onPaths = (rels) => ui.workspace.notePaths(rels);
494
+ const options = { root, paths, matchReview, onPaths };
495
+ this.diskWatcher = new DiskWatcher(options);
493
496
  return;
494
497
  }
495
498
  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,249 @@
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
+ command,
193
+ status: externalStatus(record),
194
+ exit: externalExit(record),
195
+ startedAt: Number(record.startedAt) || 0,
196
+ endedAt: Number(record.endedAt) || 0,
197
+ logName: logBase(record.log),
198
+ rawName: logBase(record.raw),
199
+ externalId: `${record.id ?? ''}`,
200
+ result: record.result ?? null,
201
+ progress: record.progress ?? null,
202
+ };
203
+ };
204
+
205
+ const whenLabel = (ms) => {
206
+ if (!ms) return '';
207
+ const date = new Date(ms);
208
+ const hh = pad2(date.getHours());
209
+ const mm = pad2(date.getMinutes());
210
+ const time = `${hh}:${mm}`;
211
+ const today = new Date();
212
+ const sameDay =
213
+ date.getFullYear() === today.getFullYear() &&
214
+ date.getMonth() === today.getMonth() &&
215
+ date.getDate() === today.getDate();
216
+ if (sameDay) return time;
217
+ const month = pad2(date.getMonth() + 1);
218
+ const day = pad2(date.getDate());
219
+ return `${month}-${day} ${time}`;
220
+ };
221
+
222
+ const elapsedText = (run, now) => {
223
+ const end = run.endedAt || now;
224
+ const ms = Math.max(0, end - (run.startedAt || end));
225
+ const sec = Math.round(ms / 1000);
226
+ if (sec < 60) return `${sec}s`;
227
+ const min = Math.floor(sec / 60);
228
+ const rest = sec % 60;
229
+ return `${min}m${pad2(rest)}s`;
230
+ };
231
+
232
+ module.exports = {
233
+ filteredOutput,
234
+ runLabel,
235
+ historyMark,
236
+ historyCounts,
237
+ countText,
238
+ historyExit,
239
+ noteTests,
240
+ RUN_KEEP,
241
+ launchedReslop,
242
+ historyName,
243
+ sameWindow,
244
+ logBase,
245
+ applyExternal,
246
+ recordedItem,
247
+ whenLabel,
248
+ elapsedText,
249
+ };