reslop 0.2.4 → 0.2.6

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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -1,94 +1,47 @@
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;
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;
6
16
 
7
- const { listCommands, reduceOutput, writeLog, parseScriptLine } = commands;
8
- const { staleLogFiles, removeStaleLogs, formatSize } = commands;
9
- const { scriptLine, saveScript, removeScript, reorderScript } = commands;
10
- const { startNpm } = commands;
17
+ const { listCommands, reduceOutput, nextLogFile, saveLogs } = commands;
18
+ const { staleLogFiles, removeStaleLogs, startNpm } = commands;
19
+ const { saveScript, removeScript, reorderScript } = commands;
11
20
 
12
- const selected = (api) => {
13
- const cursor = api.ui.nav.npmCursor;
14
- return api.state.commands[cursor];
15
- };
16
-
17
- const clampCursor = (api) => {
18
- const last = api.state.commands.length - 1;
19
- const nav = api.ui.nav;
20
- if (nav.npmCursor > last) nav.npmCursor = Math.max(0, last);
21
- if (nav.npmCursor < 0) nav.npmCursor = 0;
22
- };
21
+ const NAME_RE = /^[\w:.-]+$/;
22
+ const TESTS_RE = /^(?:TAP version \d+|\s*(?:not )?ok\b|[ℹi]\s+tests\s+\d+)/m;
23
23
 
24
- const refresh = (api) => {
25
- api.state.commands = listCommands(api.ui.top);
26
- clampCursor(api);
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;
27
31
  };
28
32
 
29
- const canEdit = (ui) => {
30
- if (ui.capabilities && ui.capabilities.changes === false) return false;
31
- if (ui.readOnly) return false;
32
- return true;
33
- };
34
-
35
- const refreshLogs = (api) => {
36
- try {
37
- const found = staleLogFiles(api.ui.top);
38
- api.state.logBytes = found.bytes;
39
- } catch (error) {
40
- api.state.logBytes = 0;
41
- api.ui.status = error.message;
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 };
42
38
  }
39
+ const doc = buildDocument(raw, '', root, status, 'test');
40
+ return { output: renderReport(doc), log: renderDocument(doc, rawFile) };
43
41
  };
44
42
 
45
- const open = (api) => {
46
- if (api.ui.mode === 'compose') return;
47
- refresh(api);
48
- refreshLogs(api);
49
- api.state.viewing = false;
50
- api.state.output = '';
51
- api.ui.nav.pane = 'npm';
52
- api.ui.status = '';
53
- api.ui.nav.clearSelection();
54
- };
55
-
56
- const scrollLog = (api, delta) => {
57
- const lines = api.state.output.split('\n');
58
- let count = lines.at(-1) === '' ? lines.length - 1 : lines.length;
59
- if (api.state.running) count += 1;
60
- const frame = api.ui.lastFrame;
61
- const bodyH = frame && frame.bodyH ? frame.bodyH : 1;
62
- const max = Math.max(0, count - logViewRows(bodyH));
63
- let start = api.state.followEnd ? max : api.state.logScroll;
64
- start = Math.min(max, Math.max(0, start + delta));
65
- api.state.followEnd = start === max;
66
- api.state.logScroll = start;
67
- };
68
-
69
- const move = (api, delta) => {
70
- if (api.state.viewing) return void scrollLog(api, delta);
71
- if (!api.state.commands.length) return;
72
- const nav = api.ui.nav;
73
- const last = api.state.commands.length - 1;
74
- const next = Math.min(last, Math.max(0, nav.npmCursor + delta));
75
- if (next === nav.npmCursor) return;
76
- nav.npmCursor = next;
77
- api.ui.status = '';
78
- };
79
-
80
- const stopRun = (api) => {
81
- api.state.running = false;
82
- api.state.child = null;
83
- if (api.ui.progress) api.ui.progress.stop('npm');
84
- };
85
-
86
- const showOutput = (api, text) => {
87
- if (!api.state.viewing || !api.state.running) return;
88
- if (api.state.stopping) return;
89
- api.state.output = text;
90
- api.ui.paint();
91
- };
43
+ const runLabel = (entry) =>
44
+ entry.kind === 'bin' ? `npm exec ${entry.name}` : `npm run ${entry.name}`;
92
45
 
93
46
  const withNotice = (text, notice) => {
94
47
  const body = `${text ?? ''}`;
@@ -98,291 +51,402 @@ const withNotice = (text, notice) => {
98
51
  return `${body}\n${notice}\n`;
99
52
  };
100
53
 
101
- const finishRun = (api, entry, result) => {
102
- const stopped = api.state.stopping === true;
103
- const reduced = stopped
104
- ? withNotice(api.state.output, 'terminated')
105
- : reduceOutput(result.text, api.ui.top, result.status);
106
- api.state.output = reduced;
107
- api.state.stopping = false;
108
- stopRun(api);
109
- try {
110
- writeLog(api.ui.top, entry.name, reduced);
111
- } catch (error) {
112
- api.ui.status = error.message;
113
- api.ui.paint();
114
- return;
115
- }
116
- if (stopped && api.state.viewing) api.ui.status = 'terminated';
117
- api.ui.paint();
54
+ const withNewline = (text) => {
55
+ const body = `${text ?? ''}`;
56
+ if (!body || body.endsWith('\n')) return body;
57
+ return `${body}\n`;
118
58
  };
119
59
 
120
- const startRun = (api, entry) => {
121
- api.state.output = '';
122
- api.state.viewing = true;
123
- api.state.followEnd = true;
124
- api.state.logScroll = 0;
125
- api.state.running = true;
126
- api.state.stopping = false;
127
- api.ui.status = '';
128
- if (api.ui.progress) api.ui.progress.start('npm');
129
- const runner = api.ui.repo && api.ui.repo.runNpmCommand;
130
- const start = typeof runner === 'function' ? runner : startNpm;
131
- api.state.child = start(
132
- api.ui.top,
133
- entry,
134
- (text) => showOutput(api, reduceOutput(text, api.ui.top, '')),
135
- (result) => finishRun(api, entry, result),
136
- );
137
- api.ui.paint();
138
- };
60
+ class NpmController {
61
+ constructor(ui) {
62
+ this.ui = ui;
63
+ this.child = null;
64
+ this.running = false;
65
+ this.reset();
66
+ }
139
67
 
140
- const runSelected = (api) => {
141
- if (api.state.viewing || api.state.child) return;
142
- const entry = selected(api);
143
- if (!entry) return;
144
- startRun(api, entry);
145
- };
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
+ }
146
91
 
147
- const rerun = (api) => {
148
- if (!api.state.viewing || api.state.running) return;
149
- const entry = selected(api);
150
- if (!entry) return;
151
- startRun(api, entry);
152
- };
92
+ get logLabel() {
93
+ if (!this.logBytes) return '';
94
+ return `old logs ${bytesToSize(this.logBytes)}`;
95
+ }
153
96
 
154
- const closeView = (api) => {
155
- if (api.state.running) {
156
- api.state.stopping = true;
157
- if (api.state.child) api.state.child.kill();
158
- if (!api.state.running) return;
159
- api.state.output = withNotice(api.state.output, 'terminated');
160
- stopRun(api);
161
- api.ui.status = 'terminated';
162
- api.ui.paint();
163
- return;
164
- }
165
- api.state.viewing = false;
166
- api.ui.status = '';
167
- api.ui.paint();
168
- };
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
+ }
169
107
 
170
- const editSelected = (api) => {
171
- if (api.state.viewing) return;
172
- if (!canEdit(api.ui)) {
173
- api.ui.status = 'read only';
174
- return;
108
+ selected() {
109
+ return this.commands[this.ui.nav.npmCursor];
175
110
  }
176
- const entry = selected(api);
177
- if (!entry || entry.kind !== 'script') {
178
- api.ui.status = 'not a script';
179
- return;
111
+
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));
180
117
  }
181
- api.state.editName = entry.name;
182
- api.ui.composer.openCompose('npm', scriptLine(entry));
183
- };
184
118
 
185
- const newCommand = (api) => {
186
- if (api.state.viewing) return;
187
- if (!canEdit(api.ui)) {
188
- api.ui.status = 'read only';
189
- return;
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
+ }
190
126
  }
191
- api.state.editName = '';
192
- api.ui.composer.openCompose('npm', '');
193
- };
194
127
 
195
- const finishEdit = (api) => {
196
- const editor = api.ui.composer.state.editor;
197
- const text = editor ? editor.text : '';
198
- const parsed = parseScriptLine(text);
199
- if (!parsed) {
200
- api.ui.status = 'name: command';
201
- return;
202
- }
203
- try {
204
- api.ui.ignoreWatch();
205
- saveScript(api.ui.top, api.state.editName, parsed);
206
- } catch (error) {
207
- api.ui.status = error.message;
208
- return;
209
- }
210
- api.ui.composer.closeCompose();
211
- refresh(api);
212
- const at = api.state.commands.findIndex(
213
- (entry) => entry.name === parsed.name,
214
- );
215
- if (at >= 0) api.ui.nav.npmCursor = at;
216
- api.ui.status = 'saved';
217
- };
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
+ }
218
140
 
219
- const askDrop = (api) => {
220
- if (api.state.viewing) return;
221
- if (!canEdit(api.ui)) {
222
- api.ui.status = 'read only';
223
- return;
224
- }
225
- const entry = selected(api);
226
- if (!entry || entry.kind !== 'script') {
227
- api.ui.status = 'not a script';
228
- return;
229
- }
230
- api.state.dropKind = 'script';
231
- api.state.dropName = entry.name;
232
- api.ui.mode = 'confirmDrop';
233
- api.ui.status = '';
234
- };
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
+ }
235
152
 
236
- const askLogs = (api) => {
237
- if (api.state.viewing) return;
238
- if (!canEdit(api.ui)) {
239
- api.ui.status = 'read only';
240
- return;
241
- }
242
- refreshLogs(api);
243
- if (!api.state.logBytes) return;
244
- api.state.dropKind = 'logs';
245
- const size = formatSize(api.state.logBytes);
246
- api.state.dropName = `logs older than 5 days (${size})`;
247
- api.ui.mode = 'confirmDrop';
248
- api.ui.status = '';
249
- };
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
+ }
163
+
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
+ }
170
+
171
+ shownOutput(raw, status) {
172
+ if (this.verbose) return withNewline(raw);
173
+ return filteredOutput(raw, this.ui.top, status).output;
174
+ }
175
+
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
+ }
182
+
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
+ }
250
219
 
251
- const confirmDrop = (api) => {
252
- const kind = api.state.dropKind;
253
- const name = api.state.dropName;
254
- api.state.dropName = '';
255
- api.state.dropKind = '';
256
- api.ui.mode = 'review';
257
- if (kind === 'logs') {
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
+ }
246
+
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
+ }
254
+
255
+ rerun() {
256
+ if (!this.viewing || this.running) return;
257
+ const entry = this.selected();
258
+ if (entry) this.startRun(entry);
259
+ }
260
+
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
+ }
280
+
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
+ }
289
+
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';
296
+ return false;
297
+ }
298
+
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
+ }
306
+
307
+ focusScript(name) {
308
+ const at = this.commands.findIndex((entry) => entry.name === name);
309
+ if (at >= 0) this.ui.nav.npmCursor = at;
310
+ }
311
+
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
+ }
319
+
320
+ edit() {
321
+ const entry = this.selectedScript();
322
+ if (entry) this.startEdit(entry.name, entry.command ?? '');
323
+ }
324
+
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) {
258
357
  try {
259
- removeStaleLogs(api.ui.top);
358
+ this.ui.ignoreWatch();
359
+ write(this.ui.top);
360
+ return true;
260
361
  } catch (error) {
261
- api.ui.status = error.message;
262
- 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';
263
375
  return;
264
376
  }
265
- refreshLogs(api);
266
- api.ui.status = 'dropped logs';
267
- return;
268
- }
269
- if (!name) return;
270
- try {
271
- api.ui.ignoreWatch();
272
- removeScript(api.ui.top, name);
273
- } catch (error) {
274
- api.ui.status = error.message;
275
- return;
276
- }
277
- refresh(api);
278
- api.ui.status = `dropped ${name}`;
279
- };
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
+ }
280
386
 
281
- const cancelDrop = (api) => {
282
- api.state.dropName = '';
283
- api.state.dropKind = '';
284
- api.ui.mode = 'review';
285
- api.ui.status = '';
286
- };
387
+ confirm(kind, name) {
388
+ this.dropKind = kind;
389
+ this.dropName = name;
390
+ this.ui.mode = 'confirmDrop';
391
+ this.ui.status = '';
392
+ }
287
393
 
288
- const reorder = (api, delta) => {
289
- if (api.state.viewing) return;
290
- if (!canEdit(api.ui)) {
291
- api.ui.status = 'read only';
292
- return;
293
- }
294
- const entry = selected(api);
295
- if (!entry || entry.kind !== 'script') {
296
- api.ui.status = 'not a script';
297
- return;
298
- }
299
- try {
300
- api.ui.ignoreWatch();
301
- if (!reorderScript(api.ui.top, entry.name, delta)) return;
302
- } catch (error) {
303
- api.ui.status = error.message;
304
- return;
305
- }
306
- refresh(api);
307
- const at = api.state.commands.findIndex((item) => item.name === entry.name);
308
- if (at >= 0) api.ui.nav.npmCursor = at;
309
- api.ui.status = '';
310
- };
394
+ askDrop() {
395
+ const entry = this.selectedScript();
396
+ if (entry) this.confirm('script', entry.name);
397
+ }
311
398
 
312
- const createNpmController = (ui) => {
313
- const state = {
314
- commands: [],
315
- viewing: false,
316
- output: '',
317
- followEnd: true,
318
- logScroll: 0,
319
- editName: '',
320
- dropName: '',
321
- dropKind: '',
322
- logBytes: 0,
323
- running: false,
324
- stopping: false,
325
- child: null,
326
- };
327
- const api = { ui, state };
328
- return {
329
- state,
330
- get commands() {
331
- return state.commands;
332
- },
333
- get viewing() {
334
- return state.viewing;
335
- },
336
- get output() {
337
- return state.output;
338
- },
339
- get dropName() {
340
- return state.dropName;
341
- },
342
- get logLabel() {
343
- if (!state.logBytes) return '';
344
- return `old logs ${formatSize(state.logBytes)}`;
345
- },
346
- get followEnd() {
347
- return state.followEnd;
348
- },
349
- get logScroll() {
350
- return state.logScroll;
351
- },
352
- get editName() {
353
- return state.editName;
354
- },
355
- get running() {
356
- return state.running;
357
- },
358
- reset() {
359
- if (state.child) state.child.kill();
360
- stopRun(api);
361
- state.commands = [];
362
- state.viewing = false;
363
- state.output = '';
364
- state.followEnd = true;
365
- state.logScroll = 0;
366
- state.editName = '';
367
- state.dropName = '';
368
- state.dropKind = '';
369
- state.logBytes = 0;
370
- state.stopping = false;
371
- },
372
- open: () => open(api),
373
- move: (delta) => move(api, delta),
374
- run: () => runSelected(api),
375
- rerun: () => rerun(api),
376
- closeView: () => closeView(api),
377
- edit: () => editSelected(api),
378
- create: () => newCommand(api),
379
- finishEdit: () => finishEdit(api),
380
- askDrop: () => askDrop(api),
381
- askLogs: () => askLogs(api),
382
- confirmDrop: () => confirmDrop(api),
383
- cancelDrop: () => cancelDrop(api),
384
- reorder: (delta) => reorder(api, delta),
385
- };
386
- };
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
+ }
387
451
 
388
- module.exports = { createNpmController };
452
+ module.exports = { NpmController };