Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: enable mutating flat-options #2802

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 157 additions & 129 deletions lib/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,10 @@ const path = require('path')
const libaccess = require('libnpmaccess')
const readPackageJson = require('read-package-json-fast')

const npm = require('./npm.js')
const output = require('./utils/output.js')
const otplease = require('./utils/otplease.js')
const usageUtil = require('./utils/usage.js')
const getIdentity = require('./utils/get-identity.js')

const usage = usageUtil(
'npm access',
'npm access public [<package>]\n' +
'npm access restricted [<package>]\n' +
'npm access grant <read-only|read-write> <scope:team> [<package>]\n' +
'npm access revoke <scope:team> [<package>]\n' +
'npm access 2fa-required [<package>]\n' +
'npm access 2fa-not-required [<package>]\n' +
'npm access ls-packages [<user>|<scope>|<scope:team>]\n' +
'npm access ls-collaborators [<package> [<user>]]\n' +
'npm access edit [<package>]'
)

const subcommands = [
'public',
'restricted',
Expand All @@ -34,152 +19,195 @@ const subcommands = [
'2fa-not-required',
]

const UsageError = (msg) =>
Object.assign(new Error(`\nUsage: ${msg}\n\n` + usage), {
code: 'EUSAGE',
})

const cmd = (args, cb) =>
access(args)
.then(x => cb(null, x))
.catch(err => err.code === 'EUSAGE'
? cb(err.message)
: cb(err)
class Access {
constructor (npm) {
this.npm = npm
}

get usage () {
return usageUtil(
'access',
'npm access public [<package>]\n' +
'npm access restricted [<package>]\n' +
'npm access grant <read-only|read-write> <scope:team> [<package>]\n' +
'npm access revoke <scope:team> [<package>]\n' +
'npm access 2fa-required [<package>]\n' +
'npm access 2fa-not-required [<package>]\n' +
'npm access ls-packages [<user>|<scope>|<scope:team>]\n' +
'npm access ls-collaborators [<package> [<user>]]\n' +
'npm access edit [<package>]'
)
}

const access = async ([cmd, ...args], cb) => {
const fn = subcommands.includes(cmd) && access[cmd]
async completion (opts) {
const argv = opts.conf.argv.remain
if (argv.length === 2)
return subcommands

switch (argv[2]) {
case 'grant':
if (argv.length === 3)
return ['read-only', 'read-write']
else
return []

case 'public':
case 'restricted':
case 'ls-packages':
case 'ls-collaborators':
case 'edit':
case '2fa-required':
case '2fa-not-required':
case 'revoke':
return []
default:
throw new Error(argv[2] + ' not recognized')
}
}

if (!cmd)
throw UsageError('Subcommand is required.')
exec (args, cb) {
this.access(args)
.then(x => cb(null, x))
.catch(err => err.code === 'EUSAGE'
? cb(err.message)
: cb(err)
)
}

if (!fn)
throw UsageError(`${cmd} is not a recognized subcommand.`)
async access ([cmd, ...args]) {
if (!cmd)
throw this.usageError('Subcommand is required.')

return fn(args, { ...npm.flatOptions })
}
if (!subcommands.includes(cmd) || !this[cmd])
throw this.usageError(`${cmd} is not a recognized subcommand.`)

const completion = async (opts) => {
const argv = opts.conf.argv.remain
if (argv.length === 2)
return subcommands
return this[cmd](args, { ...this.npm.flatOptions })
}

switch (argv[2]) {
case 'grant':
if (argv.length === 3)
return ['read-only', 'read-write']
else
return []
public ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.public)
}

case 'public':
case 'restricted':
case 'ls-packages':
case 'ls-collaborators':
case 'edit':
case '2fa-required':
case '2fa-not-required':
case 'revoke':
return []
default:
throw new Error(argv[2] + ' not recognized')
restricted ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.restricted)
}
}

access.public = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.public)
async grant ([perms, scopeteam, pkg], opts) {
if (!perms || (perms !== 'read-only' && perms !== 'read-write'))
throw this.usageError('First argument must be either `read-only` or `read-write`.')

access.restricted = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.restricted)
if (!scopeteam)
throw this.usageError('`<scope:team>` argument is required.')

access.grant = async ([perms, scopeteam, pkg], opts) => {
if (!perms || (perms !== 'read-only' && perms !== 'read-write'))
throw UsageError('First argument must be either `read-only` or `read-write`.')
const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []

if (!scopeteam)
throw UsageError('`<scope:team>` argument is required.')
if (!scope && !team) {
throw this.usageError(
'Second argument used incorrect format.\n' +
'Example: @example:developers'
)
}

const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []
return this.modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.grant(pkgName, scopeteam, perms, opts), false)
}

if (!scope && !team) {
throw UsageError(
'Second argument used incorrect format.\n' +
'Example: @example:developers'
)
async revoke ([scopeteam, pkg], opts) {
if (!scopeteam)
throw this.usageError('`<scope:team>` argument is required.')

const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []

if (!scope || !team) {
throw this.usageError(
'First argument used incorrect format.\n' +
'Example: @example:developers'
)
}

return this.modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.revoke(pkgName, scopeteam, opts))
}

return modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.grant(pkgName, scopeteam, perms, opts), false)
}
get ['2fa-required'] () {
return this.tfaRequired
}

access.revoke = async ([scopeteam, pkg], opts) => {
if (!scopeteam)
throw UsageError('`<scope:team>` argument is required.')
tfaRequired ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.tfaRequired, false)
}

const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || []
get ['2fa-not-required'] () {
return this.tfaNotRequired
}

if (!scope || !team) {
throw UsageError(
'First argument used incorrect format.\n' +
'Example: @example:developers'
)
tfaNotRequired ([pkg], opts) {
return this.modifyPackage(pkg, opts, libaccess.tfaNotRequired, false)
}

return modifyPackage(pkg, opts, (pkgName, opts) =>
libaccess.revoke(pkgName, scopeteam, opts))
}
get ['ls-packages'] () {
return this.lsPackages
}

access['2fa-required'] = access.tfaRequired = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.tfaRequired, false)
async lsPackages ([owner], opts) {
if (!owner)
owner = await getIdentity(this.npm, opts)

access['2fa-not-required'] = access.tfaNotRequired = ([pkg], opts) =>
modifyPackage(pkg, opts, libaccess.tfaNotRequired, false)
const pkgs = await libaccess.lsPackages(owner, opts)

access['ls-packages'] = access.lsPackages = async ([owner], opts) => {
if (!owner)
owner = await getIdentity(opts)
// TODO - print these out nicely (breaking change)
this.npm.output(JSON.stringify(pkgs, null, 2))
}

const pkgs = await libaccess.lsPackages(owner, opts)
get ['ls-collaborators'] () {
return this.lsCollaborators
}

// TODO - print these out nicely (breaking change)
output(JSON.stringify(pkgs, null, 2))
}
async lsCollaborators ([pkg, usr], opts) {
const pkgName = await this.getPackage(pkg, false)
const collabs = await libaccess.lsCollaborators(pkgName, usr, opts)

access['ls-collaborators'] = access.lsCollaborators = async ([pkg, usr], opts) => {
const pkgName = await getPackage(pkg, false)
const collabs = await libaccess.lsCollaborators(pkgName, usr, opts)
// TODO - print these out nicely (breaking change)
this.npm.output(JSON.stringify(collabs, null, 2))
}

// TODO - print these out nicely (breaking change)
output(JSON.stringify(collabs, null, 2))
}
async edit () {
throw new Error('edit subcommand is not implemented yet')
}

access.edit = () =>
Promise.reject(new Error('edit subcommand is not implemented yet'))

const modifyPackage = (pkg, opts, fn, requireScope = true) =>
getPackage(pkg, requireScope)
.then(pkgName => otplease(opts, opts => fn(pkgName, opts)))

const getPackage = async (name, requireScope) => {
if (name && name.trim())
return name.trim()
else {
try {
const pkg = await readPackageJson(path.resolve(npm.prefix, 'package.json'))
name = pkg.name
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(
'no package name passed to command and no package.json found'
)
} else
throw err
modifyPackage (pkg, opts, fn, requireScope = true) {
return this.getPackage(pkg, requireScope)
.then(pkgName => otplease(opts, opts => fn(pkgName, opts)))
}

async getPackage (name, requireScope) {
if (name && name.trim())
return name.trim()
else {
try {
const pkg = await readPackageJson(path.resolve(this.npm.prefix, 'package.json'))
name = pkg.name
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(
'no package name passed to command and no package.json found'
)
} else
throw err
}

if (requireScope && !name.match(/^@[^/]+\/.*$/))
throw this.usageError('This command is only available for scoped packages.')
else
return name
}
}

if (requireScope && !name.match(/^@[^/]+\/.*$/))
throw UsageError('This command is only available for scoped packages.')
else
return name
usageError (msg) {
return Object.assign(new Error(`\nUsage: ${msg}\n\n` + this.usage), {
code: 'EUSAGE',
})
}
}

module.exports = Object.assign(cmd, { usage, completion, subcommands })
module.exports = Access
Loading