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