reslop 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +27 -38
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +6 -2
  5. package/lib/api.js +2 -2
  6. package/lib/capabilities.js +10 -14
  7. package/lib/cli.js +28 -47
  8. package/lib/clipboard.js +1 -1
  9. package/lib/deps-diff.js +712 -0
  10. package/lib/deps.js +556 -32
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +34 -8
  20. package/lib/{git/branches.js → git-branches.js} +103 -10
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git.js +592 -65
  23. package/lib/github.js +28 -197
  24. package/lib/gitlab.js +24 -195
  25. package/lib/highlight/bash.js +1 -3
  26. package/lib/highlight/css.js +1 -3
  27. package/lib/highlight/js.js +11 -14
  28. package/lib/highlight.js +6 -9
  29. package/lib/keys.js +22 -9
  30. package/lib/manifest.js +330 -0
  31. package/lib/remote.js +374 -0
  32. package/lib/render/branches.js +34 -66
  33. package/lib/render/chrome.js +57 -22
  34. package/lib/render/commits.js +85 -0
  35. package/lib/render/diff.js +16 -16
  36. package/lib/render/files.js +29 -37
  37. package/lib/render/frame.js +29 -13
  38. package/lib/render/notes.js +4 -3
  39. package/lib/render/primitives.js +46 -1
  40. package/lib/{render.js → render/render.js} +6 -7
  41. package/lib/render/unit.js +41 -0
  42. package/lib/review.js +750 -14
  43. package/lib/select.js +5 -10
  44. package/lib/session/accessors.js +9 -1
  45. package/lib/session/actions.js +93 -36
  46. package/lib/session/branches.js +130 -144
  47. package/lib/session/change-actions.js +206 -124
  48. package/lib/session/commit.js +217 -62
  49. package/lib/session/compose.js +158 -13
  50. package/lib/session/dispatch.js +109 -55
  51. package/lib/session/files-pane.js +145 -142
  52. package/lib/session/items.js +131 -34
  53. package/lib/session/lifecycle.js +82 -42
  54. package/lib/session/load.js +19 -33
  55. package/lib/session/navigation.js +15 -3
  56. package/lib/session/ops.js +28 -41
  57. package/lib/session/pointer.js +58 -54
  58. package/lib/session/review.js +23 -37
  59. package/lib/session/terminal.js +6 -12
  60. package/lib/session/todos.js +8 -12
  61. package/lib/session/unit.js +208 -0
  62. package/lib/session/update.js +33 -46
  63. package/lib/session/view.js +82 -11
  64. package/lib/session/watch.js +11 -19
  65. package/lib/session.js +29 -226
  66. package/lib/source.js +3 -3
  67. package/lib/update.js +40 -75
  68. package/lib/{git/proc.js → utilities.js} +75 -11
  69. package/lib/wrap.js +1 -3
  70. package/package.json +2 -2
  71. package/lib/deps/changes.js +0 -331
  72. package/lib/deps/items.js +0 -131
  73. package/lib/deps/manifest.js +0 -145
  74. package/lib/deps/merge.js +0 -120
  75. package/lib/deps/proposals.js +0 -164
  76. package/lib/deps/reports.js +0 -180
  77. package/lib/deps/rows.js +0 -177
  78. package/lib/deps/scan.js +0 -138
  79. package/lib/deps/usage.js +0 -68
  80. package/lib/deps/versions.js +0 -119
  81. package/lib/diff.js +0 -33
  82. package/lib/git/changes.js +0 -164
  83. package/lib/git/read.js +0 -253
  84. package/lib/git/run.js +0 -100
  85. package/lib/npm/commands.js +0 -100
  86. package/lib/remote/changes.js +0 -37
  87. package/lib/remote/headers.js +0 -36
  88. package/lib/remote/http.js +0 -122
  89. package/lib/remote/notes.js +0 -94
  90. package/lib/review/format.js +0 -434
  91. package/lib/review/storage.js +0 -150
  92. package/lib/review/store.js +0 -178
  93. package/lib/review/templates.js +0 -49
  94. package/lib/session/origins.js +0 -7
  95. package/lib/sys.js +0 -84
package/lib/session.js CHANGED
@@ -1,16 +1,15 @@
1
1
  'use strict';
2
2
 
3
- const render = require('./render.js');
3
+ const render = require('./render/render.js');
4
4
  const keys = require('./keys.js');
5
5
  const review = require('./review.js');
6
- const { copyText } = require('./clipboard.js');
7
6
  const files = require('./files.js');
8
7
  const { makeTodoItem } = files;
9
8
  const { renderFrame } = render;
10
9
  const { decodeChunk } = keys;
11
10
  const { AUTOSAVE_MS, hasNotes } = review;
12
11
  const capabilities = require('./capabilities.js');
13
- const { sessionCapabilities, createReviewStorage } = capabilities;
12
+ const { sessionCapabilities } = capabilities;
14
13
  const { createTerminal, LEAVE_TERM } = require('./session/terminal.js');
15
14
  const { createLoadCoordinator } = require('./session/load.js');
16
15
  const items = require('./session/items.js');
@@ -35,16 +34,17 @@ const { createDispatcher, createInput } = require('./session/dispatch.js');
35
34
  const CURSOR_BLINK_MS = 500;
36
35
  const PROGRESS_MS = 80;
37
36
 
38
- const systemNow = () => new Date();
39
-
40
37
  const NAV_FIELDS = [
41
38
  'pane',
42
39
  'index',
43
40
  'fileCursor',
44
41
  'branchCursor',
42
+ 'commitCursor',
45
43
  'scroll',
46
44
  'listScroll',
47
45
  'reviewPath',
46
+ 'fileScope',
47
+ 'unitLine',
48
48
  'selection',
49
49
  'mouseAnchor',
50
50
  'pendingClick',
@@ -71,51 +71,34 @@ const assignOptions = (session, options) => {
71
71
  session.stdout = options.stdout;
72
72
  session.proc = options.proc ?? process;
73
73
  session.color = options.color ?? true;
74
- session.copyText = options.copyText ?? copyText;
75
- const termSize = () => {
74
+ session.getSize = () => {
76
75
  const width = session.stdout.columns ?? 80;
77
76
  const height = session.stdout.rows ?? 24;
78
77
  return { width, height };
79
78
  };
80
- session.getSize = options.getSize ?? termSize;
81
79
  session.rev = options.rev ?? null;
82
80
  session.revShort = '';
83
81
  session.sourceLabel = options.sourceLabel ?? '';
84
82
  session.repoName = options.repoName ?? '';
85
83
  session.change = options.change ?? null;
86
- session.now = options.now ?? systemNow;
87
84
  session.forceNewReview = options.newReview === true;
88
85
  session.readOnly = options.readOnly === true;
89
86
  session.capabilities = sessionCapabilities(options);
90
- const storage = options.reviewStorage ?? createReviewStorage(options);
91
87
  session.audit = options.audit === true;
92
88
  session.outdatedMap = options.outdatedMap;
93
89
  session.auditMap = options.auditMap;
94
- session.setInterval = options.setInterval ?? setInterval;
95
- session.clearInterval = options.clearInterval ?? clearInterval;
96
- session.watchOpts = {
97
- watch: options.watch,
98
- readdirSync: options.watchReaddir,
99
- statSync: options.watchStat,
100
- setTimeout: options.setTimeout ?? setTimeout,
101
- clearTimeout: options.clearTimeout ?? clearTimeout,
102
- debounceMs: options.watchDebounceMs,
103
- recursive: options.watchRecursive,
104
- };
105
90
  session.updateOpts = options.update;
106
91
  session.term = createTerminal({
107
92
  stdin: session.stdin,
108
93
  stdout: session.stdout,
109
94
  proc: session.proc,
110
- setInterval: session.setInterval,
111
- clearInterval: session.clearInterval,
112
95
  });
113
96
  session.loader = createLoadCoordinator();
114
97
  session.collection = createItemCollection();
115
98
  session.nav = createNavigation({
116
99
  startPane: options.startPane ?? 'files',
117
100
  });
118
- session.review = createReviewController({ storage });
101
+ session.review = createReviewController();
119
102
  session.progress = createProgress(
120
103
  session.term,
121
104
  () => session.tickProgress(),
@@ -123,28 +106,6 @@ const assignOptions = (session, options) => {
123
106
  );
124
107
  };
125
108
 
126
- const assignUpdater = (session) => {
127
- session.updater = createUpdateCoordinator({
128
- updateOpts: () => session.updateOpts,
129
- didLoad: () => session.loader.didLoad,
130
- getMode: () => session.mode,
131
- setMode: (mode) => {
132
- session.mode = mode;
133
- },
134
- isDone: () => session.done,
135
- uiOpen: () => session.uiOpen,
136
- paint: () => session.paint(),
137
- setStatus: (value) => {
138
- session.status = value;
139
- },
140
- startInstallProgress: () => {
141
- session.progressFrame = 0;
142
- session.progress.start('install');
143
- },
144
- stopInstallProgress: () => session.progress.stop('install'),
145
- });
146
- };
147
-
148
109
  const assignComposer = (session) => {
149
110
  session.composer = createComposer({
150
111
  nav: session.nav,
@@ -162,73 +123,10 @@ const assignComposer = (session) => {
162
123
  };
163
124
 
164
125
  const assignControllers = (session) => {
165
- const setStatus = (value) => {
166
- session.status = value;
167
- };
168
- const setMode = (mode) => {
169
- session.mode = mode;
170
- };
171
- session.ops = createOpsRunner({
172
- top: () => session.top,
173
- isDone: () => session.done,
174
- getMode: () => session.mode,
175
- setMode,
176
- getStatus: () => session.status,
177
- setStatus,
178
- paint: () => session.paint(),
179
- startProgress: () => session.startProgress(),
180
- stopProgress: () => session.stopProgress(),
181
- resetProgressFrame: () => {
182
- session.progressFrame = 0;
183
- },
184
- });
185
- session.changeActions = createChangeActions({
186
- repo: () => session.repo,
187
- top: () => session.top,
188
- capabilities: () => session.capabilities,
189
- uiOpen: () => session.uiOpen,
190
- collection: session.collection,
191
- getItems: () => session.items,
192
- currentItem: () => session.current(),
193
- nav: session.nav,
194
- fileList: () => session.fileList(),
195
- runBusy: session.ops.runBusy,
196
- reloadAfterChange: (afterLoad) => session.reloadAfterChange(afterLoad),
197
- setStatus,
198
- syncReviewPath: () => session.syncReviewPath(),
199
- syncFileCursor: () => session.syncFileCursor(),
200
- });
201
- session.commits = createCommitController({
202
- repo: () => session.repo,
203
- top: () => session.top,
204
- capabilities: () => session.capabilities,
205
- getItems: () => session.items,
206
- uiOpen: () => session.uiOpen,
207
- composer: session.composer,
208
- collection: session.collection,
209
- runner: session.ops,
210
- refreshFromRepo: (opts) => session.refreshFromRepo(opts),
211
- setMode,
212
- setStatus,
213
- isDone: () => session.done,
214
- pendingExtras: () => session.pendingExtras,
215
- });
216
- session.gitBranches = createBranchController({
217
- repo: () => session.repo,
218
- top: () => session.top,
219
- capabilities: () => session.capabilities,
220
- uiOpen: () => session.uiOpen,
221
- composer: session.composer,
222
- nav: session.nav,
223
- runner: session.ops,
224
- canCommit: session.commits.canCommit,
225
- refreshFromRepo: (opts) => session.refreshFromRepo(opts),
226
- showFiles: () => session.showFiles(),
227
- getMode: () => session.mode,
228
- setMode,
229
- getStatus: () => session.status,
230
- setStatus,
231
- });
126
+ session.ops = createOpsRunner(session);
127
+ session.changeActions = createChangeActions(session);
128
+ session.commits = createCommitController(session);
129
+ session.gitBranches = createBranchController(session);
232
130
  };
233
131
 
234
132
  const bindSessionAccessors = (session) => {
@@ -286,33 +184,12 @@ const bindViewer = (session) => {
286
184
  collection: session.collection,
287
185
  review: session.review,
288
186
  gitBranches: session.gitBranches,
187
+ commits: session.commits,
289
188
  updater: session.updater,
290
189
  ui: session,
291
190
  });
292
191
  };
293
192
 
294
- const bindFilesPane = (session) => {
295
- session.filesPane = createFilesPane({
296
- nav: session.nav,
297
- composer: session.composer,
298
- items: () => session.items,
299
- todoItem: () => session.todoItem,
300
- fileList: () => session.fileList(),
301
- setStatus: (value) => {
302
- session.status = value;
303
- },
304
- onQuit: () => session.onQuit(),
305
- onCheckoutBranch: session.gitBranches.onCheckoutBranch,
306
- onBranchMove: session.gitBranches.onBranchMove,
307
- layout: () => session.layout,
308
- setLayout: (value) => {
309
- session.layout = value;
310
- },
311
- lastFrame: () => session.lastFrame,
312
- getSize: () => session.lastSize ?? session.getSize(),
313
- });
314
- };
315
-
316
193
  const bindLifecycle = (session) => {
317
194
  session.lifecycle = createLifecycle({
318
195
  loader: session.loader,
@@ -329,104 +206,24 @@ const bindLifecycle = (session) => {
329
206
  outdatedMap: session.outdatedMap,
330
207
  auditMap: session.auditMap,
331
208
  },
332
- watch: session.watchOpts,
333
209
  ui: session,
334
210
  });
335
211
  };
336
212
 
337
213
  const bindDispatch = (session) => {
338
- const filesPane = session.filesPane;
339
- session.dispatcher = createDispatcher({
340
- getMode: () => session.mode,
341
- getPane: () => session.pane,
342
- currentTarget: () => {
343
- if (session.pane === 'files') return session.fileCursorEntry();
344
- if (session.pane === 'branches') {
345
- return session.branches[session.branchCursor];
346
- }
347
- return session.current();
348
- },
349
- composer: session.composer,
350
- changeActions: session.changeActions,
351
- commits: session.commits,
352
- gitBranches: session.gitBranches,
353
- onFileMove: filesPane.onFileMove,
354
- onBranchMove: session.gitBranches.onBranchMove,
355
- onNext: filesPane.onNext,
356
- onPrev: filesPane.onPrev,
357
- onOpenFile: filesPane.onOpenFile,
358
- onFiles: filesPane.onFiles,
359
- onQuit: () => session.onQuit(),
360
- onLayout: filesPane.onLayout,
361
- onReload: session.lifecycle.onReload,
362
- onScroll: filesPane.onScroll,
363
- onHome: filesPane.onHome,
364
- onEnd: filesPane.onEnd,
365
- scrollStep: filesPane.scrollStep,
366
- });
367
- session.pointer = createPointer({
368
- nav: session.nav,
369
- getSize: () => session.lastSize ?? session.getSize(),
370
- lastFrame: () => session.lastFrame,
371
- copyText: (text, stdout) => session.copyText(text, stdout),
372
- stdout: () => session.stdout,
373
- setStatus: (value) => {
374
- session.status = value;
375
- },
376
- dispatch: (action) => session.dispatcher.dispatch(action),
377
- getPane: () => session.pane,
378
- getMode: () => session.mode,
379
- confirmChoice: (key) => session.input.confirmChoice(key),
380
- onBranch: session.gitBranches.onBranch,
381
- onOpenFile: filesPane.onOpenFile,
382
- onCheckoutBranch: session.gitBranches.onCheckoutBranch,
383
- onTodoHit: (cursor) => session.composer.onTodoHit(cursor),
384
- selectTemplate: (index) => session.selectTemplate(index),
385
- onScroll: filesPane.onScroll,
386
- });
387
- session.input = createInput({
388
- getMode: () => session.mode,
389
- getPane: () => session.pane,
390
- composer: session.composer,
391
- pointer: session.pointer,
392
- dispatch: (action) => session.dispatcher.dispatch(action),
393
- onEscape: filesPane.onEscape,
394
- onQuit: () => session.onQuit(),
395
- runComposeCommand: (result) => session.runComposeCommand(result),
396
- quit: {
397
- ready: () => session.finishQuit('ready'),
398
- editing: () => session.finishQuit('editing'),
399
- cancel: () => session.cancelQuit(),
400
- },
401
- commit: {
402
- commit: () => session.commits.startGitCommit('commit'),
403
- amend: () => session.commits.startGitCommit('amend'),
404
- fixup: () => session.commits.startGitCommit('fixup'),
405
- cancel: session.commits.cancelCommit,
406
- },
407
- update: {
408
- accept: session.updater.accept,
409
- decline: session.updater.decline,
410
- },
411
- drop: {
412
- confirm: session.gitBranches.confirmDropBranch,
413
- cancel: session.gitBranches.cancelDropBranch,
414
- },
415
- push: {
416
- force: session.gitBranches.onForcePush,
417
- cancel: session.gitBranches.cancelPush,
418
- },
419
- });
214
+ session.dispatcher = createDispatcher(session);
215
+ session.pointer = createPointer(session);
216
+ session.input = createInput(session);
420
217
  };
421
218
 
422
219
  const composeSession = (session, options) => {
423
220
  assignOptions(session, options);
424
- assignUpdater(session);
221
+ session.updater = createUpdateCoordinator(session);
425
222
  assignComposer(session);
426
223
  assignControllers(session);
427
224
  bindSessionAccessors(session);
428
225
  bindViewer(session);
429
- bindFilesPane(session);
226
+ session.filesPane = createFilesPane(session);
430
227
  bindLifecycle(session);
431
228
  bindDispatch(session);
432
229
  };
@@ -444,6 +241,7 @@ class Session {
444
241
  if (this.composer) this.composer.reset();
445
242
  if (this.ops) this.ops.reset();
446
243
  if (this.gitBranches) this.gitBranches.reset();
244
+ if (this.commits) this.commits.reset();
447
245
  this.top = this.cwd;
448
246
  this.branch = '';
449
247
  this.status = '';
@@ -490,6 +288,10 @@ class Session {
490
288
  this.lifecycle.reloadAfterChange(afterLoad);
491
289
  }
492
290
 
291
+ ignoreWatch(ms) {
292
+ this.lifecycle.ignoreWatch(ms);
293
+ }
294
+
493
295
  applyLoaded(loaded) {
494
296
  return this.lifecycle.applyLoaded(loaded);
495
297
  }
@@ -507,7 +309,11 @@ class Session {
507
309
  height: size.height,
508
310
  color: this.color,
509
311
  });
510
- if (this.pane === 'files' || this.pane === 'branches') {
312
+ if (
313
+ this.pane === 'files' ||
314
+ this.pane === 'branches' ||
315
+ this.pane === 'commits'
316
+ ) {
511
317
  this.nav.listScroll = frame.listScroll;
512
318
  }
513
319
  const composing = this.mode === 'compose';
@@ -577,7 +383,7 @@ class Session {
577
383
  }
578
384
 
579
385
  initReview() {
580
- this.review.init(this.top, this.now(), {
386
+ this.review.init(this.top, new Date(), {
581
387
  forceNew: this.forceNewReview,
582
388
  });
583
389
  }
@@ -714,10 +520,7 @@ class Session {
714
520
  }
715
521
  const onData = (chunk) => {
716
522
  this.pushInput(chunk.toString('utf8'));
717
- if (this.done) {
718
- finish();
719
- return;
720
- }
523
+ if (this.done) return void finish();
721
524
  this.draw();
722
525
  };
723
526
  this.term.startListening({
package/lib/source.js CHANGED
@@ -8,13 +8,13 @@ const selectChangeSource = (paths) => {
8
8
  if (!paths.length) return { kind: 'local', paths };
9
9
  const first = paths[0];
10
10
  const pr = parseGithubPrUrl(first);
11
- if (pr) return { kind: 'github-pr', pr, paths: paths.slice(1) };
11
+ if (pr) return { kind: 'pr', pr, paths: paths.slice(1) };
12
12
  const mr = parseGitlabMrUrl(first);
13
- if (mr) return { kind: 'gitlab-mr', mr, paths: paths.slice(1) };
13
+ if (mr) return { kind: 'mr', mr, paths: paths.slice(1) };
14
14
  return { kind: 'local', paths };
15
15
  };
16
16
 
17
- const createLoadedSource = (loaded, kind = 'github-pr', extra = {}) => {
17
+ const createLoadedSource = (loaded, kind = 'pr', extra = {}) => {
18
18
  const load = () => {
19
19
  const items = loaded.items ?? [];
20
20
  return { ...loaded, items: [...items] };
package/lib/update.js CHANGED
@@ -1,11 +1,18 @@
1
1
  'use strict';
2
2
 
3
- const { spawn } = require('node:child_process');
4
3
  const fs = require('node:fs');
5
4
  const os = require('node:os');
6
5
  const path = require('node:path');
7
6
 
8
- const { npmBin, npmOpts } = require('./sys.js');
7
+ const { jsonParse, isHashObject } = require('metautil');
8
+ const {
9
+ parseVersion,
10
+ cmpVersion,
11
+ npmBin,
12
+ npmOpts,
13
+ runProc,
14
+ oneLine,
15
+ } = require('./utilities.js');
9
16
 
10
17
  const PKG = require('../package.json');
11
18
 
@@ -29,45 +36,28 @@ const asText = (value) => (typeof value === 'string' ? value : '');
29
36
 
30
37
  const toInt = (text) => parseInt(text, 10);
31
38
 
32
- const parseVersion = (text) => {
33
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(`${text ?? ''}`);
34
- if (!match) return null;
35
- return {
36
- major: toInt(match[1]),
37
- minor: toInt(match[2]),
38
- patch: toInt(match[3]),
39
- };
40
- };
41
-
42
- const cmpVersion = (left, right) => {
43
- if (left.major !== right.major) return left.major - right.major;
44
- if (left.minor !== right.minor) return left.minor - right.minor;
45
- return left.patch - right.patch;
46
- };
47
-
48
39
  const isNpmInstall = (pkgDir) => {
49
40
  const parent = path.basename(path.dirname(pkgDir));
50
41
  return parent === 'node_modules';
51
42
  };
52
43
 
53
- const cacheFile = (env = process.env, extra = {}) => {
54
- const home = extra.home ?? os.homedir();
55
- const platform = extra.platform ?? process.platform;
44
+ const cacheFile = (env = process.env) => {
56
45
  if (env.XDG_CACHE_HOME) {
57
46
  return path.join(env.XDG_CACHE_HOME, 'reslop', 'update.json');
58
47
  }
59
- if (platform === 'win32') {
60
- const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
48
+ if (process.platform === 'win32') {
49
+ const local =
50
+ env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
61
51
  return path.join(local, 'reslop', 'update.json');
62
52
  }
63
- return path.join(home, '.cache', 'reslop', 'update.json');
53
+ return path.join(os.homedir(), '.cache', 'reslop', 'update.json');
64
54
  };
65
55
 
66
- const readCache = (file, readFileSync = fs.readFileSync) => {
56
+ const readCache = (file) => {
67
57
  try {
68
- const text = readFileSync(file, 'utf8');
69
- const data = JSON.parse(text);
70
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
58
+ const text = fs.readFileSync(file, 'utf8');
59
+ const data = jsonParse(text);
60
+ if (!isHashObject(data)) {
71
61
  return emptyCache();
72
62
  }
73
63
  const checkedAt = toInt(data.checkedAt) || 0;
@@ -80,17 +70,15 @@ const readCache = (file, readFileSync = fs.readFileSync) => {
80
70
  }
81
71
  };
82
72
 
83
- const writeCache = (file, data, extra = {}) => {
84
- const mkdirSync = extra.mkdirSync ?? fs.mkdirSync;
85
- const writeFileSync = extra.writeFileSync ?? fs.writeFileSync;
73
+ const writeCache = (file, data) => {
86
74
  try {
87
- mkdirSync(path.dirname(file), { recursive: true });
75
+ fs.mkdirSync(path.dirname(file), { recursive: true });
88
76
  const checkedAt = data.checkedAt ?? 0;
89
77
  const compatible = data.compatible ?? '';
90
78
  const major = data.major ?? '';
91
79
  const skipped = data.skipped ?? '';
92
80
  const body = JSON.stringify({ checkedAt, compatible, major, skipped });
93
- writeFileSync(file, `${body}\n`);
81
+ fs.writeFileSync(file, `${body}\n`);
94
82
  } catch {
95
83
  // ignore cache write errors
96
84
  }
@@ -106,7 +94,7 @@ const listVersions = (data) => {
106
94
  if (!data || typeof data !== 'object') return [];
107
95
  const names = [];
108
96
  const map = data.versions;
109
- if (map && typeof map === 'object' && !Array.isArray(map)) {
97
+ if (isHashObject(map)) {
110
98
  for (const name of Object.keys(map)) names.push(name);
111
99
  }
112
100
  const tags = data['dist-tags'];
@@ -187,12 +175,6 @@ const isEnabled = (options = {}) => {
187
175
  return isNpmInstall(pkgDir);
188
176
  };
189
177
 
190
- const nowMs = (options = {}) => {
191
- if (typeof options.now === 'function') return options.now();
192
- if (typeof options.now === 'number') return options.now;
193
- return Date.now();
194
- };
195
-
196
178
  const fetchTargets = async (current, options = {}) => {
197
179
  const empty = { compatible: '', major: '' };
198
180
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -207,33 +189,18 @@ const fetchTargets = async (current, options = {}) => {
207
189
  return pickTargets(current, listVersions(data));
208
190
  };
209
191
 
210
- const npmInstall = (spec, extra = {}) =>
211
- new Promise((resolve, reject) => {
212
- const stdio = ['ignore', 'ignore', 'pipe'];
213
- const child = spawn(npmBin(), ['install', '-g', spec], npmOpts({ stdio }));
214
- let stderr = '';
215
- if (child.stderr) {
216
- child.stderr.setEncoding('utf8');
217
- child.stderr.on('data', (chunk) => {
218
- stderr += chunk;
219
- });
220
- }
221
- const timeout = extra.timeout ?? INSTALL_MS;
222
- const timer = setTimeout(() => {
223
- child.kill();
224
- reject(new Error('timed out'));
225
- }, timeout);
226
- child.on('error', (error) => {
227
- clearTimeout(timer);
228
- reject(error);
229
- });
230
- child.on('close', (status) => {
231
- clearTimeout(timer);
232
- if (status === 0) return void resolve();
233
- const line = stderr.trim().split('\n')[0];
234
- reject(new Error(line || 'npm install failed'));
235
- });
236
- });
192
+ const npmInstall = async (spec, extra = {}) => {
193
+ const timeout = extra.timeout ?? INSTALL_MS;
194
+ const result = await runProc(
195
+ npmBin(),
196
+ ['install', '-g', spec],
197
+ npmOpts({ timeout }),
198
+ );
199
+ if (result.error) throw result.error;
200
+ if (result.status !== 0) {
201
+ throw new Error(oneLine(result.stderr, 'npm install failed'));
202
+ }
203
+ };
237
204
 
238
205
  const installUpdate = async (version, options = {}) => {
239
206
  if (typeof options.install === 'function') {
@@ -246,19 +213,17 @@ const installUpdate = async (version, options = {}) => {
246
213
 
247
214
  const markSkipped = (version, options = {}) => {
248
215
  const file = options.cacheFile ?? cacheFile(options.env);
249
- const readFileSync = options.readFileSync ?? fs.readFileSync;
250
- const cache = readCache(file, readFileSync);
251
- writeCache(file, { ...cache, skipped: version }, options);
216
+ const cache = readCache(file);
217
+ writeCache(file, { ...cache, skipped: version });
252
218
  };
253
219
 
254
220
  const checkUpdate = async (options = {}) => {
255
221
  if (!isEnabled(options)) return { action: 'skip' };
256
- const now = nowMs(options);
222
+ const now = Date.now();
257
223
  const current = options.current ?? PKG_VERSION;
258
224
  const file = options.cacheFile ?? cacheFile(options.env);
259
- const readFileSync = options.readFileSync ?? fs.readFileSync;
260
225
  const interval = options.interval ?? CHECK_INTERVAL_MS;
261
- const cache = readCache(file, readFileSync);
226
+ const cache = readCache(file);
262
227
  if (isFresh(cache, now, interval)) {
263
228
  const plan = planUpdate(current, cache);
264
229
  if (plan.action === 'install') return { action: 'none' };
@@ -277,10 +242,10 @@ const checkUpdate = async (options = {}) => {
277
242
  major: targets.major,
278
243
  };
279
244
  if (!targets.compatible && !targets.major) {
280
- writeCache(file, { ...cache, checkedAt: now }, options);
245
+ writeCache(file, { ...cache, checkedAt: now });
281
246
  return { action: 'none' };
282
247
  }
283
- writeCache(file, next, options);
248
+ writeCache(file, next);
284
249
  return planUpdate(current, next);
285
250
  };
286
251