config.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. // don't expand so that we only assemble the set of defaults when needed
  2. const configDefs = require('./utils/config/index.js')
  3. const mkdirp = require('mkdirp-infer-owner')
  4. const { dirname } = require('path')
  5. const { promisify } = require('util')
  6. const fs = require('fs')
  7. const readFile = promisify(fs.readFile)
  8. const writeFile = promisify(fs.writeFile)
  9. const { spawn } = require('child_process')
  10. const { EOL } = require('os')
  11. const ini = require('ini')
  12. const localeCompare = require('@isaacs/string-locale-compare')('en')
  13. // take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into
  14. // { key: value, k2: v2, k3: v3 }
  15. const keyValues = args => {
  16. const kv = {}
  17. for (let i = 0; i < args.length; i++) {
  18. const arg = args[i].split('=')
  19. const key = arg.shift()
  20. const val = arg.length ? arg.join('=')
  21. : i < args.length - 1 ? args[++i]
  22. : ''
  23. kv[key.trim()] = val.trim()
  24. }
  25. return kv
  26. }
  27. const publicVar = k => !/^(\/\/[^:]+:)?_/.test(k)
  28. const BaseCommand = require('./base-command.js')
  29. class Config extends BaseCommand {
  30. static get description () {
  31. return 'Manage the npm configuration files'
  32. }
  33. /* istanbul ignore next - see test/lib/load-all-commands.js */
  34. static get name () {
  35. return 'config'
  36. }
  37. /* istanbul ignore next - see test/lib/load-all-commands.js */
  38. static get usage () {
  39. return [
  40. 'set <key>=<value> [<key>=<value> ...]',
  41. 'get [<key> [<key> ...]]',
  42. 'delete <key> [<key> ...]',
  43. 'list [--json]',
  44. 'edit',
  45. ]
  46. }
  47. /* istanbul ignore next - see test/lib/load-all-commands.js */
  48. static get params () {
  49. return [
  50. 'json',
  51. 'global',
  52. 'editor',
  53. 'location',
  54. 'long',
  55. ]
  56. }
  57. async completion (opts) {
  58. const argv = opts.conf.argv.remain
  59. if (argv[1] !== 'config')
  60. argv.unshift('config')
  61. if (argv.length === 2) {
  62. const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit']
  63. if (opts.partialWord !== 'l')
  64. cmds.push('list')
  65. return cmds
  66. }
  67. const action = argv[2]
  68. switch (action) {
  69. case 'set':
  70. // todo: complete with valid values, if possible.
  71. if (argv.length > 3)
  72. return []
  73. // fallthrough
  74. /* eslint no-fallthrough:0 */
  75. case 'get':
  76. case 'delete':
  77. case 'rm':
  78. return Object.keys(configDefs.definitions)
  79. case 'edit':
  80. case 'list':
  81. case 'ls':
  82. default:
  83. return []
  84. }
  85. }
  86. exec (args, cb) {
  87. this.config(args).then(() => cb()).catch(cb)
  88. }
  89. execWorkspaces (args, filters, cb) {
  90. this.npm.log.warn('config', 'This command does not support workspaces.')
  91. this.exec(args, cb)
  92. }
  93. async config ([action, ...args]) {
  94. this.npm.log.disableProgress()
  95. try {
  96. switch (action) {
  97. case 'set':
  98. await this.set(args)
  99. break
  100. case 'get':
  101. await this.get(args)
  102. break
  103. case 'delete':
  104. case 'rm':
  105. case 'del':
  106. await this.del(args)
  107. break
  108. case 'list':
  109. case 'ls':
  110. await (this.npm.flatOptions.json ? this.listJson() : this.list())
  111. break
  112. case 'edit':
  113. await this.edit()
  114. break
  115. default:
  116. throw this.usageError()
  117. }
  118. } finally {
  119. this.npm.log.enableProgress()
  120. }
  121. }
  122. async set (args) {
  123. if (!args.length)
  124. throw this.usageError()
  125. const where = this.npm.flatOptions.location
  126. for (const [key, val] of Object.entries(keyValues(args))) {
  127. this.npm.log.info('config', 'set %j %j', key, val)
  128. this.npm.config.set(key, val || '', where)
  129. if (!this.npm.config.validate(where))
  130. this.npm.log.warn('config', 'omitting invalid config values')
  131. }
  132. await this.npm.config.save(where)
  133. }
  134. async get (keys) {
  135. if (!keys.length)
  136. return this.list()
  137. const out = []
  138. for (const key of keys) {
  139. if (!publicVar(key))
  140. throw `The ${key} option is protected, and cannot be retrieved in this way`
  141. const pref = keys.length > 1 ? `${key}=` : ''
  142. out.push(pref + this.npm.config.get(key))
  143. }
  144. this.npm.output(out.join('\n'))
  145. }
  146. async del (keys) {
  147. if (!keys.length)
  148. throw this.usageError()
  149. const where = this.npm.flatOptions.location
  150. for (const key of keys)
  151. this.npm.config.delete(key, where)
  152. await this.npm.config.save(where)
  153. }
  154. async edit () {
  155. const e = this.npm.flatOptions.editor
  156. const where = this.npm.flatOptions.location
  157. const file = this.npm.config.data.get(where).source
  158. // save first, just to make sure it's synced up
  159. // this also removes all the comments from the last time we edited it.
  160. await this.npm.config.save(where)
  161. const data = (
  162. await readFile(file, 'utf8').catch(() => '')
  163. ).replace(/\r\n/g, '\n')
  164. const entries = Object.entries(configDefs.defaults)
  165. const defData = entries.reduce((str, [key, val]) => {
  166. const obj = { [key]: val }
  167. const i = ini.stringify(obj)
  168. .replace(/\r\n/g, '\n') // normalizes output from ini.stringify
  169. .replace(/\n$/m, '')
  170. .replace(/^/g, '; ')
  171. .replace(/\n/g, '\n; ')
  172. .split('\n')
  173. return str + '\n' + i
  174. }, '')
  175. const tmpData = `;;;;
  176. ; npm ${where}config file: ${file}
  177. ; this is a simple ini-formatted file
  178. ; lines that start with semi-colons are comments
  179. ; run \`npm help 7 config\` for documentation of the various options
  180. ;
  181. ; Configs like \`@scope:registry\` map a scope to a given registry url.
  182. ;
  183. ; Configs like \`//<hostname>/:_authToken\` are auth that is restricted
  184. ; to the registry host specified.
  185. ${data.split('\n').sort(localeCompare).join('\n').trim()}
  186. ;;;;
  187. ; all available options shown below with default values
  188. ;;;;
  189. ${defData}
  190. `.split('\n').join(EOL)
  191. await mkdirp(dirname(file))
  192. await writeFile(file, tmpData, 'utf8')
  193. await new Promise((resolve, reject) => {
  194. const [bin, ...args] = e.split(/\s+/)
  195. const editor = spawn(bin, [...args, file], { stdio: 'inherit' })
  196. editor.on('exit', (code) => {
  197. if (code)
  198. return reject(new Error(`editor process exited with code: ${code}`))
  199. return resolve()
  200. })
  201. })
  202. }
  203. async list () {
  204. const msg = []
  205. // long does not have a flattener
  206. const long = this.npm.config.get('long')
  207. for (const [where, { data, source }] of this.npm.config.data.entries()) {
  208. if (where === 'default' && !long)
  209. continue
  210. const keys = Object.keys(data).sort(localeCompare)
  211. if (!keys.length)
  212. continue
  213. msg.push(`; "${where}" config from ${source}`, '')
  214. for (const k of keys) {
  215. const v = publicVar(k) ? JSON.stringify(data[k]) : '(protected)'
  216. const src = this.npm.config.find(k)
  217. const overridden = src !== where
  218. msg.push((overridden ? '; ' : '') +
  219. `${k} = ${v} ${overridden ? `; overridden by ${src}` : ''}`)
  220. }
  221. msg.push('')
  222. }
  223. if (!long) {
  224. msg.push(
  225. `; node bin location = ${process.execPath}`,
  226. `; cwd = ${process.cwd()}`,
  227. `; HOME = ${process.env.HOME}`,
  228. '; Run `npm config ls -l` to show all defaults.'
  229. )
  230. }
  231. this.npm.output(msg.join('\n').trim())
  232. }
  233. async listJson () {
  234. const publicConf = {}
  235. for (const key in this.npm.config.list[0]) {
  236. if (!publicVar(key))
  237. continue
  238. publicConf[key] = this.npm.config.get(key)
  239. }
  240. this.npm.output(JSON.stringify(publicConf, null, 2))
  241. }
  242. }
  243. module.exports = Config