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