owner.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. module.exports = owner
  2. const BB = require('bluebird')
  3. const log = require('npmlog')
  4. const npa = require('libnpm/parse-arg')
  5. const npmConfig = require('./config/figgy-config.js')
  6. const npmFetch = require('libnpm/fetch')
  7. const output = require('./utils/output.js')
  8. const otplease = require('./utils/otplease.js')
  9. const packument = require('libnpm/packument')
  10. const readLocalPkg = BB.promisify(require('./utils/read-local-package.js'))
  11. const usage = require('./utils/usage')
  12. const whoami = BB.promisify(require('./whoami.js'))
  13. owner.usage = usage(
  14. 'owner',
  15. 'npm owner add <user> [<@scope>/]<pkg>' +
  16. '\nnpm owner rm <user> [<@scope>/]<pkg>' +
  17. '\nnpm owner ls [<@scope>/]<pkg>'
  18. )
  19. owner.completion = function (opts, cb) {
  20. const argv = opts.conf.argv.remain
  21. if (argv.length > 4) return cb()
  22. if (argv.length <= 2) {
  23. var subs = ['add', 'rm']
  24. if (opts.partialWord === 'l') subs.push('ls')
  25. else subs.push('ls', 'list')
  26. return cb(null, subs)
  27. }
  28. BB.try(() => {
  29. const opts = npmConfig()
  30. return whoami([], true).then(username => {
  31. const un = encodeURIComponent(username)
  32. let byUser, theUser
  33. switch (argv[2]) {
  34. case 'ls':
  35. // FIXME: there used to be registry completion here, but it stopped
  36. // making sense somewhere around 50,000 packages on the registry
  37. return
  38. case 'rm':
  39. if (argv.length > 3) {
  40. theUser = encodeURIComponent(argv[3])
  41. byUser = `/-/by-user/${theUser}|${un}`
  42. return npmFetch.json(byUser, opts).then(d => {
  43. return d[theUser].filter(
  44. // kludge for server adminery.
  45. p => un === 'isaacs' || d[un].indexOf(p) === -1
  46. )
  47. })
  48. }
  49. // else fallthrough
  50. /* eslint no-fallthrough:0 */
  51. case 'add':
  52. if (argv.length > 3) {
  53. theUser = encodeURIComponent(argv[3])
  54. byUser = `/-/by-user/${theUser}|${un}`
  55. return npmFetch.json(byUser, opts).then(d => {
  56. var mine = d[un] || []
  57. var theirs = d[theUser] || []
  58. return mine.filter(p => theirs.indexOf(p) === -1)
  59. })
  60. } else {
  61. // just list all users who aren't me.
  62. return npmFetch.json('/-/users', opts).then(list => {
  63. return Object.keys(list).filter(n => n !== un)
  64. })
  65. }
  66. default:
  67. return cb()
  68. }
  69. })
  70. }).nodeify(cb)
  71. }
  72. function UsageError () {
  73. throw Object.assign(new Error(owner.usage), {code: 'EUSAGE'})
  74. }
  75. function owner ([action, ...args], cb) {
  76. const opts = npmConfig()
  77. BB.try(() => {
  78. switch (action) {
  79. case 'ls': case 'list': return ls(args[0], opts)
  80. case 'add': return add(args[0], args[1], opts)
  81. case 'rm': case 'remove': return rm(args[0], args[1], opts)
  82. default: UsageError()
  83. }
  84. }).then(
  85. data => cb(null, data),
  86. err => err.code === 'EUSAGE' ? cb(err.message) : cb(err)
  87. )
  88. }
  89. function ls (pkg, opts) {
  90. if (!pkg) {
  91. return readLocalPkg().then(pkg => {
  92. if (!pkg) { UsageError() }
  93. return ls(pkg, opts)
  94. })
  95. }
  96. const spec = npa(pkg)
  97. return packument(spec, opts.concat({fullMetadata: true})).then(
  98. data => {
  99. var owners = data.maintainers
  100. if (!owners || !owners.length) {
  101. output('admin party!')
  102. } else {
  103. output(owners.map(o => `${o.name} <${o.email}>`).join('\n'))
  104. }
  105. return owners
  106. },
  107. err => {
  108. log.error('owner ls', "Couldn't get owner data", pkg)
  109. throw err
  110. }
  111. )
  112. }
  113. function add (user, pkg, opts) {
  114. if (!user) { UsageError() }
  115. if (!pkg) {
  116. return readLocalPkg().then(pkg => {
  117. if (!pkg) { UsageError() }
  118. return add(user, pkg, opts)
  119. })
  120. }
  121. log.verbose('owner add', '%s to %s', user, pkg)
  122. const spec = npa(pkg)
  123. return withMutation(spec, user, opts, (u, owners) => {
  124. if (!owners) owners = []
  125. for (var i = 0, l = owners.length; i < l; i++) {
  126. var o = owners[i]
  127. if (o.name === u.name) {
  128. log.info(
  129. 'owner add',
  130. 'Already a package owner: ' + o.name + ' <' + o.email + '>'
  131. )
  132. return false
  133. }
  134. }
  135. owners.push(u)
  136. return owners
  137. })
  138. }
  139. function rm (user, pkg, opts) {
  140. if (!user) { UsageError() }
  141. if (!pkg) {
  142. return readLocalPkg().then(pkg => {
  143. if (!pkg) { UsageError() }
  144. return add(user, pkg, opts)
  145. })
  146. }
  147. log.verbose('owner rm', '%s from %s', user, pkg)
  148. const spec = npa(pkg)
  149. return withMutation(spec, user, opts, function (u, owners) {
  150. let found = false
  151. const m = owners.filter(function (o) {
  152. var match = (o.name === user)
  153. found = found || match
  154. return !match
  155. })
  156. if (!found) {
  157. log.info('owner rm', 'Not a package owner: ' + user)
  158. return false
  159. }
  160. if (!m.length) {
  161. throw new Error(
  162. 'Cannot remove all owners of a package. Add someone else first.'
  163. )
  164. }
  165. return m
  166. })
  167. }
  168. function withMutation (spec, user, opts, mutation) {
  169. return BB.try(() => {
  170. if (user) {
  171. const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}`
  172. return npmFetch.json(uri, opts).then(mutate_, err => {
  173. log.error('owner mutate', 'Error getting user data for %s', user)
  174. throw err
  175. })
  176. } else {
  177. return mutate_(null)
  178. }
  179. })
  180. function mutate_ (u) {
  181. if (user && (!u || u.error)) {
  182. throw new Error(
  183. "Couldn't get user data for " + user + ': ' + JSON.stringify(u)
  184. )
  185. }
  186. if (u) u = { name: u.name, email: u.email }
  187. return packument(spec, opts.concat({
  188. fullMetadata: true
  189. })).then(data => {
  190. // save the number of maintainers before mutation so that we can figure
  191. // out if maintainers were added or removed
  192. const beforeMutation = data.maintainers.length
  193. const m = mutation(u, data.maintainers)
  194. if (!m) return // handled
  195. if (m instanceof Error) throw m // error
  196. data = {
  197. _id: data._id,
  198. _rev: data._rev,
  199. maintainers: m
  200. }
  201. const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}`
  202. return otplease(opts, opts => {
  203. const reqOpts = opts.concat({
  204. method: 'PUT',
  205. body: data,
  206. spec
  207. })
  208. return npmFetch.json(dataPath, reqOpts)
  209. }).then(data => {
  210. if (data.error) {
  211. throw new Error('Failed to update package metadata: ' + JSON.stringify(data))
  212. } else if (m.length > beforeMutation) {
  213. output('+ %s (%s)', user, spec.name)
  214. } else if (m.length < beforeMutation) {
  215. output('- %s (%s)', user, spec.name)
  216. }
  217. return data
  218. })
  219. })
  220. }
  221. }