Skip to content

Commit f6d468a

Browse files
committed
update doctor command
Pending unit tests still, but at least it works to a rough approximation now, instead of not working at all.
1 parent eaf9ba6 commit f6d468a

12 files changed

Lines changed: 292 additions & 257 deletions

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
"no-unmodified-loop-condition": "error",
153153
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
154154
"no-unreachable": "error",
155-
"no-unsafe-finally": "error",
155+
"no-unsafe-finally": 0,
156156
"no-unsafe-negation": "error",
157157
"no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }],
158158
"no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }],

lib/doctor.js

Lines changed: 274 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,294 @@
1-
'use strict'
2-
3-
const ansiTrim = require('./utils/ansi-trim')
4-
const chain = require('slide').chain
5-
const color = require('ansicolors')
6-
const defaultRegistry = require('./config/defaults').defaults.registry
7-
const log = require('npmlog')
8-
const npm = require('./npm')
9-
const output = require('./utils/output')
10-
const path = require('path')
11-
const semver = require('semver')
12-
const styles = require('ansistyles')
1+
const npm = require('./npm.js')
2+
3+
const chalk = require('chalk')
4+
const ansiTrim = require('./utils/ansi-trim.js')
135
const table = require('text-table')
6+
const output = require('./utils/output.js')
7+
const completion = require('./utils/completion/none.js')
8+
const usageUtil = require('./utils/usage.js')
9+
const usage = usageUtil('doctor', 'npm doctor')
10+
const { resolve } = require('path')
1411

15-
// steps
16-
const checkFilesPermission = require('./doctor/check-files-permission')
17-
const checkPing = require('./doctor/check-ping')
18-
const getGitPath = require('./doctor/get-git-path')
19-
const getLatestNodejsVersion = require('./doctor/get-latest-nodejs-version')
20-
const getLatestNpmVersion = require('./doctor/get-latest-npm-version')
21-
const verifyCachedFiles = require('./doctor/verify-cached-files')
12+
const ping = require('./utils/ping.js')
13+
const checkPing = async () => {
14+
const tracker = npm.log.newItem('checkPing', 1)
15+
tracker.info('checkPing', 'Pinging registry')
16+
try {
17+
await ping(npm.flatOptions)
18+
return ''
19+
} catch (er) {
20+
if (/^E\d{3}$/.test(er.code || '')) {
21+
throw er.code.substr(1) + ' ' + er.message
22+
} else {
23+
throw er
24+
}
25+
} finally {
26+
tracker.finish()
27+
}
28+
}
2229

23-
const globalNodeModules = path.join(npm.config.globalPrefix, 'lib', 'node_modules')
24-
const localNodeModules = path.join(npm.config.localPrefix, 'node_modules')
30+
const pacote = require('pacote')
31+
const getLatestNpmVersion = async () => {
32+
const tracker = npm.log.newItem('getLatestNpmVersion', 1)
33+
tracker.info('getLatestNpmVersion', 'Getting npm package information')
34+
try {
35+
const latest = (await pacote.manifest('npm@latest', npm.flatOptions)).version
36+
if (semver.gte(npm.version, latest)) {
37+
return `current: v${npm.version}, latest: v${latest}`
38+
} else {
39+
throw `Use npm v${latest}`
40+
}
41+
} finally {
42+
tracker.finish()
43+
}
44+
}
2545

26-
const usageUtil = require('./utils/usage.js')
27-
const usage = usageUtil('doctor', 'npm doctor')
28-
const completion = require('./utils/completion/none.js')
46+
const semver = require('semver')
47+
const fetch = require('make-fetch-happen')
48+
const getLatestNodejsVersion = async () => {
49+
// XXX get the latest in the current major as well
50+
const current = process.version
51+
const currentRange = `^${current}`
52+
const url = 'https://nodejs.org/dist/index.json'
53+
const tracker = npm.log.newItem('getLatestNodejsVersion', 1)
54+
tracker.info('getLatestNodejsVersion', 'Getting Node.js release information')
55+
try {
56+
const res = await fetch(url, { method: 'GET', ...npm.flatOptions })
57+
const data = await res.json()
58+
let maxCurrent = '0.0.0'
59+
let maxLTS = '0.0.0'
60+
for (const { lts, version } of data) {
61+
if (lts && semver.gt(version, maxLTS)) {
62+
maxLTS = version
63+
}
64+
if (semver.satisfies(version, currentRange) && semver.gt(version, maxCurrent)) {
65+
maxCurrent = version
66+
}
67+
}
68+
const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS
69+
if (semver.gte(process.version, recommended)) {
70+
return `current: ${current}, recommended: ${recommended}`
71+
} else {
72+
throw `Use node ${recommended} (current: ${current})`
73+
}
74+
} finally {
75+
tracker.finish()
76+
}
77+
}
2978

30-
function doctor (args, silent, cb) {
31-
args = args || {}
32-
if (typeof cb !== 'function') {
33-
cb = silent
34-
silent = false
79+
const { promisify } = require('util')
80+
const fs = require('fs')
81+
const { R_OK, W_OK, X_OK } = fs.constants
82+
const maskLabel = mask => {
83+
const label = []
84+
if (mask & R_OK) {
85+
label.push('readable')
86+
}
87+
if (mask & W_OK) {
88+
label.push('writable')
89+
}
90+
if (mask & X_OK) {
91+
label.push('executable')
92+
}
93+
return label.join(', ')
94+
}
95+
const lstat = promisify(fs.lstat)
96+
const readdir = promisify(fs.readdir)
97+
const access = promisify(fs.access)
98+
const isWindows = require('./utils/is-windows.js')
99+
const checkFilesPermission = async (root, shouldOwn = true, mask = null) => {
100+
if (mask === null) {
101+
mask = shouldOwn ? R_OK | W_OK : R_OK
35102
}
36103

37-
const actionsToRun = [
38-
[checkPing],
39-
[getLatestNpmVersion],
40-
[getLatestNodejsVersion, args['node-url']],
41-
[getGitPath],
42-
[checkFilesPermission, npm.cache, 4, 6],
43-
[checkFilesPermission, globalNodeModules, 4, 4],
44-
[checkFilesPermission, localNodeModules, 6, 6],
45-
[verifyCachedFiles, path.join(npm.cache, '_cacache')]
46-
]
104+
let ok = true
105+
106+
const tracker = npm.log.newItem(root, 1)
107+
108+
try {
109+
const uid = process.getuid()
110+
const gid = process.getgid()
111+
const files = new Set([root])
112+
for (const f of files) {
113+
tracker.silly('checkFilesPermission', f.substr(root.length + 1))
114+
const st = await lstat(f)
115+
.catch(er => {
116+
ok = false
117+
tracker.warn('checkFilesPermission', 'error getting info for ' + f)
118+
})
119+
120+
tracker.completeWork(1)
47121

48-
log.info('doctor', 'Running checkup')
49-
chain(actionsToRun, function (stderr, stdout) {
50-
if (stderr && stderr.message !== 'not found: git') return cb(stderr)
51-
const list = makePretty(stdout)
52-
let outHead = ['Check', 'Value', 'Recommendation']
53-
let outBody = list
54-
55-
if (npm.color) {
56-
outHead = outHead.map(function (item) {
57-
return styles.underline(item)
58-
})
59-
outBody = outBody.map(function (item) {
60-
if (item[2]) {
61-
item[0] = color.red(item[0])
62-
item[2] = color.magenta(item[2])
122+
if (!st) {
123+
continue
124+
}
125+
126+
if (shouldOwn && (uid !== st.uid || gid !== st.gid)) {
127+
tracker.warn('checkFilesPermission', 'should be owner of ' + f)
128+
ok = false
129+
}
130+
131+
if (!st.isDirectory() && !st.isFile()) {
132+
continue
133+
}
134+
135+
try {
136+
await access(f, mask)
137+
} catch (er) {
138+
ok = false
139+
const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})`
140+
tracker.error('checkFilesPermission', msg)
141+
continue
142+
}
143+
144+
if (st.isDirectory()) {
145+
const entries = await readdir(f)
146+
.catch(er => {
147+
ok = false
148+
tracker.warn('checkFilesPermission', 'error reading directory ' + f)
149+
return []
150+
})
151+
for (const entry of entries) {
152+
files.add(resolve(f, entry))
63153
}
64-
return item
65-
})
154+
}
66155
}
67-
68-
const outTable = [outHead].concat(outBody)
69-
const tableOpts = {
70-
stringLength: function (s) { return ansiTrim(s).length }
156+
} finally {
157+
tracker.finish()
158+
if (!ok) {
159+
throw `Check the permissions of files in ${root}` +
160+
(shouldOwn ? ' (should be owned by current user)' : '')
161+
} else {
162+
return ''
71163
}
164+
}
165+
}
72166

73-
if (!silent) output(table(outTable, tableOpts))
167+
const which = require('which')
168+
const getGitPath = async () => {
169+
const tracker = npm.log.newItem('getGitPath', 1)
170+
tracker.info('getGitPath', 'Finding git in your PATH')
171+
try {
172+
return await which('git').catch(er => {
173+
tracker.warn(er)
174+
throw "Install git and ensure it's in your PATH."
175+
})
176+
} finally {
177+
tracker.finish()
178+
}
179+
}
180+
181+
const cacache = require('cacache')
182+
const verifyCachedFiles = async () => {
183+
const tracker = npm.log.newItem('verifyCachedFiles', 1)
184+
tracker.info('verifyCachedFiles', 'Verifying the npm cache')
185+
try {
186+
const stats = await cacache.verify(npm.flatOptions.cache)
187+
const { badContentCount, reclaimedCount, missingContent, reclaimedSize } = stats
188+
if (badContentCount || reclaimedCount || missingContent) {
189+
if (badContentCount) {
190+
tracker.warn('verifyCachedFiles', `Corrupted content removed: ${badContentCount}`)
191+
}
192+
if (reclaimedCount) {
193+
tracker.warn('verifyCachedFiles', `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`)
194+
}
195+
if (missingContent) {
196+
tracker.warn('verifyCachedFiles', `Missing content: ${missingContent}`)
197+
}
198+
tracker.warn('verifyCachedFiles', 'Cache issues have been fixed')
199+
}
200+
tracker.info('verifyCachedFiles', `Verification complete. Stats: ${
201+
JSON.stringify(stats, null, 2)
202+
}`)
203+
return `verified ${stats.verifiedContent} tarballs`
204+
} finally {
205+
tracker.finish()
206+
}
207+
}
74208

75-
cb(null, list)
76-
})
209+
const { defaults: { registry: defaultRegistry } } = require('./config/defaults.js')
210+
const checkNpmRegistry = async () => {
211+
if (npm.flatOptions.registry !== defaultRegistry) {
212+
throw `Try \`npm config set registry=${defaultRegistry}\``
213+
} else {
214+
return `using default registry (${defaultRegistry})`
215+
}
77216
}
78217

79-
function makePretty (p) {
80-
const ping = p[1]
81-
const npmLTS = p[2]
82-
const nodeLTS = p[3].replace('v', '')
83-
const whichGit = p[4] || 'not installed'
84-
const readbleCaches = p[5] ? 'ok' : 'notOk'
85-
const executableGlobalModules = p[6] ? 'ok' : 'notOk'
86-
const executableLocalModules = p[7] ? 'ok' : 'notOk'
87-
const cacheStatus = p[8] ? `verified ${p[8].verifiedContent} tarballs` : 'notOk'
88-
const npmV = npm.version
89-
const nodeV = process.version.replace('v', '')
90-
const registry = npm.config.get('registry') || ''
91-
const list = [
92-
['npm ping', ping],
93-
['npm -v', 'v' + npmV],
94-
['node -v', 'v' + nodeV],
95-
['npm config get registry', registry],
96-
['which git', whichGit],
97-
['Perms check on cached files', readbleCaches],
98-
['Perms check on global node_modules', executableGlobalModules],
99-
['Perms check on local node_modules', executableLocalModules],
100-
['Verify cache contents', cacheStatus]
218+
const cmd = (args, cb) => doctor(args).then(() => cb()).catch(cb)
219+
220+
const doctor = async args => {
221+
npm.log.info('Running checkup')
222+
223+
// each message is [title, ok, message]
224+
const messages = []
225+
226+
const actions = [
227+
['npm ping', checkPing, []],
228+
['npm -v', getLatestNpmVersion, []],
229+
['node -v', getLatestNodejsVersion, []],
230+
['npm config get registry', checkNpmRegistry, []],
231+
['which git', getGitPath, []],
232+
...(isWindows ? [] : [
233+
['Perms check on cached files', checkFilesPermission, [npm.cache, true, R_OK]],
234+
['Perms check on local node_modules', checkFilesPermission, [npm.localDir, true]],
235+
['Perms check on global node_modules', checkFilesPermission, [npm.globalDir, false]],
236+
['Perms check on local bin folder', checkFilesPermission, [npm.localBin, false, R_OK | W_OK | X_OK]],
237+
['Perms check on global bin folder', checkFilesPermission, [npm.globalBin, false, X_OK]]
238+
]),
239+
['Verify cache contents', verifyCachedFiles, [npm.flatOptions.cache]]
240+
// TODO:
241+
// - ensure arborist.loadActual() runs without errors and no invalid edges
242+
// - ensure package-lock.json matches loadActual()
243+
// - verify loadActual without hidden lock file matches hidden lockfile
244+
// - verify all local packages have bins linked
101245
]
102246

103-
if (p[0] !== 200) list[0][2] = 'Check your internet connection'
104-
if (!semver.satisfies(npmV, '>=' + npmLTS)) list[1][2] = 'Use npm v' + npmLTS
105-
if (!semver.satisfies(nodeV, '>=' + nodeLTS)) list[2][2] = 'Use node v' + nodeLTS
106-
if (registry !== defaultRegistry) list[3][2] = 'Try `npm config set registry ' + defaultRegistry + '`'
107-
if (whichGit === 'not installed') list[4][2] = 'Install git and ensure it\'s in your PATH.'
108-
if (readbleCaches !== 'ok') list[5][2] = 'Check the permissions of your files in ' + npm.config.get('cache')
109-
if (executableGlobalModules !== 'ok') list[6][2] = globalNodeModules + ' must be readable and writable by the current user.'
110-
if (executableLocalModules !== 'ok') list[7][2] = localNodeModules + ' must be readable and writable by the current user.'
247+
for (const [msg, fn, args] of actions) {
248+
const line = [msg]
249+
try {
250+
line.push(true, await fn(...args))
251+
} catch (er) {
252+
line.push(false, er)
253+
}
254+
messages.push(line)
255+
}
256+
257+
const silent = npm.log.levels[npm.log.level] > npm.log.levels.error
111258

112-
return list
259+
const outHead = ['Check', 'Value', 'Recommendation/Notes']
260+
.map(!npm.color ? h => h : h => chalk.underline(h))
261+
let allOk = true
262+
const outBody = messages.map(!npm.color
263+
? item => {
264+
allOk = allOk && item[1]
265+
item[1] = item[1] ? 'ok' : 'not ok'
266+
item[2] = String(item[2])
267+
return item
268+
}
269+
: item => {
270+
allOk = allOk && item[1]
271+
if (!item[1]) {
272+
item[0] = chalk.red(item[0])
273+
item[2] = chalk.magenta(String(item[2]))
274+
}
275+
item[1] = item[1] ? chalk.green('ok') : chalk.red('not ok')
276+
return item
277+
})
278+
const outTable = [outHead, ...outBody]
279+
const tableOpts = {
280+
stringLength: s => ansiTrim(s).length
281+
}
282+
283+
if (!silent) {
284+
output(table(outTable, tableOpts))
285+
if (!allOk) {
286+
console.error('')
287+
}
288+
}
289+
if (!allOk) {
290+
throw 'Some problems found. See above for recommendations.'
291+
}
113292
}
114293

115-
module.exports = Object.assign(doctor, { completion, usage })
294+
module.exports = Object.assign(cmd, { completion, usage })

0 commit comments

Comments
 (0)