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
package/lib/files.js CHANGED
@@ -6,9 +6,44 @@ const { listFilePath } = manifest;
6
6
  const REVIEW_DIR = '.review';
7
7
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
8
8
  const TASKS_FILE = 'TODOs';
9
- const REPO_TASKS_LABEL =
10
- 'Project Tasks: Backlog, Issues, Bug Reports, Feature Requests';
9
+ const REPO_TASKS_LABEL = [
10
+ 'Project Tasks: Feature requests, Bug reports,',
11
+ 'Technical debt, Research, Security',
12
+ ].join(' ');
13
+ const TASK_KINDS = [
14
+ { id: 'features', title: 'Feature requests' },
15
+ { id: 'bugs', title: 'Bug reports' },
16
+ { id: 'debt', title: 'Technical debt' },
17
+ { id: 'research', title: 'Research' },
18
+ { id: 'security', title: 'Security' },
19
+ ];
20
+ const TASK_KIND_IDS = new Set(TASK_KINDS.map((kind) => kind.id));
21
+ const TASK_KIND_ALIAS = { backlog: 'debt', issues: 'debt' };
22
+
23
+ const taskKind = (todo) => {
24
+ const kind = todo && todo.kind;
25
+ if (TASK_KIND_IDS.has(kind)) return kind;
26
+ return TASK_KIND_ALIAS[kind] ?? 'debt';
27
+ };
28
+
29
+ const taskTitle = (id) => {
30
+ for (const kind of TASK_KINDS) {
31
+ if (kind.id === id) return kind.title;
32
+ }
33
+ return TASK_KINDS[0].title;
34
+ };
11
35
  const TOTAL_LABEL = 'total';
36
+ const AGE_UNIT = {
37
+ second: 's',
38
+ minute: 'm',
39
+ hour: 'h',
40
+ day: 'd',
41
+ week: 'w',
42
+ month: 'mo',
43
+ year: 'y',
44
+ };
45
+ const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
46
+ const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
12
47
 
13
48
  const isPathInScope = (rel, paths) => {
14
49
  if (!paths.length) return true;
@@ -20,20 +55,17 @@ const isPathInScope = (rel, paths) => {
20
55
  return false;
21
56
  };
22
57
 
23
- const itemPath = (item) => {
24
- if (!item || !item.file) return '';
25
- return item.file.newPath || item.file.oldPath || '';
26
- };
58
+ const itemPath = (item) => item?.file?.newPath || item?.file?.oldPath || '';
27
59
 
28
60
  const listPath = (item) => listFilePath(itemPath(item));
29
61
 
30
- const isTaskItem = (item) => !!(item && item.origin === 'task');
62
+ const isTaskItem = (item) => item?.origin === 'task';
31
63
 
32
64
  const isReadOnlyOrigin = (origin) => origin === 'commit' || origin === 'pr';
33
65
 
34
- const isTasksEntry = (entry) => !!(entry && entry.kind === 'tasks');
66
+ const isTasksEntry = (entry) => entry?.kind === 'tasks';
35
67
 
36
- const isTotalEntry = (entry) => !!(entry && entry.kind === 'total');
68
+ const isTotalEntry = (entry) => entry?.kind === 'total';
37
69
 
38
70
  const makeTaskItem = () => ({
39
71
  origin: 'task',
@@ -53,52 +85,30 @@ const makeTaskItem = () => ({
53
85
  });
54
86
 
55
87
  const fileStatus = (origins) => {
56
- const present = new Set(origins);
57
- const seen = ORIGIN_ORDER.filter((origin) => present.has(origin));
88
+ const seen = ORIGIN_ORDER.filter((origin) => origins.includes(origin));
58
89
  const key = seen.join('+');
59
- if (!key) return '';
60
- if (key === 'staged+unstaged') return 'partial';
61
- return key;
90
+ return key === 'staged+unstaged' ? 'partial' : key;
62
91
  };
63
92
 
64
93
  const lineDelta = ({ hunk, blockId }) => {
65
94
  const delta = { added: 0, removed: 0 };
66
- if (!hunk) return delta;
67
- const { lines } = hunk;
68
- if (!lines) return delta;
95
+ if (!hunk || !hunk.lines) return delta;
69
96
  const scoped = typeof blockId === 'number';
70
- for (const { type, blockId: lineBlock } of lines) {
71
- if (scoped && lineBlock !== blockId) continue;
72
- if (type === 'add') delta.added += 1;
73
- else if (type === 'del') delta.removed += 1;
97
+ for (const line of hunk.lines) {
98
+ if (scoped && line.blockId !== blockId) continue;
99
+ if (line.type === 'add') delta.added += 1;
100
+ else if (line.type === 'del') delta.removed += 1;
74
101
  }
75
102
  return delta;
76
103
  };
77
104
 
78
- const AGE_UNIT = {
79
- second: 's',
80
- minute: 'm',
81
- hour: 'h',
82
- day: 'd',
83
- week: 'w',
84
- month: 'mo',
85
- year: 'y',
86
- };
87
-
88
- const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
89
- const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
90
-
91
105
  const shortUnit = (n, unit) => `${n}${AGE_UNIT[unit]} ago`;
92
106
 
93
107
  const shortAge = (text) => {
94
108
  const raw = `${text ?? ''}`.trim();
95
- if (!raw) return '';
96
109
  if (/^yesterday$/i.test(raw)) return '1d ago';
97
110
  const match = AGE_AGO.exec(raw);
98
- if (match) {
99
- const unit = AGE_UNIT[match[2].toLowerCase()];
100
- if (unit) return `${match[1]}${unit} ago`;
101
- }
111
+ if (match) return shortUnit(match[1], match[2].toLowerCase());
102
112
  const brief = AGE_SHORT.exec(raw);
103
113
  if (brief) return `${brief[1]}${brief[2].toLowerCase()} ago`;
104
114
  return raw;
@@ -118,6 +128,18 @@ const relativeAge = (thenMs, nowMs = Date.now()) => {
118
128
  return shortUnit(Math.round(days / 365), 'year');
119
129
  };
120
130
 
131
+ const emptyCounts = () => ({
132
+ remaining: 0,
133
+ added: 0,
134
+ removed: 0,
135
+ stagedAdded: 0,
136
+ stagedRemoved: 0,
137
+ unstagedAdded: 0,
138
+ unstagedRemoved: 0,
139
+ staged: 0,
140
+ unstaged: 0,
141
+ });
142
+
121
143
  const addLineDelta = (group, origin, delta) => {
122
144
  group.added += delta.added;
123
145
  group.removed += delta.removed;
@@ -141,19 +163,12 @@ const originLineDelta = (entry) => {
141
163
  }
142
164
  const added = entry.added ?? 0;
143
165
  const removed = entry.removed ?? 0;
144
- if ((entry.status ?? '') === 'staged') {
145
- return {
146
- stagedAdded: added,
147
- stagedRemoved: removed,
148
- unstagedAdded: 0,
149
- unstagedRemoved: 0,
150
- };
151
- }
166
+ const staged = entry.status === 'staged';
152
167
  return {
153
- stagedAdded: 0,
154
- stagedRemoved: 0,
155
- unstagedAdded: added,
156
- unstagedRemoved: removed,
168
+ stagedAdded: staged ? added : 0,
169
+ stagedRemoved: staged ? removed : 0,
170
+ unstagedAdded: staged ? 0 : added,
171
+ unstagedRemoved: staged ? 0 : removed,
157
172
  };
158
173
  };
159
174
 
@@ -165,22 +180,8 @@ const fileEntries = (items) => {
165
180
  const rel = listPath(item);
166
181
  if (!rel) continue;
167
182
  if (!groups.has(rel)) {
168
- groups.set(rel, {
169
- path: rel,
170
- origins: [],
171
- firstIndex: i,
172
- openIndex: i,
173
- remaining: 0,
174
- added: 0,
175
- removed: 0,
176
- stagedAdded: 0,
177
- stagedRemoved: 0,
178
- unstagedAdded: 0,
179
- unstagedRemoved: 0,
180
- staged: 0,
181
- unstaged: 0,
182
- date: '',
183
- });
183
+ const fields = { origins: [], firstIndex: i, openIndex: i };
184
+ groups.set(rel, { path: rel, ...fields, ...emptyCounts(), date: '' });
184
185
  }
185
186
  const group = groups.get(rel);
186
187
  const { origin } = item;
@@ -195,47 +196,30 @@ const fileEntries = (items) => {
195
196
  }
196
197
  }
197
198
  const entries = [...groups.values()];
198
- for (const group of entries) {
199
- group.status = fileStatus(group.origins);
200
- }
199
+ for (const group of entries) group.status = fileStatus(group.origins);
201
200
  entries.sort((left, right) => left.path.localeCompare(right.path, 'en'));
202
201
  return entries;
203
202
  };
204
203
 
205
204
  const fileTotals = (files) => {
206
- const total = {
207
- path: TOTAL_LABEL,
208
- kind: 'total',
209
- status: '',
210
- remaining: 0,
211
- added: 0,
212
- removed: 0,
213
- stagedAdded: 0,
214
- stagedRemoved: 0,
215
- unstagedAdded: 0,
216
- unstagedRemoved: 0,
217
- staged: 0,
218
- unstaged: 0,
219
- };
205
+ const total = { path: TOTAL_LABEL, kind: 'total', status: '' };
206
+ Object.assign(total, emptyCounts());
220
207
  for (const entry of files) {
221
208
  if (isTasksEntry(entry) || isTotalEntry(entry)) continue;
222
209
  const remaining = entry.remaining ?? 0;
223
210
  const staged = entry.staged ?? 0;
224
- const added = entry.added ?? 0;
225
- const removed = entry.removed ?? 0;
226
211
  const lines = originLineDelta(entry);
227
212
  const listed = entry.unstaged ?? Math.max(0, remaining - staged);
228
- const untracked = (entry.status ?? '') === 'untracked';
229
- const unstaged = untracked && listed === 0 ? remaining : listed;
213
+ const untracked = entry.status === 'untracked';
230
214
  total.remaining += remaining;
231
- total.added += added;
232
- total.removed += removed;
215
+ total.added += entry.added ?? 0;
216
+ total.removed += entry.removed ?? 0;
233
217
  total.stagedAdded += lines.stagedAdded;
234
218
  total.stagedRemoved += lines.stagedRemoved;
235
219
  total.unstagedAdded += lines.unstagedAdded;
236
220
  total.unstagedRemoved += lines.unstagedRemoved;
237
221
  total.staged += staged;
238
- total.unstaged += unstaged;
222
+ total.unstaged += untracked && listed === 0 ? remaining : listed;
239
223
  }
240
224
  return total;
241
225
  };
@@ -244,6 +228,9 @@ module.exports = {
244
228
  REVIEW_DIR,
245
229
  TASKS_FILE,
246
230
  REPO_TASKS_LABEL,
231
+ TASK_KINDS,
232
+ taskKind,
233
+ taskTitle,
247
234
  TOTAL_LABEL,
248
235
  isPathInScope,
249
236
  itemPath,
@@ -254,6 +241,7 @@ module.exports = {
254
241
  isTotalEntry,
255
242
  makeTaskItem,
256
243
  fileStatus,
244
+ lineDelta,
257
245
  fileEntries,
258
246
  fileTotals,
259
247
  relativeAge,
package/lib/find.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const matchesIn = (text, query) => {
4
+ const spans = [];
5
+ if (!query || !text) return spans;
6
+ const hay = text.toLowerCase();
7
+ const needle = query.toLowerCase();
8
+ const step = needle.length;
9
+ let from = 0;
10
+ while (from <= hay.length - step) {
11
+ const at = hay.indexOf(needle, from);
12
+ if (at < 0) break;
13
+ spans.push({ start: at, end: at + step });
14
+ from = at + step;
15
+ }
16
+ return spans;
17
+ };
18
+
19
+ const fileMatches = (entries, query) => {
20
+ const hits = [];
21
+ if (!query) return hits;
22
+ for (let index = 0; index < entries.length; index++) {
23
+ const entry = entries[index];
24
+ const path = entry && entry.path ? entry.path : '';
25
+ if (!path) continue;
26
+ for (const span of matchesIn(path, query)) {
27
+ hits.push({ index, path, start: span.start, end: span.end });
28
+ }
29
+ }
30
+ return hits;
31
+ };
32
+
33
+ const activeHit = (entries, find) => {
34
+ if (!find || !find.query) return null;
35
+ const hits = fileMatches(entries, find.query);
36
+ if (!hits.length) return null;
37
+ const count = hits.length;
38
+ let at = find.at % count;
39
+ if (at < 0) at += count;
40
+ return hits[at];
41
+ };
42
+
43
+ const keptPrefix = (shown, full) => {
44
+ const mark = shown.endsWith('…') && shown !== full ? 1 : 0;
45
+ const keep = shown.length - mark;
46
+ if (!full.startsWith(shown.slice(0, keep))) return -1;
47
+ return keep;
48
+ };
49
+
50
+ const spansFor = (shown, full, query, hit) => {
51
+ const spans = matchesIn(shown, query);
52
+ if (!hit || hit.path !== full) return spans;
53
+ const keep = keptPrefix(shown, full);
54
+ if (keep < 0 || hit.start >= keep) return spans;
55
+ return spans.map((span) => ({
56
+ ...span,
57
+ on: span.start === hit.start,
58
+ }));
59
+ };
60
+
61
+ module.exports = { matchesIn, fileMatches, activeHit, spansFor };