reslop 0.2.7 → 0.3.1

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 (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,836 @@
1
+ 'use strict';
2
+
3
+ const { withNotice, withNewline, clamp } = require('../../common/utilities.js');
4
+ const commands = require('../../runs/commands.js');
5
+ const { RunPanel } = require('../run-panel.js');
6
+ const report = require('../../report/render.js');
7
+ const { loggedText } = report;
8
+ const runStore = require('../../runs/runs.js');
9
+ const { readRuns } = runStore;
10
+ const { foldSteps } = require('../../dashboard/model.js');
11
+ const {
12
+ listCommands,
13
+ saveScript,
14
+ removeScript,
15
+ reorderScript,
16
+ } = require('../../runs/scripts.js');
17
+ const { reduceOutput } = require('../../runs/output.js');
18
+ const {
19
+ nextLogFile,
20
+ saveLogs,
21
+ readSavedRuns,
22
+ readLogPair,
23
+ staleLogFiles,
24
+ removeStaleLogs,
25
+ } = require('../../runs/logs.js');
26
+ const { startNpm } = commands;
27
+ const historyPart = require('./history.js');
28
+ const { launchedReslop, RUN_KEEP, historyCounts, historyExit } = historyPart;
29
+ const { historyName, historyMark, countText, elapsedText } = historyPart;
30
+ const { whenLabel, runLabel, logBase, applyExternal } = historyPart;
31
+ const { recordedItem, sameWindow, noteTests, filteredOutput } = historyPart;
32
+ const { size } = require('../../common/format.js');
33
+
34
+ const NAME_RE = /^[\w:.-]+$/;
35
+
36
+ const OUTPUT_MS = 60;
37
+
38
+ const newRun = (id, fields) => ({
39
+ id,
40
+ raw: '',
41
+ output: '',
42
+ child: null,
43
+ stopping: false,
44
+ followEnd: true,
45
+ logScroll: 0,
46
+ result: null,
47
+ progress: null,
48
+ ...fields,
49
+ });
50
+
51
+ class NpmController extends RunPanel {
52
+ constructor(ui) {
53
+ super(ui);
54
+ this.reset();
55
+ }
56
+
57
+ reset() {
58
+ for (const run of this.runs ?? []) {
59
+ if (run.status === 'running' && run.child) run.child.kill();
60
+ }
61
+ this.runs = [];
62
+ this.nextId = 1;
63
+ this.viewId = 0;
64
+ this.focus = 'commands';
65
+ this.runCursor = 0;
66
+ this.runScroll = 0;
67
+ this.commands = [];
68
+ this.viewing = false;
69
+ this.verbose = false;
70
+ this.editName = '';
71
+ this.editField = '';
72
+ this.draftName = '';
73
+ this.draftCommand = '';
74
+ this.dropName = '';
75
+ this.dropKind = '';
76
+ this.logBytes = 0;
77
+ this.ui.progress.stop('npm');
78
+ }
79
+
80
+ get logLabel() {
81
+ if (!this.logBytes) return '';
82
+ return `old logs ${size(this.logBytes)}`;
83
+ }
84
+
85
+ get running() {
86
+ return this.runs.some((run) => run.status === 'running');
87
+ }
88
+
89
+ view(active) {
90
+ return {
91
+ npmCommands: this.commands,
92
+ npmCursor: this.ui.nav.npmCursor,
93
+ npmView: this.viewing,
94
+ npmOutput: this.output,
95
+ npmFollow: this.followEnd,
96
+ npmScroll: this.logScroll,
97
+ npmRuns: this.sessionRows(),
98
+ npmRunCursor: this.runCursor,
99
+ npmFocus: this.focus,
100
+ npmRunScroll: this.runScroll,
101
+ npmEditName: this.editName,
102
+ npmEditField: this.editField,
103
+ npmDraftName: this.draftName,
104
+ npmDraftCommand: this.draftCommand,
105
+ npmRunning: this.viewedRunning(),
106
+ npmLogs: active ? this.logLabel : '',
107
+ };
108
+ }
109
+
110
+ get output() {
111
+ const run = this.viewedRun();
112
+ if (!run) return '';
113
+ if (this.verbose) return withNewline(run.raw);
114
+ this.syncOutput(run);
115
+ return run.output ?? '';
116
+ }
117
+
118
+ get followEnd() {
119
+ const run = this.viewedRun();
120
+ if (!run) return true;
121
+ return run.followEnd !== false;
122
+ }
123
+
124
+ set followEnd(value) {
125
+ const run = this.viewedRun();
126
+ if (run) run.followEnd = value;
127
+ }
128
+
129
+ get logScroll() {
130
+ return this.viewedRun()?.logScroll ?? 0;
131
+ }
132
+
133
+ set logScroll(value) {
134
+ const run = this.viewedRun();
135
+ if (run) run.logScroll = value;
136
+ }
137
+
138
+ dashboardRuns() {
139
+ return this.historyRuns().map((run) => ({
140
+ name: run.name,
141
+ kind: run.kind,
142
+ command: run.command,
143
+ label: run.label,
144
+ running: run.status === 'running',
145
+ exit: run.exit,
146
+ startedAt: run.startedAt || 0,
147
+ endedAt: run.endedAt || 0,
148
+ progress: run.progress,
149
+ result: run.result,
150
+ }));
151
+ }
152
+
153
+ get lastRun() {
154
+ const live = this.runs.find((run) => run.status === 'running');
155
+ const run = live || this.runs.at(-1);
156
+ if (!run) return null;
157
+ return {
158
+ label: run.label,
159
+ running: run.status === 'running',
160
+ exit: run.exit,
161
+ startedAt: run.startedAt,
162
+ endedAt: run.endedAt,
163
+ };
164
+ }
165
+
166
+ viewedRun() {
167
+ return this.runs.find((run) => run.id === this.viewId) || null;
168
+ }
169
+
170
+ viewedRunning() {
171
+ if (!this.viewing) return this.running;
172
+ const run = this.viewedRun();
173
+ return Boolean(run && run.status === 'running');
174
+ }
175
+
176
+ historyRuns() {
177
+ const runs = this.runs.filter((run) => !launchedReslop(run));
178
+ return foldSteps(runs).slice(0, RUN_KEEP);
179
+ }
180
+
181
+ sessionRows() {
182
+ const now = Date.now();
183
+ return this.historyRuns().map((run) => {
184
+ const counts = historyCounts(run);
185
+ const exitText = historyExit(run, counts);
186
+ return {
187
+ id: run.id,
188
+ name: historyName(run),
189
+ live: run.status === 'running',
190
+ mark: historyMark(run),
191
+ done: countText(counts?.done),
192
+ ok: countText(counts?.ok),
193
+ fail: countText(counts?.fail),
194
+ total: counts ? countText(counts.total) : exitText,
195
+ exitLabel: exitText !== '',
196
+ okN: counts ? counts.ok : null,
197
+ failN: counts ? counts.fail : null,
198
+ elapsed: elapsedText(run, now),
199
+ when: whenLabel(run.startedAt || run.endedAt || 0),
200
+ };
201
+ });
202
+ }
203
+
204
+ clampRuns() {
205
+ const last = Math.max(0, this.historyRuns().length - 1);
206
+ this.runCursor = clamp(this.runCursor, 0, last);
207
+ }
208
+
209
+ focusRun(run) {
210
+ const at = this.commands.findIndex(
211
+ (entry) => entry.name === run.name && entry.kind === run.kind,
212
+ );
213
+ if (at >= 0) this.ui.nav.npmCursor = at;
214
+ const index = this.historyRuns().findIndex((item) => item.id === run.id);
215
+ if (index >= 0) this.runCursor = index;
216
+ }
217
+
218
+ selected() {
219
+ return this.commands[this.ui.nav.npmCursor];
220
+ }
221
+
222
+ refresh() {
223
+ this.commands = listCommands(this.ui.top);
224
+ const last = this.commands.length - 1;
225
+ const nav = this.ui.nav;
226
+ nav.npmCursor = clamp(nav.npmCursor, 0, last);
227
+ }
228
+
229
+ refreshLogs() {
230
+ try {
231
+ this.logBytes = staleLogFiles(this.ui.top).bytes;
232
+ } catch (error) {
233
+ this.logBytes = 0;
234
+ this.ui.status = error.message;
235
+ }
236
+ }
237
+
238
+ adopt(item) {
239
+ const run = newRun(this.nextId, {
240
+ name: item.name,
241
+ kind: item.kind,
242
+ script: item.script ?? '',
243
+ command: item.command ?? '',
244
+ label: item.kind === 'command' ? historyName(item) : runLabel(item),
245
+ status: item.status,
246
+ exit: item.exit,
247
+ startedAt: item.startedAt,
248
+ endedAt: item.endedAt,
249
+ logName: item.logName,
250
+ rawName: item.rawName,
251
+ externalId: item.externalId || '',
252
+ result: item.result ?? null,
253
+ progress: item.progress ?? null,
254
+ saved: true,
255
+ hydrated: false,
256
+ });
257
+ this.nextId += 1;
258
+ return run;
259
+ }
260
+
261
+ knownLogs() {
262
+ const known = new Set();
263
+ for (const run of this.runs) {
264
+ if (run.logName) known.add(run.logName);
265
+ if (run.externalId) known.add(run.externalId);
266
+ }
267
+ return known;
268
+ }
269
+
270
+ findRecorded(key, logName) {
271
+ return this.runs.find((run) => {
272
+ if (key && run.externalId === key) return true;
273
+ return Boolean(logName && run.externalId && run.logName === logName);
274
+ });
275
+ }
276
+
277
+ absorbRecorded(recorded, known, added) {
278
+ let changed = false;
279
+ for (const record of recorded) {
280
+ const logName = logBase(record.log);
281
+ const key = `${record.id ?? ''}`;
282
+ const found = this.findRecorded(key, logName);
283
+ if (found) {
284
+ if (applyExternal(found, record)) changed = true;
285
+ continue;
286
+ }
287
+ const seenLog = logName && known.has(logName);
288
+ const seenKey = key && known.has(key);
289
+ if (seenLog || seenKey) continue;
290
+ if (logName) known.add(logName);
291
+ if (key) known.add(key);
292
+ added.push(this.adopt(recordedItem(record)));
293
+ changed = true;
294
+ }
295
+ return changed;
296
+ }
297
+
298
+ settleLost(recorded) {
299
+ const live = new Set(recorded.map((record) => `${record.id ?? ''}`));
300
+ let changed = false;
301
+ for (const run of this.runs) {
302
+ const external = run.externalId && run.status === 'running';
303
+ if (!external || live.has(run.externalId)) continue;
304
+ run.status = 'stopped';
305
+ run.exit = '';
306
+ run.endedAt ||= Date.now();
307
+ changed = true;
308
+ }
309
+ return changed;
310
+ }
311
+
312
+ storeAdded(added) {
313
+ if (!added.length) return;
314
+ const running = this.runs.filter((run) => run.status === 'running');
315
+ const done = this.runs.filter((run) => run.status !== 'running');
316
+ done.push(...added);
317
+ done.sort((left, right) => {
318
+ const at = left.startedAt - right.startedAt;
319
+ if (at) return at;
320
+ return left.id - right.id;
321
+ });
322
+ this.runs = [...done, ...running];
323
+ }
324
+
325
+ followAdded(added) {
326
+ const current = this.viewedRun();
327
+ if (!this.viewing || !current?.externalId || !current.script) return false;
328
+ if (current.followEnd === false) return false;
329
+ const next = added
330
+ .filter(
331
+ (run) =>
332
+ run.script === current.script &&
333
+ run.startedAt >= current.startedAt &&
334
+ sameWindow(current, run),
335
+ )
336
+ .sort((left, right) => left.startedAt - right.startedAt)
337
+ .at(-1);
338
+ if (!next) return false;
339
+ this.viewId = next.id;
340
+ this.focusRun(next);
341
+ return true;
342
+ }
343
+
344
+ followExternal() {
345
+ this.syncProgress();
346
+ if (this.ui.nav.pane === 'npm') this.ui.paint();
347
+ }
348
+
349
+ refreshRecorded() {
350
+ let recorded = null;
351
+ try {
352
+ recorded = readRuns(this.ui.top);
353
+ } catch {
354
+ // unreadable runs
355
+ }
356
+ if (!recorded) return;
357
+ const added = [];
358
+ let changed = this.absorbRecorded(recorded, this.knownLogs(), added);
359
+ if (this.settleLost(recorded)) changed = true;
360
+ this.storeAdded(added);
361
+ if (this.followAdded(added)) changed = true;
362
+ if (this.hydrate(this.viewedRun())) changed = true;
363
+ if (changed) this.followExternal();
364
+ }
365
+
366
+ loadSaved() {
367
+ const known = this.knownLogs();
368
+ let saved = null;
369
+ let recorded = null;
370
+ try {
371
+ saved = readSavedRuns(this.ui.top, this.commands);
372
+ recorded = readRuns(this.ui.top);
373
+ } catch (error) {
374
+ this.ui.status = error.message;
375
+ }
376
+ if (!saved || !recorded) return;
377
+ const added = [];
378
+ for (const item of saved) {
379
+ if (!item.logName || known.has(item.logName)) continue;
380
+ known.add(item.logName);
381
+ added.push(this.adopt(item));
382
+ }
383
+ const changed = this.absorbRecorded(recorded, known, added);
384
+ this.storeAdded(added);
385
+ if (changed) this.followExternal();
386
+ }
387
+
388
+ siblingText(run) {
389
+ const name = historyName(run);
390
+ for (const other of this.runs) {
391
+ if (other === run || historyName(other) !== name) continue;
392
+ if (!sameWindow(run, other)) continue;
393
+ const saved = readLogPair(this.ui.top, other.logName, other.rawName);
394
+ const output = other.output || saved.output;
395
+ const raw = other.raw || saved.raw;
396
+ if (!output && !raw) continue;
397
+ return { output, raw: raw || output };
398
+ }
399
+ return { output: '', raw: '' };
400
+ }
401
+
402
+ hydrate(run) {
403
+ if (!run || !run.logName) return false;
404
+ const settled = run.externalId && run.status !== 'running';
405
+ if (!settled && (run.hydrated || run.output || run.raw)) return false;
406
+ const text = readLogPair(this.ui.top, run.logName, run.rawName);
407
+ const saved = text.output || text.raw ? text : this.siblingText(run);
408
+ if (!saved.output && !saved.raw) return false;
409
+ const raw = saved.raw || saved.output;
410
+ if (run.output === saved.output && run.raw === raw) return false;
411
+ run.output = saved.output;
412
+ run.raw = raw;
413
+ run.hydrated = true;
414
+ return true;
415
+ }
416
+
417
+ open() {
418
+ if (this.ui.mode === 'compose') return;
419
+ this.refresh();
420
+ this.refreshLogs();
421
+ const fresh = this.runs.length === 0;
422
+ this.loadSaved();
423
+ if (fresh) {
424
+ if (this.historyRuns().length) this.runCursor = 0;
425
+ }
426
+ this.viewing = false;
427
+ this.verbose = false;
428
+ const nav = this.ui.nav;
429
+ nav.pane = 'npm';
430
+ nav.resetListScroll('npm');
431
+ this.ui.status = '';
432
+ nav.clearSelection();
433
+ }
434
+
435
+ syncProgress() {
436
+ if (this.running) this.ui.progress.start('npm');
437
+ else this.ui.progress.stop('npm');
438
+ }
439
+
440
+ placeRuns(start) {
441
+ const bar = this.ui.lastFrame && this.ui.lastFrame.scrollBar;
442
+ if (!bar) return;
443
+ super.placeRuns(start);
444
+ const top = this.runScroll;
445
+ const last = Math.max(0, this.historyRuns().length - 1);
446
+ if (this.runCursor < top) this.runCursor = top;
447
+ const bottom = Math.min(last, top + bar.rows - 1);
448
+ if (this.runCursor > bottom) this.runCursor = bottom;
449
+ }
450
+
451
+ scrollLog(delta) {
452
+ this.scrollLogBy(delta, this.output, this.viewedRunning());
453
+ }
454
+
455
+ moveRun(delta) {
456
+ const rows = this.historyRuns();
457
+ if (!rows.length) return;
458
+ const last = rows.length - 1;
459
+ const next = clamp(this.runCursor + delta, 0, last);
460
+ if (next === this.runCursor) return;
461
+ this.runCursor = next;
462
+ this.ui.status = '';
463
+ this.ui.paint();
464
+ }
465
+
466
+ move(delta) {
467
+ if (this.viewing) return void this.scrollLog(delta);
468
+ if (this.focus === 'runs') return void this.moveRun(delta);
469
+ if (!this.commands.length) return;
470
+ const nav = this.ui.nav;
471
+ const last = this.commands.length - 1;
472
+ const next = clamp(nav.npmCursor + delta, 0, last);
473
+ if (next === nav.npmCursor) return;
474
+ nav.npmCursor = next;
475
+ this.clampRuns();
476
+ this.ui.status = '';
477
+ }
478
+
479
+ focusCommands() {
480
+ if (this.focus === 'commands') return;
481
+ this.focus = 'commands';
482
+ this.ui.paint();
483
+ }
484
+
485
+ focusRuns() {
486
+ if (this.focus === 'runs') return;
487
+ this.focus = 'runs';
488
+ this.clampRuns();
489
+ this.ui.paint();
490
+ }
491
+
492
+ toggleFocus() {
493
+ if (this.focus === 'runs') this.focusCommands();
494
+ else this.focusRuns();
495
+ }
496
+
497
+ showOutput(run, text) {
498
+ if (run.status !== 'running' || run.stopping) return;
499
+ run.raw = text;
500
+ run.stale = true;
501
+ const wait = OUTPUT_MS - (Date.now() - (run.shownAt ?? 0));
502
+ if (wait <= 0) return void this.flushOutput(run);
503
+ const flush = () => this.flushOutput(run);
504
+ this.ui.term.later(`npm-output-${run.id}`, flush, wait);
505
+ }
506
+
507
+ syncOutput(run) {
508
+ if (!run.stale) return false;
509
+ run.stale = false;
510
+ run.shownAt = Date.now();
511
+ noteTests(run, this.runs, run.raw);
512
+ run.output = filteredOutput(run.raw, this.ui.top, '').output;
513
+ return true;
514
+ }
515
+
516
+ flushOutput(run) {
517
+ if (!this.syncOutput(run)) return;
518
+ const pane = this.ui.nav.pane;
519
+ if (pane === 'npm' || pane === 'dashboard') this.ui.paint();
520
+ }
521
+
522
+ async finishRun(run, entry, result) {
523
+ const ui = this.ui;
524
+ const stopped = run.stopping === true;
525
+ const raw = stopped ? withNotice(run.raw, 'terminated') : result.text;
526
+ let slot = null;
527
+ try {
528
+ slot = nextLogFile(ui.top, entry.name);
529
+ } catch (error) {
530
+ ui.status = error.message;
531
+ }
532
+ const rawFile = slot ? slot.rawName : '';
533
+ const stoppedOutput = () => {
534
+ const output = withNotice(
535
+ reduceOutput(run.raw, ui.top, ''),
536
+ 'terminated',
537
+ );
538
+ return { output, log: output };
539
+ };
540
+ const filtered = stopped
541
+ ? stoppedOutput()
542
+ : filteredOutput(result.text, ui.top, result.status, rawFile);
543
+ run.raw = raw;
544
+ run.stale = false;
545
+ run.exit = stopped ? '' : result.status;
546
+ run.status = stopped ? 'stopped' : 'exited';
547
+ run.output = filtered.output;
548
+ if (filtered.result) run.result = filtered.result;
549
+ run.endedAt = Date.now();
550
+ run.child = null;
551
+ run.stopping = false;
552
+ const watched = this.viewing && this.viewId === run.id;
553
+ if (stopped && watched) ui.status = 'terminated';
554
+ this.syncProgress();
555
+ ui.paint();
556
+ if (launchedReslop(run)) return;
557
+ if (!slot) return;
558
+ run.logName = slot.name;
559
+ run.rawName = slot.rawName;
560
+ try {
561
+ await saveLogs(slot, loggedText(filtered.log, slot.rawName), raw, run);
562
+ } catch (error) {
563
+ ui.status = error.message;
564
+ ui.paint();
565
+ }
566
+ }
567
+
568
+ startRun(entry, openLog = false) {
569
+ const ui = this.ui;
570
+ const run = newRun(this.nextId, {
571
+ name: entry.name,
572
+ kind: entry.kind,
573
+ command: entry.command ?? '',
574
+ label: runLabel(entry),
575
+ status: 'running',
576
+ exit: '',
577
+ startedAt: Date.now(),
578
+ endedAt: 0,
579
+ });
580
+ this.nextId += 1;
581
+ this.runs.push(run);
582
+ if (openLog) {
583
+ this.viewing = true;
584
+ this.viewId = run.id;
585
+ }
586
+ this.focusRun(run);
587
+ ui.status = '';
588
+ this.syncProgress();
589
+ const start = (ui.repo && ui.repo.runNpmCommand) || startNpm;
590
+ const child = start(
591
+ ui.top,
592
+ entry,
593
+ (text) => this.showOutput(run, text),
594
+ (result) => this.finishRun(run, entry, result),
595
+ );
596
+ if (run.status === 'running') run.child = child;
597
+ ui.paint();
598
+ return child;
599
+ }
600
+
601
+ openView(id) {
602
+ const run = this.runs.find((item) => item.id === id);
603
+ if (!run) return;
604
+ this.hydrate(run);
605
+ this.verbose = false;
606
+ this.viewing = true;
607
+ this.viewId = id;
608
+ this.ui.status = '';
609
+ this.ui.paint();
610
+ }
611
+
612
+ run() {
613
+ if (this.viewing) return null;
614
+ if (this.focus === 'runs') {
615
+ const picked = this.historyRuns()[this.runCursor];
616
+ if (picked) {
617
+ this.openView(picked.id);
618
+ return null;
619
+ }
620
+ }
621
+ const entry = this.selected();
622
+ if (!entry) return null;
623
+ this.verbose = false;
624
+ return this.startRun(entry);
625
+ }
626
+
627
+ rerun() {
628
+ if (!this.viewing) return;
629
+ const current = this.viewedRun();
630
+ const entry = current
631
+ ? { name: current.name, kind: current.kind, command: current.command }
632
+ : this.selected();
633
+ if (entry && entry.name) this.startRun(entry, true);
634
+ }
635
+
636
+ closeView() {
637
+ this.viewing = false;
638
+ this.verbose = false;
639
+ this.ui.status = '';
640
+ this.ui.paint();
641
+ }
642
+
643
+ stop(run = this.viewedRun()) {
644
+ if (!run || run.status !== 'running') return;
645
+ this.syncOutput(run);
646
+ run.stopping = true;
647
+ if (run.child) run.child.kill();
648
+ if (run.status !== 'running') return void this.syncProgress();
649
+ run.raw = withNotice(run.raw, 'terminated');
650
+ run.output = withNotice(run.output, 'terminated');
651
+ run.status = 'stopped';
652
+ run.exit = '';
653
+ run.endedAt = Date.now();
654
+ run.child = null;
655
+ const watched = this.viewing && this.viewId === run.id;
656
+ if (watched) this.ui.status = 'terminated';
657
+ this.syncProgress();
658
+ this.ui.paint();
659
+ }
660
+
661
+ stopAll() {
662
+ for (const run of this.runs) this.stop(run);
663
+ }
664
+
665
+ toggleVerbose() {
666
+ if (!this.viewing || !this.viewedRun()) return;
667
+ this.verbose = !this.verbose;
668
+ this.ui.status = this.verbose ? 'verbose' : 'filtered';
669
+ this.ui.paint();
670
+ }
671
+
672
+ editable() {
673
+ if (this.viewing) return false;
674
+ const ui = this.ui;
675
+ const caps = ui.capabilities;
676
+ if (!ui.readOnly && (!caps || caps.changes !== false)) return true;
677
+ ui.status = 'read only';
678
+ return false;
679
+ }
680
+
681
+ selectedScript() {
682
+ if (!this.editable()) return null;
683
+ const entry = this.selected();
684
+ if (entry && entry.kind === 'script') return entry;
685
+ this.ui.status = 'not a script';
686
+ return null;
687
+ }
688
+
689
+ focusScript(name) {
690
+ const at = this.commands.findIndex((entry) => entry.name === name);
691
+ if (at >= 0) this.ui.nav.npmCursor = at;
692
+ }
693
+
694
+ startEdit(name, command) {
695
+ this.editName = name;
696
+ this.editField = 'name';
697
+ this.draftName = name;
698
+ this.draftCommand = command;
699
+ this.ui.composer.openCompose('npm', name);
700
+ }
701
+
702
+ edit() {
703
+ const entry = this.selectedScript();
704
+ if (entry) this.startEdit(entry.name, entry.command ?? '');
705
+ }
706
+
707
+ create() {
708
+ if (this.editable()) this.startEdit('', '');
709
+ }
710
+
711
+ switchField(editor, field, text) {
712
+ this.editField = field;
713
+ editor.replace(text);
714
+ this.ui.status = '';
715
+ }
716
+
717
+ finishName(editor) {
718
+ const name = editor.text.trim();
719
+ if (!name || !NAME_RE.test(name)) {
720
+ this.ui.status = 'name';
721
+ return false;
722
+ }
723
+ this.draftName = name;
724
+ this.switchField(editor, 'command', this.draftCommand);
725
+ return true;
726
+ }
727
+
728
+ focusField(field) {
729
+ const editor = this.ui.editor;
730
+ if (!editor) return false;
731
+ if (field === this.editField) return true;
732
+ if (field === 'command') return this.finishName(editor);
733
+ this.draftCommand = editor.text;
734
+ this.switchField(editor, 'name', this.draftName);
735
+ return true;
736
+ }
737
+
738
+ writeScripts(write) {
739
+ try {
740
+ this.ui.ignoreWatch();
741
+ write(this.ui.top);
742
+ return true;
743
+ } catch (error) {
744
+ this.ui.status = error.message;
745
+ return false;
746
+ }
747
+ }
748
+
749
+ finishEdit() {
750
+ const ui = this.ui;
751
+ const editor = ui.composer.editor;
752
+ if (!editor) return;
753
+ if (this.editField !== 'command') return void this.finishName(editor);
754
+ const command = editor.text.trim();
755
+ if (!command) {
756
+ ui.status = 'command';
757
+ return;
758
+ }
759
+ const parsed = { name: this.draftName, command };
760
+ const write = (top) => saveScript(top, this.editName, parsed);
761
+ if (!this.writeScripts(write)) return;
762
+ this.editField = '';
763
+ ui.composer.closeCompose();
764
+ this.refresh();
765
+ this.focusScript(parsed.name);
766
+ ui.status = 'saved';
767
+ }
768
+
769
+ confirm(kind, name) {
770
+ this.dropKind = kind;
771
+ this.dropName = name;
772
+ this.ui.mode = 'confirmDrop';
773
+ this.ui.status = '';
774
+ }
775
+
776
+ askDrop() {
777
+ const entry = this.selectedScript();
778
+ if (entry) this.confirm('script', entry.name);
779
+ }
780
+
781
+ askLogs() {
782
+ if (!this.editable()) return;
783
+ this.refreshLogs();
784
+ if (!this.logBytes) return;
785
+ const total = size(this.logBytes);
786
+ this.confirm('logs', `logs older than 5 days (${total})`);
787
+ }
788
+
789
+ cancelDrop() {
790
+ this.dropName = '';
791
+ this.dropKind = '';
792
+ this.ui.mode = 'review';
793
+ this.ui.status = '';
794
+ }
795
+
796
+ confirmDrop() {
797
+ const ui = this.ui;
798
+ const kind = this.dropKind;
799
+ const name = this.dropName;
800
+ this.dropName = '';
801
+ this.dropKind = '';
802
+ ui.mode = 'review';
803
+ if (kind === 'logs') {
804
+ try {
805
+ removeStaleLogs(ui.top);
806
+ } catch (error) {
807
+ ui.status = error.message;
808
+ return void this.refreshLogs();
809
+ }
810
+ this.refreshLogs();
811
+ ui.status = 'dropped logs';
812
+ return;
813
+ }
814
+ if (!name) return;
815
+ if (!this.writeScripts((top) => removeScript(top, name))) return;
816
+ this.refresh();
817
+ ui.status = `dropped ${name}`;
818
+ }
819
+
820
+ reorder(delta) {
821
+ const entry = this.selectedScript();
822
+ if (!entry) return;
823
+ let moved = false;
824
+ const write = (top) => {
825
+ moved = reorderScript(top, entry.name, delta);
826
+ };
827
+ if (!this.writeScripts(write) || !moved) return;
828
+ this.refresh();
829
+ this.focusScript(entry.name);
830
+ this.ui.status = '';
831
+ }
832
+ }
833
+
834
+ module.exports = {
835
+ NpmController,
836
+ };