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
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,31 @@ 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;
39
+ const packages = require('./session/packages.js');
40
+ const { PackageController } = packages;
41
41
  const accessors = require('./session/accessors.js');
42
- const { bindAccessors, bindGetters, bindAlias } = accessors;
42
+ const { bindAccessors, bindGetters } = accessors;
43
43
  const view = require('./session/view.js');
44
- const { createView } = view;
45
44
  const filesPane = require('./session/files-pane.js');
46
- const { createFilesPane } = filesPane;
47
45
  const lifecycle = require('./session/lifecycle.js');
48
- const { createLifecycle } = lifecycle;
49
- const pointer = require('./session/pointer.js');
50
- const { createPointer } = pointer;
46
+ const { Lifecycle } = lifecycle;
51
47
  const dispatch = require('./session/dispatch.js');
52
- const { createDispatcher, createInput } = dispatch;
48
+ const { dispatchAction, confirmChoice, handleEvent } = dispatch;
49
+ const dashboard = require('./session/dashboard.js');
50
+ const { Dashboard } = dashboard;
51
+ const workspace = require('./session/workspace.js');
52
+ const { Workspace } = workspace;
53
53
 
54
54
  const PROGRESS_MS = 160;
55
55
 
@@ -77,7 +77,6 @@ const COMPOSER_FIELDS = [
77
77
  'composeKind',
78
78
  'commitKind',
79
79
  'composeTaskId',
80
- 'composeBaseline',
81
80
  'templateIndex',
82
81
  'templateFocus',
83
82
  ];
@@ -90,11 +89,6 @@ const assignOptions = (session, options) => {
90
89
  session.stdout = options.stdout;
91
90
  session.proc = options.proc ?? process;
92
91
  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
92
  session.rev = options.rev ?? null;
99
93
  session.revShort = '';
100
94
  session.sourceLabel = options.sourceLabel ?? '';
@@ -107,41 +101,35 @@ const assignOptions = (session, options) => {
107
101
  session.outdatedMap = options.outdatedMap;
108
102
  session.auditMap = options.auditMap;
109
103
  session.updateOpts = options.update;
110
- session.term = createTerminal({
111
- stdin: session.stdin,
112
- stdout: session.stdout,
113
- proc: session.proc,
114
- });
104
+ session.dashboardHome = options.startPane === 'dashboard';
105
+ session.workspaceRoot = options.workspace ? options.workspace.root : '';
106
+ session.workspaceInfo = options.workspace ?? null;
107
+ };
108
+
109
+ const assignParts = (session, options) => {
110
+ const { stdin, stdout, proc } = session;
111
+ session.term = new Terminal({ stdin, stdout, proc });
115
112
  session.loader = new LoadCoordinator();
116
113
  session.collection = new ItemCollection();
117
114
  session.nav = new Navigation({ startPane: options.startPane ?? 'files' });
118
115
  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) => {
116
+ const tick = () => session.tickProgress();
117
+ session.progress = new Progress(session.term, tick, PROGRESS_MS);
118
+ session.updater = new UpdateCoordinator(session);
119
+ session.composer = new Composer(session);
135
120
  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);
121
+ session.commits = new CommitController(session);
122
+ session.gitBranches = new BranchController(session);
123
+ session.npm = new NpmController(session);
124
+ session.packages = new PackageController(session);
125
+ session.dashboard = new Dashboard(session);
126
+ session.workspace = new Workspace(session);
127
+ session.lifecycle = new Lifecycle(session);
140
128
  };
141
129
 
142
- const bindSessionAccessors = (session) => {
130
+ const bindFields = (session) => {
143
131
  bindAccessors(session, session.nav, NAV_FIELDS);
144
- bindAccessors(session, session.composer.state, COMPOSER_FIELDS);
132
+ bindAccessors(session, session.composer, COMPOSER_FIELDS);
145
133
  bindAccessors(session, session.loader, ['pendingExtras']);
146
134
  bindGetters(session, session.loader, ['didLoad']);
147
135
  bindAccessors(session, session.collection, ['items']);
@@ -150,76 +138,37 @@ const bindSessionAccessors = (session) => {
150
138
  bindAccessors(session, session.ops, ['gitBusy', 'busy']);
151
139
  bindAccessors(session, session.gitBranches, ['branches', 'dropName']);
152
140
  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
141
  };
207
142
 
208
143
  class Session {
209
144
  constructor(options) {
210
- composeSession(this, options);
145
+ assignOptions(this, options);
146
+ assignParts(this, options);
147
+ bindFields(this);
211
148
  this.resetState();
212
149
  }
213
150
 
151
+ get notes() {
152
+ return this.review.store;
153
+ }
154
+
155
+ getSize() {
156
+ const width = this.stdout.columns ?? 80;
157
+ const height = this.stdout.rows ?? 24;
158
+ return { width, height };
159
+ }
160
+
214
161
  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();
162
+ this.collection.reset();
163
+ this.nav.reset();
164
+ this.review.reset();
165
+ this.composer.reset();
166
+ this.ops.reset();
167
+ this.gitBranches.reset();
168
+ this.commits.reset();
169
+ this.npm.reset();
170
+ this.dashboard.reset();
171
+ this.workspace.reset();
223
172
  this.top = this.cwd;
224
173
  this.branch = '';
225
174
  this.status = '';
@@ -233,12 +182,12 @@ class Session {
233
182
  this.loadError = null;
234
183
  this.uiOpen = false;
235
184
  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();
185
+ this.lifecycle.stopWatch();
186
+ this.lifecycle.clearNpmExtras();
187
+ this.term.resetCache();
188
+ this.loader.reset();
189
+ this.updater.reset();
190
+ this.progress.clear();
242
191
  }
243
192
 
244
193
  ensureRepo() {
@@ -271,8 +220,7 @@ class Session {
271
220
  }
272
221
 
273
222
  paint() {
274
- if (!this.uiOpen) return;
275
- this.draw();
223
+ if (this.uiOpen) this.draw();
276
224
  }
277
225
 
278
226
  draw() {
@@ -294,6 +242,8 @@ class Session {
294
242
  const composing = this.mode === 'compose';
295
243
  this.term.paint(frame, size, composing ? frame.cursor : null);
296
244
  this.scheduleAutosave();
245
+ this.dashboard.afterDraw();
246
+ this.workspace.afterDraw();
297
247
  }
298
248
 
299
249
  scheduleAutosave() {
@@ -304,19 +254,11 @@ class Session {
304
254
  }
305
255
 
306
256
  view() {
307
- return this.viewer.view();
257
+ return view.view(this);
308
258
  }
309
259
 
310
260
  fileList() {
311
- return this.viewer.fileList();
312
- }
313
-
314
- counts() {
315
- return this.viewer.counts();
316
- }
317
-
318
- viewStatus() {
319
- return this.viewer.viewStatus();
261
+ return view.fileList(this);
320
262
  }
321
263
 
322
264
  current() {
@@ -324,31 +266,35 @@ class Session {
324
266
  }
325
267
 
326
268
  fileCursorEntry() {
327
- return this.changeActions.fileCursorEntry();
269
+ return filesPane.fileCursorEntry(this);
328
270
  }
329
271
 
330
272
  clampIndex() {
331
- this.filesPane.clampIndex();
273
+ filesPane.clampIndex(this);
332
274
  }
333
275
 
334
276
  syncReviewPath() {
335
- this.filesPane.syncReviewPath();
277
+ filesPane.syncReviewPath(this);
336
278
  }
337
279
 
338
280
  syncFileCursor() {
339
- this.filesPane.syncFileCursor();
281
+ filesPane.syncFileCursor(this);
340
282
  }
341
283
 
342
284
  showFiles() {
343
- this.filesPane.showFiles();
285
+ filesPane.showFiles(this);
344
286
  }
345
287
 
346
288
  dispatch(action) {
347
- this.dispatcher.dispatch(action);
289
+ dispatchAction(this, action);
290
+ }
291
+
292
+ confirmChoice(key) {
293
+ confirmChoice(this, key);
348
294
  }
349
295
 
350
296
  handleEvent(event) {
351
- this.input.handleEvent(event);
297
+ handleEvent(this, event);
352
298
  }
353
299
 
354
300
  pushInput(text) {
@@ -376,7 +322,7 @@ class Session {
376
322
  this.status = result.error.message;
377
323
  return false;
378
324
  }
379
- if (this.lifecycle) this.lifecycle.noteReviewStamp();
325
+ this.lifecycle.noteReviewStamp();
380
326
  return result.wrote;
381
327
  }
382
328
 
@@ -385,22 +331,11 @@ class Session {
385
331
  if (result.kind === 'commit') this.commits.finishGitCommit();
386
332
  if (result.kind === 'branch') this.gitBranches.finishCreateBranch();
387
333
  if (result.kind === 'npm') this.npm.finishEdit();
334
+ if (result.kind === 'package') this.packages.finishAdd();
388
335
  }
389
336
 
390
337
  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();
338
+ this.runComposeCommand(this.composer.templates.selectTemplate(index));
404
339
  }
405
340
 
406
341
  autosave() {
@@ -461,6 +396,7 @@ class Session {
461
396
  this.uiOpen = true;
462
397
  this.term.enter();
463
398
  try {
399
+ if (this.nav.pane === 'repos') this.workspace.start();
464
400
  this.openLoad();
465
401
  this.openUpdate();
466
402
  this.draw();
@@ -487,6 +423,8 @@ class Session {
487
423
  if (settled) return;
488
424
  settled = true;
489
425
  this.lifecycle.stopWatch();
426
+ this.dashboard.stop();
427
+ this.workspace.stop();
490
428
  this.term.stopListening();
491
429
  this.progress.clear();
492
430
  this.term.clearTimers();
@@ -503,16 +441,12 @@ class Session {
503
441
  if (this.done) return void finish();
504
442
  this.draw();
505
443
  };
506
- this.term.startListening({
507
- onData,
508
- onResize: () => this.draw(),
509
- });
444
+ this.term.startListening({ onData, onResize: () => this.draw() });
510
445
  this.lifecycle.startWatch();
446
+ if (this.dashboardHome) this.dashboard.start();
447
+ if (this.nav.pane === 'repos') this.workspace.start();
511
448
  });
512
449
  }
513
450
  }
514
451
 
515
- module.exports = {
516
- LEAVE_TERM,
517
- Session,
518
- };
452
+ 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,