1const spawn = require('@npmcli/promise-spawn') 2const path = require('path') 3const openUrl = require('../utils/open-url.js') 4const { glob } = require('glob') 5const localeCompare = require('@isaacs/string-locale-compare')('en') 6const { deref } = require('../utils/cmd-list.js') 7 8const globify = pattern => pattern.split('\\').join('/') 9const BaseCommand = require('../base-command.js') 10 11// Strips out the number from foo.7 or foo.7. or foo.7.tgz 12// We don't currently compress our man pages but if we ever did this would 13// seamlessly continue supporting it 14const manNumberRegex = /\.(\d+)(\.[^/\\]*)?$/ 15// hardcoded names for mansections 16// XXX: these are used in the docs workspace and should be exported 17// from npm so section names can changed more easily 18const manSectionNames = { 19 1: 'commands', 20 5: 'configuring-npm', 21 7: 'using-npm', 22} 23 24class Help extends BaseCommand { 25 static description = 'Get help on npm' 26 static name = 'help' 27 static usage = ['<term> [<terms..>]'] 28 static params = ['viewer'] 29 30 static async completion (opts, npm) { 31 if (opts.conf.argv.remain.length > 2) { 32 return [] 33 } 34 const g = path.resolve(npm.npmRoot, 'man/man[0-9]/*.[0-9]') 35 let files = await glob(globify(g)) 36 // preserve glob@8 behavior 37 files = files.sort((a, b) => a.localeCompare(b, 'en')) 38 39 return Object.keys(files.reduce(function (acc, file) { 40 file = path.basename(file).replace(/\.[0-9]+$/, '') 41 file = file.replace(/^npm-/, '') 42 acc[file] = true 43 return acc 44 }, { help: true })) 45 } 46 47 async exec (args) { 48 // By default we search all of our man subdirectories, but if the user has 49 // asked for a specific one we limit the search to just there 50 const manSearch = /^\d+$/.test(args[0]) ? `man${args.shift()}` : 'man*' 51 52 if (!args.length) { 53 return this.npm.output(this.npm.usage) 54 } 55 56 // npm help foo bar baz: search topics 57 if (args.length > 1) { 58 return this.helpSearch(args) 59 } 60 61 // `npm help package.json` 62 const arg = (deref(args[0]) || args[0]).replace('.json', '-json') 63 64 // find either section.n or npm-section.n 65 const f = globify(path.resolve(this.npm.npmRoot, `man/${manSearch}/?(npm-)${arg}.[0-9]*`)) 66 67 const [man] = await glob(f).then(r => r.sort((a, b) => { 68 // Because the glob is (subtly) different from manNumberRegex, 69 // we can't rely on it passing. 70 const aManNumberMatch = a.match(manNumberRegex)?.[1] || 999 71 const bManNumberMatch = b.match(manNumberRegex)?.[1] || 999 72 if (aManNumberMatch !== bManNumberMatch) { 73 return aManNumberMatch - bManNumberMatch 74 } 75 return localeCompare(a, b) 76 })) 77 78 return man ? this.viewMan(man) : this.helpSearch(args) 79 } 80 81 helpSearch (args) { 82 return this.npm.exec('help-search', args) 83 } 84 85 async viewMan (man) { 86 const viewer = this.npm.config.get('viewer') 87 88 if (viewer === 'browser') { 89 return openUrl(this.npm, this.htmlMan(man), 'help available at the following URL', true) 90 } 91 92 let args = ['man', [man]] 93 if (viewer === 'woman') { 94 args = ['emacsclient', ['-e', `(woman-find-file '${man}')`]] 95 } 96 97 return spawn(...args, { stdio: 'inherit' }).catch(err => { 98 if (err.code) { 99 throw new Error(`help process exited with code: ${err.code}`) 100 } else { 101 throw err 102 } 103 }) 104 } 105 106 // Returns the path to the html version of the man page 107 htmlMan (man) { 108 const sect = manSectionNames[man.match(manNumberRegex)[1]] 109 const f = path.basename(man).replace(manNumberRegex, '') 110 return 'file:///' + path.resolve(this.npm.npmRoot, `docs/output/${sect}/${f}.html`) 111 } 112} 113module.exports = Help 114