Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
doctor.js344 linesDownload Raw Back to commands
1const cacache = require('cacache')
2const { access, lstat, readdir, constants: { R_OK, W_OK, X_OK } } = require('node:fs/promises')
3const npmFetch = require('make-fetch-happen')
4const which = require('which')
5const pacote = require('pacote')
6const { resolve } = require('node:path')
7const semver = require('semver')
8const { log, output } = require('proc-log')
9const ping = require('../utils/ping.js')
10const { defaults } = require('@npmcli/config/lib/definitions')
11const BaseCommand = require('../base-cmd.js')
12
13const maskLabel = mask => {
14  const label = []
15  if (mask & R_OK) {
16    label.push('readable')
17  }
18
19  if (mask & W_OK) {
20    label.push('writable')
21  }
22
23  if (mask & X_OK) {
24    label.push('executable')
25  }
26
27  return label.join(', ')
28}
29
30const checks = [
31  {
32    // Ping is left in as a legacy command but is listed as "connection" to make more sense to more people
33    groups: ['connection', 'ping', 'registry'],
34    title: 'Connecting to the registry',
35    cmd: 'checkPing',
36  }, {
37    groups: ['versions'],
38    title: 'Checking npm version',
39    cmd: 'getLatestNpmVersion',
40  }, {
41    groups: ['versions'],
42    title: 'Checking node version',
43    cmd: 'getLatestNodejsVersion',
44  }, {
45    groups: ['registry'],
46    title: 'Checking configured npm registry',
47    cmd: 'checkNpmRegistry',
48  }, {
49    groups: ['environment'],
50    title: 'Checking for git executable in PATH',
51    cmd: 'getGitPath',
52  }, {
53    groups: ['environment'],
54    title: 'Checking for global bin folder in PATH',
55    cmd: 'getBinPath',
56  }, {
57    groups: ['permissions', 'cache'],
58    title: 'Checking permissions on cached files (this may take awhile)',
59    cmd: 'checkCachePermission',
60    windows: false,
61  }, {
62    groups: ['permissions'],
63    title: 'Checking permissions on local node_modules (this may take awhile)',
64    cmd: 'checkLocalModulesPermission',
65    windows: false,
66  }, {
67    groups: ['permissions'],
68    title: 'Checking permissions on global node_modules (this may take awhile)',
69    cmd: 'checkGlobalModulesPermission',
70    windows: false,
71  }, {
72    groups: ['permissions'],
73    title: 'Checking permissions on local bin folder',
74    cmd: 'checkLocalBinPermission',
75    windows: false,
76  }, {
77    groups: ['permissions'],
78    title: 'Checking permissions on global bin folder',
79    cmd: 'checkGlobalBinPermission',
80    windows: false,
81  }, {
82    groups: ['cache'],
83    title: 'Verifying cache contents (this may take awhile)',
84    cmd: 'verifyCachedFiles',
85    windows: false,
86  },
87  // TODO:
88  // group === 'dependencies'?
89  //   - ensure arborist.loadActual() runs without errors and no invalid edges
90  //   - ensure package-lock.json matches loadActual()
91  //   - verify loadActual without hidden lock file matches hidden lockfile
92  // group === '???'
93  //   - verify all local packages have bins linked
94  // What is the fix for these?
95]
96
97class Doctor extends BaseCommand {
98  static description = 'Check the health of your npm environment'
99  static name = 'doctor'
100  static params = ['registry']
101  static ignoreImplicitWorkspace = false
102  static usage = [`[${checks.flatMap(s => s.groups)
103    .filter((value, index, self) => self.indexOf(value) === index && value !== 'ping')
104    .join('] [')}]`]
105
106  async exec (args) {
107    log.info('doctor', 'Running checkup')
108    let allOk = true
109
110    const actions = this.actions(args)
111
112    const chalk = this.npm.chalk
113    for (const { title, cmd } of actions) {
114      this.output(title)
115      // TODO when we have an in progress indicator that could go here
116      let result
117      try {
118        result = await this[cmd]()
119        this.output(`${chalk.green('Ok')}${result ? `\n${result}` : ''}\n`)
120      } catch (err) {
121        allOk = false
122        this.output(`${chalk.red('Not ok')}\n${chalk.cyan(err)}\n`)
123      }
124    }
125
126    if (!allOk) {
127      if (this.npm.silent) {
128        throw new Error('Some problems found. Check logs or disable silent mode for recommendations.')
129      } else {
130        throw new Error('Some problems found. See above for recommendations.')
131      }
132    }
133  }
134
135  async checkPing () {
136    log.info('doctor', 'Pinging registry')
137    try {
138      await ping({ ...this.npm.flatOptions, retry: false })
139      return ''
140    } catch (er) {
141      if (/^E\d{3}$/.test(er.code || '')) {
142        throw er.code.slice(1) + ' ' + er.message
143      } else {
144        throw er.message
145      }
146    }
147  }
148
149  async getLatestNpmVersion () {
150    log.info('doctor', 'Getting npm package information')
151    const latest = (await pacote.manifest('npm@latest', this.npm.flatOptions)).version
152    if (semver.gte(this.npm.version, latest)) {
153      return `current: v${this.npm.version}, latest: v${latest}`
154    } else {
155      throw `Use npm v${latest}`
156    }
157  }
158
159  async getLatestNodejsVersion () {
160    // XXX get the latest in the current major as well
161    const current = process.version
162    const currentRange = `^${current}`
163    const url = 'https://nodejs.org/dist/index.json'
164    log.info('doctor', 'Getting Node.js release information')
165    const res = await npmFetch(url, { method: 'GET', ...this.npm.flatOptions })
166    const data = await res.json()
167    let maxCurrent = '0.0.0'
168    let maxLTS = '0.0.0'
169    for (const { lts, version } of data) {
170      if (lts && semver.gt(version, maxLTS)) {
171        maxLTS = version
172      }
173
174      if (semver.satisfies(version, currentRange) && semver.gt(version, maxCurrent)) {
175        maxCurrent = version
176      }
177    }
178    const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS
179    if (semver.gte(process.version, recommended)) {
180      return `current: ${current}, recommended: ${recommended}`
181    } else {
182      throw `Use node ${recommended} (current: ${current})`
183    }
184  }
185
186  async getBinPath () {
187    log.info('doctor', 'getBinPath', 'Finding npm global bin in your PATH')
188    if (!process.env.PATH.includes(this.npm.globalBin)) {
189      throw new Error(`Add ${this.npm.globalBin} to your $PATH`)
190    }
191    return this.npm.globalBin
192  }
193
194  async checkCachePermission () {
195    return this.checkFilesPermission(this.npm.cache, true, R_OK)
196  }
197
198  async checkLocalModulesPermission () {
199    return this.checkFilesPermission(this.npm.localDir, true, R_OK | W_OK, true)
200  }
201
202  async checkGlobalModulesPermission () {
203    return this.checkFilesPermission(this.npm.globalDir, false, R_OK)
204  }
205
206  async checkLocalBinPermission () {
207    return this.checkFilesPermission(this.npm.localBin, false, R_OK | W_OK | X_OK, true)
208  }
209
210  async checkGlobalBinPermission () {
211    return this.checkFilesPermission(this.npm.globalBin, false, X_OK)
212  }
213
214  async checkFilesPermission (root, shouldOwn, mask, missingOk) {
215    let ok = true
216
217    try {
218      const uid = process.getuid()
219      const gid = process.getgid()
220      const files = new Set([root])
221      for (const f of files) {
222        const st = await lstat(f).catch(er => {
223          // if it can't be missing, or if it can and the error wasn't that it was missing
224          if (!missingOk || er.code !== 'ENOENT') {
225            ok = false
226            log.warn('doctor', 'checkFilesPermission', 'error getting info for ' + f)
227          }
228        })
229
230        if (!st) {
231          continue
232        }
233
234        if (shouldOwn && (uid !== st.uid || gid !== st.gid)) {
235          log.warn('doctor', 'checkFilesPermission', 'should be owner of ' + f)
236          ok = false
237        }
238
239        if (!st.isDirectory() && !st.isFile()) {
240          continue
241        }
242
243        try {
244          await access(f, mask)
245        } catch {
246          ok = false
247          const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})`
248          log.error('doctor', 'checkFilesPermission', msg)
249          continue
250        }
251
252        if (st.isDirectory()) {
253          const entries = await readdir(f).catch(() => {
254            ok = false
255            log.warn('doctor', 'checkFilesPermission', 'error reading directory ' + f)
256            return []
257          })
258          for (const entry of entries) {
259            files.add(resolve(f, entry))
260          }
261        }
262      }
263    } finally {
264      if (!ok) {
265        throw (
266          `Check the permissions of files in ${root}` +
267          (shouldOwn ? ' (should be owned by current user)' : '')
268        )
269      } else {
270        return ''
271      }
272    }
273  }
274
275  async getGitPath () {
276    log.info('doctor', 'Finding git in your PATH')
277    return await which('git').catch(er => {
278      log.warn('doctor', 'getGitPath', er)
279      throw new Error("Install git and ensure it's in your PATH.")
280    })
281  }
282
283  async verifyCachedFiles () {
284    log.info('doctor', 'verifyCachedFiles', 'Verifying the npm cache')
285
286    const stats = await cacache.verify(this.npm.flatOptions.cache)
287    const { badContentCount, reclaimedCount, missingContent, reclaimedSize } = stats
288    if (badContentCount || reclaimedCount || missingContent) {
289      if (badContentCount) {
290        log.warn('doctor', 'verifyCachedFiles', `Corrupted content removed: ${badContentCount}`)
291      }
292
293      if (reclaimedCount) {
294        log.warn(
295          'doctor',
296          'verifyCachedFiles',
297          `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`
298        )
299      }
300
301      if (missingContent) {
302        log.warn('doctor', 'verifyCachedFiles', `Missing content: ${missingContent}`)
303      }
304
305      log.warn('doctor', 'verifyCachedFiles', 'Cache issues have been fixed')
306    }
307    log.info(
308      'doctor',
309      'verifyCachedFiles',
310        `Verification complete. Stats: ${JSON.stringify(stats, null, 2)}`
311    )
312    return `verified ${stats.verifiedContent} tarballs`
313  }
314
315  async checkNpmRegistry () {
316    if (this.npm.flatOptions.registry !== defaults.registry) {
317      throw `Try \`npm config set registry=${defaults.registry}\``
318    } else {
319      return `using default registry (${defaults.registry})`
320    }
321  }
322
323  output (...args) {
324    // TODO display layer should do this
325    if (!this.npm.silent) {
326      output.standard(...args)
327    }
328  }
329
330  actions (params) {
331    return checks.filter(subcmd => {
332      if (process.platform === 'win32' && subcmd.windows === false) {
333        return false
334      }
335      if (params.length) {
336        return params.some(param => subcmd.groups.includes(param))
337      }
338      return true
339    })
340  }
341}
342
343module.exports = Doctor
344 
codekingpro/portable-devtools · Team Ai