reslop 0.2.6 → 0.3.0

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 +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  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 +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  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} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -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 +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -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/dashboard/blocks.js +490 -0
  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 +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  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} +19 -61
  72. package/lib/render/tiles.js +203 -47
  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} +46 -14
  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 +9 -0
  81. package/lib/review/format.js +426 -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 +958 -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 +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -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} +166 -59
  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 +72 -59
  126. package/lib/session/workspace.js +416 -0
  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} +2 -2
  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 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  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,815 @@
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
+ command: item.command ?? '',
243
+ label: item.kind === 'command' ? historyName(item) : runLabel(item),
244
+ status: item.status,
245
+ exit: item.exit,
246
+ startedAt: item.startedAt,
247
+ endedAt: item.endedAt,
248
+ logName: item.logName,
249
+ rawName: item.rawName,
250
+ externalId: item.externalId || '',
251
+ result: item.result ?? null,
252
+ progress: item.progress ?? null,
253
+ saved: true,
254
+ hydrated: false,
255
+ });
256
+ this.nextId += 1;
257
+ return run;
258
+ }
259
+
260
+ knownLogs() {
261
+ const known = new Set();
262
+ for (const run of this.runs) {
263
+ if (run.logName) known.add(run.logName);
264
+ if (run.externalId) known.add(run.externalId);
265
+ }
266
+ return known;
267
+ }
268
+
269
+ findRecorded(key, logName) {
270
+ return this.runs.find((run) => {
271
+ if (key && run.externalId === key) return true;
272
+ return Boolean(logName && run.externalId && run.logName === logName);
273
+ });
274
+ }
275
+
276
+ absorbRecorded(recorded, known, added) {
277
+ let changed = false;
278
+ for (const record of recorded) {
279
+ const logName = logBase(record.log);
280
+ const key = `${record.id ?? ''}`;
281
+ const found = this.findRecorded(key, logName);
282
+ if (found) {
283
+ if (applyExternal(found, record)) changed = true;
284
+ continue;
285
+ }
286
+ const seenLog = logName && known.has(logName);
287
+ const seenKey = key && known.has(key);
288
+ if (seenLog || seenKey) continue;
289
+ if (logName) known.add(logName);
290
+ if (key) known.add(key);
291
+ added.push(this.adopt(recordedItem(record)));
292
+ changed = true;
293
+ }
294
+ return changed;
295
+ }
296
+
297
+ settleLost(recorded) {
298
+ const live = new Set(recorded.map((record) => `${record.id ?? ''}`));
299
+ let changed = false;
300
+ for (const run of this.runs) {
301
+ const external = run.externalId && run.status === 'running';
302
+ if (!external || live.has(run.externalId)) continue;
303
+ run.status = 'stopped';
304
+ run.exit = '';
305
+ run.endedAt ||= Date.now();
306
+ changed = true;
307
+ }
308
+ return changed;
309
+ }
310
+
311
+ storeAdded(added) {
312
+ if (!added.length) return;
313
+ const running = this.runs.filter((run) => run.status === 'running');
314
+ const done = this.runs.filter((run) => run.status !== 'running');
315
+ done.push(...added);
316
+ done.sort((left, right) => {
317
+ const at = left.startedAt - right.startedAt;
318
+ if (at) return at;
319
+ return left.id - right.id;
320
+ });
321
+ this.runs = [...done, ...running];
322
+ }
323
+
324
+ followExternal() {
325
+ this.syncProgress();
326
+ if (this.ui.nav.pane === 'npm') this.ui.paint();
327
+ }
328
+
329
+ refreshRecorded() {
330
+ let recorded = null;
331
+ try {
332
+ recorded = readRuns(this.ui.top);
333
+ } catch {
334
+ // unreadable runs
335
+ }
336
+ if (!recorded) return;
337
+ const added = [];
338
+ let changed = this.absorbRecorded(recorded, this.knownLogs(), added);
339
+ if (this.settleLost(recorded)) changed = true;
340
+ if (this.hydrate(this.viewedRun())) changed = true;
341
+ this.storeAdded(added);
342
+ if (changed) this.followExternal();
343
+ }
344
+
345
+ loadSaved() {
346
+ const known = this.knownLogs();
347
+ let saved = null;
348
+ let recorded = null;
349
+ try {
350
+ saved = readSavedRuns(this.ui.top, this.commands);
351
+ recorded = readRuns(this.ui.top);
352
+ } catch (error) {
353
+ this.ui.status = error.message;
354
+ }
355
+ if (!saved || !recorded) return;
356
+ const added = [];
357
+ for (const item of saved) {
358
+ if (!item.logName || known.has(item.logName)) continue;
359
+ known.add(item.logName);
360
+ added.push(this.adopt(item));
361
+ }
362
+ const changed = this.absorbRecorded(recorded, known, added);
363
+ this.storeAdded(added);
364
+ if (changed) this.followExternal();
365
+ }
366
+
367
+ siblingText(run) {
368
+ const name = historyName(run);
369
+ for (const other of this.runs) {
370
+ if (other === run || historyName(other) !== name) continue;
371
+ if (!sameWindow(run, other)) continue;
372
+ const saved = readLogPair(this.ui.top, other.logName, other.rawName);
373
+ const output = other.output || saved.output;
374
+ const raw = other.raw || saved.raw;
375
+ if (!output && !raw) continue;
376
+ return { output, raw: raw || output };
377
+ }
378
+ return { output: '', raw: '' };
379
+ }
380
+
381
+ hydrate(run) {
382
+ if (!run || !run.logName) return false;
383
+ const settled = run.externalId && run.status !== 'running';
384
+ if (!settled && (run.hydrated || run.output || run.raw)) return false;
385
+ const text = readLogPair(this.ui.top, run.logName, run.rawName);
386
+ const saved = text.output || text.raw ? text : this.siblingText(run);
387
+ if (!saved.output && !saved.raw) return false;
388
+ const raw = saved.raw || saved.output;
389
+ if (run.output === saved.output && run.raw === raw) return false;
390
+ run.output = saved.output;
391
+ run.raw = raw;
392
+ run.hydrated = true;
393
+ return true;
394
+ }
395
+
396
+ open() {
397
+ if (this.ui.mode === 'compose') return;
398
+ this.refresh();
399
+ this.refreshLogs();
400
+ const fresh = this.runs.length === 0;
401
+ this.loadSaved();
402
+ if (fresh) {
403
+ if (this.historyRuns().length) this.runCursor = 0;
404
+ }
405
+ this.viewing = false;
406
+ this.verbose = false;
407
+ const nav = this.ui.nav;
408
+ nav.pane = 'npm';
409
+ nav.resetListScroll('npm');
410
+ this.ui.status = '';
411
+ nav.clearSelection();
412
+ }
413
+
414
+ syncProgress() {
415
+ if (this.running) this.ui.progress.start('npm');
416
+ else this.ui.progress.stop('npm');
417
+ }
418
+
419
+ placeRuns(start) {
420
+ const bar = this.ui.lastFrame && this.ui.lastFrame.scrollBar;
421
+ if (!bar) return;
422
+ super.placeRuns(start);
423
+ const top = this.runScroll;
424
+ const last = Math.max(0, this.historyRuns().length - 1);
425
+ if (this.runCursor < top) this.runCursor = top;
426
+ const bottom = Math.min(last, top + bar.rows - 1);
427
+ if (this.runCursor > bottom) this.runCursor = bottom;
428
+ }
429
+
430
+ scrollLog(delta) {
431
+ this.scrollLogBy(delta, this.output, this.viewedRunning());
432
+ }
433
+
434
+ moveRun(delta) {
435
+ const rows = this.historyRuns();
436
+ if (!rows.length) return;
437
+ const last = rows.length - 1;
438
+ const next = clamp(this.runCursor + delta, 0, last);
439
+ if (next === this.runCursor) return;
440
+ this.runCursor = next;
441
+ this.ui.status = '';
442
+ this.ui.paint();
443
+ }
444
+
445
+ move(delta) {
446
+ if (this.viewing) return void this.scrollLog(delta);
447
+ if (this.focus === 'runs') return void this.moveRun(delta);
448
+ if (!this.commands.length) return;
449
+ const nav = this.ui.nav;
450
+ const last = this.commands.length - 1;
451
+ const next = clamp(nav.npmCursor + delta, 0, last);
452
+ if (next === nav.npmCursor) return;
453
+ nav.npmCursor = next;
454
+ this.clampRuns();
455
+ this.ui.status = '';
456
+ }
457
+
458
+ focusCommands() {
459
+ if (this.focus === 'commands') return;
460
+ this.focus = 'commands';
461
+ this.ui.paint();
462
+ }
463
+
464
+ focusRuns() {
465
+ if (this.focus === 'runs') return;
466
+ this.focus = 'runs';
467
+ this.clampRuns();
468
+ this.ui.paint();
469
+ }
470
+
471
+ toggleFocus() {
472
+ if (this.focus === 'runs') this.focusCommands();
473
+ else this.focusRuns();
474
+ }
475
+
476
+ showOutput(run, text) {
477
+ if (run.status !== 'running' || run.stopping) return;
478
+ run.raw = text;
479
+ run.stale = true;
480
+ const wait = OUTPUT_MS - (Date.now() - (run.shownAt ?? 0));
481
+ if (wait <= 0) return void this.flushOutput(run);
482
+ const flush = () => this.flushOutput(run);
483
+ this.ui.term.later(`npm-output-${run.id}`, flush, wait);
484
+ }
485
+
486
+ syncOutput(run) {
487
+ if (!run.stale) return false;
488
+ run.stale = false;
489
+ run.shownAt = Date.now();
490
+ noteTests(run, this.runs, run.raw);
491
+ run.output = filteredOutput(run.raw, this.ui.top, '').output;
492
+ return true;
493
+ }
494
+
495
+ flushOutput(run) {
496
+ if (!this.syncOutput(run)) return;
497
+ const pane = this.ui.nav.pane;
498
+ if (pane === 'npm' || pane === 'dashboard') this.ui.paint();
499
+ }
500
+
501
+ async finishRun(run, entry, result) {
502
+ const ui = this.ui;
503
+ const stopped = run.stopping === true;
504
+ const raw = stopped ? withNotice(run.raw, 'terminated') : result.text;
505
+ let slot = null;
506
+ try {
507
+ slot = nextLogFile(ui.top, entry.name);
508
+ } catch (error) {
509
+ ui.status = error.message;
510
+ }
511
+ const rawFile = slot ? slot.rawName : '';
512
+ const stoppedOutput = () => {
513
+ const output = withNotice(
514
+ reduceOutput(run.raw, ui.top, ''),
515
+ 'terminated',
516
+ );
517
+ return { output, log: output };
518
+ };
519
+ const filtered = stopped
520
+ ? stoppedOutput()
521
+ : filteredOutput(result.text, ui.top, result.status, rawFile);
522
+ run.raw = raw;
523
+ run.stale = false;
524
+ run.exit = stopped ? '' : result.status;
525
+ run.status = stopped ? 'stopped' : 'exited';
526
+ run.output = filtered.output;
527
+ if (filtered.result) run.result = filtered.result;
528
+ run.endedAt = Date.now();
529
+ run.child = null;
530
+ run.stopping = false;
531
+ const watched = this.viewing && this.viewId === run.id;
532
+ if (stopped && watched) ui.status = 'terminated';
533
+ this.syncProgress();
534
+ ui.paint();
535
+ if (launchedReslop(run)) return;
536
+ if (!slot) return;
537
+ run.logName = slot.name;
538
+ run.rawName = slot.rawName;
539
+ try {
540
+ await saveLogs(slot, loggedText(filtered.log, slot.rawName), raw, run);
541
+ } catch (error) {
542
+ ui.status = error.message;
543
+ ui.paint();
544
+ }
545
+ }
546
+
547
+ startRun(entry, openLog = false) {
548
+ const ui = this.ui;
549
+ const run = newRun(this.nextId, {
550
+ name: entry.name,
551
+ kind: entry.kind,
552
+ command: entry.command ?? '',
553
+ label: runLabel(entry),
554
+ status: 'running',
555
+ exit: '',
556
+ startedAt: Date.now(),
557
+ endedAt: 0,
558
+ });
559
+ this.nextId += 1;
560
+ this.runs.push(run);
561
+ if (openLog) {
562
+ this.viewing = true;
563
+ this.viewId = run.id;
564
+ }
565
+ this.focusRun(run);
566
+ ui.status = '';
567
+ this.syncProgress();
568
+ const start = (ui.repo && ui.repo.runNpmCommand) || startNpm;
569
+ const child = start(
570
+ ui.top,
571
+ entry,
572
+ (text) => this.showOutput(run, text),
573
+ (result) => this.finishRun(run, entry, result),
574
+ );
575
+ if (run.status === 'running') run.child = child;
576
+ ui.paint();
577
+ return child;
578
+ }
579
+
580
+ openView(id) {
581
+ const run = this.runs.find((item) => item.id === id);
582
+ if (!run) return;
583
+ this.hydrate(run);
584
+ this.verbose = false;
585
+ this.viewing = true;
586
+ this.viewId = id;
587
+ this.ui.status = '';
588
+ this.ui.paint();
589
+ }
590
+
591
+ run() {
592
+ if (this.viewing) return null;
593
+ if (this.focus === 'runs') {
594
+ const picked = this.historyRuns()[this.runCursor];
595
+ if (picked) {
596
+ this.openView(picked.id);
597
+ return null;
598
+ }
599
+ }
600
+ const entry = this.selected();
601
+ if (!entry) return null;
602
+ this.verbose = false;
603
+ return this.startRun(entry);
604
+ }
605
+
606
+ rerun() {
607
+ if (!this.viewing) return;
608
+ const current = this.viewedRun();
609
+ const entry = current
610
+ ? { name: current.name, kind: current.kind, command: current.command }
611
+ : this.selected();
612
+ if (entry && entry.name) this.startRun(entry, true);
613
+ }
614
+
615
+ closeView() {
616
+ this.viewing = false;
617
+ this.verbose = false;
618
+ this.ui.status = '';
619
+ this.ui.paint();
620
+ }
621
+
622
+ stop(run = this.viewedRun()) {
623
+ if (!run || run.status !== 'running') return;
624
+ this.syncOutput(run);
625
+ run.stopping = true;
626
+ if (run.child) run.child.kill();
627
+ if (run.status !== 'running') return void this.syncProgress();
628
+ run.raw = withNotice(run.raw, 'terminated');
629
+ run.output = withNotice(run.output, 'terminated');
630
+ run.status = 'stopped';
631
+ run.exit = '';
632
+ run.endedAt = Date.now();
633
+ run.child = null;
634
+ const watched = this.viewing && this.viewId === run.id;
635
+ if (watched) this.ui.status = 'terminated';
636
+ this.syncProgress();
637
+ this.ui.paint();
638
+ }
639
+
640
+ stopAll() {
641
+ for (const run of this.runs) this.stop(run);
642
+ }
643
+
644
+ toggleVerbose() {
645
+ if (!this.viewing || !this.viewedRun()) return;
646
+ this.verbose = !this.verbose;
647
+ this.ui.status = this.verbose ? 'verbose' : 'filtered';
648
+ this.ui.paint();
649
+ }
650
+
651
+ editable() {
652
+ if (this.viewing) return false;
653
+ const ui = this.ui;
654
+ const caps = ui.capabilities;
655
+ if (!ui.readOnly && (!caps || caps.changes !== false)) return true;
656
+ ui.status = 'read only';
657
+ return false;
658
+ }
659
+
660
+ selectedScript() {
661
+ if (!this.editable()) return null;
662
+ const entry = this.selected();
663
+ if (entry && entry.kind === 'script') return entry;
664
+ this.ui.status = 'not a script';
665
+ return null;
666
+ }
667
+
668
+ focusScript(name) {
669
+ const at = this.commands.findIndex((entry) => entry.name === name);
670
+ if (at >= 0) this.ui.nav.npmCursor = at;
671
+ }
672
+
673
+ startEdit(name, command) {
674
+ this.editName = name;
675
+ this.editField = 'name';
676
+ this.draftName = name;
677
+ this.draftCommand = command;
678
+ this.ui.composer.openCompose('npm', name);
679
+ }
680
+
681
+ edit() {
682
+ const entry = this.selectedScript();
683
+ if (entry) this.startEdit(entry.name, entry.command ?? '');
684
+ }
685
+
686
+ create() {
687
+ if (this.editable()) this.startEdit('', '');
688
+ }
689
+
690
+ switchField(editor, field, text) {
691
+ this.editField = field;
692
+ editor.replace(text);
693
+ this.ui.status = '';
694
+ }
695
+
696
+ finishName(editor) {
697
+ const name = editor.text.trim();
698
+ if (!name || !NAME_RE.test(name)) {
699
+ this.ui.status = 'name';
700
+ return false;
701
+ }
702
+ this.draftName = name;
703
+ this.switchField(editor, 'command', this.draftCommand);
704
+ return true;
705
+ }
706
+
707
+ focusField(field) {
708
+ const editor = this.ui.editor;
709
+ if (!editor) return false;
710
+ if (field === this.editField) return true;
711
+ if (field === 'command') return this.finishName(editor);
712
+ this.draftCommand = editor.text;
713
+ this.switchField(editor, 'name', this.draftName);
714
+ return true;
715
+ }
716
+
717
+ writeScripts(write) {
718
+ try {
719
+ this.ui.ignoreWatch();
720
+ write(this.ui.top);
721
+ return true;
722
+ } catch (error) {
723
+ this.ui.status = error.message;
724
+ return false;
725
+ }
726
+ }
727
+
728
+ finishEdit() {
729
+ const ui = this.ui;
730
+ const editor = ui.composer.editor;
731
+ if (!editor) return;
732
+ if (this.editField !== 'command') return void this.finishName(editor);
733
+ const command = editor.text.trim();
734
+ if (!command) {
735
+ ui.status = 'command';
736
+ return;
737
+ }
738
+ const parsed = { name: this.draftName, command };
739
+ const write = (top) => saveScript(top, this.editName, parsed);
740
+ if (!this.writeScripts(write)) return;
741
+ this.editField = '';
742
+ ui.composer.closeCompose();
743
+ this.refresh();
744
+ this.focusScript(parsed.name);
745
+ ui.status = 'saved';
746
+ }
747
+
748
+ confirm(kind, name) {
749
+ this.dropKind = kind;
750
+ this.dropName = name;
751
+ this.ui.mode = 'confirmDrop';
752
+ this.ui.status = '';
753
+ }
754
+
755
+ askDrop() {
756
+ const entry = this.selectedScript();
757
+ if (entry) this.confirm('script', entry.name);
758
+ }
759
+
760
+ askLogs() {
761
+ if (!this.editable()) return;
762
+ this.refreshLogs();
763
+ if (!this.logBytes) return;
764
+ const total = size(this.logBytes);
765
+ this.confirm('logs', `logs older than 5 days (${total})`);
766
+ }
767
+
768
+ cancelDrop() {
769
+ this.dropName = '';
770
+ this.dropKind = '';
771
+ this.ui.mode = 'review';
772
+ this.ui.status = '';
773
+ }
774
+
775
+ confirmDrop() {
776
+ const ui = this.ui;
777
+ const kind = this.dropKind;
778
+ const name = this.dropName;
779
+ this.dropName = '';
780
+ this.dropKind = '';
781
+ ui.mode = 'review';
782
+ if (kind === 'logs') {
783
+ try {
784
+ removeStaleLogs(ui.top);
785
+ } catch (error) {
786
+ ui.status = error.message;
787
+ return void this.refreshLogs();
788
+ }
789
+ this.refreshLogs();
790
+ ui.status = 'dropped logs';
791
+ return;
792
+ }
793
+ if (!name) return;
794
+ if (!this.writeScripts((top) => removeScript(top, name))) return;
795
+ this.refresh();
796
+ ui.status = `dropped ${name}`;
797
+ }
798
+
799
+ reorder(delta) {
800
+ const entry = this.selectedScript();
801
+ if (!entry) return;
802
+ let moved = false;
803
+ const write = (top) => {
804
+ moved = reorderScript(top, entry.name, delta);
805
+ };
806
+ if (!this.writeScripts(write) || !moved) return;
807
+ this.refresh();
808
+ this.focusScript(entry.name);
809
+ this.ui.status = '';
810
+ }
811
+ }
812
+
813
+ module.exports = {
814
+ NpmController,
815
+ };