11const cacache = require ( 'cacache' )
22const fs = require ( 'fs' )
33const fetch = require ( 'make-fetch-happen' )
4- const table = require ( 'text-table ' )
4+ const Table = require ( 'cli-table3 ' )
55const which = require ( 'which' )
66const pacote = require ( 'pacote' )
77const { resolve } = require ( 'path' )
88const semver = require ( 'semver' )
99const { promisify } = require ( 'util' )
1010const log = require ( '../utils/log-shim.js' )
11- const ansiTrim = require ( '../utils/ansi-trim.js' )
1211const ping = require ( '../utils/ping.js' )
1312const {
1413 registry : { default : defaultRegistry } ,
@@ -17,6 +16,7 @@ const lstat = promisify(fs.lstat)
1716const readdir = promisify ( fs . readdir )
1817const access = promisify ( fs . access )
1918const { R_OK , W_OK , X_OK } = fs . constants
19+
2020const maskLabel = mask => {
2121 const label = [ ]
2222 if ( mask & R_OK ) {
@@ -34,76 +34,105 @@ const maskLabel = mask => {
3434 return label . join ( ', ' )
3535}
3636
37+ const subcommands = [
38+ {
39+ groups : [ 'ping' , 'registry' ] ,
40+ title : 'npm ping' ,
41+ cmd : 'checkPing' ,
42+ } , {
43+ groups : [ 'versions' ] ,
44+ title : 'npm -v' ,
45+ cmd : 'getLatestNpmVersion' ,
46+ } , {
47+ groups : [ 'versions' ] ,
48+ title : 'node -v' ,
49+ cmd : 'getLatestNodejsVersion' ,
50+ } , {
51+ groups : [ 'registry' ] ,
52+ title : 'npm config get registry' ,
53+ cmd : 'checkNpmRegistry' ,
54+ } , {
55+ groups : [ 'environment' ] ,
56+ title : 'git executable in PATH' ,
57+ cmd : 'getGitPath' ,
58+ } , {
59+ groups : [ 'environment' ] ,
60+ title : 'global bin folder in PATH' ,
61+ cmd : 'getBinPath' ,
62+ } , {
63+ groups : [ 'permissions' , 'cache' ] ,
64+ title : 'Perms check on cached files' ,
65+ cmd : 'checkCachePermission' ,
66+ windows : false ,
67+ } , {
68+ groups : [ 'permissions' ] ,
69+ title : 'Perms check on local node_modules' ,
70+ cmd : 'checkLocalModulesPermission' ,
71+ windows : false ,
72+ } , {
73+ groups : [ 'permissions' ] ,
74+ title : 'Perms check on global node_modules' ,
75+ cmd : 'checkGlobalModulesPermission' ,
76+ windows : false ,
77+ } , {
78+ groups : [ 'permissions' ] ,
79+ title : 'Perms check on local bin folder' ,
80+ cmd : 'checkLocalBinPermission' ,
81+ windows : false ,
82+ } , {
83+ groups : [ 'permissions' ] ,
84+ title : 'Perms check on global bin folder' ,
85+ cmd : 'checkGlobalBinPermission' ,
86+ windows : false ,
87+ } , {
88+ groups : [ 'cache' ] ,
89+ title : 'Verify cache contents' ,
90+ cmd : 'verifyCachedFiles' ,
91+ windows : false ,
92+ } ,
93+ // TODO:
94+ // group === 'dependencies'?
95+ // - ensure arborist.loadActual() runs without errors and no invalid edges
96+ // - ensure package-lock.json matches loadActual()
97+ // - verify loadActual without hidden lock file matches hidden lockfile
98+ // group === '???'
99+ // - verify all local packages have bins linked
100+ // What is the fix for these?
101+ ]
37102const BaseCommand = require ( '../base-command.js' )
38103class Doctor extends BaseCommand {
39104 static description = 'Check your npm environment'
40105 static name = 'doctor'
41106 static params = [ 'registry' ]
42107 static ignoreImplicitWorkspace = false
108+ static usage = [ `[${ subcommands . flatMap ( s => s . groups )
109+ . filter ( ( value , index , self ) => self . indexOf ( value ) === index )
110+ . join ( '] [' ) } ]`]
111+
112+ static subcommands = subcommands
113+
114+ // minimum width of check column, enough for the word `Check`
115+ #checkWidth = 5
43116
44117 async exec ( args ) {
45118 log . info ( 'Running checkup' )
119+ let allOk = true
46120
47- // each message is [title, ok, message]
48- const messages = [ ]
49-
50- const actions = [
51- [ 'npm ping' , 'checkPing' , [ ] ] ,
52- [ 'npm -v' , 'getLatestNpmVersion' , [ ] ] ,
53- [ 'node -v' , 'getLatestNodejsVersion' , [ ] ] ,
54- [ 'npm config get registry' , 'checkNpmRegistry' , [ ] ] ,
55- [ 'which git' , 'getGitPath' , [ ] ] ,
56- ...( process . platform === 'win32'
57- ? [ ]
58- : [
59- [
60- 'Perms check on cached files' ,
61- 'checkFilesPermission' ,
62- [ this . npm . cache , true , R_OK ] ,
63- ] , [
64- 'Perms check on local node_modules' ,
65- 'checkFilesPermission' ,
66- [ this . npm . localDir , true , R_OK | W_OK , true ] ,
67- ] , [
68- 'Perms check on global node_modules' ,
69- 'checkFilesPermission' ,
70- [ this . npm . globalDir , false , R_OK ] ,
71- ] , [
72- 'Perms check on local bin folder' ,
73- 'checkFilesPermission' ,
74- [ this . npm . localBin , false , R_OK | W_OK | X_OK , true ] ,
75- ] , [
76- 'Perms check on global bin folder' ,
77- 'checkFilesPermission' ,
78- [ this . npm . globalBin , false , X_OK ] ,
79- ] ,
80- ] ) ,
81- [
82- 'Verify cache contents' ,
83- 'verifyCachedFiles' ,
84- [ this . npm . flatOptions . cache ] ,
85- ] ,
86- // TODO:
87- // - ensure arborist.loadActual() runs without errors and no invalid edges
88- // - ensure package-lock.json matches loadActual()
89- // - verify loadActual without hidden lock file matches hidden lockfile
90- // - verify all local packages have bins linked
91- ]
121+ const actions = this . actions ( args )
122+ this . #checkWidth = actions . reduce ( ( length , item ) =>
123+ Math . max ( item . title . length , length ) , this . #checkWidth)
92124
125+ if ( ! this . npm . silent ) {
126+ this . output ( [ 'Check' , 'Value' , 'Recommendation/Notes' ] . map ( h => this . npm . chalk . underline ( h ) ) )
127+ }
93128 // Do the actual work
94- for ( const [ msg , fn , args ] of actions ) {
95- const line = [ msg ]
129+ for ( const { title , cmd } of actions ) {
130+ const item = [ title ]
96131 try {
97- line . push ( true , await this [ fn ] ( ... args ) )
98- } catch ( er ) {
99- line . push ( false , er )
132+ item . push ( true , await this [ cmd ] ( ) )
133+ } catch ( err ) {
134+ item . push ( false , err )
100135 }
101- messages . push ( line )
102- }
103-
104- const outHead = [ 'Check' , 'Value' , 'Recommendation/Notes' ] . map ( h => this . npm . chalk . underline ( h ) )
105- let allOk = true
106- const outBody = messages . map ( item => {
107136 if ( ! item [ 1 ] ) {
108137 allOk = false
109138 item [ 0 ] = this . npm . chalk . red ( item [ 0 ] )
@@ -112,18 +141,18 @@ class Doctor extends BaseCommand {
112141 } else {
113142 item [ 1 ] = this . npm . chalk . green ( 'ok' )
114143 }
115- return item
116- } )
117- const outTable = [ outHead , ...outBody ]
118- const tableOpts = {
119- stringLength : s => ansiTrim ( s ) . length ,
144+ if ( ! this . npm . silent ) {
145+ this . output ( item )
146+ }
120147 }
121148
122- if ( ! this . npm . silent ) {
123- this . npm . output ( table ( outTable , tableOpts ) )
124- }
125149 if ( ! allOk ) {
126- throw new Error ( 'Some problems found. See above for recommendations.' )
150+ if ( this . npm . silent ) {
151+ /* eslint-disable-next-line max-len */
152+ throw new Error ( 'Some problems found. Check logs or disable silent mode for recommendations.' )
153+ } else {
154+ throw new Error ( 'Some problems found. See above for recommendations.' )
155+ }
127156 }
128157 }
129158
@@ -191,6 +220,35 @@ class Doctor extends BaseCommand {
191220 }
192221 }
193222
223+ async getBinPath ( dir ) {
224+ const tracker = log . newItem ( 'getBinPath' , 1 )
225+ tracker . info ( 'getBinPath' , 'Finding npm global bin in your PATH' )
226+ if ( ! process . env . PATH . includes ( this . npm . globalBin ) ) {
227+ throw new Error ( `Add ${ this . npm . globalBin } to your $PATH` )
228+ }
229+ return this . npm . globalBin
230+ }
231+
232+ async checkCachePermission ( ) {
233+ return this . checkFilesPermission ( this . npm . cache , true , R_OK )
234+ }
235+
236+ async checkLocalModulesPermission ( ) {
237+ return this . checkFilesPermission ( this . npm . localDir , true , R_OK | W_OK , true )
238+ }
239+
240+ async checkGlobalModulesPermission ( ) {
241+ return this . checkFilesPermission ( this . npm . globalDir , false , R_OK )
242+ }
243+
244+ async checkLocalBinPermission ( ) {
245+ return this . checkFilesPermission ( this . npm . localBin , false , R_OK | W_OK | X_OK , true )
246+ }
247+
248+ async checkGlobalBinPermission ( ) {
249+ return this . checkFilesPermission ( this . npm . globalBin , false , X_OK )
250+ }
251+
194252 async checkFilesPermission ( root , shouldOwn , mask , missingOk ) {
195253 let ok = true
196254
@@ -264,7 +322,7 @@ class Doctor extends BaseCommand {
264322 try {
265323 return await which ( 'git' ) . catch ( er => {
266324 tracker . warn ( er )
267- throw "Install git and ensure it's in your PATH."
325+ throw new Error ( "Install git and ensure it's in your PATH." )
268326 } )
269327 } finally {
270328 tracker . finish ( )
@@ -312,6 +370,42 @@ class Doctor extends BaseCommand {
312370 return `using default registry (${ defaultRegistry } )`
313371 }
314372 }
373+
374+ output ( row ) {
375+ const t = new Table ( {
376+ chars : { top : '' ,
377+ 'top-mid' : '' ,
378+ 'top-left' : '' ,
379+ 'top-right' : '' ,
380+ bottom : '' ,
381+ 'bottom-mid' : '' ,
382+ 'bottom-left' : '' ,
383+ 'bottom-right' : '' ,
384+ left : '' ,
385+ 'left-mid' : '' ,
386+ mid : '' ,
387+ 'mid-mid' : '' ,
388+ right : '' ,
389+ 'right-mid' : '' ,
390+ middle : ' ' } ,
391+ style : { 'padding-left' : 0 , 'padding-right' : 0 } ,
392+ colWidths : [ this . #checkWidth, 6 ] ,
393+ } )
394+ t . push ( row )
395+ this . npm . output ( t . toString ( ) )
396+ }
397+
398+ actions ( params ) {
399+ return this . constructor . subcommands . filter ( subcmd => {
400+ if ( process . platform === 'win32' && subcmd . windows === false ) {
401+ return false
402+ }
403+ if ( params . length ) {
404+ return params . some ( param => subcmd . groups . includes ( param ) )
405+ }
406+ return true
407+ } )
408+ }
315409}
316410
317411module . exports = Doctor
0 commit comments