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