update doctor command · npm/cli@f6d468a (original) (raw)
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')
`
13
5
`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')
`
14
11
``
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
`+
}
`
22
29
``
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
`+
}
`
25
45
``
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: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>c</mi><mi>u</mi><mi>r</mi><mi>r</mi><mi>e</mi><mi>n</mi><mi>t</mi></mrow><mo separator="true">,</mo><mi>r</mi><mi>e</mi><mi>c</mi><mi>o</mi><mi>m</mi><mi>m</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><mi>d</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">{current}, recommended: </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">c</span><span class="mord mathnormal">u</span><span class="mord mathnormal">rre</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">reco</span><span class="mord mathnormal">mm</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">d</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{recommended}
``
``
71
`+
} else {
`
``
72
`` +
throw Use node <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>r</mi><mi>e</mi><mi>c</mi><mi>o</mi><mi>m</mi><mi>m</mi><mi>e</mi><mi>n</mi><mi>d</mi><mi>e</mi><mi>d</mi></mrow><mo stretchy="false">(</mo><mi>c</mi><mi>u</mi><mi>r</mi><mi>r</mi><mi>e</mi><mi>n</mi><mi>t</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">{recommended} (current: </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">reco</span><span class="mord mathnormal">mm</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">d</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span></span><span class="mopen">(</span><span class="mord mathnormal">c</span><span class="mord mathnormal">u</span><span class="mord mathnormal">rre</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{current})
``
``
73
`+
}
`
``
74
`+
} finally {
`
``
75
`+
tracker.finish()
`
``
76
`+
}
`
``
77
`+
}
`
29
78
``
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
`
35
102
`}
`
36
103
``
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)
`
47
121
``
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 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mo stretchy="false">(</mo><mi>e</mi><mi>x</mi><mi>p</mi><mi>e</mi><mi>c</mi><mi>t</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">{f} (expect: </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10764em;">f</span></span><span class="mopen">(</span><span class="mord mathnormal">e</span><span class="mord mathnormal">x</span><span class="mord mathnormal">p</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{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))
`
63
153
`}
`
64
``
`-
return item
`
65
``
`-
})
`
``
154
`+
}
`
66
155
`}
`
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 ''
`
71
163
`}
`
``
164
`+
}
`
``
165
`+
}
`
72
166
``
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: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>r</mi><mi>e</mi><mi>c</mi><mi>l</mi><mi>a</mi><mi>i</mi><mi>m</mi><mi>e</mi><mi>d</mi><mi>C</mi><mi>o</mi><mi>u</mi><mi>n</mi><mi>t</mi></mrow><mo stretchy="false">(</mo></mrow><annotation encoding="application/x-tex">{reclaimedCount} (</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">rec</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">aim</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mord mathnormal" style="margin-right:0.07153em;">C</span><span class="mord mathnormal">o</span><span class="mord mathnormal">u</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span></span><span class="mopen">(</span></span></span></span>{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
`+
}
`
74
208
``
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
`+
}
`
77
216
`}
`
78
217
``
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
`
101
245
`]
`
102
246
``
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
`
111
258
``
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
`+
}
`
113
292
`}
`
114
293
``
115
``
`-
module.exports = Object.assign(doctor, { completion, usage })
`
``
294
`+
module.exports = Object.assign(cmd, { completion, usage })
`