owner.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. const npa = require('npm-package-arg')
  2. const npmFetch = require('npm-registry-fetch')
  3. const pacote = require('pacote')
  4. const { log, output } = require('proc-log')
  5. const { otplease } = require('../utils/auth.js')
  6. const pkgJson = require('@npmcli/package-json')
  7. const BaseCommand = require('../base-cmd.js')
  8. const { redact } = require('@npmcli/redact')
  9. const readJson = async (path) => {
  10. try {
  11. const { content } = await pkgJson.normalize(path)
  12. return content
  13. } catch {
  14. return {}
  15. }
  16. }
  17. class Owner extends BaseCommand {
  18. static description = 'Manage package owners'
  19. static name = 'owner'
  20. static params = [
  21. 'registry',
  22. 'otp',
  23. 'workspace',
  24. 'workspaces',
  25. ]
  26. static usage = [
  27. 'add <user> <package-spec>',
  28. 'rm <user> <package-spec>',
  29. 'ls <package-spec>',
  30. ]
  31. static workspaces = true
  32. static ignoreImplicitWorkspace = false
  33. static async completion (opts, npm) {
  34. const argv = opts.conf.argv.remain
  35. if (argv.length > 3) {
  36. return []
  37. }
  38. if (argv[1] !== 'owner') {
  39. argv.unshift('owner')
  40. }
  41. if (argv.length === 2) {
  42. return ['add', 'rm', 'ls']
  43. }
  44. // reaches registry in order to autocomplete rm
  45. if (argv[2] === 'rm') {
  46. if (npm.global) {
  47. return []
  48. }
  49. const { name } = await readJson(npm.prefix)
  50. if (!name) {
  51. return []
  52. }
  53. const spec = npa(name)
  54. const data = await pacote.packument(spec, {
  55. ...npm.flatOptions,
  56. fullMetadata: true,
  57. _isRoot: true,
  58. })
  59. if (data && data.maintainers && data.maintainers.length) {
  60. return data.maintainers.map(m => m.name)
  61. }
  62. }
  63. return []
  64. }
  65. async exec ([action, ...args]) {
  66. if (action === 'ls' || action === 'list') {
  67. await this.ls(args[0])
  68. } else if (action === 'add') {
  69. await this.changeOwners(args[0], args[1], 'add')
  70. } else if (action === 'rm' || action === 'remove') {
  71. await this.changeOwners(args[0], args[1], 'rm')
  72. } else {
  73. throw this.usageError()
  74. }
  75. }
  76. async execWorkspaces ([action, ...args]) {
  77. await this.setWorkspaces()
  78. // ls pkg or owner add/rm package
  79. if ((action === 'ls' && args.length > 0) || args.length > 1) {
  80. const implicitWorkspaces = this.npm.config.get('workspace', 'default')
  81. if (implicitWorkspaces.length === 0) {
  82. log.warn(`Ignoring specified workspace(s)`)
  83. }
  84. return this.exec([action, ...args])
  85. }
  86. for (const [name] of this.workspaces) {
  87. if (action === 'ls' || action === 'list') {
  88. await this.ls(name)
  89. } else if (action === 'add') {
  90. await this.changeOwners(args[0], name, 'add')
  91. } else if (action === 'rm' || action === 'remove') {
  92. await this.changeOwners(args[0], name, 'rm')
  93. } else {
  94. throw this.usageError()
  95. }
  96. }
  97. }
  98. async ls (pkg) {
  99. pkg = await this.getPkg(this.npm.prefix, pkg)
  100. const spec = npa(pkg)
  101. try {
  102. const packumentOpts = {
  103. ...this.npm.flatOptions,
  104. fullMetadata:
  105. true,
  106. preferOnline: true,
  107. _isRoot: true,
  108. }
  109. const { maintainers } = await pacote.packument(spec, packumentOpts)
  110. if (!maintainers || !maintainers.length) {
  111. output.standard('no admin found')
  112. } else {
  113. output.standard(maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
  114. }
  115. } catch (err) {
  116. log.error('owner ls', "Couldn't get owner data", redact(pkg))
  117. throw err
  118. }
  119. }
  120. async getPkg (prefix, pkg) {
  121. if (!pkg) {
  122. if (this.npm.global) {
  123. throw this.usageError()
  124. }
  125. const { name } = await readJson(prefix)
  126. if (!name) {
  127. throw this.usageError()
  128. }
  129. return name
  130. }
  131. return pkg
  132. }
  133. async changeOwners (user, pkg, addOrRm) {
  134. if (!user) {
  135. throw this.usageError()
  136. }
  137. pkg = await this.getPkg(this.npm.prefix, pkg)
  138. log.verbose(`owner ${addOrRm}`, '%s to %s', user, pkg)
  139. const spec = npa(pkg)
  140. const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}`
  141. let u
  142. try {
  143. u = await npmFetch.json(uri, this.npm.flatOptions)
  144. } catch (err) {
  145. log.error('owner mutate', `Error getting user data for ${user}`)
  146. throw err
  147. }
  148. // normalize user data
  149. u = { name: u.name, email: u.email }
  150. const data = await pacote.packument(spec, {
  151. ...this.npm.flatOptions,
  152. fullMetadata: true,
  153. preferOnline: true,
  154. _isRoot: true,
  155. })
  156. const owners = data.maintainers || []
  157. let maintainers
  158. if (addOrRm === 'add') {
  159. const existing = owners.find(o => o.name === u.name)
  160. if (existing) {
  161. log.info(
  162. 'owner add',
  163. `Already a package owner: ${existing.name} <${existing.email}>`
  164. )
  165. return
  166. }
  167. maintainers = [
  168. ...owners,
  169. u,
  170. ]
  171. } else {
  172. maintainers = owners.filter(o => o.name !== u.name)
  173. if (maintainers.length === owners.length) {
  174. log.info('owner rm', 'Not a package owner: ' + u.name)
  175. return false
  176. }
  177. if (!maintainers.length) {
  178. throw Object.assign(
  179. new Error(
  180. 'Cannot remove all owners of a package. Add someone else first.'
  181. ),
  182. { code: 'EOWNERRM' }
  183. )
  184. }
  185. }
  186. const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}`
  187. try {
  188. const res = await otplease(this.npm, this.npm.flatOptions, opts => {
  189. return npmFetch.json(dataPath, {
  190. ...opts,
  191. method: 'PUT',
  192. body: {
  193. _id: data._id,
  194. _rev: data._rev,
  195. maintainers,
  196. },
  197. spec,
  198. })
  199. })
  200. if (addOrRm === 'add') {
  201. output.standard(`+ ${user} (${spec.name})`)
  202. } else {
  203. output.standard(`- ${user} (${spec.name})`)
  204. }
  205. return res
  206. } catch (err) {
  207. throw Object.assign(
  208. new Error('Failed to update package: ' + JSON.stringify(err.message)),
  209. { code: 'EOWNERMUTATE' }
  210. )
  211. }
  212. }
  213. }
  214. module.exports = Owner