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 }  ) 
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