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