reslop 0.2.1 → 0.2.4

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 (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
package/lib/deps.js CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const { jsonParse, isHashObject } = require('metautil');
6
- const { itemPath } = require('./files.js');
5
+ const metautil = require('metautil');
6
+ const { jsonParse, isHashObject } = metautil;
7
+ const files = require('./files.js');
8
+ const { itemPath } = files;
7
9
  const manifest = require('./manifest.js');
8
10
  const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
9
11
  const { listFilePath, readSections, lockEntries, lockPackageCount } = manifest;
@@ -73,6 +75,39 @@ const pkgHasExportEntry = (pkg) => {
73
75
  return Object.keys(exported).length > 0;
74
76
  };
75
77
 
78
+ const isRuntimeFile = (value) => {
79
+ if (typeof value !== 'string' || !value) return false;
80
+ return !value.endsWith('.d.ts');
81
+ };
82
+
83
+ const hasRuntimeExport = (value) => {
84
+ if (isRuntimeFile(value)) return true;
85
+ if (Array.isArray(value)) return value.some(hasRuntimeExport);
86
+ if (!value || typeof value !== 'object') return false;
87
+ const keys = Object.keys(value);
88
+ for (const key of keys) {
89
+ if (key === 'types' || key === 'typings') continue;
90
+ if (hasRuntimeExport(value[key])) return true;
91
+ }
92
+ return false;
93
+ };
94
+
95
+ const pkgHasBin = (pkg) => {
96
+ if (!pkg || typeof pkg !== 'object') return false;
97
+ const bin = pkg.bin;
98
+ if (typeof bin === 'string') return !!bin;
99
+ if (!bin || typeof bin !== 'object' || Array.isArray(bin)) return false;
100
+ return Object.keys(bin).length > 0;
101
+ };
102
+
103
+ const pkgExportsLibrary = (pkg) => {
104
+ if (!pkg || typeof pkg !== 'object') return false;
105
+ if (pkgHasBin(pkg)) return false;
106
+ if (isRuntimeFile(pkg.main)) return true;
107
+ if (isRuntimeFile(pkg.module)) return true;
108
+ return hasRuntimeExport(pkg.exports);
109
+ };
110
+
76
111
  const SKIP_WALK = ['node_modules', '.git', '.review', 'coverage', 'dist'];
77
112
 
78
113
  const collectUsedFromManifest = (used, rel) => {
@@ -156,8 +191,8 @@ const readDepPkg = (root, pkgDir, name) => {
156
191
 
157
192
  const depHasExportEntry = (root, pkgDir, name) => {
158
193
  const pkg = readDepPkg(root, pkgDir, name);
159
- if (!pkg) return true;
160
- return pkgHasExportEntry(pkg);
194
+ if (!pkg) return false;
195
+ return pkgExportsLibrary(pkg);
161
196
  };
162
197
 
163
198
  const collectExportEntries = (root, pkgDir, names) => {
@@ -180,14 +215,7 @@ const declaredDepNames = (root, pkgDir) => {
180
215
  return [];
181
216
  }
182
217
  const maps = readSections(jsonParse(text));
183
- let size = 0;
184
- for (const key of SECTION_KEYS) size += maps[key].size;
185
- const names = new Array(size);
186
- let i = 0;
187
- for (const key of SECTION_KEYS) {
188
- for (const name of maps[key].keys()) names[i++] = name;
189
- }
190
- return names;
218
+ return SECTION_KEYS.flatMap((key) => [...maps[key].keys()]);
191
219
  };
192
220
 
193
221
  const collectExportMeta = (root, extra = {}) => {
@@ -342,16 +370,7 @@ const markChanges = (changes, report, kind) => {
342
370
  });
343
371
  };
344
372
 
345
- const markAuditChanges = (changes, audit) =>
346
- markChanges(changes, audit, 'audit');
347
-
348
- const markOutdatedChanges = (changes, outdated) =>
349
- markChanges(changes, outdated, 'outdated');
350
-
351
- const depExports = (exportEntries, name) => {
352
- if (!exportEntries || !exportEntries.has(name)) return true;
353
- return exportEntries.get(name) === true;
354
- };
373
+ const depExports = (exportEntries, name) => exportEntries?.get(name) === true;
355
374
 
356
375
  const isAddedDep = (change) => {
357
376
  if (change.action !== 'added') return false;
@@ -389,23 +408,12 @@ const unusedDeclaredNames = (pkg, used) => {
389
408
  };
390
409
 
391
410
  const markUnusedChanges = (changes, used, pkg, exportEntries) => {
392
- if (!pkgHasExportEntry(pkg)) return changes;
393
- if (!used || !used.size) return changes;
394
- const { length } = changes;
395
- const marked = new Array(length);
396
- for (let i = 0; i < length; i++) {
397
- const change = changes[i];
398
- if (!isAddedDep(change) || used.has(change.name)) {
399
- marked[i] = change;
400
- continue;
401
- }
402
- if (!depExports(exportEntries, change.name)) {
403
- marked[i] = change;
404
- continue;
405
- }
406
- marked[i] = { ...change, unused: true, propose: true };
407
- }
408
- return marked;
411
+ if (!pkgHasExportEntry(pkg) || !used?.size) return changes;
412
+ return changes.map((change) => {
413
+ if (!isAddedDep(change) || used.has(change.name)) return change;
414
+ if (!depExports(exportEntries, change.name)) return change;
415
+ return { ...change, unused: true, propose: true };
416
+ });
409
417
  };
410
418
 
411
419
  const unusedDeclaredChanges = (pkg, used, exportEntries) => {
@@ -511,18 +519,16 @@ const loadExportEntries = (names, extra = {}, pkgDir) => {
511
519
  if (!missing.length) return supplied ?? new Map();
512
520
  const found = collectExportEntries(extra.root, dir, missing);
513
521
  if (!supplied) return found;
514
- const merged = new Map(supplied);
515
- for (const name of found.keys()) merged.set(name, found.get(name));
516
- return merged;
522
+ return new Map([...supplied, ...found]);
517
523
  };
518
524
 
519
525
  const groupedSynthetics = (synthetic) => {
520
526
  const byPath = new Map();
521
- for (const key of synthetic.keys()) {
527
+ for (const [key, items] of synthetic) {
522
528
  const dir = key.slice(key.indexOf('\0') + 1);
523
529
  const rel = relOf(dir, MANIFEST);
524
530
  const list = byPath.get(rel) ?? [];
525
- for (const item of synthetic.get(key)) list.push(item);
531
+ for (const item of items) list.push(item);
526
532
  byPath.set(rel, list);
527
533
  }
528
534
  return byPath;
@@ -530,12 +536,10 @@ const groupedSynthetics = (synthetic) => {
530
536
 
531
537
  const spliceAtSlots = (out, synthetic, slot) => {
532
538
  const byPath = groupedSynthetics(synthetic);
533
- const inserts = new Array(byPath.size);
534
- let i = 0;
535
- for (const rel of byPath.keys()) {
536
- const at = slot.has(rel) ? slot.get(rel) : out.length;
537
- inserts[i++] = { at, items: byPath.get(rel) };
538
- }
539
+ const inserts = [...byPath].map(([rel, items]) => ({
540
+ at: slot.get(rel) ?? out.length,
541
+ items,
542
+ }));
539
543
  inserts.sort((left, right) => right.at - left.at);
540
544
  for (const insert of inserts) {
541
545
  out.splice(insert.at, 0, ...insert.items);
@@ -570,8 +574,7 @@ const foldDepItems = (
570
574
  const folded = new Set();
571
575
  const synthetic = new Map();
572
576
  const leftoverNames = new Map();
573
- for (const key of groups.keys()) {
574
- const group = groups.get(key);
577
+ for (const [key, group] of groups) {
575
578
  const summary = summarizeGroup(group, readSides);
576
579
  const unusedNames = unusedChangeNames(summary.changes, usedNames);
577
580
  const exportEntries = loadExportEntries(unusedNames, extra, group.dir);
@@ -581,8 +584,8 @@ const foldDepItems = (
581
584
  summary.pkg,
582
585
  exportEntries,
583
586
  );
584
- const audited = markAuditChanges(unused, audit);
585
- const changes = markOutdatedChanges(audited, outdated);
587
+ const audited = markChanges(unused, audit, 'audit');
588
+ const changes = markChanges(audited, outdated, 'outdated');
586
589
  const next = { ...summary, changes };
587
590
  leftoverNames.set(key, foldedDepNames(next.changes));
588
591
  const taken = takeManifestItems(next);
package/lib/detect.js CHANGED
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
- const { fileExt } = require('metautil');
4
+ const metautil = require('metautil');
5
+ const { fileExt } = metautil;
5
6
 
6
- const { mapLang } = require('./highlight.js');
7
+ const highlight = require('./highlight.js');
8
+ const { mapLang } = highlight;
7
9
 
8
10
  const PROSE_BASENAMES = [
9
11
  'license',
package/lib/diff/align.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { diffChars, tokenSimilarity } = require('./inline.js');
3
+ const inline = require('./inline.js');
4
+ const { diffChars, tokenSimilarity } = inline;
4
5
 
5
6
  const PAIR_MIN_SIM = 0.3;
6
7
  const PAIR_HEAD_WEIGHT = 0.4;
package/lib/diff/diff.js CHANGED
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { diffChars } = require('./inline.js');
4
- const { isCtxType, pairIndices, attachInline } = require('./align.js');
5
- const { parseDiff } = require('./parse.js');
3
+ const inline = require('./inline.js');
4
+ const { diffChars } = inline;
5
+ const align = require('./align.js');
6
+ const { isCtxType, pairIndices, attachInline } = align;
7
+ const parse = require('./parse.js');
8
+ const { parseDiff } = parse;
6
9
  const patch = require('./patch.js');
7
10
  const { splitHunk, flattenBlock, countSides, formatPatch } = patch;
8
11
  const display = require('./display.js');
@@ -2,7 +2,8 @@
2
2
 
3
3
  const align = require('./align.js');
4
4
  const { isCtxType, attachInline, LAYOUT_ALIGN } = align;
5
- const { flattenBlock } = require('./patch.js');
5
+ const patch = require('./patch.js');
6
+ const { flattenBlock } = patch;
6
7
 
7
8
  const DISPLAY_CONTEXT = 3;
8
9
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { graphemes } = require('../ansi.js');
3
+ const ansi = require('../ansi.js');
4
+ const { graphemes } = ansi;
4
5
 
5
6
  const LCS_CELL_LIMIT = 250000;
6
7
  const NOISY_TINY_SPANS = 6;
package/lib/diff/patch.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { isCtxType } = require('./align.js');
3
+ const align = require('./align.js');
4
+ const { isCtxType } = align;
4
5
 
5
6
  const LINE_MARK = {
6
7
  add: '+',
package/lib/diff/unit.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { attachInline } = require('./align.js');
4
- const { listPath } = require('../files.js');
3
+ const align = require('./align.js');
4
+ const { attachInline } = align;
5
+ const files = require('../files.js');
6
+ const { listPath } = files;
5
7
 
6
8
  const splitEditor = (text) => {
7
9
  const lines = `${text}`.split('\n');
package/lib/editor.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { graphemes } = require('./ansi.js');
4
- const { wrapMove } = require('./wrap.js');
3
+ const ansi = require('./ansi.js');
4
+ const { graphemes, visibleWidth } = ansi;
5
+ const wrap = require('./wrap.js');
6
+ const { wrapMove } = wrap;
5
7
 
6
8
  const UNDO_CAP = 100;
7
9
 
@@ -59,6 +61,7 @@ class Editor {
59
61
  this.undo = [];
60
62
  this.redo = [];
61
63
  this.wantCol = null;
64
+ this.scrollCol = 0;
62
65
  }
63
66
 
64
67
  snapshot() {
@@ -132,6 +135,19 @@ class Editor {
132
135
  return { line, col };
133
136
  }
134
137
 
138
+ visibleLineCol() {
139
+ const { col, line } = this.linePos();
140
+ const row = this.text.split('\n')[line] ?? '';
141
+ return visibleWidth(row.slice(0, col));
142
+ }
143
+
144
+ reveal(width) {
145
+ const col = this.visibleLineCol();
146
+ const room = Math.max(1, width);
147
+ if (col < this.scrollCol) this.scrollCol = col;
148
+ if (col >= this.scrollCol + room) this.scrollCol = col - room + 1;
149
+ }
150
+
135
151
  moveLine(delta, width) {
136
152
  if (width !== undefined) {
137
153
  const col = this.wantCol;
@@ -142,8 +158,10 @@ class Editor {
142
158
  }
143
159
  const rows = this.text.split('\n');
144
160
  const { line, col } = this.linePos();
145
- const next = line + delta;
146
- if (next < 0 || next >= rows.length) return;
161
+ let next = line + delta;
162
+ if (next < 0) next = 0;
163
+ if (next >= rows.length) next = rows.length - 1;
164
+ if (next === line) return;
147
165
  const row = rows[next];
148
166
  const c = Math.min(col, row.length);
149
167
  let cursor = 0;
package/lib/files.js CHANGED
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const { listFilePath } = require('./manifest.js');
3
+ const manifest = require('./manifest.js');
4
+ const { listFilePath } = manifest;
4
5
 
5
6
  const REVIEW_DIR = '.review';
6
7
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
7
8
  const TODO_FILE = 'TODOs';
8
- const REPO_TODOS_LABEL = 'Repository TODOs and Issues';
9
+ const REPO_TODOS_LABEL = 'Project Backlog and Issues';
9
10
  const TOTAL_LABEL = 'total';
10
11
 
11
12
  const isPathInScope = (rel, paths) => {
@@ -73,23 +74,47 @@ const lineDelta = ({ hunk, blockId }) => {
73
74
  return delta;
74
75
  };
75
76
 
76
- const countAgo = (n, unit) => {
77
- const label = n === 1 ? unit : `${unit}s`;
78
- return `${n} ${label} ago`;
77
+ const AGE_UNIT = {
78
+ second: 's',
79
+ minute: 'm',
80
+ hour: 'h',
81
+ day: 'd',
82
+ week: 'w',
83
+ month: 'mo',
84
+ year: 'y',
85
+ };
86
+
87
+ const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
88
+ const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
89
+
90
+ const shortUnit = (n, unit) => `${n}${AGE_UNIT[unit]} ago`;
91
+
92
+ const shortAge = (text) => {
93
+ const raw = `${text ?? ''}`.trim();
94
+ if (!raw) return '';
95
+ if (/^yesterday$/i.test(raw)) return '1d ago';
96
+ const match = AGE_AGO.exec(raw);
97
+ if (match) {
98
+ const unit = AGE_UNIT[match[2].toLowerCase()];
99
+ if (unit) return `${match[1]}${unit} ago`;
100
+ }
101
+ const brief = AGE_SHORT.exec(raw);
102
+ if (brief) return `${brief[1]}${brief[2].toLowerCase()} ago`;
103
+ return raw;
79
104
  };
80
105
 
81
106
  const relativeAge = (thenMs, nowMs = Date.now()) => {
82
107
  const diff = Math.max(0, Math.round((nowMs - thenMs) / 1000));
83
- if (diff < 90) return countAgo(diff, 'second');
108
+ if (diff < 90) return shortUnit(diff, 'second');
84
109
  const minutes = Math.round(diff / 60);
85
- if (minutes < 90) return countAgo(minutes, 'minute');
110
+ if (minutes < 90) return shortUnit(minutes, 'minute');
86
111
  const hours = Math.round(minutes / 60);
87
- if (hours < 36) return countAgo(hours, 'hour');
112
+ if (hours < 36) return shortUnit(hours, 'hour');
88
113
  const days = Math.round(hours / 24);
89
- if (days < 14) return countAgo(days, 'day');
90
- if (days < 70) return countAgo(Math.round(days / 7), 'week');
91
- if (days < 365) return countAgo(Math.round(days / 30), 'month');
92
- return countAgo(Math.round(days / 365), 'year');
114
+ if (days < 14) return shortUnit(days, 'day');
115
+ if (days < 70) return shortUnit(Math.round(days / 7), 'week');
116
+ if (days < 365) return shortUnit(Math.round(days / 30), 'month');
117
+ return shortUnit(Math.round(days / 365), 'year');
93
118
  };
94
119
 
95
120
  const addLineDelta = (group, origin, delta) => {
@@ -231,4 +256,5 @@ module.exports = {
231
256
  fileEntries,
232
257
  fileTotals,
233
258
  relativeAge,
259
+ shortAge,
234
260
  };