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