Mini Shell
'use strict'
const Buffer = require('safe-buffer').Buffer
const promisify = require('./util.js').promisify
const child = require('./child')
const fs = require('fs')
const parseArgs = require('./parse-args.js')
const path = require('path')
const which = promisify(require('which'))
module.exports = npx
module.exports.parseArgs = parseArgs
function npx (argv) {
const shell = argv['shell-auto-fallback']
if (shell || shell === '') {
const fallback = require('./auto-fallback.js')(
shell, process.env.SHELL, argv
)
if (fallback) {
return console.log(fallback)
} else {
process.exitCode = 1
return
}
}
if (!argv.call && (!argv.command || !argv.package)) {
!argv.q && console.error(Y()`\nERROR: You must supply a command.\n`)
!argv.q && parseArgs.showHelp()
process.exitCode = 1
return
}
const startTime = Date.now()
// First, we look to see if we're inside an npm project, and grab its
// bin path. This is exactly the same as running `$ npm bin`.
return localBinPath(process.cwd()).then(local => {
if (local) {
// Local project paths take priority. Go ahead and prepend it.
process.env.PATH = `${local}${path.delimiter}${process.env.PATH}`
}
return Promise.all([
// Figuring out if a command exists, early on, lets us maybe
// short-circuit a few things later. This bit here primarily benefits
// calls like `$ npx foo`, where we might just be trying to invoke
// a single command and use whatever is already in the path.
argv.command && getExistingPath(argv.command, argv),
// The `-c` flag involves special behavior when used: in this case,
// we take a bit of extra time to pick up npm's full lifecycle script
// environment (so you can use `$npm_package_xxxxx` and company).
// Without that flag, we just use the current env.
argv.call && local && getEnv(argv)
]).then(args => {
const existing = args[0]
const newEnv = args[1]
if (newEnv) {
// NOTE - we don't need to manipulate PATH further here, because
// npm has already done so. And even added the node-gyp path!
Object.assign(process.env, newEnv)
}
if ((!existing && !argv.call) || argv.packageRequested) {
// We only fire off the updateNotifier if we're installing things
if (argv.npxPkg) {
try {
require('update-notifier')({
pkg: require(argv.npxPkg)
}).notify()
} catch (e) {}
}
// Some npm packages need to be installed. Let's install them!
return ensurePackages(argv.package, argv).then(results => {
if (results && results.added && results.updated && !argv.q) {
console.error(Y()`npx: installed ${
results.added.length + results.updated.length
} in ${(Date.now() - startTime) / 1000}s`)
}
if (
argv.command &&
!existing &&
!argv.packageRequested &&
argv.package.length === 1
) {
return promisify(fs.readdir)(results.bin).then(bins => {
if (process.platform === 'win32') {
bins = bins.filter(b => b !== 'etc' && b !== 'node_modules')
}
if (bins.length < 1) {
throw new Error(Y()`command not found: ${argv.command}`)
}
const cmd = new RegExp(`^${argv.command}(?:\\.cmd)?$`, 'i')
const matching = bins.find(b => b.match(cmd))
return path.resolve(results.bin, bins[matching] || bins[0])
}, err => {
if (err.code === 'ENOENT') {
throw new Error(Y()`command not found: ${argv.command}`)
} else {
throw err
}
})
} else {
return existing
}
})
} else {
// We can skip any extra installation, 'cause everything exists.
return existing
}
}).then(existing => {
return execCommand(existing, argv)
}).catch(err => {
!argv.q && console.error(err.message)
process.exitCode = err.exitCode || 1
})
})
}
module.exports._localBinPath = localBinPath
function localBinPath (cwd) {
return require('./get-prefix.js')(cwd).then(prefix => {
return prefix && path.join(prefix, 'node_modules', '.bin')
})
}
module.exports._getEnv = getEnv
function getEnv (opts) {
const args = ['run', 'env', '--parseable']
return findNodeScript(opts.npm, {isLocal: true}).then(npmPath => {
if (npmPath) {
args.unshift(child.escapeArg(opts.npm))
return process.argv[0]
} else {
return opts.npm
}
}).then(npmPath => {
return child.exec(npmPath, args)
}).then(require('dotenv').parse)
}
module.exports._ensurePackages = ensurePackages
function ensurePackages (specs, opts) {
return (
opts.cache ? Promise.resolve(opts.cache) : getNpmCache(opts)
).then(cache => {
const prefix = path.join(cache, '_npx', process.pid.toString())
const bins = process.platform === 'win32'
? prefix
: path.join(prefix, 'bin')
const rimraf = require('rimraf')
process.on('exit', () => rimraf.sync(prefix))
return promisify(rimraf)(bins).then(() => {
return installPackages(specs, prefix, opts)
}).then(info => {
// This will make temp bins _higher priority_ than even local bins.
// This is intentional, since npx assumes that if you went through
// the trouble of doing `-p`, you're rather have that one. Right? ;)
process.env.PATH = `${bins}${path.delimiter}${process.env.PATH}`
if (!info) { info = {} }
info.prefix = prefix
info.bin = bins
return info
})
})
}
module.exports._getExistingPath = getExistingPath
function getExistingPath (command, opts) {
if (opts.isLocal) {
return Promise.resolve(command)
} else if (
opts.cmdHadVersion || opts.packageRequested || opts.ignoreExisting
) {
return Promise.resolve(false)
} else {
return which(command).catch(err => {
if (err.code === 'ENOENT') {
if (opts.install === false) {
err.exitCode = 127
throw err
}
} else {
throw err
}
})
}
}
module.exports._getNpmCache = getNpmCache
function getNpmCache (opts) {
const args = ['config', 'get', 'cache', '--parseable']
if (opts.userconfig) {
args.push('--userconfig', child.escapeArg(opts.userconfig, true))
}
return findNodeScript(opts.npm, {isLocal: true}).then(npmPath => {
if (npmPath) {
// This one is NOT escaped as a path because it's handed to Node.
args.unshift(child.escapeArg(opts.npm))
return process.argv[0]
} else {
return opts.npm
}
}).then(npmPath => {
return child.exec(npmPath, args)
}).then(cache => cache.trim())
}
module.exports._buildArgs = buildArgs
function buildArgs (specs, prefix, opts) {
const args = ['install'].concat(specs)
args.push('--global', '--prefix', prefix)
if (opts.cache) args.push('--cache', opts.cache)
if (opts.userconfig) args.push('--userconfig', opts.userconfig)
args.push('--loglevel', 'error', '--json')
return args
}
module.exports._installPackages = installPackages
function installPackages (specs, prefix, opts) {
const args = buildArgs(specs, prefix, opts)
return findNodeScript(opts.npm, {isLocal: true}).then(npmPath => {
if (npmPath) {
args.unshift(
process.platform === 'win32'
? child.escapeArg(opts.npm)
: opts.npm
)
return process.argv[0]
} else {
return opts.npm
}
}).then(npmPath => {
return process.platform === 'win32' ? child.escapeArg(npmPath, true) : npmPath
}).then(npmPath => {
return child.spawn(npmPath, args, {
stdio: opts.installerStdio
? opts.installerStdio
: [0, 'pipe', opts.q ? 'ignore' : 2]
}).then(deets => {
try {
return deets.stdout ? JSON.parse(deets.stdout) : null
} catch (e) { }
}, err => {
if (err.exitCode) {
err.message = Y()`Install for ${specs} failed with code ${err.exitCode}`
}
throw err
})
})
}
module.exports._execCommand = execCommand
function execCommand (_existing, argv) {
return findNodeScript(_existing, argv).then(existing => {
const argvCmdOpts = argv.cmdOpts || []
if (existing && !argv.alwaysSpawn && !argv.nodeArg && !argv.shell && existing !== process.argv[1]) {
const Module = require('module')
// let it take over the process. This means we can skip node startup!
if (!argv.noYargs) {
// blow away built-up yargs crud
require('yargs').reset()
}
process.argv = [
process.argv[0], // Current node binary
existing // node script path. `runMain()` will set this as the new main
].concat(argvCmdOpts) // options for the cmd itself
Module.runMain() // ✨MAGIC✨. Sorry-not-sorry
} else if (!existing && argv.nodeArg && argv.nodeArg.length) {
throw new Error(Y()`ERROR: --node-arg/-n can only be used on packages with node scripts.`)
} else {
let cmd = existing
let cmdOpts = argvCmdOpts
if (existing) {
cmd = process.argv[0]
if (process.platform === 'win32') {
cmd = child.escapeArg(cmd, true)
}
// If we know we're running a run script and we got a --node-arg,
// we need to fudge things a bit to get them working right.
cmdOpts = argv.nodeArg
if (cmdOpts) {
cmdOpts = Array.isArray(cmdOpts) ? cmdOpts : [cmdOpts]
} else {
cmdOpts = []
}
// It's valid for a single arg to be a string of multiple
// space-separated node args.
// Example: `$ npx -n '--inspect --harmony --debug' ...`
cmdOpts = cmdOpts.reduce((acc, arg) => {
return acc.concat(arg.split(/\s+/))
}, [])
cmdOpts = cmdOpts.concat(existing, argvCmdOpts)
}
const opts = Object.assign({}, argv, { cmdOpts })
return child.runCommand(cmd, opts).catch(err => {
if (err.isOperational && err.exitCode) {
// At this point, we want to treat errors from the child as if
// we were just running the command. That means no extra msg logging
process.exitCode = err.exitCode
} else {
// But if it's not just a regular child-level error, blow up normally
throw err
}
})
}
})
}
module.exports._findNodeScript = findNodeScript
function findNodeScript (existing, opts) {
if (!existing) {
return Promise.resolve(false)
} else {
return promisify(fs.stat)(existing).then(stat => {
if (opts && opts.isLocal && path.extname(existing) === '.js') {
return existing
} else if (opts && opts.isLocal && stat.isDirectory()) {
// npx will execute the directory itself
try {
const pkg = require(path.resolve(existing, 'package.json'))
const target = path.resolve(existing, pkg.bin || pkg.main || 'index.js')
return findNodeScript(target, opts).then(script => {
if (script) {
return script
} else {
throw new Error(Y()`command not found: ${target}`)
}
})
} catch (e) {
throw new Error(Y()`command not found: ${existing}`)
}
} else if (process.platform !== 'win32') {
const bytecount = 400
const buf = Buffer.alloc(bytecount)
return promisify(fs.open)(existing, 'r').then(fd => {
return promisify(fs.read)(fd, buf, 0, bytecount, 0).then(() => {
return promisify(fs.close)(fd)
}, err => {
return promisify(fs.close)(fd).then(() => { throw err })
})
}).then(() => {
const re = /#!\s*(?:\/usr\/bin\/env\s*node|\/usr\/local\/bin\/node|\/usr\/bin\/node)\s*\r?\n/i
return buf.toString('utf8').match(re) && existing
})
} else if (process.platform === 'win32') {
const buf = Buffer.alloc(1000)
return promisify(fs.open)(existing, 'r').then(fd => {
return promisify(fs.read)(fd, buf, 0, 1000, 0).then(() => {
return promisify(fs.close)(fd)
}, err => {
return promisify(fs.close)(fd).then(() => { throw err })
})
}).then(() => {
return buf.toString('utf8').trim()
}).then(str => {
const cmd = /"%~dp0\\node\.exe"\s+"%~dp0\\(.*)"\s+%\*/
const mingw = /"\$basedir\/node"\s+"\$basedir\/(.*)"\s+"\$@"/i
return str.match(cmd) || str.match(mingw)
}).then(match => {
return match && path.join(path.dirname(existing), match[1])
})
}
})
}
}
function Y () {
return require('./y.js')
}
Zerion Mini Shell 1.0