heroku
Version:
CLI to interact with Heroku
236 lines (195 loc) • 5.88 kB
JavaScript
const fs = require('node:fs')
const inquirer = require('inquirer')
function choicesPrompt(description, choices, required, defaultValue) {
return inquirer.prompt([{
type: 'list',
name: 'choices',
message: description,
choices,
default: defaultValue,
validate(input) {
if (!required || input) {
return true
}
return `${description} is required`
},
}])
}
function prompt(description, required) {
return inquirer.prompt([{
type: 'input',
name: 'input',
message: description,
validate(input) {
if (!required || input.trim()) {
return true
}
return `${description} is required`
},
}])
}
function filePrompt(description, defaultPath) {
return inquirer.prompt([{
type: 'input',
name: 'path',
message: description,
default: defaultPath,
validate(input) {
if (fs.existsSync(input)) {
return true
}
return 'File does not exist. Please enter a valid file path.'
},
}])
}
const showBooleanPrompt = async (commandFlag, userInputMap, defaultOption) => {
const {description, name: flagOrArgName} = commandFlag
const {choices} = await choicesPrompt(description, [
{name: 'yes', value: true},
{name: 'no', value: false},
], defaultOption)
// user cancelled
if (choices === undefined || choices === 'Cancel') {
return true
}
if (choices) {
userInputMap.set(flagOrArgName, {input: true})
}
return false
}
const showOtherDialog = async (commandFlagOrArg, userInputMap) => {
const {description, default: defaultValue, options, required, name: flagOrArgName} = commandFlagOrArg
let input
const isFileInput = description?.includes('absolute path')
if (isFileInput) {
input = await filePrompt(description, '')
} else if (options) {
const choices = options.map(option => ({name: option, value: option}))
input = await choicesPrompt(`Select the ${description}`, choices, required, defaultValue)
} else {
input = await prompt(`${description.slice(0, 1).toUpperCase()}${description.slice(1)} (${required ? 'required' : 'optional - press "Enter" to bypass'})`, required)
}
if (input === undefined) {
return true
}
if (input !== '') {
userInputMap.set(flagOrArgName, input)
}
return false
}
function collectInputsFromManifest(flagsOrArgsManifest, omitOptional) {
const requiredInputs = []
const optionalInputs = []
// Prioritize options over booleans to
// prevent the user from yo-yo back and
// forth between the different input dialogs
const keysByType = Object.keys(flagsOrArgsManifest).sort((a, b) => {
const {type: aType} = flagsOrArgsManifest[a]
const {type: bType} = flagsOrArgsManifest[b]
if (aType === bType) {
return 0
}
if (aType === 'option') {
return -1
}
if (bType === 'option') {
return 1
}
return 0
})
keysByType.forEach(key => {
const isRequired = Reflect.get(flagsOrArgsManifest[key], 'required');
(isRequired ? requiredInputs : optionalInputs).push(key)
})
// Prioritize required inputs
// over optional inputs when
// prompting the user.
// required inputs are sorted
// alphabetically. optional
// inputs are sorted alphabetically
// and then pushed to the end of
// the list.
requiredInputs.sort((a, b) => {
if (a < b) {
return -1
}
if (a > b) {
return 1
}
return 0
})
// Include optional only when not explicitly omitted
return omitOptional ? requiredInputs : [...requiredInputs, ...optionalInputs]
}
async function getInput(flagsOrArgsManifest, userInputMap, omitOptional) {
const flagsOrArgs = collectInputsFromManifest(flagsOrArgsManifest, omitOptional)
for (const flagOrArg of flagsOrArgs) {
const {name, description, type, hidden} = flagsOrArgsManifest[flagOrArg]
if (userInputMap.has(name)) {
continue
}
// hidden args and flags may be exposed later
// based on the user type. For now, skip them.
if (!description || hidden) {
continue
}
const cancelled = await (type === 'boolean' ? showBooleanPrompt : showOtherDialog)(flagsOrArgsManifest[flagOrArg], userInputMap)
if (cancelled) {
return true
}
}
return false
}
async function promptForInputs(commandName, commandManifest, userArgs, userFlags) {
const {args, flags} = commandManifest
const userInputByArg = new Map()
Object.keys(args).forEach((argKey, index) => {
if (userArgs[index]) {
userInputByArg.set(argKey, userArgs[index])
}
})
let cancelled = await getInput(args, userInputByArg)
if (cancelled) {
return {userInputByArg}
}
const userInputByFlag = new Map()
Object.keys(flags).forEach(flagKey => {
const {name, char} = flags[flagKey]
if (userFlags[name] || userFlags[char]) {
userInputByFlag.set(flagKey, userFlags[flagKey])
}
})
cancelled = await getInput(flags, userInputByFlag)
if (cancelled) {
return
}
return {userInputByArg, userInputByFlag}
}
module.exports.promptUser = async (config, commandName, args, flags) => {
const commandMeta = config.findCommand(commandName)
if (!commandMeta) {
process.stderr.write(`"${commandName}" not a valid command\n$ `)
return
}
const {userInputByArg, userInputByFlag} = await promptForInputs(commandName, commandMeta, args, flags)
try {
for (const [, {input: argValue}] of userInputByArg) {
if (argValue) {
args.push(argValue)
}
}
for (const [flagName, {input: flagValue}] of userInputByFlag) {
if (!flagValue) {
continue
}
if (flagValue === true) {
args.push(`--${flagName}`)
continue
}
args.push(`--${flagName}`, flagValue)
}
return args
} catch (error) {
process.stderr.write(error.message)
}
}