smpn
Version:
Better NPM CLI search
231 lines (215 loc) • 6.79 kB
JavaScript
const got = require('got')
const open = require('open')
const chalk = require('chalk')
const program = require('commander')
const inquirer = require('inquirer')
const install = require('npm-install-package')
const pkg = require('./package.json')
program
.version(pkg.version)
.usage('[options] [search terms]')
.option('-a --author [name]', 'Search for a specific author')
.option('-s --scope [scope]', 'Restrict search to a specific scope')
.option(
'-k --keywords [keywords]',
'Filter results to packages with the specific keywords. Comma seperated, no spaces in between!'
)
.option('-d --not-deprecated', 'Restrict to non-deprecated results')
.option('-D --deprecated', 'Restrict to deprecated results')
.option('-u --not-unstable', 'Restrict to results above v1.0.0')
.option('-U --unstable', 'Restrict to results beneath v1.0.0')
.option('-i --not-insecure', 'Restrict to secure results')
.option('-I --insecure', 'Restrict to insecure/vulnerable results')
.option('-b --no-boost-exact', 'Do not boost exact search matches')
.option(
'-e --score-effect [number]',
'Set the score effect value. Default: 15.3'
)
.option(
'-q --quality-weight [number]',
'Specify a quality weight. Default: 1.95'
)
.option(
'-p --popularity-weight [number]',
'Specify a popularity weight. Default: 3.3'
)
.option(
'-m --maintenance-weight [number]',
'Specify a maintenance weight. Default: 2.05'
)
.parse(process.argv)
const plusify = terms => {
let result
if (!terms[0]) {
console.error('You need to pass search terms!')
process.exit()
}
if (
typeof terms === 'array' ? (result = terms) : (result = [terms.join('+')])
) {
if (program.author) result.push(`author:${program.author}`)
}
if (program.scope) result.push(`scope:${program.scope}`)
if (program.keywords) result.push(`keywords:${program.keywords}`)
if (program.notDeprecated || program.notUnstable || program.notInsecure) {
let not = []
if (program.notDeprecated) not.push('deprecated')
if (program.notUnstable) not.push('unstable')
if (program.notInsecure) not.push('insecure')
result.push(`not:${not.join(',')}`)
}
if (program.deprecated || program.unstable || program.insecure) {
let is = []
if (program.deprecated) is.push('deprecated')
if (program.unstable) is.push('unstable')
if (program.insecure) is.push('insecure')
result.push(`is:${is.join(',')}`)
}
if (program.noBoostExact) result.push('boost-exact:false')
if (program.scoreEffect) result.push(`score-effect:${program.scoreEffect}`)
if (program.qualityWeight) {
result.push(`quality-weight:${program.qualityWeight}`)
}
if (program.popularityWeight) {
result.push(`popularity-weight:${program.popularityWeight}`)
}
if (program.maintenanceWeight) {
result.push(`maintenance-weight:${program.maintenanceWeight}`)
}
return result.join('+')
}
const longest = a => {
let c = 0,
d = 0,
l = 0,
i = a.length
if (i) {
while (i--) {
d = a[i].length
if (d > c) {
l = i
c = d
}
}
}
return a[l].length
}
const stat = (s, type) => {
let f = Math.floor(s * 100).toString() + '%'
f = f.length < 3 ? f.padStart(3) : f
let output
switch (type) {
case 'quality':
output = chalk.green.dim(f)
break
case 'popularity':
output = chalk.yellow.dim(f)
break
case 'maintenance':
output = chalk.blue.dim(f)
break
default:
output = chalk.bgRed(f)
}
return output
}
const name = (n, longN) => {
return chalk.bold(padBack(n, longN))
}
const version = (v, longV) => {
return chalk.gray.dim(padBack(v, longV))
}
const padBack = (s, n) => {
return s.length < n ? s.padEnd(n) : s
}
const formatResult = (pkg, scores, longestName, longestVersion) => {
return {
name: `${name(pkg.name, longestName)} ${version(
pkg.version,
longestVersion
)} ${stat(scores.detail.quality, 'quality')} ${stat(
scores.detail.popularity,
'popularity'
)} ${stat(scores.detail.maintenance, 'maintenance')} ${stat(
scores.final
)} ${chalk.dim(pkg.description)}`,
value: {
name: pkg.name,
url: pkg.links.npm
}
}
}
const search = terms => {
got(`https://api.npms.io/v2/search?q=` + plusify(terms))
.then(res => {
const results = JSON.parse(res.body)
if (results.total === 0) return console.error('No results found!')
const longestName = longest(results.results.map(n => n.package.name))
const longestVersion = longest(
results.results.map(n => n.package.version)
)
const choices = results.results.map(n =>
formatResult(n.package, n.score, longestName, longestVersion)
)
inquirer
.prompt({
type: 'list',
name: 'lib',
message: `Here is what we found:
${chalk.green.dim('<Quality>')} ${chalk.yellow.dim(
'<Popularity>'
)} ${chalk.blue.dim('<Maintenance>')} ${chalk.bgRed('<Overall>')}`,
choices: [...choices, new inquirer.Separator()]
})
.then(answer => {
let vectors = [
'Open in browser',
'Install without saving',
'Install to dependencies',
'Install to devDependencies'
]
let choices = vectors.map(v => {
return {
name: v,
value: {
name: v,
lib: answer.lib
}
}
})
inquirer
.prompt({
type: 'list',
name: 'vector',
message: 'What do you want to do with this module?',
choices: [...choices, new inquirer.Separator()]
})
.then(ansr => {
if (ansr.vector.name === 'Open in browser') {
open(ansr.vector.lib.url)
return
}
const installOps = {
save: ansr.vector.name === 'Install to dependencies',
saveDev: ansr.vector.name === 'Install to devDependencies'
}
install(ansr.vector.lib.name, installOps, err => {
if (err) {
return console.error(
"Something went wrong with the install. I'm afraid you're on your own for this one."
)
}
console.log(
chalk.cyan.underline(
`Package ${ansr.vector.lib.name} installed successfully!`
)
)
})
})
// open(answer.lib.url)
})
})
.catch(e => console.log(e))
}
search(program.args)