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
@@ -1,469 +1,452 @@
1
1
  'use strict';
2
2
 
3
+ const metautil = require('metautil');
4
+ const { bytesToSize } = metautil;
3
5
  const commands = require('../npm-commands.js');
4
6
  const npm = require('../render/npm.js');
5
- const { logViewRows } = npm;
6
-
7
- const { listCommands, reduceOutput, writeLog } = commands;
8
- const { staleLogFiles, removeStaleLogs, formatSize } = commands;
7
+ const { logViewRows, expandLogLines } = npm;
8
+ const ansi = require('../ansi.js');
9
+ const { stripAnsi } = ansi;
10
+ const reportParse = require('../report-parse.js');
11
+ const { buildDocument } = reportParse;
12
+ const reportRender = require('../report-render.js');
13
+ const { renderDocument, loggedText } = reportRender;
14
+ const reportView = require('../render/report.js');
15
+ const { renderReport } = reportView;
16
+
17
+ const { listCommands, reduceOutput, nextLogFile, saveLogs } = commands;
18
+ const { staleLogFiles, removeStaleLogs, startNpm } = commands;
9
19
  const { saveScript, removeScript, reorderScript } = commands;
10
20
 
11
21
  const NAME_RE = /^[\w:.-]+$/;
12
- const { startNpm } = commands;
13
-
14
- const selected = (api) => {
15
- const cursor = api.ui.nav.npmCursor;
16
- return api.state.commands[cursor];
22
+ const TESTS_RE = /^(?:TAP version \d+|\s*(?:not )?ok\b|[ℹi]\s+tests\s+\d+)/m;
23
+
24
+ const logRowCount = (text, width, running) => {
25
+ const visual = width > 0 ? expandLogLines(text, width).length : 0;
26
+ const extra = running ? 1 : 0;
27
+ if (visual) return visual + extra;
28
+ const lines = `${text ?? ''}`.split('\n');
29
+ const count = lines.at(-1) === '' ? lines.length - 1 : lines.length;
30
+ return count + extra;
31
+ };
32
+
33
+ const filteredOutput = (raw, root, status, rawFile) => {
34
+ const text = stripAnsi(`${raw ?? ''}`);
35
+ if (status === '' || !TESTS_RE.test(text)) {
36
+ const output = reduceOutput(raw, root, status);
37
+ return { output, log: output };
38
+ }
39
+ const doc = buildDocument(raw, '', root, status, 'test');
40
+ return { output: renderReport(doc), log: renderDocument(doc, rawFile) };
17
41
  };
18
42
 
19
- const clampCursor = (api) => {
20
- const last = api.state.commands.length - 1;
21
- const nav = api.ui.nav;
22
- if (nav.npmCursor > last) nav.npmCursor = Math.max(0, last);
23
- if (nav.npmCursor < 0) nav.npmCursor = 0;
24
- };
43
+ const runLabel = (entry) =>
44
+ entry.kind === 'bin' ? `npm exec ${entry.name}` : `npm run ${entry.name}`;
25
45
 
26
- const refresh = (api) => {
27
- api.state.commands = listCommands(api.ui.top);
28
- clampCursor(api);
46
+ const withNotice = (text, notice) => {
47
+ const body = `${text ?? ''}`;
48
+ if (body.endsWith(`${notice}\n`)) return body;
49
+ if (!body) return `${notice}\n`;
50
+ if (body.endsWith('\n')) return `${body}${notice}\n`;
51
+ return `${body}\n${notice}\n`;
29
52
  };
30
53
 
31
- const canEdit = (ui) => {
32
- if (ui.capabilities && ui.capabilities.changes === false) return false;
33
- if (ui.readOnly) return false;
34
- return true;
54
+ const withNewline = (text) => {
55
+ const body = `${text ?? ''}`;
56
+ if (!body || body.endsWith('\n')) return body;
57
+ return `${body}\n`;
35
58
  };
36
59
 
37
- const refreshLogs = (api) => {
38
- try {
39
- const found = staleLogFiles(api.ui.top);
40
- api.state.logBytes = found.bytes;
41
- } catch (error) {
42
- api.state.logBytes = 0;
43
- api.ui.status = error.message;
60
+ class NpmController {
61
+ constructor(ui) {
62
+ this.ui = ui;
63
+ this.child = null;
64
+ this.running = false;
65
+ this.reset();
44
66
  }
45
- };
46
-
47
- const open = (api) => {
48
- if (api.ui.mode === 'compose') return;
49
- refresh(api);
50
- refreshLogs(api);
51
- api.state.viewing = false;
52
- api.state.output = '';
53
- const nav = api.ui.nav;
54
- nav.pane = 'npm';
55
- nav.resetListScroll('npm');
56
- api.ui.status = '';
57
- nav.clearSelection();
58
- };
59
67
 
60
- const scrollLog = (api, delta) => {
61
- const lines = api.state.output.split('\n');
62
- let count = lines.at(-1) === '' ? lines.length - 1 : lines.length;
63
- if (api.state.running) count += 1;
64
- const frame = api.ui.lastFrame;
65
- const bodyH = frame && frame.bodyH ? frame.bodyH : 1;
66
- const max = Math.max(0, count - logViewRows(bodyH));
67
- let start = api.state.followEnd ? max : api.state.logScroll;
68
- start = Math.min(max, Math.max(0, start + delta));
69
- api.state.followEnd = start === max;
70
- api.state.logScroll = start;
71
- };
68
+ reset() {
69
+ if (this.child) this.child.kill();
70
+ this.stopRun();
71
+ this.commands = [];
72
+ this.viewing = false;
73
+ this.output = '';
74
+ this.followEnd = true;
75
+ this.logScroll = 0;
76
+ this.editName = '';
77
+ this.editField = '';
78
+ this.draftName = '';
79
+ this.draftCommand = '';
80
+ this.verbose = false;
81
+ this.raw = '';
82
+ this.exitStatus = '';
83
+ this.dropName = '';
84
+ this.dropKind = '';
85
+ this.logBytes = 0;
86
+ this.stopping = false;
87
+ this.runLabel = '';
88
+ this.startedAt = 0;
89
+ this.endedAt = 0;
90
+ }
72
91
 
73
- const move = (api, delta) => {
74
- if (api.state.viewing) return void scrollLog(api, delta);
75
- if (!api.state.commands.length) return;
76
- const nav = api.ui.nav;
77
- const last = api.state.commands.length - 1;
78
- const next = Math.min(last, Math.max(0, nav.npmCursor + delta));
79
- if (next === nav.npmCursor) return;
80
- nav.npmCursor = next;
81
- api.ui.status = '';
82
- };
92
+ get logLabel() {
93
+ if (!this.logBytes) return '';
94
+ return `old logs ${bytesToSize(this.logBytes)}`;
95
+ }
83
96
 
84
- const stopRun = (api) => {
85
- api.state.running = false;
86
- api.state.child = null;
87
- api.ui.progress.stop('npm');
88
- };
97
+ get lastRun() {
98
+ if (!this.startedAt) return null;
99
+ return {
100
+ label: this.runLabel,
101
+ running: this.running,
102
+ exit: this.exitStatus,
103
+ startedAt: this.startedAt,
104
+ endedAt: this.endedAt,
105
+ };
106
+ }
89
107
 
90
- const shownOutput = (api, raw, status) => {
91
- if (!api.state.verbose) return reduceOutput(raw, api.ui.top, status);
92
- const text = `${raw ?? ''}`;
93
- if (!text) return '';
94
- if (text.endsWith('\n')) return text;
95
- return `${text}\n`;
96
- };
108
+ selected() {
109
+ return this.commands[this.ui.nav.npmCursor];
110
+ }
97
111
 
98
- const showOutput = (api, text) => {
99
- if (!api.state.viewing || !api.state.running) return;
100
- if (api.state.stopping) return;
101
- api.state.raw = text;
102
- api.state.output = shownOutput(api, text, '');
103
- api.ui.paint();
104
- };
112
+ refresh() {
113
+ this.commands = listCommands(this.ui.top);
114
+ const last = this.commands.length - 1;
115
+ const nav = this.ui.nav;
116
+ nav.npmCursor = Math.max(0, Math.min(nav.npmCursor, last));
117
+ }
105
118
 
106
- const withNotice = (text, notice) => {
107
- const body = `${text ?? ''}`;
108
- if (body.endsWith(`${notice}\n`)) return body;
109
- if (!body) return `${notice}\n`;
110
- if (body.endsWith('\n')) return `${body}${notice}\n`;
111
- return `${body}\n${notice}\n`;
112
- };
119
+ refreshLogs() {
120
+ try {
121
+ this.logBytes = staleLogFiles(this.ui.top).bytes;
122
+ } catch (error) {
123
+ this.logBytes = 0;
124
+ this.ui.status = error.message;
125
+ }
126
+ }
113
127
 
114
- const finishRun = (api, entry, result) => {
115
- const stopped = api.state.stopping === true;
116
- const raw = stopped ? withNotice(api.state.raw, 'terminated') : result.text;
117
- const reduced = stopped
118
- ? withNotice(reduceOutput(api.state.raw, api.ui.top, ''), 'terminated')
119
- : reduceOutput(result.text, api.ui.top, result.status);
120
- api.state.raw = raw;
121
- api.state.exitStatus = stopped ? '' : result.status;
122
- api.state.output = api.state.verbose ? shownOutput(api, raw, '') : reduced;
123
- api.state.stopping = false;
124
- stopRun(api);
125
- try {
126
- writeLog(api.ui.top, entry.name, reduced);
127
- } catch (error) {
128
- api.ui.status = error.message;
129
- api.ui.paint();
130
- return;
131
- }
132
- if (stopped && api.state.viewing) api.ui.status = 'terminated';
133
- api.ui.paint();
134
- };
128
+ open() {
129
+ if (this.ui.mode === 'compose') return;
130
+ this.refresh();
131
+ this.refreshLogs();
132
+ this.viewing = false;
133
+ this.output = '';
134
+ const nav = this.ui.nav;
135
+ nav.pane = 'npm';
136
+ nav.resetListScroll('npm');
137
+ this.ui.status = '';
138
+ nav.clearSelection();
139
+ }
135
140
 
136
- const startRun = (api, entry) => {
137
- api.state.output = '';
138
- api.state.raw = '';
139
- api.state.exitStatus = '';
140
- api.state.viewing = true;
141
- api.state.followEnd = true;
142
- api.state.logScroll = 0;
143
- api.state.running = true;
144
- api.state.stopping = false;
145
- api.ui.status = '';
146
- api.ui.progress.start('npm');
147
- const runner = api.ui.repo && api.ui.repo.runNpmCommand;
148
- const start = typeof runner === 'function' ? runner : startNpm;
149
- const child = start(
150
- api.ui.top,
151
- entry,
152
- (text) => showOutput(api, text),
153
- (result) => finishRun(api, entry, result),
154
- );
155
- if (api.state.running) api.state.child = child;
156
- api.ui.paint();
157
- };
141
+ scrollLog(delta) {
142
+ const { lastFrame, lastSize } = this.ui;
143
+ const width = lastSize && lastSize.width ? lastSize.width : 0;
144
+ const count = logRowCount(this.output, width, this.running);
145
+ const bodyH = lastFrame && lastFrame.bodyH ? lastFrame.bodyH : 1;
146
+ const max = Math.max(0, count - logViewRows(bodyH));
147
+ const from = this.followEnd ? max : this.logScroll;
148
+ const start = Math.min(max, Math.max(0, from + delta));
149
+ this.followEnd = start === max;
150
+ this.logScroll = start;
151
+ }
158
152
 
159
- const runSelected = (api) => {
160
- if (api.state.viewing || api.state.child) return;
161
- const entry = selected(api);
162
- if (!entry) return;
163
- api.state.verbose = false;
164
- startRun(api, entry);
165
- };
153
+ move(delta) {
154
+ if (this.viewing) return void this.scrollLog(delta);
155
+ if (!this.commands.length) return;
156
+ const nav = this.ui.nav;
157
+ const last = this.commands.length - 1;
158
+ const next = Math.min(last, Math.max(0, nav.npmCursor + delta));
159
+ if (next === nav.npmCursor) return;
160
+ nav.npmCursor = next;
161
+ this.ui.status = '';
162
+ }
166
163
 
167
- const rerun = (api) => {
168
- if (!api.state.viewing || api.state.running) return;
169
- const entry = selected(api);
170
- if (!entry) return;
171
- startRun(api, entry);
172
- };
164
+ stopRun() {
165
+ if (this.running) this.endedAt = Date.now();
166
+ this.running = false;
167
+ this.child = null;
168
+ this.ui.progress.stop('npm');
169
+ }
173
170
 
174
- const closeView = (api) => {
175
- if (api.state.running) {
176
- api.state.stopping = true;
177
- if (api.state.child) api.state.child.kill();
178
- if (!api.state.running) return;
179
- api.state.raw = withNotice(api.state.raw, 'terminated');
180
- api.state.output = api.state.verbose
181
- ? shownOutput(api, api.state.raw, '')
182
- : withNotice(api.state.output, 'terminated');
183
- stopRun(api);
184
- api.ui.status = 'terminated';
185
- api.ui.paint();
186
- return;
187
- }
188
- api.state.viewing = false;
189
- api.state.verbose = false;
190
- api.ui.status = '';
191
- api.ui.paint();
192
- };
171
+ shownOutput(raw, status) {
172
+ if (this.verbose) return withNewline(raw);
173
+ return filteredOutput(raw, this.ui.top, status).output;
174
+ }
193
175
 
194
- const toggleVerbose = (api) => {
195
- if (!api.state.viewing) return;
196
- api.state.verbose = !api.state.verbose;
197
- const status = api.state.running ? '' : api.state.exitStatus;
198
- api.state.output = shownOutput(api, api.state.raw, status);
199
- api.ui.status = api.state.verbose ? 'verbose' : 'filtered';
200
- api.ui.paint();
201
- };
176
+ showOutput(text) {
177
+ if (!this.viewing || !this.running || this.stopping) return;
178
+ this.raw = text;
179
+ this.output = this.shownOutput(text, '');
180
+ this.ui.paint();
181
+ }
202
182
 
203
- const editable = (api) => {
204
- if (api.state.viewing) return false;
205
- if (canEdit(api.ui)) return true;
206
- api.ui.status = 'read only';
207
- return false;
208
- };
183
+ async finishRun(entry, result) {
184
+ const ui = this.ui;
185
+ const stopped = this.stopping === true;
186
+ const raw = stopped ? withNotice(this.raw, 'terminated') : result.text;
187
+ let slot = null;
188
+ try {
189
+ slot = nextLogFile(ui.top, entry.name);
190
+ } catch (error) {
191
+ ui.status = error.message;
192
+ }
193
+ const rawFile = slot ? slot.rawName : '';
194
+ const stoppedOutput = () => {
195
+ const output = withNotice(
196
+ reduceOutput(this.raw, ui.top, ''),
197
+ 'terminated',
198
+ );
199
+ return { output, log: output };
200
+ };
201
+ const filtered = stopped
202
+ ? stoppedOutput()
203
+ : filteredOutput(result.text, ui.top, result.status, rawFile);
204
+ this.raw = raw;
205
+ this.exitStatus = stopped ? '' : result.status;
206
+ this.output = this.verbose ? this.shownOutput(raw, '') : filtered.output;
207
+ this.stopping = false;
208
+ this.stopRun();
209
+ if (stopped && this.viewing) ui.status = 'terminated';
210
+ ui.paint();
211
+ if (!slot) return;
212
+ try {
213
+ await saveLogs(slot, loggedText(filtered.log, slot.rawName), raw);
214
+ } catch (error) {
215
+ ui.status = error.message;
216
+ ui.paint();
217
+ }
218
+ }
209
219
 
210
- const selectedScript = (api) => {
211
- if (!editable(api)) return null;
212
- const entry = selected(api);
213
- if (entry && entry.kind === 'script') return entry;
214
- api.ui.status = 'not a script';
215
- return null;
216
- };
220
+ startRun(entry) {
221
+ const ui = this.ui;
222
+ this.output = '';
223
+ this.raw = '';
224
+ this.exitStatus = '';
225
+ this.viewing = true;
226
+ this.followEnd = true;
227
+ this.logScroll = 0;
228
+ this.running = true;
229
+ this.stopping = false;
230
+ this.runLabel = runLabel(entry);
231
+ this.startedAt = Date.now();
232
+ this.endedAt = 0;
233
+ ui.status = '';
234
+ ui.progress.start('npm');
235
+ const start = (ui.repo && ui.repo.runNpmCommand) || startNpm;
236
+ const child = start(
237
+ ui.top,
238
+ entry,
239
+ (text) => this.showOutput(text),
240
+ (result) => this.finishRun(entry, result),
241
+ );
242
+ if (this.running) this.child = child;
243
+ ui.paint();
244
+ return child;
245
+ }
217
246
 
218
- const focusScript = (api, name) => {
219
- const at = api.state.commands.findIndex((entry) => entry.name === name);
220
- if (at >= 0) api.ui.nav.npmCursor = at;
221
- };
247
+ run() {
248
+ if (this.viewing || this.child) return null;
249
+ const entry = this.selected();
250
+ if (!entry) return null;
251
+ this.verbose = false;
252
+ return this.startRun(entry);
253
+ }
222
254
 
223
- const editSelected = (api) => {
224
- const entry = selectedScript(api);
225
- if (!entry) return;
226
- api.state.editName = entry.name;
227
- api.state.editField = 'name';
228
- api.state.draftName = entry.name;
229
- api.state.draftCommand = entry.command ?? '';
230
- api.ui.composer.openCompose('npm', entry.name);
231
- };
255
+ rerun() {
256
+ if (!this.viewing || this.running) return;
257
+ const entry = this.selected();
258
+ if (entry) this.startRun(entry);
259
+ }
232
260
 
233
- const newCommand = (api) => {
234
- if (!editable(api)) return;
235
- api.state.editName = '';
236
- api.state.editField = 'name';
237
- api.state.draftName = '';
238
- api.state.draftCommand = '';
239
- api.ui.composer.openCompose('npm', '');
240
- };
261
+ closeView() {
262
+ const ui = this.ui;
263
+ if (!this.running) {
264
+ this.viewing = false;
265
+ this.verbose = false;
266
+ ui.status = '';
267
+ return void ui.paint();
268
+ }
269
+ this.stopping = true;
270
+ if (this.child) this.child.kill();
271
+ if (!this.running) return;
272
+ this.raw = withNotice(this.raw, 'terminated');
273
+ this.output = this.verbose
274
+ ? this.shownOutput(this.raw, '')
275
+ : withNotice(this.output, 'terminated');
276
+ this.stopRun();
277
+ ui.status = 'terminated';
278
+ ui.paint();
279
+ }
241
280
 
242
- const switchField = (api, editor, field, text) => {
243
- api.state.editField = field;
244
- editor.replace(text);
245
- api.ui.status = '';
246
- };
281
+ toggleVerbose() {
282
+ if (!this.viewing) return;
283
+ this.verbose = !this.verbose;
284
+ const status = this.running ? '' : this.exitStatus;
285
+ this.output = this.shownOutput(this.raw, status);
286
+ this.ui.status = this.verbose ? 'verbose' : 'filtered';
287
+ this.ui.paint();
288
+ }
247
289
 
248
- const finishName = (api, editor) => {
249
- const name = editor.text.trim();
250
- if (!name || !NAME_RE.test(name)) {
251
- api.ui.status = 'name';
290
+ editable() {
291
+ if (this.viewing) return false;
292
+ const ui = this.ui;
293
+ const caps = ui.capabilities;
294
+ if (!ui.readOnly && (!caps || caps.changes !== false)) return true;
295
+ ui.status = 'read only';
252
296
  return false;
253
297
  }
254
- api.state.draftName = name;
255
- switchField(api, editor, 'command', api.state.draftCommand);
256
- return true;
257
- };
258
298
 
259
- const focusField = (api, field) => {
260
- const editor = api.ui.editor;
261
- if (!editor) return false;
262
- if (field === api.state.editField) return true;
263
- if (field === 'command') return finishName(api, editor);
264
- api.state.draftCommand = editor.text;
265
- switchField(api, editor, 'name', api.state.draftName ?? '');
266
- return true;
267
- };
299
+ selectedScript() {
300
+ if (!this.editable()) return null;
301
+ const entry = this.selected();
302
+ if (entry && entry.kind === 'script') return entry;
303
+ this.ui.status = 'not a script';
304
+ return null;
305
+ }
268
306
 
269
- const finishEdit = (api) => {
270
- const editor = api.ui.composer.state.editor;
271
- if (!editor) return;
272
- if (api.state.editField !== 'command') return void finishName(api, editor);
273
- const command = editor.text.trim();
274
- if (!command) {
275
- api.ui.status = 'command';
276
- return;
277
- }
278
- const parsed = { name: api.state.draftName, command };
279
- try {
280
- api.ui.ignoreWatch();
281
- saveScript(api.ui.top, api.state.editName, parsed);
282
- } catch (error) {
283
- api.ui.status = error.message;
284
- return;
285
- }
286
- api.state.editField = '';
287
- api.ui.composer.closeCompose();
288
- refresh(api);
289
- focusScript(api, parsed.name);
290
- api.ui.status = 'saved';
291
- };
307
+ focusScript(name) {
308
+ const at = this.commands.findIndex((entry) => entry.name === name);
309
+ if (at >= 0) this.ui.nav.npmCursor = at;
310
+ }
292
311
 
293
- const askDrop = (api) => {
294
- const entry = selectedScript(api);
295
- if (!entry) return;
296
- api.state.dropKind = 'script';
297
- api.state.dropName = entry.name;
298
- api.ui.mode = 'confirmDrop';
299
- api.ui.status = '';
300
- };
312
+ startEdit(name, command) {
313
+ this.editName = name;
314
+ this.editField = 'name';
315
+ this.draftName = name;
316
+ this.draftCommand = command;
317
+ this.ui.composer.openCompose('npm', name);
318
+ }
301
319
 
302
- const askLogs = (api) => {
303
- if (!editable(api)) return;
304
- refreshLogs(api);
305
- if (!api.state.logBytes) return;
306
- api.state.dropKind = 'logs';
307
- const size = formatSize(api.state.logBytes);
308
- api.state.dropName = `logs older than 5 days (${size})`;
309
- api.ui.mode = 'confirmDrop';
310
- api.ui.status = '';
311
- };
320
+ edit() {
321
+ const entry = this.selectedScript();
322
+ if (entry) this.startEdit(entry.name, entry.command ?? '');
323
+ }
312
324
 
313
- const confirmDrop = (api) => {
314
- const kind = api.state.dropKind;
315
- const name = api.state.dropName;
316
- api.state.dropName = '';
317
- api.state.dropKind = '';
318
- api.ui.mode = 'review';
319
- if (kind === 'logs') {
325
+ create() {
326
+ if (this.editable()) this.startEdit('', '');
327
+ }
328
+
329
+ switchField(editor, field, text) {
330
+ this.editField = field;
331
+ editor.replace(text);
332
+ this.ui.status = '';
333
+ }
334
+
335
+ finishName(editor) {
336
+ const name = editor.text.trim();
337
+ if (!name || !NAME_RE.test(name)) {
338
+ this.ui.status = 'name';
339
+ return false;
340
+ }
341
+ this.draftName = name;
342
+ this.switchField(editor, 'command', this.draftCommand);
343
+ return true;
344
+ }
345
+
346
+ focusField(field) {
347
+ const editor = this.ui.editor;
348
+ if (!editor) return false;
349
+ if (field === this.editField) return true;
350
+ if (field === 'command') return this.finishName(editor);
351
+ this.draftCommand = editor.text;
352
+ this.switchField(editor, 'name', this.draftName);
353
+ return true;
354
+ }
355
+
356
+ writeScripts(write) {
320
357
  try {
321
- removeStaleLogs(api.ui.top);
358
+ this.ui.ignoreWatch();
359
+ write(this.ui.top);
360
+ return true;
322
361
  } catch (error) {
323
- api.ui.status = error.message;
324
- refreshLogs(api);
362
+ this.ui.status = error.message;
363
+ return false;
364
+ }
365
+ }
366
+
367
+ finishEdit() {
368
+ const ui = this.ui;
369
+ const editor = ui.composer.editor;
370
+ if (!editor) return;
371
+ if (this.editField !== 'command') return void this.finishName(editor);
372
+ const command = editor.text.trim();
373
+ if (!command) {
374
+ ui.status = 'command';
325
375
  return;
326
376
  }
327
- refreshLogs(api);
328
- api.ui.status = 'dropped logs';
329
- return;
330
- }
331
- if (!name) return;
332
- try {
333
- api.ui.ignoreWatch();
334
- removeScript(api.ui.top, name);
335
- } catch (error) {
336
- api.ui.status = error.message;
337
- return;
338
- }
339
- refresh(api);
340
- api.ui.status = `dropped ${name}`;
341
- };
377
+ const parsed = { name: this.draftName, command };
378
+ const write = (top) => saveScript(top, this.editName, parsed);
379
+ if (!this.writeScripts(write)) return;
380
+ this.editField = '';
381
+ ui.composer.closeCompose();
382
+ this.refresh();
383
+ this.focusScript(parsed.name);
384
+ ui.status = 'saved';
385
+ }
342
386
 
343
- const cancelDrop = (api) => {
344
- api.state.dropName = '';
345
- api.state.dropKind = '';
346
- api.ui.mode = 'review';
347
- api.ui.status = '';
348
- };
387
+ confirm(kind, name) {
388
+ this.dropKind = kind;
389
+ this.dropName = name;
390
+ this.ui.mode = 'confirmDrop';
391
+ this.ui.status = '';
392
+ }
349
393
 
350
- const reorder = (api, delta) => {
351
- const entry = selectedScript(api);
352
- if (!entry) return;
353
- try {
354
- api.ui.ignoreWatch();
355
- if (!reorderScript(api.ui.top, entry.name, delta)) return;
356
- } catch (error) {
357
- api.ui.status = error.message;
358
- return;
359
- }
360
- refresh(api);
361
- focusScript(api, entry.name);
362
- api.ui.status = '';
363
- };
394
+ askDrop() {
395
+ const entry = this.selectedScript();
396
+ if (entry) this.confirm('script', entry.name);
397
+ }
364
398
 
365
- const npmState = () => ({
366
- commands: [],
367
- viewing: false,
368
- output: '',
369
- followEnd: true,
370
- logScroll: 0,
371
- editName: '',
372
- editField: '',
373
- draftName: '',
374
- draftCommand: '',
375
- verbose: false,
376
- raw: '',
377
- exitStatus: '',
378
- dropName: '',
379
- dropKind: '',
380
- logBytes: 0,
381
- running: false,
382
- stopping: false,
383
- child: null,
384
- });
385
-
386
- const createNpmController = (ui) => {
387
- const state = npmState();
388
- const api = { ui, state };
389
- return {
390
- state,
391
- get commands() {
392
- return state.commands;
393
- },
394
- get viewing() {
395
- return state.viewing;
396
- },
397
- get output() {
398
- return state.output;
399
- },
400
- get dropName() {
401
- return state.dropName;
402
- },
403
- get logLabel() {
404
- if (!state.logBytes) return '';
405
- return `old logs ${formatSize(state.logBytes)}`;
406
- },
407
- get followEnd() {
408
- return state.followEnd;
409
- },
410
- get logScroll() {
411
- return state.logScroll;
412
- },
413
- get editName() {
414
- return state.editName;
415
- },
416
- get editField() {
417
- return state.editField;
418
- },
419
- get draftName() {
420
- return state.draftName;
421
- },
422
- get draftCommand() {
423
- return state.draftCommand;
424
- },
425
- get verbose() {
426
- return state.verbose;
427
- },
428
- get running() {
429
- return state.running;
430
- },
431
- reset() {
432
- if (state.child) state.child.kill();
433
- stopRun(api);
434
- state.commands = [];
435
- state.viewing = false;
436
- state.output = '';
437
- state.followEnd = true;
438
- state.logScroll = 0;
439
- state.editName = '';
440
- state.editField = '';
441
- state.draftName = '';
442
- state.draftCommand = '';
443
- state.verbose = false;
444
- state.raw = '';
445
- state.exitStatus = '';
446
- state.dropName = '';
447
- state.dropKind = '';
448
- state.logBytes = 0;
449
- state.stopping = false;
450
- },
451
- open: () => open(api),
452
- move: (delta) => move(api, delta),
453
- run: () => runSelected(api),
454
- rerun: () => rerun(api),
455
- closeView: () => closeView(api),
456
- toggleVerbose: () => toggleVerbose(api),
457
- edit: () => editSelected(api),
458
- create: () => newCommand(api),
459
- finishEdit: () => finishEdit(api),
460
- focusField: (field) => focusField(api, field),
461
- askDrop: () => askDrop(api),
462
- askLogs: () => askLogs(api),
463
- confirmDrop: () => confirmDrop(api),
464
- cancelDrop: () => cancelDrop(api),
465
- reorder: (delta) => reorder(api, delta),
466
- };
467
- };
399
+ askLogs() {
400
+ if (!this.editable()) return;
401
+ this.refreshLogs();
402
+ if (!this.logBytes) return;
403
+ const size = bytesToSize(this.logBytes);
404
+ this.confirm('logs', `logs older than 5 days (${size})`);
405
+ }
406
+
407
+ cancelDrop() {
408
+ this.dropName = '';
409
+ this.dropKind = '';
410
+ this.ui.mode = 'review';
411
+ this.ui.status = '';
412
+ }
413
+
414
+ confirmDrop() {
415
+ const ui = this.ui;
416
+ const kind = this.dropKind;
417
+ const name = this.dropName;
418
+ this.dropName = '';
419
+ this.dropKind = '';
420
+ ui.mode = 'review';
421
+ if (kind === 'logs') {
422
+ try {
423
+ removeStaleLogs(ui.top);
424
+ } catch (error) {
425
+ ui.status = error.message;
426
+ return void this.refreshLogs();
427
+ }
428
+ this.refreshLogs();
429
+ ui.status = 'dropped logs';
430
+ return;
431
+ }
432
+ if (!name) return;
433
+ if (!this.writeScripts((top) => removeScript(top, name))) return;
434
+ this.refresh();
435
+ ui.status = `dropped ${name}`;
436
+ }
437
+
438
+ reorder(delta) {
439
+ const entry = this.selectedScript();
440
+ if (!entry) return;
441
+ let moved = false;
442
+ const write = (top) => {
443
+ moved = reorderScript(top, entry.name, delta);
444
+ };
445
+ if (!this.writeScripts(write) || !moved) return;
446
+ this.refresh();
447
+ this.focusScript(entry.name);
448
+ this.ui.status = '';
449
+ }
450
+ }
468
451
 
469
- module.exports = { createNpmController };
452
+ module.exports = { NpmController };