reslop 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -3,10 +3,12 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
+ const utilities = require('../utilities.js');
7
+ const { isAbort } = utilities;
6
8
  const sessionLoad = require('./load.js');
7
- const { isAbortError, tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
9
+ const { tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
8
10
  const watch = require('./watch.js');
9
- const { createDiskWatcher, DEBOUNCE_MS } = watch;
11
+ const { DiskWatcher, DEBOUNCE_MS } = watch;
10
12
  const files = require('../files.js');
11
13
  const { itemPath, relativeAge } = files;
12
14
  const deps = require('../deps.js');
@@ -16,34 +18,7 @@ const { hasNotes, loadReview } = review;
16
18
  const reviewMerge = require('../review-merge.js');
17
19
  const { mergeReview } = reviewMerge;
18
20
 
19
- const loadOptions = (api, extra = {}) => {
20
- const { loadConfig, ui } = api;
21
- return {
22
- commit: ui.rev,
23
- audit: loadConfig.audit,
24
- outdatedMap: loadConfig.outdatedMap,
25
- auditMap: loadConfig.auditMap,
26
- ...extra,
27
- };
28
- };
29
-
30
- const shouldDeferExtras = (api) => {
31
- const { loadConfig } = api;
32
- const audit = !!loadConfig.audit;
33
- const waitingOutdated = loadConfig.outdatedMap === undefined;
34
- const waitingAudit = loadConfig.auditMap === undefined;
35
- return audit && (waitingOutdated || waitingAudit);
36
- };
37
-
38
- const endReadLoop = (api) => {
39
- const finish = api.ui.finishReadLoop;
40
- if (finish) finish();
41
- };
42
-
43
- const markLoaded = (api) => {
44
- api.loader.didLoad = true;
45
- api.updater.maybePrompt();
46
- };
21
+ const WATCH_IGNORE_MS = 500;
47
22
 
48
23
  const isWorktree = (ui) => !ui.rev && !ui.change;
49
24
 
@@ -53,503 +28,482 @@ const kindFromExtra = (extra = {}) => {
53
28
  return 'reload';
54
29
  };
55
30
 
56
- const finishIfEmpty = (api, kind) => {
57
- const { collection, review: notes, ui, nav } = api;
58
- if (collection.items.length) return;
59
- if (notes.store && hasNotes(notes.store)) return;
60
- if (kind === 'keep') return;
61
- if (isWorktree(ui)) {
62
- if (nav.pane === 'diff') nav.pane = 'files';
63
- return;
64
- }
65
- ui.done = true;
66
- ui.exitCode = 0;
67
- ui.status = 'nothing to review';
68
- ui.emptyReview = kind === 'open';
69
- endReadLoop(api);
70
- };
71
-
72
- const failOpen = (api, error) => {
73
- if (isAbortError(error)) return;
74
- const { ui, ops } = api;
75
- ui.loadError = error;
76
- ui.done = true;
77
- ui.exitCode = 1;
78
- ops.setBusy('');
79
- ui.stopProgress();
80
- endReadLoop(api);
81
- };
82
-
83
- const restoreAfterLoad = (api, here, wasTodo) => {
84
- const { collection, nav, ui } = api;
85
- nav.tasksOpen = wasTodo;
86
- if (here && !wasTodo) {
87
- const idx = collection.findRestoredIndex(here);
88
- if (idx >= 0) nav.index = idx;
89
- }
90
- ui.clampIndex();
91
- nav.restoreFileCursor(ui.fileList());
92
- ui.syncReviewPath();
93
- ui.syncFileCursor();
31
+ const statStamp = (file) => {
32
+ try {
33
+ const stat = fs.statSync(file);
34
+ return `${stat.mtimeMs}:${stat.size}`;
35
+ } catch {
36
+ return 'missing';
37
+ }
94
38
  };
95
39
 
96
- const itemRels = (collection) => {
97
- const rels = [];
98
- for (const item of collection.items) {
40
+ const itemRels = (items) => {
41
+ const rels = new Set();
42
+ for (const item of items) {
99
43
  const rel = itemPath(item);
100
- if (!rel) continue;
101
- if (rels.includes(rel)) continue;
102
- rels.push(rel);
44
+ if (rel) rels.add(rel);
103
45
  }
104
- return rels;
46
+ return [...rels];
105
47
  };
106
48
 
107
- const fileDatesFromDisk = (top, rels) => {
108
- const dates = new Map();
109
- if (!top) return dates;
49
+ const stampItemDates = (top, items) => {
50
+ if (!top) return;
110
51
  const now = Date.now();
111
- for (const rel of rels) {
52
+ const dates = new Map();
53
+ for (const rel of itemRels(items)) {
112
54
  try {
113
- const st = fs.statSync(path.join(top, rel));
114
- dates.set(rel, relativeAge(st.mtimeMs, now));
55
+ const stat = fs.statSync(path.join(top, rel));
56
+ dates.set(rel, relativeAge(stat.mtimeMs, now));
115
57
  } catch {
116
58
  continue;
117
59
  }
118
60
  }
119
- return dates;
120
- };
121
-
122
- const stampItemDates = (items, dates) => {
123
61
  for (const item of items) {
124
62
  const date = dates.get(itemPath(item));
125
63
  if (date) item.date = date;
126
64
  }
127
65
  };
128
66
 
129
- const syncUnitEditor = (api) => {
130
- const composer = api.ui.composer;
131
- if (!composer || typeof composer.applyDiskText !== 'function') return;
132
- if (api.nav.pane !== 'unit') return;
133
- const rel = api.nav.reviewPath;
134
- if (!rel) return;
135
- const repo = api.ui.repo;
136
- if (!repo || typeof repo.fileText !== 'function') return;
137
- const text = repo.fileText(api.ui.top || api.loadConfig.cwd, rel, api.ui.rev);
138
- composer.applyDiskText(text);
67
+ const npmRels = (items) => {
68
+ const rels = new Set([MANIFEST, LOCKFILE]);
69
+ for (const item of items) {
70
+ const rel = itemPath(item);
71
+ if (depFileMeta(rel)) rels.add(rel);
72
+ }
73
+ return [...rels].sort();
139
74
  };
140
75
 
141
- const syncItemWatches = (api) => {
142
- if (!api.diskWatcher) return;
143
- const rels = itemRels(api.collection);
144
- if (api.nav.pane === 'unit' && api.nav.reviewPath) {
145
- if (!rels.includes(api.nav.reviewPath)) rels.push(api.nav.reviewPath);
76
+ class Lifecycle {
77
+ constructor(ui) {
78
+ this.ui = ui;
79
+ this.diskWatcher = null;
80
+ this.watchRetry = null;
81
+ this.reviewStamp = '';
82
+ this.ignoreWatchUntil = 0;
83
+ this.cachedExtras = null;
146
84
  }
147
- api.diskWatcher.watchFiles(rels);
148
- };
149
85
 
150
- const applyLoaded = (api, loaded) => {
151
- const { loader, collection, review: notes, nav, ui } = api;
152
- if (loaded.generation !== undefined) {
153
- if (!loader.accept(loaded.generation, ui.done)) return false;
154
- }
155
- const wasTodo = nav.tasksOpen === true;
156
- const here = ui.current();
157
- ui.top = loaded.top ?? api.loadConfig.cwd;
158
- collection.replace(loaded.items);
159
- stampItemDates(
160
- collection.items,
161
- fileDatesFromDisk(ui.top, itemRels(collection)),
162
- );
163
- if (loaded.rev) ui.rev = loaded.rev;
164
- if (loaded.revShort) {
165
- ui.revShort = loaded.revShort;
166
- } else if (ui.rev && !ui.revShort) {
167
- ui.revShort = ui.rev.slice(0, 7);
168
- }
169
- if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
170
- if (loaded.change) ui.change = loaded.change;
171
- if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
172
- const repository = ui.change ? ui.change.repository : '';
173
- if (!ui.repoName && repository) ui.repoName = repository;
174
- if (!notes.store) ui.initReview();
175
- notes.applyImported(loaded.imported);
176
- restoreAfterLoad(api, here, wasTodo);
177
- markLoaded(api);
178
- loader.pendingExtras = loaded.pending === true;
179
- syncItemWatches(api);
180
- syncUnitEditor(api);
181
- return true;
182
- };
86
+ get root() {
87
+ return this.ui.top || this.ui.cwd;
88
+ }
183
89
 
184
- const load = (api) => {
185
- const { loadConfig } = api;
186
- const loaded = loadConfig.repo.load(
187
- loadConfig.cwd,
188
- loadConfig.paths,
189
- loadOptions(api),
190
- );
191
- applyLoaded(api, loaded);
192
- };
90
+ loadOptions(extra = {}) {
91
+ const ui = this.ui;
92
+ return {
93
+ commit: ui.rev,
94
+ audit: ui.audit,
95
+ outdatedMap: ui.outdatedMap,
96
+ auditMap: ui.auditMap,
97
+ ...extra,
98
+ };
99
+ }
193
100
 
194
- const fileStamp = (top, rel) => {
195
- const full = path.join(top, rel);
196
- try {
197
- const stat = fs.statSync(full);
198
- return `${stat.mtimeMs}:${stat.size}`;
199
- } catch {
200
- return 'missing';
101
+ endReadLoop() {
102
+ const finish = this.ui.finishReadLoop;
103
+ if (finish) finish();
201
104
  }
202
- };
203
105
 
204
- const npmRels = (items) => {
205
- const rels = [MANIFEST, LOCKFILE];
206
- for (const item of items) {
207
- const rel = itemPath(item);
208
- if (!depFileMeta(rel)) continue;
209
- if (rels.includes(rel)) continue;
210
- rels.push(rel);
106
+ finishIfEmpty(kind) {
107
+ const ui = this.ui;
108
+ if (ui.items.length) return;
109
+ const store = ui.review.store;
110
+ if (store && hasNotes(store)) return;
111
+ if (kind === 'keep') return;
112
+ if (isWorktree(ui)) {
113
+ if (ui.nav.pane === 'diff') ui.nav.pane = 'files';
114
+ return;
115
+ }
116
+ ui.done = true;
117
+ ui.exitCode = 0;
118
+ ui.status = 'nothing to review';
119
+ ui.emptyReview = kind === 'open';
120
+ this.endReadLoop();
211
121
  }
212
- rels.sort();
213
- return rels;
214
- };
215
122
 
216
- const npmStamp = (api) => {
217
- const top = api.ui.top || api.loadConfig.cwd;
218
- const rels = npmRels(api.collection.items);
219
- const parts = new Array(rels.length);
220
- for (let i = 0; i < rels.length; i++) {
221
- parts[i] = `${rels[i]}:${fileStamp(top, rels[i])}`;
123
+ failOpen(error) {
124
+ if (isAbort(error)) return;
125
+ const ui = this.ui;
126
+ ui.loadError = error;
127
+ ui.done = true;
128
+ ui.exitCode = 1;
129
+ ui.ops.setBusy('');
130
+ ui.stopProgress();
131
+ this.endReadLoop();
222
132
  }
223
- return parts.join('|');
224
- };
225
133
 
226
- const npmExtrasStale = (api) => {
227
- const cached = api.npmExtras;
228
- if (!cached || !api.loadConfig.audit) return false;
229
- return cached.stamp !== npmStamp(api);
230
- };
134
+ restoreAfterLoad(here, wasTasks) {
135
+ const { nav, collection } = this.ui;
136
+ nav.tasksOpen = wasTasks;
137
+ if (here && !wasTasks) {
138
+ const idx = collection.findRestoredIndex(here);
139
+ if (idx >= 0) nav.index = idx;
140
+ }
141
+ this.ui.clampIndex();
142
+ nav.restoreFileCursor(this.ui.fileList());
143
+ this.ui.syncReviewPath();
144
+ this.ui.syncFileCursor();
145
+ }
231
146
 
232
- const cacheNpmExtras = (api, loaded) => {
233
- api.npmExtras = {
234
- stamp: npmStamp(api),
235
- auditMap: loaded.auditMap ?? null,
236
- outdatedMap: loaded.outdatedMap ?? null,
237
- usedNames: loaded.usedNames ?? null,
238
- exportEntries: loaded.exportEntries,
239
- };
240
- };
147
+ syncUnitEditor() {
148
+ const ui = this.ui;
149
+ const rel = ui.nav.reviewPath;
150
+ if (ui.nav.pane !== 'unit' || !rel || !ui.repo.fileText) return;
151
+ ui.composer.applyDiskText(ui.repo.fileText(this.root, rel, ui.rev));
152
+ }
241
153
 
242
- const clearNpmExtras = (api) => {
243
- api.npmExtras = null;
244
- };
154
+ syncItemWatches() {
155
+ if (!this.diskWatcher) return;
156
+ const nav = this.ui.nav;
157
+ const rels = itemRels(this.ui.items);
158
+ if (nav.pane === 'unit' && nav.reviewPath) {
159
+ if (!rels.includes(nav.reviewPath)) rels.push(nav.reviewPath);
160
+ }
161
+ this.diskWatcher.watchFiles(rels);
162
+ }
245
163
 
246
- const isWanted = (api, gen) => {
247
- if (!api.loader.isCurrent(gen)) return false;
248
- if (!api.ui.done) return true;
249
- api.ops.clearBusy();
250
- return false;
251
- };
164
+ applyLoaded(loaded) {
165
+ const ui = this.ui;
166
+ if (ui.nav.pane === 'repos') return false;
167
+ const { loader, collection, nav } = ui;
168
+ if (loaded.generation !== undefined) {
169
+ if (!loader.isCurrent(loaded.generation, ui.done)) return false;
170
+ }
171
+ const wasTasks = nav.tasksOpen === true;
172
+ const here = ui.current();
173
+ ui.top = loaded.top ?? ui.cwd;
174
+ collection.replace(loaded.items);
175
+ stampItemDates(ui.top, collection.items);
176
+ if (loaded.rev) ui.rev = loaded.rev;
177
+ if (loaded.revShort) ui.revShort = loaded.revShort;
178
+ else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
179
+ if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
180
+ if (loaded.change) ui.change = loaded.change;
181
+ if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
182
+ const repository = ui.change ? ui.change.repository : '';
183
+ if (!ui.repoName && repository) ui.repoName = repository;
184
+ if (!ui.review.store) ui.initReview();
185
+ ui.review.applyImported(loaded.imported);
186
+ this.restoreAfterLoad(here, wasTasks);
187
+ loader.didLoad = true;
188
+ ui.updater.maybePrompt();
189
+ loader.pendingExtras = loaded.pending === true;
190
+ this.syncItemWatches();
191
+ this.syncUnitEditor();
192
+ ui.dashboard.noteLoaded();
193
+ return true;
194
+ }
252
195
 
253
- const followExtras = async (api, loaded, gen, kind) => {
254
- const extra = loadOptions(api, {
255
- paths: api.loadConfig.paths,
256
- signal: api.loader.signal,
257
- });
258
- let next;
259
- try {
260
- next = await fetchExtras(api.loadConfig.repo, loaded, extra);
261
- } catch (error) {
262
- if (api.loader.isCurrent(gen)) failOpen(api, error);
263
- return;
264
- }
265
- if (!isWanted(api, gen)) return;
266
- if (next) {
267
- applyLoaded(api, tagLoaded(next, gen));
268
- cacheNpmExtras(api, next);
269
- } else {
270
- api.loader.pendingExtras = false;
271
- }
272
- api.ops.clearBusy();
273
- api.ui.paint();
274
- finishIfEmpty(api, kind);
275
- };
196
+ load() {
197
+ const ui = this.ui;
198
+ const loaded = ui.repo.load(ui.cwd, ui.paths, this.loadOptions());
199
+ this.applyLoaded(loaded);
200
+ }
276
201
 
277
- const npmReuseOpts = (api, extra = {}) => {
278
- const opts = {};
279
- if (extra.signal) opts.signal = extra.signal;
280
- const cached = api.npmExtras;
281
- if (!cached) return opts;
282
- if (cached.stamp !== npmStamp(api)) return opts;
283
- opts.deferExtras = false;
284
- opts.collect = false;
285
- opts.auditMap = cached.auditMap;
286
- opts.outdatedMap = cached.outdatedMap;
287
- opts.usedNames = cached.usedNames;
288
- opts.exportEntries = cached.exportEntries;
289
- return opts;
290
- };
202
+ npmStamp() {
203
+ const root = this.root;
204
+ const rels = npmRels(this.ui.items);
205
+ const stamp = (rel) => `${rel}:${statStamp(path.join(root, rel))}`;
206
+ return rels.map(stamp).join('|');
207
+ }
291
208
 
292
- const snapshotOpts = (api, extra = {}) =>
293
- loadOptions(api, {
294
- deferExtras: shouldDeferExtras(api),
295
- ...npmReuseOpts(api, extra),
296
- });
209
+ npmExtrasStale() {
210
+ const cached = this.cachedExtras;
211
+ if (!cached || !this.ui.audit) return false;
212
+ return cached.stamp !== this.npmStamp();
213
+ }
297
214
 
298
- const resetNavAfterLoad = (api, extra = {}) => {
299
- api.nav.scroll = 0;
300
- api.nav.clearSelection();
301
- if (extra.afterLoad) extra.afterLoad();
302
- };
215
+ cacheNpmExtras(loaded) {
216
+ this.cachedExtras = {
217
+ stamp: this.npmStamp(),
218
+ auditMap: loaded.auditMap ?? null,
219
+ outdatedMap: loaded.outdatedMap ?? null,
220
+ usedNames: loaded.usedNames ?? null,
221
+ exportEntries: loaded.exportEntries,
222
+ };
223
+ }
303
224
 
304
- const refreshListsForPane = (api) => {
305
- const pane = api.ui.nav.pane;
306
- if (pane === 'commits' && api.ui.commits) api.ui.commits.refresh();
307
- };
225
+ clearNpmExtras() {
226
+ this.cachedExtras = null;
227
+ }
308
228
 
309
- const afterSnapshot = (api, kind, extra = {}) => {
310
- refreshListsForPane(api);
311
- if (kind !== 'open' && kind !== 'watch') {
312
- return void resetNavAfterLoad(api, extra);
229
+ npmExtras() {
230
+ if (this.cachedExtras) return this.cachedExtras;
231
+ const { auditMap, outdatedMap } = this.ui;
232
+ if (!auditMap && !outdatedMap) return null;
233
+ return { auditMap: auditMap ?? null, outdatedMap: outdatedMap ?? null };
313
234
  }
314
- if (extra.afterLoad) extra.afterLoad();
315
- };
316
235
 
317
- const beginSnapshot = (api, extra) => {
318
- if (extra.quiet === true) return;
319
- api.ui.progressFrame = 0;
320
- api.ops.setBusy('loading');
321
- api.ui.paint();
322
- };
236
+ isWanted(gen) {
237
+ if (!this.ui.loader.isCurrent(gen)) return false;
238
+ if (!this.ui.done) return true;
239
+ this.ui.ops.clearBusy();
240
+ return false;
241
+ }
323
242
 
324
- const runSnapshot = async (api, gen, kind, extra = {}) => {
325
- beginSnapshot(api, extra);
326
- let loaded;
327
- try {
328
- loaded = await fetchSnapshot(
329
- api.loadConfig.repo,
330
- api.loadConfig.cwd,
331
- api.loadConfig.paths,
332
- snapshotOpts(api, {
333
- signal: api.loader.signal,
334
- keepView: extra.keepView,
335
- }),
336
- );
337
- } catch (error) {
338
- if (api.loader.isCurrent(gen)) failOpen(api, error);
339
- return;
340
- }
341
- if (!isWanted(api, gen)) return;
342
- applyLoaded(api, tagLoaded(loaded, gen));
343
- afterSnapshot(api, kind, extra);
344
- if (loaded.pending === true || npmExtrasStale(api)) {
345
- api.ops.setBusy('checking npm');
346
- if (kind === 'open' || extra.quiet === true) api.ui.paint();
347
- await followExtras(api, loaded, gen, kind);
348
- return;
349
- }
350
- api.ops.clearBusy();
351
- finishIfEmpty(api, kind);
352
- if (extra.doneStatus && !api.ui.status && !api.ops.busy) {
353
- api.ui.status = extra.doneStatus;
354
- }
355
- api.ui.paint();
356
- };
243
+ async followExtras(loaded, gen, kind) {
244
+ const ui = this.ui;
245
+ const extra = this.loadOptions({
246
+ paths: ui.paths,
247
+ signal: ui.loader.signal,
248
+ });
249
+ let next;
250
+ try {
251
+ next = await fetchExtras(ui.repo, loaded, extra);
252
+ } catch (error) {
253
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
254
+ return;
255
+ }
256
+ if (!this.isWanted(gen)) return;
257
+ if (next) {
258
+ this.applyLoaded(tagLoaded(next, gen));
259
+ this.cacheNpmExtras(next);
260
+ } else {
261
+ ui.loader.pendingExtras = false;
262
+ }
263
+ ui.ops.clearBusy();
264
+ ui.paint();
265
+ this.finishIfEmpty(kind);
266
+ }
357
267
 
358
- const openLoad = (api) => {
359
- const { loader } = api;
360
- if (loader.promise) return loader.promise;
361
- if (loader.didLoad && !loader.pendingExtras) {
362
- loader.promise = Promise.resolve();
363
- return loader.promise;
268
+ snapshotOpts(extra = {}) {
269
+ const ui = this.ui;
270
+ const waiting = ui.outdatedMap === undefined || ui.auditMap === undefined;
271
+ const opts = { deferExtras: ui.audit && waiting };
272
+ if (extra.signal) opts.signal = extra.signal;
273
+ const cached = this.cachedExtras;
274
+ if (cached && cached.stamp === this.npmStamp()) {
275
+ opts.deferExtras = false;
276
+ opts.collect = false;
277
+ opts.auditMap = cached.auditMap;
278
+ opts.outdatedMap = cached.outdatedMap;
279
+ opts.usedNames = cached.usedNames;
280
+ opts.exportEntries = cached.exportEntries;
281
+ }
282
+ return this.loadOptions(opts);
364
283
  }
365
- const gen = loader.bump();
366
- loader.promise = runSnapshot(api, gen, 'open');
367
- return loader.promise;
368
- };
369
284
 
370
- const loadReady = async (api) => {
371
- api.ui.ensureRepo();
372
- await openLoad(api);
373
- if (api.ui.loadError) throw api.ui.loadError;
374
- };
285
+ afterSnapshot(kind, extra = {}) {
286
+ const nav = this.ui.nav;
287
+ if (nav.pane === 'commits') this.ui.commits.refresh();
288
+ if (kind !== 'open' && kind !== 'watch') {
289
+ nav.scroll = 0;
290
+ nav.clearSelection();
291
+ }
292
+ if (extra.afterLoad) extra.afterLoad();
293
+ }
375
294
 
376
- const refreshFromRepo = (api, extra = {}) => {
377
- api.loader.promise = null;
378
- const gen = api.loader.bump();
379
- const kind = kindFromExtra(extra);
380
- if (api.ui.uiOpen && api.loadConfig.repo.loadAsync) {
381
- return void runSnapshot(api, gen, kind, extra);
382
- }
383
- const loaded = api.loadConfig.repo.load(
384
- api.loadConfig.cwd,
385
- api.loadConfig.paths,
386
- snapshotOpts(api, extra),
387
- );
388
- applyLoaded(api, tagLoaded(loaded, gen));
389
- afterSnapshot(api, kind, extra);
390
- if (loaded.pending === true || npmExtrasStale(api)) {
391
- api.ops.setBusy('checking npm');
392
- return void followExtras(api, loaded, gen, kind);
393
- }
394
- if (kind === 'keep') return;
395
- finishIfEmpty(api, kind);
396
- if (extra.doneStatus && !api.ui.done && !api.loader.pendingExtras) {
397
- api.ui.status = extra.doneStatus;
398
- }
399
- if (kind === 'watch') api.ui.paint();
400
- };
295
+ async runSnapshot(gen, kind, extra = {}) {
296
+ const ui = this.ui;
297
+ if (extra.quiet !== true) {
298
+ ui.progressFrame = 0;
299
+ ui.ops.setBusy('loading');
300
+ ui.paint();
301
+ }
302
+ let loaded;
303
+ try {
304
+ const opts = this.snapshotOpts({ signal: ui.loader.signal });
305
+ loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
306
+ } catch (error) {
307
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
308
+ return;
309
+ }
310
+ if (!this.isWanted(gen)) return;
311
+ this.applyLoaded(tagLoaded(loaded, gen));
312
+ this.afterSnapshot(kind, extra);
313
+ if (loaded.pending === true || this.npmExtrasStale()) {
314
+ ui.ops.setBusy('checking npm');
315
+ if (kind === 'open' || extra.quiet === true) ui.paint();
316
+ await this.followExtras(loaded, gen, kind);
317
+ return;
318
+ }
319
+ ui.ops.clearBusy();
320
+ this.finishIfEmpty(kind);
321
+ if (extra.doneStatus && !ui.status && !ui.ops.busy) {
322
+ ui.status = extra.doneStatus;
323
+ }
324
+ ui.paint();
325
+ }
401
326
 
402
- const reloadAfterChange = (api, afterLoad) => {
403
- const hereItem = api.ui.current();
404
- const hereIndex = api.nav.index;
405
- const restore = () => {
406
- const idx = api.collection.findRestoredIndex(hereItem);
407
- api.nav.index = idx >= 0 ? idx : hereIndex;
408
- api.ui.clampIndex();
409
- api.ui.syncReviewPath();
410
- api.ui.syncFileCursor();
411
- if (afterLoad) afterLoad();
412
- };
413
- if (api.ui.uiOpen && api.loadConfig.repo.loadAsync) {
414
- return void refreshFromRepo(api, { afterLoad: restore });
415
- }
416
- refreshFromRepo(api);
417
- restore();
418
- };
327
+ openLoad() {
328
+ const loader = this.ui.loader;
329
+ if (this.ui.nav.pane === 'repos') {
330
+ loader.didLoad = true;
331
+ loader.promise = Promise.resolve();
332
+ return loader.promise;
333
+ }
334
+ if (loader.promise) return loader.promise;
335
+ if (loader.didLoad && !loader.pendingExtras) {
336
+ loader.promise = Promise.resolve();
337
+ } else {
338
+ loader.promise = this.runSnapshot(loader.bump(), 'open');
339
+ }
340
+ return loader.promise;
341
+ }
419
342
 
420
- const onReload = (api) => {
421
- api.collection.dismissed.clear();
422
- api.ui.status = '';
423
- refreshFromRepo(api, { doneStatus: 'reloaded' });
424
- };
343
+ async loadReady() {
344
+ if (this.ui.nav.pane === 'repos') {
345
+ this.ui.loader.didLoad = true;
346
+ return;
347
+ }
348
+ this.ui.ensureRepo();
349
+ await this.openLoad();
350
+ if (this.ui.loadError) throw this.ui.loadError;
351
+ }
425
352
 
426
- const clearWatchRetry = (api) => {
427
- if (api.watchRetry === null) return;
428
- clearTimeout(api.watchRetry);
429
- api.watchRetry = null;
430
- };
353
+ refreshFromRepo(extra = {}) {
354
+ const ui = this.ui;
355
+ ui.loader.promise = null;
356
+ const gen = ui.loader.bump();
357
+ const kind = kindFromExtra(extra);
358
+ if (ui.uiOpen && ui.repo.loadAsync) {
359
+ return void this.runSnapshot(gen, kind, extra);
360
+ }
361
+ const loaded = ui.repo.load(ui.cwd, ui.paths, this.snapshotOpts(extra));
362
+ this.applyLoaded(tagLoaded(loaded, gen));
363
+ this.afterSnapshot(kind, extra);
364
+ if (loaded.pending === true || this.npmExtrasStale()) {
365
+ ui.ops.setBusy('checking npm');
366
+ return void this.followExtras(loaded, gen, kind);
367
+ }
368
+ if (kind === 'keep') return;
369
+ this.finishIfEmpty(kind);
370
+ if (extra.doneStatus && !ui.done && !ui.loader.pendingExtras) {
371
+ ui.status = extra.doneStatus;
372
+ }
373
+ if (kind === 'watch') ui.paint();
374
+ }
431
375
 
432
- const shouldDeferWatch = (api) => {
433
- const { ui, ops } = api;
434
- if (ui.done) return true;
435
- if (!ui.uiOpen) return true;
436
- if (!api.loader.didLoad) return true;
437
- if (ops.gitBusy) return true;
438
- if (ui.mode === 'compose') {
439
- const kind = ui.composer && ui.composer.state.composeKind;
440
- if (kind === 'file') return false;
441
- }
442
- if (ui.mode !== 'review') return true;
443
- return false;
444
- };
376
+ reloadAfterChange(afterLoad) {
377
+ const ui = this.ui;
378
+ const hereItem = ui.current();
379
+ const hereIndex = ui.nav.index;
380
+ const restore = () => {
381
+ const idx = ui.collection.findRestoredIndex(hereItem);
382
+ ui.nav.index = idx >= 0 ? idx : hereIndex;
383
+ ui.clampIndex();
384
+ ui.syncReviewPath();
385
+ ui.syncFileCursor();
386
+ if (afterLoad) afterLoad();
387
+ };
388
+ if (ui.uiOpen && ui.repo.loadAsync) {
389
+ return void this.refreshFromRepo({ afterLoad: restore });
390
+ }
391
+ this.refreshFromRepo();
392
+ restore();
393
+ }
445
394
 
446
- const WATCH_IGNORE_MS = 500;
395
+ onReload() {
396
+ this.ui.collection.dismissed.clear();
397
+ this.ui.status = '';
398
+ this.refreshFromRepo({ doneStatus: 'reloaded' });
399
+ }
447
400
 
448
- const ignoreWatch = (api, ms = WATCH_IGNORE_MS) => {
449
- api.ignoreWatchUntil = Date.now() + ms;
450
- };
401
+ clearWatchRetry() {
402
+ if (this.watchRetry === null) return;
403
+ clearTimeout(this.watchRetry);
404
+ this.watchRetry = null;
405
+ }
451
406
 
452
- const onDiskChange = (api) => {
453
- if (Date.now() < (api.ignoreWatchUntil ?? 0)) return;
454
- if (shouldDeferWatch(api)) {
455
- if (api.watchRetry !== null) return;
456
- api.watchRetry = setTimeout(() => {
457
- api.watchRetry = null;
458
- onDiskChange(api);
459
- }, DEBOUNCE_MS);
460
- if (typeof api.watchRetry.unref === 'function') api.watchRetry.unref();
461
- return;
407
+ shouldDeferWatch() {
408
+ const ui = this.ui;
409
+ if (ui.done || !ui.uiOpen || !ui.loader.didLoad) return true;
410
+ if (ui.ops.gitBusy) return true;
411
+ if (ui.mode === 'compose' && ui.composeKind === 'file') return false;
412
+ return ui.mode !== 'review';
462
413
  }
463
- refreshFromRepo(api, { keepView: true, quiet: true });
464
- };
465
414
 
466
- const catchUpWatch = (api) => {
467
- clearWatchRetry(api);
468
- if (shouldDeferWatch(api)) return void onDiskChange(api);
469
- refreshFromRepo(api, { keepView: true, quiet: true });
470
- };
415
+ ignoreWatch(ms = WATCH_IGNORE_MS) {
416
+ this.ignoreWatchUntil = Date.now() + ms;
417
+ }
471
418
 
472
- const reviewFileStamp = (api) => {
473
- const store = api.review && api.review.store;
474
- if (!store || !store.reviewPath) return '';
475
- try {
476
- const stat = fs.statSync(store.reviewPath);
477
- return `${stat.mtimeMs}:${stat.size}`;
478
- } catch {
479
- return 'missing';
419
+ onDiskChange() {
420
+ if (this.ui.nav.pane === 'repos') return;
421
+ if (Date.now() < this.ignoreWatchUntil) return;
422
+ if (!this.shouldDeferWatch()) {
423
+ return void this.refreshFromRepo({ keepView: true, quiet: true });
424
+ }
425
+ if (this.watchRetry !== null) return;
426
+ this.watchRetry = setTimeout(() => {
427
+ this.watchRetry = null;
428
+ this.onDiskChange();
429
+ }, DEBOUNCE_MS);
430
+ this.watchRetry.unref();
480
431
  }
481
- };
482
432
 
483
- const noteReviewStamp = (api) => {
484
- api.reviewStamp = reviewFileStamp(api);
485
- };
433
+ catchUpWatch() {
434
+ this.clearWatchRetry();
435
+ if (this.shouldDeferWatch()) return void this.onDiskChange();
436
+ this.refreshFromRepo({ keepView: true, quiet: true });
437
+ }
486
438
 
487
- const applyReviewChange = (api) => {
488
- if (!api.ui || api.ui.done) return;
489
- const stamp = reviewFileStamp(api);
490
- if (!stamp || stamp === api.reviewStamp) return;
491
- if (stamp === 'missing') {
492
- api.reviewStamp = stamp;
493
- return;
494
- }
495
- const store = api.review && api.review.store;
496
- if (!store) return;
497
- let disk;
498
- try {
499
- disk = loadReview(store.reviewPath, store.templates);
500
- } catch {
501
- return;
439
+ reviewFileStamp() {
440
+ const store = this.ui.review.store;
441
+ if (!store || !store.reviewPath) return '';
442
+ return statStamp(store.reviewPath);
502
443
  }
503
- mergeReview(store, disk);
504
- api.reviewStamp = stamp;
505
- api.ui.paint();
506
- };
507
444
 
508
- const stopWatch = (api) => {
509
- clearWatchRetry(api);
510
- api.reviewStamp = '';
511
- if (!api.diskWatcher) return;
512
- api.diskWatcher.close();
513
- api.diskWatcher = null;
514
- };
445
+ noteReviewStamp() {
446
+ this.reviewStamp = this.reviewFileStamp();
447
+ }
515
448
 
516
- const startWatch = (api) => {
517
- stopWatch(api);
518
- if (!isWorktree(api.ui)) return;
519
- const root = api.ui.top || api.loadConfig.cwd;
520
- if (!root) return;
521
- api.diskWatcher = createDiskWatcher({
522
- root,
523
- paths: api.loadConfig.paths,
524
- onChange: () => onDiskChange(api),
525
- onReview: () => applyReviewChange(api),
526
- });
527
- syncItemWatches(api);
528
- noteReviewStamp(api);
529
- };
449
+ applyReviewChange() {
450
+ if (this.ui.done) return;
451
+ const stamp = this.reviewFileStamp();
452
+ if (!stamp || stamp === this.reviewStamp) return;
453
+ if (stamp === 'missing') {
454
+ this.reviewStamp = stamp;
455
+ return;
456
+ }
457
+ const store = this.ui.review.store;
458
+ let disk;
459
+ try {
460
+ disk = loadReview(store.reviewPath, store.templates);
461
+ } catch {
462
+ return;
463
+ }
464
+ mergeReview(store, disk);
465
+ this.reviewStamp = stamp;
466
+ this.ui.paint();
467
+ }
530
468
 
531
- const createLifecycle = (api) => {
532
- api.diskWatcher = null;
533
- api.watchRetry = null;
534
- api.reviewStamp = '';
535
- api.ignoreWatchUntil = 0;
536
- api.npmExtras = null;
537
- return {
538
- load: () => load(api),
539
- openLoad: () => openLoad(api),
540
- loadReady: () => loadReady(api),
541
- refreshFromRepo: (extra) => refreshFromRepo(api, extra),
542
- reloadAfterChange: (afterLoad) => reloadAfterChange(api, afterLoad),
543
- onReload: () => onReload(api),
544
- onDiskChange: () => onDiskChange(api),
545
- applyReviewChange: () => applyReviewChange(api),
546
- noteReviewStamp: () => noteReviewStamp(api),
547
- catchUpWatch: () => catchUpWatch(api),
548
- startWatch: () => startWatch(api),
549
- stopWatch: () => stopWatch(api),
550
- ignoreWatch: (ms) => ignoreWatch(api, ms),
551
- clearNpmExtras: () => clearNpmExtras(api),
552
- };
553
- };
469
+ stopWatch() {
470
+ this.clearWatchRetry();
471
+ this.reviewStamp = '';
472
+ if (!this.diskWatcher) return;
473
+ this.diskWatcher.close();
474
+ this.diskWatcher = null;
475
+ }
476
+
477
+ startWatch() {
478
+ this.stopWatch();
479
+ const ui = this.ui;
480
+ if (ui.nav.pane === 'repos') {
481
+ const root = ui.workspaceRoot;
482
+ if (!root) return;
483
+ const info = ui.workspaceInfo;
484
+ const repos = info ? info.repos : [];
485
+ const paths = ['.'];
486
+ for (const repo of repos) paths.push(path.join(repo.name, '.git'));
487
+ this.diskWatcher = new DiskWatcher({
488
+ root,
489
+ paths,
490
+ matchReview: (rel) => rel.includes('/.review/'),
491
+ onPaths: (rels) => ui.workspace.notePaths(rels),
492
+ });
493
+ return;
494
+ }
495
+ const root = this.root;
496
+ if (!isWorktree(this.ui) || !root) return;
497
+ this.diskWatcher = new DiskWatcher({
498
+ root,
499
+ paths: this.ui.paths,
500
+ onChange: () => this.onDiskChange(),
501
+ onReview: () => this.applyReviewChange(),
502
+ onPaths: (paths) => this.ui.dashboard.noteDisk(paths),
503
+ });
504
+ this.syncItemWatches();
505
+ this.noteReviewStamp();
506
+ }
507
+ }
554
508
 
555
- module.exports = { createLifecycle };
509
+ module.exports = { Lifecycle };