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,212 @@
1
+ 'use strict';
2
+
3
+ const tiles = require('./tiles.js');
4
+ const { seg, spinner } = tiles;
5
+ const table = require('./dash-table.js');
6
+ const { ago, duration, isHot, waiting, cell, cellSegs, flexCell } = table;
7
+ const { tableLines, stat, pairRows, titleAside, captionOver } = table;
8
+
9
+ const NAME_MAX = 18;
10
+ const RUN_ICON = {
11
+ passed: ['✔', 'add'],
12
+ failed: ['✖', 'error'],
13
+ aborted: ['⊘', 'muted'],
14
+ };
15
+
16
+ const blankCell = () => cell('', 'muted', 'r');
17
+
18
+ const ageCell = (at, now) => cell(at ? ago(at, now) : '', 'muted', 'r');
19
+
20
+ const rebaseStep = (rebase) => {
21
+ const total = rebase.total ? `/${rebase.total}` : '';
22
+ return `${rebase.step || '?'}${total}`;
23
+ };
24
+
25
+ const commitRow = (entry, ctx, hot) => [
26
+ cell(entry.short, hot ? 'warn' : 'sha', 'l', true),
27
+ flexCell(entry.subject, hot ? 'warn' : 'text'),
28
+ ageCell(entry.at, ctx.now),
29
+ ];
30
+
31
+ const commitsBlock = (model, width, height, ctx, tile) => {
32
+ const data = model.commits;
33
+ if (!data.ready) return waiting('reading git…');
34
+ const { total, recent } = data.info;
35
+ const badge = [];
36
+ if (data.rebase) {
37
+ const step = rebaseStep(data.rebase);
38
+ badge.push(seg(`⟳ rebase ${step}`, 'warn', true));
39
+ }
40
+ const titleLine = titleAside(tile, [seg(`${total}`, 'muted')], width, badge);
41
+ if (!recent.length) return { titleLine, lines: [[seg('none', 'muted')]] };
42
+ const hot = isHot(data.changedAt, ctx.now);
43
+ const shown = recent.slice(0, Math.max(0, height));
44
+ const rows = shown.map((entry, i) => commitRow(entry, ctx, hot && i === 0));
45
+ return { titleLine, lines: tableLines(rows, width) };
46
+ };
47
+
48
+ const trackText = (entry) => {
49
+ if (entry.gone) return 'gone';
50
+ if (!entry.upstream) return '';
51
+ const ahead = entry.ahead ? `↑${entry.ahead}` : '';
52
+ const behind = entry.behind ? `↓${entry.behind}` : '';
53
+ return ahead + behind || '=';
54
+ };
55
+
56
+ const branchOrder = (list) =>
57
+ [...list].sort((left, right) => {
58
+ if (left.current !== right.current) return left.current ? -1 : 1;
59
+ return right.at - left.at || left.name.localeCompare(right.name, 'en');
60
+ });
61
+
62
+ const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
63
+
64
+ const branchRow = (entry, data, ctx) => {
65
+ const hot = isHot(data.hot.get(entry.name) ?? 0, ctx.now);
66
+ const tone = hot ? blinkTone(ctx.frame) : 'text';
67
+ const mark = entry.current ? '▶' : ' ';
68
+ const name = entry.name.slice(0, NAME_MAX);
69
+ const track = trackText(entry);
70
+ const trackTone = entry.behind || entry.gone ? 'error' : 'warn';
71
+ return [
72
+ cellSegs([
73
+ seg(mark, 'sha', true),
74
+ seg(` ${name}`, tone, entry.current || hot),
75
+ ]),
76
+ cell(track, track === '=' ? 'muted' : trackTone),
77
+ flexCell(entry.subject),
78
+ ageCell(entry.at, ctx.now),
79
+ ];
80
+ };
81
+
82
+ const branchExtras = (data, ctx) => {
83
+ const rows = [];
84
+ if (data.rebase) {
85
+ const step = cell(rebaseStep(data.rebase), 'warn', 'l', true);
86
+ rows.push([cell('rebase', 'muted'), step]);
87
+ }
88
+ const last = data.switches[0];
89
+ if (last) {
90
+ rows.push([
91
+ cell('switched', 'muted'),
92
+ flexCell(`${last.from} → ${last.to}`),
93
+ ageCell(last.at, ctx.now),
94
+ ]);
95
+ }
96
+ return rows;
97
+ };
98
+
99
+ const branchesBlock = (model, width, height, ctx, tile) => {
100
+ const data = model.branches;
101
+ if (!data.ready) return waiting('reading git…');
102
+ const badge = data.rebase ? [seg('⟳', 'warn', true)] : [];
103
+ const aside = [seg(`${data.list.length}`, 'muted')];
104
+ const titleLine = titleAside(tile, aside, width, badge);
105
+ if (!data.list.length) {
106
+ return { titleLine, lines: [[seg('no branches', 'muted')]] };
107
+ }
108
+ const extras = branchExtras(data, ctx);
109
+ const room = Math.max(1, height - extras.length);
110
+ const shown = branchOrder(data.list).slice(0, room);
111
+ const rows = shown.map((entry) => branchRow(entry, data, ctx));
112
+ const lines = tableLines(rows, width);
113
+ const fits = extras.slice(0, Math.max(0, height - lines.length));
114
+ return { titleLine, lines: [...lines, ...tableLines(fits, width)] };
115
+ };
116
+
117
+ const runIcon = (run, ctx) => {
118
+ if (run.status === 'running') return [spinner(ctx.frame), 'warn'];
119
+ return RUN_ICON[run.status] ?? ['?', 'muted'];
120
+ };
121
+
122
+ const countCell = (value, tone) => {
123
+ if (typeof value !== 'number') return blankCell();
124
+ return cell(`${value}`, tone, 'r');
125
+ };
126
+
127
+ const failTone = (value) => (value ? 'error' : 'muted');
128
+
129
+ const testMetrics = (done, ok, fail, total) => [
130
+ countCell(done, 'text'),
131
+ countCell(ok, ok ? 'add' : 'muted'),
132
+ countCell(fail, failTone(fail)),
133
+ countCell(total, 'text'),
134
+ ];
135
+
136
+ const runMetrics = (run) => {
137
+ const result = run.result;
138
+ if (result && typeof result.tests === 'number') {
139
+ const fail = result.failed ?? 0;
140
+ const ok = result.passed ?? Math.max(0, result.tests - fail);
141
+ return testMetrics(result.tests, ok, fail, result.tests);
142
+ }
143
+ const live = run.status === 'running' && run.source !== 'reslop';
144
+ if (!live) return [blankCell(), blankCell(), blankCell(), blankCell()];
145
+ const total = run.expected > 0 ? run.expected : null;
146
+ return testMetrics(run.done + run.failed, run.done, run.failed, total);
147
+ };
148
+
149
+ const lintNote = (run) => {
150
+ const result = run.result;
151
+ if (!result || typeof result.tests === 'number') return [];
152
+ const { errors, warnings } = result;
153
+ if (typeof errors !== 'number') return [];
154
+ const note = [seg(` ${errors} err`, failTone(errors))];
155
+ if (typeof warnings !== 'number') return note;
156
+ note.push(seg(` ${warnings} warn`, warnings ? 'warn' : 'muted'));
157
+ return note;
158
+ };
159
+
160
+ const durationCell = (run, ctx) => {
161
+ const running = run.status === 'running';
162
+ const end = running ? ctx.now : run.endedAt;
163
+ if (!run.startedAt || !end) return blankCell();
164
+ return cell(duration(end - run.startedAt), running ? 'warn' : 'muted', 'r');
165
+ };
166
+
167
+ const named = (icon, tone, name, nameTone) => ({
168
+ segs: [seg(icon, tone, true), seg(' '), seg(name, nameTone)],
169
+ align: 'l',
170
+ flex: true,
171
+ min: 1,
172
+ });
173
+
174
+ const runHeader = () => [
175
+ cell(''),
176
+ cell('done', 'muted', 'r'),
177
+ cell('ok', 'muted', 'r'),
178
+ cell('fail', 'muted', 'r'),
179
+ cell('total', 'muted', 'r'),
180
+ cell('duration', 'muted', 'r'),
181
+ ];
182
+
183
+ const runRow = (run, ctx) => {
184
+ const icon = runIcon(run, ctx);
185
+ const tone = run.status === 'running' ? 'text' : 'muted';
186
+ const label = named(icon[0], icon[1], run.name, tone);
187
+ label.segs.push(...lintNote(run));
188
+ return [label, ...runMetrics(run), durationCell(run, ctx)];
189
+ };
190
+
191
+ const runsBlock = (model, width, height, ctx, tile) => {
192
+ const rows = [];
193
+ if (model.busy) {
194
+ rows.push([named(spinner(ctx.frame), 'warn', model.busy, 'warn')]);
195
+ }
196
+ for (const run of model.runs) rows.push(runRow(run, ctx));
197
+ if (!rows.length) {
198
+ const items = [
199
+ stat('status', 'idle', 'muted'),
200
+ stat('usage', 'reslop t -- <command>', 'muted'),
201
+ ];
202
+ return { badge: [], lines: tableLines(pairRows(items, 1), width) };
203
+ }
204
+ const shown = rows.slice(0, Math.max(1, height));
205
+ const lines = tableLines([runHeader(), ...shown], width);
206
+ return {
207
+ titleLine: captionOver(tile, lines[0]),
208
+ lines: lines.slice(1),
209
+ };
210
+ };
211
+
212
+ module.exports = { commitsBlock, branchesBlock, runsBlock, runMetrics };
@@ -0,0 +1,496 @@
1
+ 'use strict';
2
+
3
+ const files = require('../files.js');
4
+ const { shortAge } = files;
5
+ const npmInfo = require('../dashboard/npm.js');
6
+ const { TOP_PACKAGES } = npmInfo;
7
+ const utilities = require('../utilities.js');
8
+ const { parseVersion, cmpVersion } = utilities;
9
+ const ansi = require('../ansi.js');
10
+ const { THEME, visibleWidth } = ansi;
11
+ const { minuteOf } = require('../dashboard/model.js');
12
+ const tiles = require('./tiles.js');
13
+ const { seg, barSegments, spinner } = tiles;
14
+ const table = require('./dash-table.js');
15
+ const { num, size, delta, waiting, cell, cellSegs, flexCell } = table;
16
+ const { tableLines, pickGroups, labelOf, stat, pairRows } = table;
17
+ const { titleAside, captionOver } = table;
18
+
19
+ const BAR_W = 6;
20
+ const FOLDER = '📁';
21
+ const MARK_GAP = 1;
22
+ const ACTIVITY_LABEL = 'activity ';
23
+ const PULSE = '∙';
24
+ const ACTIVITY_MARK = [
25
+ ['fail', 'x'],
26
+ ['commit', '◉'],
27
+ ['pass', '*'],
28
+ ['abort', '!'],
29
+ ['branch', '⎇'],
30
+ ['rebase', '↻'],
31
+ ['npm', '✦'],
32
+ ];
33
+ const UNKNOWN = '…';
34
+
35
+ const share = (value, total) => (total > 0 ? value / total : 0);
36
+
37
+ const markCell = (isDir) => cell(isDir ? FOLDER : '');
38
+
39
+ const nameCell = (key, isDir, hot = false) => {
40
+ const name = cell(labelOf(key, isDir), hot ? 'warn' : 'sha', 'l', hot);
41
+ name.gap = MARK_GAP;
42
+ return name;
43
+ };
44
+
45
+ const fileName = (key, isDir, hot) => {
46
+ const name = nameCell(key, isDir, hot);
47
+ name.flex = true;
48
+ name.keep = true;
49
+ return name;
50
+ };
51
+
52
+ const fileRow = (group, isDir, data) => {
53
+ const hot = (isDir ? data.hot.dirs : data.hot.exts).has(group.key);
54
+ const ratio = share(group.bytes, data.total.bytes);
55
+ return [
56
+ markCell(isDir),
57
+ fileName(group.key, isDir, hot),
58
+ cell(`${group.files}`, 'muted', 'r'),
59
+ cell(size(group.bytes), 'text', 'r'),
60
+ cell(num(group.lines), 'muted', 'r'),
61
+ cellSegs(barSegments(ratio, BAR_W, 'sha')),
62
+ ];
63
+ };
64
+
65
+ const filesTotal = (data, ctx) => {
66
+ const { total } = data;
67
+ const name = cell('');
68
+ name.flex = true;
69
+ name.gap = MARK_GAP;
70
+ const row = [
71
+ cell(''),
72
+ name,
73
+ cell(`${total.files}`, 'text', 'r', true),
74
+ cell(size(total.bytes), 'text', 'r', true),
75
+ cell(num(total.lines), 'text', 'r', true),
76
+ cell(''),
77
+ ];
78
+ const moved = data.delta ? data.delta.lines : 0;
79
+ const change = delta(moved, ctx.now, data.delta?.at);
80
+ if (change) row.push(cellSegs([change]));
81
+ return row;
82
+ };
83
+
84
+ const filesBlock = (model, width, height, ctx, tile) => {
85
+ const data = model.files;
86
+ if (!data.ready) return waiting('scanning…');
87
+ const picked = pickGroups(data.dirs, data.exts, height - 1);
88
+ const labels = [
89
+ cell(''),
90
+ cell(''),
91
+ cell('files', 'muted', 'r'),
92
+ cell('size', 'muted', 'r'),
93
+ cell('lines', 'muted', 'r'),
94
+ cell(''),
95
+ ];
96
+ const rows = [labels];
97
+ for (const group of picked.dirs) rows.push(fileRow(group, true, data));
98
+ for (const group of picked.exts) rows.push(fileRow(group, false, data));
99
+ rows.push(filesTotal(data, ctx));
100
+ const lines = tableLines(rows, width);
101
+ return {
102
+ titleLine: captionOver(tile, lines[0]),
103
+ lines: lines.slice(1, -1),
104
+ footer: lines.at(-1),
105
+ };
106
+ };
107
+
108
+ const ratio = (sign, part, total) => `${sign}${part}/${total}`;
109
+
110
+ const changeCells = (group, bold = false) => [
111
+ cell(ratio('+', group.stagedAdded, group.added), 'add', 'r', bold),
112
+ cell(ratio('-', group.stagedRemoved, group.removed), 'del', 'r', bold),
113
+ cell(`${group.staged}/${group.remaining}`, 'text', 'r', bold),
114
+ cell(shortAge(group.date), 'muted', 'r'),
115
+ ];
116
+
117
+ const diffRow = (group, isDir) => [
118
+ markCell(isDir),
119
+ nameCell(group.key, isDir),
120
+ ...changeCells(group),
121
+ ];
122
+
123
+ const diffsHeader = (data, ctx) => {
124
+ const name = cell('');
125
+ name.gap = MARK_GAP;
126
+ const row = [cell(''), name, ...changeCells(data.totals, true)];
127
+ const moved = data.delta ? data.delta.added + data.delta.removed : 0;
128
+ const change = delta(moved, ctx.now, data.delta?.at);
129
+ if (change) row[row.length - 1] = cellSegs([change], 'r');
130
+ return row;
131
+ };
132
+
133
+ const heatIndex = (level) => {
134
+ if (level >= 301) return 4;
135
+ if (level >= 81) return 3;
136
+ if (level >= 16) return 2;
137
+ if (level >= 1) return 1;
138
+ return 0;
139
+ };
140
+
141
+ const heatColor = (index) => {
142
+ if (index === 1) return THEME.heat1;
143
+ if (index === 2) return THEME.heat2;
144
+ if (index === 3) return THEME.heat3;
145
+ if (index === 4) return THEME.heat4;
146
+ return null;
147
+ };
148
+
149
+ const markGlyph = (marks) => {
150
+ const list = marks ?? [];
151
+ for (const [kind, glyph] of ACTIVITY_MARK) {
152
+ if (list.includes(kind)) return glyph;
153
+ }
154
+ return '';
155
+ };
156
+
157
+ const diffGlyph = (bucket) => {
158
+ if (!bucket) return '';
159
+ const added = bucket.added || 0;
160
+ const removed = bucket.removed || 0;
161
+ if (removed > added) return '-';
162
+ if (added > 0 || removed > 0 || bucket.level > 0) return '+';
163
+ return '';
164
+ };
165
+
166
+ const pulseGlyph = (level) => {
167
+ const index = heatIndex(level);
168
+ if (index >= 4) return '◎';
169
+ if (index >= 3) return '•';
170
+ return '◦';
171
+ };
172
+
173
+ const activityCell = (bucket, blink, track, pulse) => {
174
+ const level = bucket ? bucket.level : 0;
175
+ const glyph = markGlyph(bucket ? bucket.marks : null);
176
+ const index = heatIndex(level);
177
+ const mark = glyph || diffGlyph(bucket);
178
+ const phase = blink ? pulseGlyph(level) : PULSE;
179
+ const ch = pulse ? phase : mark || ' ';
180
+ const item = seg(ch, blink ? 'blink' : 'heat', Boolean(glyph) && !pulse);
181
+ let bg = track ? THEME.heatTrack : null;
182
+ const heat = heatColor(index);
183
+ if (heat) bg = heat;
184
+ if (blink) bg = THEME.heatBlink;
185
+ item.bg = bg;
186
+ return item;
187
+ };
188
+
189
+ const activityCells = (activity, ctx, width) => {
190
+ const minutes = activity && activity.minutes ? activity.minutes : [];
191
+ const byAt = new Map();
192
+ let start = null;
193
+ for (const row of minutes) {
194
+ byAt.set(row.at, row);
195
+ if (start === null || row.at < start) start = row.at;
196
+ }
197
+ const current = minuteOf(ctx.now);
198
+ const flash = Boolean(activity && activity.live);
199
+ const on = Math.abs(ctx.frame ?? 0) % 2 === 1;
200
+ const cells = [];
201
+ for (let i = 0; i < width; i++) {
202
+ const at = current - (width - 1 - i);
203
+ const last = i === width - 1;
204
+ const pulse = flash && last;
205
+ const blink = pulse && on;
206
+ const track = start !== null && at >= start;
207
+ cells.push(activityCell(byAt.get(at), blink, track, pulse));
208
+ }
209
+ return cells;
210
+ };
211
+
212
+ const activityFooter = (data, width, ctx) => {
213
+ const room = width - 1 - ACTIVITY_LABEL.length;
214
+ if (room < 2) return null;
215
+ const cells = activityCells(data.activity, ctx, room);
216
+ return [seg(ACTIVITY_LABEL, 'muted'), ...cells];
217
+ };
218
+
219
+ const diffsBlock = (model, width, height, ctx, tile) => {
220
+ const data = model.diffs;
221
+ if (!data.files) {
222
+ const rows = pairRows([stat('changes', 'working tree clean', 'muted')], 1);
223
+ return { badge: [], lines: tableLines(rows, width) };
224
+ }
225
+ const footer = activityFooter(data, width, ctx);
226
+ const room = footer ? height - 1 : height;
227
+ const picked = pickGroups(data.dirs, data.exts, room);
228
+ const rows = [diffsHeader(data, ctx)];
229
+ for (const group of picked.dirs) rows.push(diffRow(group, true));
230
+ for (const group of picked.exts) rows.push(diffRow(group, false));
231
+ const lines = tableLines(rows, width, 2, true);
232
+ return {
233
+ titleLine: captionOver(tile, lines[0]),
234
+ lines: lines.slice(1),
235
+ footer,
236
+ footerFlush: true,
237
+ };
238
+ };
239
+
240
+ const countSegs = (label, value) => [
241
+ seg(`${label}: `, 'muted'),
242
+ seg(`${value}`, 'text'),
243
+ ];
244
+
245
+ const markSegs = (value, tone, mark) => {
246
+ if (!value) return [];
247
+ return [seg(' '), seg(mark), seg(`${value}`, tone, true)];
248
+ };
249
+
250
+ const npmAside = (data) => {
251
+ const modules = data.modules;
252
+ return [
253
+ ...countSegs('deps', data.deps),
254
+ seg(' '),
255
+ ...countSegs('dev', data.dev),
256
+ seg(' '),
257
+ ...countSegs('all', modules.count),
258
+ seg(' (', 'muted'),
259
+ seg(size(modules.bytes), 'text'),
260
+ seg(')', 'muted'),
261
+ ...markSegs(data.audit, 'error', '🚨 '),
262
+ ...markSegs(data.outdated, 'warn', '⚠️ '),
263
+ ];
264
+ };
265
+
266
+ const npmStatus = (data) => {
267
+ if (!data || !data.ready || !data.hasManifest) return '';
268
+ const modules = data.modules || { count: 0, bytes: 0 };
269
+ let text = `deps: ${data.deps} dev: ${data.dev}`;
270
+ text += ` all: ${modules.count} (${size(modules.bytes)})`;
271
+ if (data.audit) text += ` 🚨 ${data.audit}`;
272
+ if (data.outdated) text += ` ⚠️ ${data.outdated}`;
273
+ return text;
274
+ };
275
+
276
+ const cmpText = (left, right) => {
277
+ const a = parseVersion(left);
278
+ const b = parseVersion(right);
279
+ if (!a || !b) return null;
280
+ return cmpVersion(a, b);
281
+ };
282
+
283
+ const boundHolds = (cmp, op) => {
284
+ if (op === '<') return cmp < 0;
285
+ if (op === '<=') return cmp <= 0;
286
+ if (op === '>') return cmp > 0;
287
+ return cmp >= 0;
288
+ };
289
+
290
+ const inClause = (version, clause) => {
291
+ const ver = parseVersion(version);
292
+ if (!ver) return false;
293
+ const bounds = [...clause.matchAll(/(>=|<=|>|<)\s*(\d+\.\d+\.\d+)/g)];
294
+ if (!bounds.length) return false;
295
+ for (const match of bounds) {
296
+ const bound = parseVersion(match[2]);
297
+ if (!bound) return false;
298
+ if (!boundHolds(cmpVersion(ver, bound), match[1])) return false;
299
+ }
300
+ return true;
301
+ };
302
+
303
+ const inRange = (version, range) =>
304
+ `${range ?? ''}`.split('||').some((clause) => inClause(version, clause));
305
+
306
+ const hasRange = (range) => /[<>]=?\s*\d+\.\d+\.\d+/.test(`${range ?? ''}`);
307
+
308
+ const vulnTone = (text, pkg) => {
309
+ if (!pkg.audit || !text || text === UNKNOWN) return '';
310
+ const fix = pkg.fix || '';
311
+ if (fix) {
312
+ const cmp = cmpText(text, fix);
313
+ if (cmp === null) return text === pkg.current ? 'error' : '';
314
+ return cmp < 0 ? 'error' : 'add';
315
+ }
316
+ const range = pkg.range || '';
317
+ if (hasRange(range)) {
318
+ if (inRange(text, range)) return 'error';
319
+ const cmp = cmpText(text, pkg.current);
320
+ if (cmp !== null && cmp > 0) return 'add';
321
+ }
322
+ return text === pkg.current ? 'error' : '';
323
+ };
324
+
325
+ const isOutdated = (pkg) => {
326
+ const current = pkg.current || '';
327
+ const wanted = pkg.wanted || '';
328
+ const latest = pkg.latest || '';
329
+ if (!current || current === UNKNOWN) return false;
330
+ if (wanted && wanted !== UNKNOWN && wanted !== current) return true;
331
+ return Boolean(latest && latest !== UNKNOWN && latest !== current);
332
+ };
333
+
334
+ const fixesOutdated = (text, pkg) => {
335
+ if (!isOutdated(pkg) || !text || text === UNKNOWN) return false;
336
+ const current = pkg.current || '';
337
+ const wanted = pkg.wanted || '';
338
+ const latest = pkg.latest || '';
339
+ if (text === wanted && wanted !== current) return true;
340
+ return text === latest && latest !== current;
341
+ };
342
+
343
+ const versionTone = (text, pkg) => {
344
+ const vuln = vulnTone(text, pkg);
345
+ if (vuln) return vuln;
346
+ if (fixesOutdated(text, pkg)) return 'add';
347
+ if (text === pkg.current && isOutdated(pkg)) return 'warn';
348
+ return 'muted';
349
+ };
350
+
351
+ const packageMark = (pkg, marks) => {
352
+ if (!marks) return cell('');
353
+ const segs = [];
354
+ if (pkg.audit) segs.push(seg('🛑 vulnerability detected', 'error'));
355
+ if (isOutdated(pkg)) {
356
+ if (segs.length) segs.push(seg(' '));
357
+ segs.push(seg('⚠️ outdated', 'warn'));
358
+ }
359
+ if (!segs.length) return cell('');
360
+ return cellSegs(segs);
361
+ };
362
+
363
+ const nameTone = (pkg) => {
364
+ if (pkg.transitive) return 'pkg';
365
+ if (pkg.dev) return 'dev';
366
+ return 'dep';
367
+ };
368
+
369
+ const packageName = (pkg) => {
370
+ const segs = [seg(pkg.name, nameTone(pkg))];
371
+ if (pkg.chain) segs.push(seg(` 🢒 ${pkg.chain}`, 'chain'));
372
+ return {
373
+ segs,
374
+ align: 'l',
375
+ flex: true,
376
+ keep: true,
377
+ measure: visibleWidth(pkg.name),
378
+ };
379
+ };
380
+
381
+ const versionCell = (text, pkg) => cell(text, versionTone(text, pkg), 'r');
382
+
383
+ const packageHead = () => {
384
+ const title = flexCell('');
385
+ title.keep = true;
386
+ return [
387
+ title,
388
+ cell(''),
389
+ cell('current', 'muted', 'r'),
390
+ cell('wanted', 'muted', 'r'),
391
+ cell('latest', 'muted', 'r'),
392
+ cell('size', 'muted', 'r'),
393
+ ];
394
+ };
395
+
396
+ const packageRow = (pkg, marks) => {
397
+ const current = pkg.current || UNKNOWN;
398
+ const wanted = pkg.wanted || UNKNOWN;
399
+ const latest = pkg.latest || UNKNOWN;
400
+ return [
401
+ packageName(pkg),
402
+ packageMark(pkg, marks),
403
+ versionCell(current, pkg),
404
+ versionCell(wanted, pkg),
405
+ versionCell(latest, pkg),
406
+ cell(size(pkg.bytes), 'muted', 'r'),
407
+ ];
408
+ };
409
+
410
+ const packageLines = (packages, width, limit) => {
411
+ const all = limit === null;
412
+ const headed = packages.length > 0 && (all || limit > 1);
413
+ const room = all ? packages.length : Math.max(0, limit - (headed ? 1 : 0));
414
+ const shown = packages.slice(0, room);
415
+ const rows = shown.map((pkg) => packageRow(pkg, all));
416
+ if (headed) rows.unshift(packageHead());
417
+ return tableLines(rows, width);
418
+ };
419
+
420
+ const npmContent = (data, width, limit, ctx, tile) => {
421
+ if (!data || !data.ready) return waiting('reading package.json…');
422
+ if (!data.hasManifest) return waiting('no package.json');
423
+ const badge = data.running ? [seg(spinner(ctx.frame), 'warn', true)] : [];
424
+ const titleLine = titleAside(tile, npmAside(data), width, badge);
425
+ const listed = data.modules && data.modules.packages;
426
+ const packages = listed ? listed : [];
427
+ const short = limit === null ? packages : packages.slice(0, TOP_PACKAGES);
428
+ return { titleLine, lines: packageLines(short, width, limit) };
429
+ };
430
+
431
+ const npmBlock = (model, width, height, ctx, tile) =>
432
+ npmContent(model.npm, width, height, ctx, tile);
433
+
434
+ const TASK_GAP = 2;
435
+
436
+ const ratioLabel = (done, total) => `${done}/${total}`;
437
+
438
+ const taskSpecs = (data) => {
439
+ const rows = [{ title: '', done: data.done, total: data.total, bold: true }];
440
+ for (const kind of data.kinds) {
441
+ rows.push({
442
+ title: kind.title,
443
+ done: kind.done,
444
+ total: kind.total,
445
+ bold: false,
446
+ });
447
+ }
448
+ return rows;
449
+ };
450
+
451
+ const widest = (rows, read) => {
452
+ let width = 0;
453
+ for (const row of rows) {
454
+ const size = read(row).length;
455
+ if (size > width) width = size;
456
+ }
457
+ return width;
458
+ };
459
+
460
+ const taskBarWidth = (width, nameW, ratioW) => {
461
+ const room = width - nameW - ratioW - TASK_GAP * 2;
462
+ return room > 0 ? room : 0;
463
+ };
464
+
465
+ const taskLine = (row, barW) => {
466
+ const tone = row.bold ? 'text' : 'muted';
467
+ const line = [cell(row.title, 'text', 'l', row.bold)];
468
+ if (barW > 0) {
469
+ line.push(cellSegs(barSegments(share(row.done, row.total), barW)));
470
+ }
471
+ line.push(cell(ratioLabel(row.done, row.total), tone, 'r', row.bold));
472
+ return line;
473
+ };
474
+
475
+ const tasksBlock = (model, width, height, ctx, tile) => {
476
+ const specs = taskSpecs(model.tasks);
477
+ const nameW = widest(specs, (row) => row.title);
478
+ const ratioW = widest(specs, (row) => ratioLabel(row.done, row.total));
479
+ const barW = taskBarWidth(width, nameW, ratioW);
480
+ const rows = specs.map((row) => taskLine(row, barW));
481
+ const lines = tableLines(rows, width, TASK_GAP);
482
+ const shown = Math.max(0, height);
483
+ return {
484
+ titleLine: captionOver(tile, lines[0]),
485
+ lines: lines.slice(1, 1 + shown),
486
+ };
487
+ };
488
+
489
+ module.exports = {
490
+ filesBlock,
491
+ diffsBlock,
492
+ npmBlock,
493
+ npmContent,
494
+ npmStatus,
495
+ tasksBlock,
496
+ };