reslop 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +197 -402
  14. package/lib/deps.js +113 -240
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +76 -144
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +155 -141
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +14 -14
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
package/lib/session.js CHANGED
@@ -1,19 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const render = require('./render/render.js');
4
+ const { renderFrame } = render;
4
5
  const keys = require('./keys.js');
6
+ const { decodeChunk } = keys;
5
7
  const review = require('./review.js');
8
+ const { AUTOSAVE_MS, hasNotes } = review;
6
9
  const files = require('./files.js');
7
10
  const { makeTaskItem } = files;
8
- const { renderFrame } = render;
9
- const { decodeChunk } = keys;
10
- const { AUTOSAVE_MS, hasNotes } = review;
11
11
  const capabilities = require('./capabilities.js');
12
+ const { sessionCapabilities } = capabilities;
12
13
  const actions = require('./session/actions.js');
13
14
  const { LIST_PANES } = actions;
14
- const { sessionCapabilities } = capabilities;
15
15
  const terminal = require('./session/terminal.js');
16
- const { createTerminal, LEAVE_TERM } = terminal;
16
+ const { Terminal, LEAVE_TERM } = terminal;
17
17
  const load = require('./session/load.js');
18
18
  const { LoadCoordinator } = load;
19
19
  const items = require('./session/items.js');
@@ -25,31 +25,27 @@ const { ReviewController } = sessionReview;
25
25
  const update = require('./session/update.js');
26
26
  const { UpdateCoordinator } = update;
27
27
  const progress = require('./session/progress.js');
28
- const { createProgress } = progress;
28
+ const { Progress } = progress;
29
29
  const compose = require('./session/compose.js');
30
- const { createComposer } = compose;
30
+ const { Composer } = compose;
31
31
  const ops = require('./session/ops.js');
32
32
  const { OpsRunner } = ops;
33
- const changeActions = require('./session/change-actions.js');
34
- const { createChangeActions } = changeActions;
35
33
  const commit = require('./session/commit.js');
36
- const { createCommitController } = commit;
34
+ const { CommitController } = commit;
37
35
  const branches = require('./session/branches.js');
38
- const { createBranchController } = branches;
36
+ const { BranchController } = branches;
39
37
  const npm = require('./session/npm.js');
40
- const { createNpmController } = npm;
38
+ const { NpmController } = npm;
41
39
  const accessors = require('./session/accessors.js');
42
- const { bindAccessors, bindGetters, bindAlias } = accessors;
40
+ const { bindAccessors, bindGetters } = accessors;
43
41
  const view = require('./session/view.js');
44
- const { createView } = view;
45
42
  const filesPane = require('./session/files-pane.js');
46
- const { createFilesPane } = filesPane;
47
43
  const lifecycle = require('./session/lifecycle.js');
48
- const { createLifecycle } = lifecycle;
49
- const pointer = require('./session/pointer.js');
50
- const { createPointer } = pointer;
44
+ const { Lifecycle } = lifecycle;
51
45
  const dispatch = require('./session/dispatch.js');
52
- const { createDispatcher, createInput } = dispatch;
46
+ const { dispatchAction, confirmChoice, handleEvent } = dispatch;
47
+ const dashboard = require('./session/dashboard.js');
48
+ const { Dashboard } = dashboard;
53
49
 
54
50
  const PROGRESS_MS = 160;
55
51
 
@@ -77,7 +73,6 @@ const COMPOSER_FIELDS = [
77
73
  'composeKind',
78
74
  'commitKind',
79
75
  'composeTaskId',
80
- 'composeBaseline',
81
76
  'templateIndex',
82
77
  'templateFocus',
83
78
  ];
@@ -90,11 +85,6 @@ const assignOptions = (session, options) => {
90
85
  session.stdout = options.stdout;
91
86
  session.proc = options.proc ?? process;
92
87
  session.color = options.color ?? true;
93
- session.getSize = () => {
94
- const width = session.stdout.columns ?? 80;
95
- const height = session.stdout.rows ?? 24;
96
- return { width, height };
97
- };
98
88
  session.rev = options.rev ?? null;
99
89
  session.revShort = '';
100
90
  session.sourceLabel = options.sourceLabel ?? '';
@@ -107,41 +97,31 @@ const assignOptions = (session, options) => {
107
97
  session.outdatedMap = options.outdatedMap;
108
98
  session.auditMap = options.auditMap;
109
99
  session.updateOpts = options.update;
110
- session.term = createTerminal({
111
- stdin: session.stdin,
112
- stdout: session.stdout,
113
- proc: session.proc,
114
- });
100
+ session.dashboardHome = options.startPane === 'dashboard';
101
+ };
102
+
103
+ const assignParts = (session, options) => {
104
+ const { stdin, stdout, proc } = session;
105
+ session.term = new Terminal({ stdin, stdout, proc });
115
106
  session.loader = new LoadCoordinator();
116
107
  session.collection = new ItemCollection();
117
108
  session.nav = new Navigation({ startPane: options.startPane ?? 'files' });
118
109
  session.review = new ReviewController();
119
- session.progress = createProgress(
120
- session.term,
121
- () => session.tickProgress(),
122
- PROGRESS_MS,
123
- );
124
- };
125
-
126
- const assignComposer = (session) => {
127
- session.composer = createComposer({
128
- nav: session.nav,
129
- review: session.review,
130
- ui: session,
131
- });
132
- };
133
-
134
- const assignControllers = (session) => {
110
+ const tick = () => session.tickProgress();
111
+ session.progress = new Progress(session.term, tick, PROGRESS_MS);
112
+ session.updater = new UpdateCoordinator(session);
113
+ session.composer = new Composer(session);
135
114
  session.ops = new OpsRunner(session);
136
- session.changeActions = createChangeActions(session);
137
- session.commits = createCommitController(session);
138
- session.gitBranches = createBranchController(session);
139
- session.npm = createNpmController(session);
115
+ session.commits = new CommitController(session);
116
+ session.gitBranches = new BranchController(session);
117
+ session.npm = new NpmController(session);
118
+ session.dashboard = new Dashboard(session);
119
+ session.lifecycle = new Lifecycle(session);
140
120
  };
141
121
 
142
- const bindSessionAccessors = (session) => {
122
+ const bindFields = (session) => {
143
123
  bindAccessors(session, session.nav, NAV_FIELDS);
144
- bindAccessors(session, session.composer.state, COMPOSER_FIELDS);
124
+ bindAccessors(session, session.composer, COMPOSER_FIELDS);
145
125
  bindAccessors(session, session.loader, ['pendingExtras']);
146
126
  bindGetters(session, session.loader, ['didLoad']);
147
127
  bindAccessors(session, session.collection, ['items']);
@@ -150,76 +130,36 @@ const bindSessionAccessors = (session) => {
150
130
  bindAccessors(session, session.ops, ['gitBusy', 'busy']);
151
131
  bindAccessors(session, session.gitBranches, ['branches', 'dropName']);
152
132
  bindGetters(session, session.term, ['lastFrame', 'lastSize']);
153
- bindAlias(session, 'notes', () => session.review.store);
154
- };
155
-
156
- const bindViewer = (session) => {
157
- session.viewer = createView({
158
- nav: session.nav,
159
- composer: session.composer,
160
- collection: session.collection,
161
- review: session.review,
162
- gitBranches: session.gitBranches,
163
- commits: session.commits,
164
- npm: session.npm,
165
- updater: session.updater,
166
- ui: session,
167
- });
168
- };
169
-
170
- const bindLifecycle = (session) => {
171
- session.lifecycle = createLifecycle({
172
- loader: session.loader,
173
- collection: session.collection,
174
- review: session.review,
175
- nav: session.nav,
176
- ops: session.ops,
177
- updater: session.updater,
178
- loadConfig: {
179
- repo: session.repo,
180
- cwd: session.cwd,
181
- paths: session.paths,
182
- audit: session.audit,
183
- outdatedMap: session.outdatedMap,
184
- auditMap: session.auditMap,
185
- },
186
- ui: session,
187
- });
188
- };
189
-
190
- const bindDispatch = (session) => {
191
- session.dispatcher = createDispatcher(session);
192
- session.pointer = createPointer(session);
193
- session.input = createInput(session);
194
- };
195
-
196
- const composeSession = (session, options) => {
197
- assignOptions(session, options);
198
- session.updater = new UpdateCoordinator(session);
199
- assignComposer(session);
200
- assignControllers(session);
201
- bindSessionAccessors(session);
202
- bindViewer(session);
203
- session.filesPane = createFilesPane(session);
204
- bindLifecycle(session);
205
- bindDispatch(session);
206
133
  };
207
134
 
208
135
  class Session {
209
136
  constructor(options) {
210
- composeSession(this, options);
137
+ assignOptions(this, options);
138
+ assignParts(this, options);
139
+ bindFields(this);
211
140
  this.resetState();
212
141
  }
213
142
 
143
+ get notes() {
144
+ return this.review.store;
145
+ }
146
+
147
+ getSize() {
148
+ const width = this.stdout.columns ?? 80;
149
+ const height = this.stdout.rows ?? 24;
150
+ return { width, height };
151
+ }
152
+
214
153
  resetState() {
215
- if (this.collection) this.collection.reset();
216
- if (this.nav) this.nav.reset();
217
- if (this.review) this.review.reset();
218
- if (this.composer) this.composer.reset();
219
- if (this.ops) this.ops.reset();
220
- if (this.gitBranches) this.gitBranches.reset();
221
- if (this.commits) this.commits.reset();
222
- if (this.npm) this.npm.reset();
154
+ this.collection.reset();
155
+ this.nav.reset();
156
+ this.review.reset();
157
+ this.composer.reset();
158
+ this.ops.reset();
159
+ this.gitBranches.reset();
160
+ this.commits.reset();
161
+ this.npm.reset();
162
+ this.dashboard.reset();
223
163
  this.top = this.cwd;
224
164
  this.branch = '';
225
165
  this.status = '';
@@ -233,12 +173,12 @@ class Session {
233
173
  this.loadError = null;
234
174
  this.uiOpen = false;
235
175
  this.finishReadLoop = null;
236
- if (this.lifecycle) this.lifecycle.stopWatch();
237
- if (this.lifecycle) this.lifecycle.clearNpmExtras();
238
- if (this.term) this.term.resetCache();
239
- if (this.loader) this.loader.reset();
240
- if (this.updater) this.updater.reset();
241
- if (this.progress) this.progress.clear();
176
+ this.lifecycle.stopWatch();
177
+ this.lifecycle.clearNpmExtras();
178
+ this.term.resetCache();
179
+ this.loader.reset();
180
+ this.updater.reset();
181
+ this.progress.clear();
242
182
  }
243
183
 
244
184
  ensureRepo() {
@@ -271,8 +211,7 @@ class Session {
271
211
  }
272
212
 
273
213
  paint() {
274
- if (!this.uiOpen) return;
275
- this.draw();
214
+ if (this.uiOpen) this.draw();
276
215
  }
277
216
 
278
217
  draw() {
@@ -294,6 +233,7 @@ class Session {
294
233
  const composing = this.mode === 'compose';
295
234
  this.term.paint(frame, size, composing ? frame.cursor : null);
296
235
  this.scheduleAutosave();
236
+ this.dashboard.afterDraw();
297
237
  }
298
238
 
299
239
  scheduleAutosave() {
@@ -304,19 +244,11 @@ class Session {
304
244
  }
305
245
 
306
246
  view() {
307
- return this.viewer.view();
247
+ return view.view(this);
308
248
  }
309
249
 
310
250
  fileList() {
311
- return this.viewer.fileList();
312
- }
313
-
314
- counts() {
315
- return this.viewer.counts();
316
- }
317
-
318
- viewStatus() {
319
- return this.viewer.viewStatus();
251
+ return view.fileList(this);
320
252
  }
321
253
 
322
254
  current() {
@@ -324,31 +256,35 @@ class Session {
324
256
  }
325
257
 
326
258
  fileCursorEntry() {
327
- return this.changeActions.fileCursorEntry();
259
+ return filesPane.fileCursorEntry(this);
328
260
  }
329
261
 
330
262
  clampIndex() {
331
- this.filesPane.clampIndex();
263
+ filesPane.clampIndex(this);
332
264
  }
333
265
 
334
266
  syncReviewPath() {
335
- this.filesPane.syncReviewPath();
267
+ filesPane.syncReviewPath(this);
336
268
  }
337
269
 
338
270
  syncFileCursor() {
339
- this.filesPane.syncFileCursor();
271
+ filesPane.syncFileCursor(this);
340
272
  }
341
273
 
342
274
  showFiles() {
343
- this.filesPane.showFiles();
275
+ filesPane.showFiles(this);
344
276
  }
345
277
 
346
278
  dispatch(action) {
347
- this.dispatcher.dispatch(action);
279
+ dispatchAction(this, action);
280
+ }
281
+
282
+ confirmChoice(key) {
283
+ confirmChoice(this, key);
348
284
  }
349
285
 
350
286
  handleEvent(event) {
351
- this.input.handleEvent(event);
287
+ handleEvent(this, event);
352
288
  }
353
289
 
354
290
  pushInput(text) {
@@ -376,7 +312,7 @@ class Session {
376
312
  this.status = result.error.message;
377
313
  return false;
378
314
  }
379
- if (this.lifecycle) this.lifecycle.noteReviewStamp();
315
+ this.lifecycle.noteReviewStamp();
380
316
  return result.wrote;
381
317
  }
382
318
 
@@ -388,19 +324,7 @@ class Session {
388
324
  }
389
325
 
390
326
  selectTemplate(index) {
391
- this.runComposeCommand(this.composer.selectTemplate(index));
392
- }
393
-
394
- idleNoteText() {
395
- return this.composer.idleNoteText();
396
- }
397
-
398
- shownTemplates() {
399
- return this.composer.shownTemplates();
400
- }
401
-
402
- startDraftCompose() {
403
- this.composer.startDraftCompose();
327
+ this.runComposeCommand(this.composer.templates.selectTemplate(index));
404
328
  }
405
329
 
406
330
  autosave() {
@@ -487,6 +411,7 @@ class Session {
487
411
  if (settled) return;
488
412
  settled = true;
489
413
  this.lifecycle.stopWatch();
414
+ this.dashboard.stop();
490
415
  this.term.stopListening();
491
416
  this.progress.clear();
492
417
  this.term.clearTimers();
@@ -503,16 +428,11 @@ class Session {
503
428
  if (this.done) return void finish();
504
429
  this.draw();
505
430
  };
506
- this.term.startListening({
507
- onData,
508
- onResize: () => this.draw(),
509
- });
431
+ this.term.startListening({ onData, onResize: () => this.draw() });
510
432
  this.lifecycle.startWatch();
433
+ if (this.dashboardHome) this.dashboard.start();
511
434
  });
512
435
  }
513
436
  }
514
437
 
515
- module.exports = {
516
- LEAVE_TERM,
517
- Session,
518
- };
438
+ module.exports = { LEAVE_TERM, Session };
package/lib/update.js CHANGED
@@ -21,19 +21,10 @@ const INSTALL_MS = 120000;
21
21
  const REGISTRY = 'https://registry.npmjs.org';
22
22
 
23
23
  const RELEASE = /^\d+\.\d+\.\d+$/;
24
- const emptyCache = () => ({
25
- checkedAt: 0,
26
- compatible: '',
27
- major: '',
28
- skipped: '',
29
- });
24
+ const NO_TARGETS = { compatible: '', major: '' };
30
25
 
31
- const toInt = (text) => parseInt(text, 10);
32
-
33
- const isNpmInstall = (pkgDir) => {
34
- const parent = path.basename(path.dirname(pkgDir));
35
- return parent === 'node_modules';
36
- };
26
+ const isNpmInstall = (pkgDir) =>
27
+ path.basename(path.dirname(pkgDir)) === 'node_modules';
37
28
 
38
29
  const cacheFile = (env = process.env) => {
39
30
  if (env.XDG_CACHE_HOME) {
@@ -49,29 +40,26 @@ const cacheFile = (env = process.env) => {
49
40
 
50
41
  const readCache = (file) => {
51
42
  try {
52
- const text = fs.readFileSync(file, 'utf8');
53
- const data = jsonParse(text);
54
- if (!isHashObject(data)) {
55
- return emptyCache();
43
+ const data = jsonParse(fs.readFileSync(file, 'utf8'));
44
+ if (isHashObject(data)) {
45
+ return {
46
+ checkedAt: parseInt(data.checkedAt, 10) || 0,
47
+ compatible: asText(data.compatible),
48
+ major: asText(data.major) || asText(data.latest),
49
+ skipped: asText(data.skipped) || asText(data.handled),
50
+ };
56
51
  }
57
- const checkedAt = toInt(data.checkedAt) || 0;
58
- const compatible = asText(data.compatible);
59
- const major = asText(data.major) || asText(data.latest);
60
- const skipped = asText(data.skipped) || asText(data.handled);
61
- return { checkedAt, compatible, major, skipped };
62
52
  } catch {
63
- return emptyCache();
53
+ // missing or unreadable cache
64
54
  }
55
+ return { checkedAt: 0, compatible: '', major: '', skipped: '' };
65
56
  };
66
57
 
67
58
  const writeCache = (file, data) => {
59
+ const { checkedAt, compatible, major, skipped } = data;
60
+ const body = JSON.stringify({ checkedAt, compatible, major, skipped });
68
61
  try {
69
62
  fs.mkdirSync(path.dirname(file), { recursive: true });
70
- const checkedAt = data.checkedAt ?? 0;
71
- const compatible = data.compatible ?? '';
72
- const major = data.major ?? '';
73
- const skipped = data.skipped ?? '';
74
- const body = JSON.stringify({ checkedAt, compatible, major, skipped });
75
63
  fs.writeFileSync(file, `${body}\n`);
76
64
  } catch {
77
65
  // ignore cache write errors
@@ -79,76 +67,43 @@ const writeCache = (file, data) => {
79
67
  };
80
68
 
81
69
  const isFresh = (cache, now, interval = CHECK_INTERVAL_MS) => {
82
- if (!cache || !cache.checkedAt) return false;
83
- if (now < cache.checkedAt) return false;
70
+ if (!cache.checkedAt || now < cache.checkedAt) return false;
84
71
  return now - cache.checkedAt < interval;
85
72
  };
86
73
 
87
74
  const listVersions = (data) => {
88
75
  if (!data || typeof data !== 'object') return [];
89
76
  const names = [];
90
- const map = data.versions;
91
- if (isHashObject(map)) {
92
- for (const name of Object.keys(map)) names.push(name);
93
- }
77
+ if (isHashObject(data.versions)) names.push(...Object.keys(data.versions));
94
78
  const tags = data['dist-tags'];
95
79
  if (tags && typeof tags.latest === 'string') names.push(tags.latest);
96
80
  if (typeof data.version === 'string') names.push(data.version);
97
- const listed = [];
98
- for (const name of names) {
99
- if (!RELEASE.test(name)) continue;
100
- if (listed.includes(name)) continue;
101
- listed.push(name);
102
- }
103
- return listed;
81
+ return [...new Set(names)].filter((name) => RELEASE.test(name));
104
82
  };
105
83
 
106
- const pickNewerName = (best, name, ver) => {
84
+ const newer = (best, name) => {
107
85
  if (!best) return name;
108
- const prev = parseVersion(best);
109
- if (!prev || cmpVersion(ver, prev) > 0) return name;
110
- return best;
86
+ return cmpVersion(parseVersion(name), parseVersion(best)) > 0 ? name : best;
111
87
  };
112
88
 
113
89
  const pickTargets = (current, names) => {
114
90
  const from = parseVersion(current);
115
- if (!from) return { compatible: '', major: '' };
91
+ if (!from) return NO_TARGETS;
116
92
  let compatible = '';
117
93
  let major = '';
118
94
  for (const name of names) {
119
95
  const ver = parseVersion(name);
120
- if (!ver) continue;
121
96
  if (cmpVersion(ver, from) <= 0) continue;
122
- if (ver.major === from.major) {
123
- compatible = pickNewerName(compatible, name, ver);
124
- continue;
125
- }
126
- if (ver.major > from.major) major = pickNewerName(major, name, ver);
97
+ if (ver.major === from.major) compatible = newer(compatible, name);
98
+ else if (ver.major > from.major) major = newer(major, name);
127
99
  }
128
100
  return { compatible, major };
129
101
  };
130
102
 
131
- const classifyLatest = (current, latest) => {
132
- const from = parseVersion(current);
133
- const to = parseVersion(latest);
134
- if (!from || !to || cmpVersion(to, from) <= 0) {
135
- return { compatible: '', major: '' };
136
- }
137
- if (to.major > from.major) return { compatible: '', major: latest };
138
- return { compatible: latest, major: '' };
139
- };
140
-
141
- const planUpdate = (current, extra = {}) => {
103
+ const planUpdate = (current, targets) => {
142
104
  const from = parseVersion(current);
143
105
  if (!from) return { action: 'none' };
144
- let compatible = extra.compatible ?? '';
145
- let major = extra.major ?? '';
146
- const skipped = extra.skipped ?? extra.handled ?? '';
147
- if (!compatible && !major && extra.latest) {
148
- const classified = classifyLatest(current, extra.latest);
149
- compatible = classified.compatible;
150
- major = classified.major;
151
- }
106
+ const { compatible, major, skipped } = targets;
152
107
  const compatVer = parseVersion(compatible);
153
108
  if (compatVer && compatVer.major === from.major) {
154
109
  if (cmpVersion(compatVer, from) > 0) {
@@ -162,53 +117,40 @@ const planUpdate = (current, extra = {}) => {
162
117
  return { action: 'confirm', current, latest: major };
163
118
  };
164
119
 
165
- const isEnabled = (options = {}) => {
166
- if (options.enabled === false) return false;
167
- if (options.enabled === true) return true;
168
- const pkgDir = options.pkgDir ?? PKG_DIR;
169
- return isNpmInstall(pkgDir);
120
+ const isEnabled = (options) => {
121
+ if (typeof options.enabled === 'boolean') return options.enabled;
122
+ return isNpmInstall(options.pkgDir ?? PKG_DIR);
170
123
  };
171
124
 
172
- const fetchTargets = async (current, options = {}) => {
173
- const empty = { compatible: '', major: '' };
125
+ const fetchTargets = async (current, options) => {
174
126
  const fetchImpl = options.fetch ?? globalThis.fetch;
175
- if (typeof fetchImpl !== 'function') return empty;
176
- const encoded = encodeURIComponent(PKG_NAME);
177
- const url = `${REGISTRY}/${encoded}`;
127
+ if (typeof fetchImpl !== 'function') return NO_TARGETS;
128
+ const url = `${REGISTRY}/${encodeURIComponent(PKG_NAME)}`;
178
129
  const headers = { accept: 'application/json' };
179
130
  const signal = options.signal ?? AbortSignal.timeout(FETCH_MS);
180
131
  const response = await fetchImpl(url, { headers, signal });
181
- if (!response || !response.ok) return empty;
132
+ if (!response || !response.ok) return NO_TARGETS;
182
133
  const data = await response.json();
183
134
  return pickTargets(current, listVersions(data));
184
135
  };
185
136
 
186
- const npmInstall = async (spec, extra = {}) => {
187
- const timeout = extra.timeout ?? INSTALL_MS;
188
- const result = await runProc(
189
- npmBin(),
190
- ['install', '-g', spec],
191
- npmOpts({ timeout }),
192
- );
193
- if (result.error) throw result.error;
194
- if (result.status !== 0) {
195
- throw new Error(oneLine(result.stderr, 'npm install failed'));
196
- }
197
- };
198
-
199
137
  const installUpdate = async (version, options = {}) => {
200
138
  if (typeof options.install === 'function') {
201
139
  await options.install(version);
202
140
  return;
203
141
  }
204
- const spec = `${PKG_NAME}@${version}`;
205
- await npmInstall(spec, options);
142
+ const timeout = options.timeout ?? INSTALL_MS;
143
+ const args = ['install', '-g', `${PKG_NAME}@${version}`];
144
+ const result = await runProc(npmBin(), args, npmOpts({ timeout }));
145
+ if (result.error) throw result.error;
146
+ if (result.status !== 0) {
147
+ throw new Error(oneLine(result.stderr, 'npm install failed'));
148
+ }
206
149
  };
207
150
 
208
151
  const markSkipped = (version, options = {}) => {
209
152
  const file = options.cacheFile ?? cacheFile(options.env);
210
- const cache = readCache(file);
211
- writeCache(file, { ...cache, skipped: version });
153
+ writeCache(file, { ...readCache(file), skipped: version });
212
154
  };
213
155
 
214
156
  const checkUpdate = async (options = {}) => {
@@ -216,37 +158,28 @@ const checkUpdate = async (options = {}) => {
216
158
  const now = Date.now();
217
159
  const current = options.current ?? PKG_VERSION;
218
160
  const file = options.cacheFile ?? cacheFile(options.env);
219
- const interval = options.interval ?? CHECK_INTERVAL_MS;
220
161
  const cache = readCache(file);
221
- if (isFresh(cache, now, interval)) {
162
+ if (isFresh(cache, now, options.interval)) {
222
163
  const plan = planUpdate(current, cache);
223
- if (plan.action === 'install') return { action: 'none' };
224
- return plan;
164
+ return plan.action === 'install' ? { action: 'none' } : plan;
225
165
  }
226
- let targets = { compatible: '', major: '' };
166
+ let targets = NO_TARGETS;
227
167
  try {
228
168
  targets = await fetchTargets(current, options);
229
169
  } catch {
230
170
  // ignore registry errors
231
171
  }
232
- const next = {
233
- ...cache,
234
- checkedAt: now,
235
- compatible: targets.compatible,
236
- major: targets.major,
237
- };
238
172
  if (!targets.compatible && !targets.major) {
239
173
  writeCache(file, { ...cache, checkedAt: now });
240
174
  return { action: 'none' };
241
175
  }
176
+ const next = { ...cache, checkedAt: now, ...targets };
242
177
  writeCache(file, next);
243
178
  return planUpdate(current, next);
244
179
  };
245
180
 
246
181
  module.exports = {
247
182
  CHECK_INTERVAL_MS,
248
- parseVersion,
249
- cmpVersion,
250
183
  isNpmInstall,
251
184
  cacheFile,
252
185
  readCache,