@sanity/cli
Version:
Sanity CLI tool for managing Sanity installations, managing plugins, schemas and datasets
220 lines (189 loc) • 6.8 kB
text/typescript
import chalk from 'chalk'
import {cloneDeep, sortBy} from 'lodash'
import {baseCommands} from './commands'
import {debug} from './debug'
import cliOutputter from './outputters/cliOutputter'
import {cliPackageManager} from './packageManager'
import {prompt as cliPrompter} from './prompters/cliPrompter'
import {
type CliCommandArguments,
type CliCommandContext,
type CliCommandDefinition,
type CliCommandGroupDefinition,
type CliConfig,
type CliOutputter,
type CliPrompter,
type CommandRunnerOptions,
type ResolvedCliCommand,
type SanityJson,
} from './types'
import {getClientWrapper} from './util/clientWrapper'
import {
generateCommandDocumentation,
generateCommandsDocumentation,
} from './util/generateCommandsDocumentation'
import {type CliConfigResult} from './util/getCliConfig'
import {isCommandGroup} from './util/isCommandGroup'
import {getNoSuchCommandText} from './util/noSuchCommandText'
interface Handlers {
outputter: CliOutputter
prompter: CliPrompter
}
type CommandOrGroup = CliCommandDefinition | CliCommandGroupDefinition
export class CommandRunner {
public handlers: Handlers
public commands: CommandOrGroup[]
public commandGroups: Record<string, (CliCommandDefinition | CliCommandGroupDefinition)[]>
constructor(handlers: Handlers, commands: CommandOrGroup[] = baseCommands) {
this.handlers = handlers
this.commands = sortBy(commands, 'name')
this.commandGroups = {}
for (const cmd of this.commands) {
const group = ('group' in cmd && cmd.group) || 'default'
this.commandGroups[group] = this.commandGroups[group] || []
this.commandGroups[group].push(cmd)
}
if (!handlers.outputter || !handlers.prompter) {
throw new Error('`prompter` and `outputter` handlers must be defined')
}
}
async runCommand(
commandOrGroup: string,
args: CliCommandArguments,
options: CommandRunnerOptions,
): Promise<unknown> {
if (!commandOrGroup) {
this.handlers.outputter.print(generateCommandsDocumentation(this.commandGroups))
return Promise.resolve()
}
// We might modify arguments for easier parsing in subcommand
const cmdArgs = cloneDeep(args)
const subCommandName = args.argsWithoutOptions[0]
const commandInfo = this.resolveCommand(commandOrGroup, subCommandName)
if (!commandInfo) {
throw new Error(getNoSuchCommandText(commandOrGroup, subCommandName, this.commandGroups))
}
const command = commandInfo.command
if (!isCommandGroup(command) && command.group && command.group !== 'default') {
cmdArgs.argsWithoutOptions = args.argsWithoutOptions.slice(1)
}
const output = this.handlers.outputter
const prompt = this.handlers.prompter
const {cliConfig, ...commandOptions} = options
const apiClient = getClientWrapper(
cliConfig?.config?.api || null,
cliConfig?.path || (cliConfig?.version === 2 ? 'sanity.json' : 'sanity.cli.js'),
)
const context: CliCommandContext = {
output,
prompt,
apiClient,
chalk,
cliPackageManager,
...commandOptions,
commandRunner: this,
...getVersionedContextParams(cliConfig),
}
if (isCommandGroup(command)) {
return context.output.print(generateCommandsDocumentation(this.commandGroups, command.name))
}
if (typeof command.action !== 'function') {
const cmdName = command.name || commandOrGroup || '<unknown>'
debug(`Command "${cmdName}" doesnt have a valid "action"-property, showing help`)
const groupName = command.group && command.group !== 'default' ? command.group : null
return context.output.print(generateCommandDocumentation(command, groupName, subCommandName))
}
debug(`Running command "${command.name}"`)
return command.action(cmdArgs, context)
}
resolveCommand(commandOrGroup: string, subCommandName?: string): ResolvedCliCommand | null {
// First, see if there is a group with the given name, if a subcommand is provided
if (this.commandGroups[commandOrGroup] && subCommandName) {
debug(`Found group for name "${commandOrGroup}", resolving subcommand`)
const subCommand = this.resolveSubcommand(
this.commandGroups[commandOrGroup],
subCommandName,
commandOrGroup,
)
debug(
subCommand
? `Subcommand resolved to "${subCommand.commandName}"`
: `Subcommand with name "${subCommandName}" not found`,
)
return subCommand
}
// No group? See if there's a command within the default group
debug(`No group found with name "${commandOrGroup}", looking for command`)
const command = this.commandGroups.default.find((cmd) => cmd.name === commandOrGroup)
if (command) {
debug(`Found command in default group with name "${commandOrGroup}"`)
return {
command: command,
commandName: command.name,
parentName: 'default',
isGroup: false,
isCommand: true,
}
}
debug(`No default command with name "${commandOrGroup}" found, giving up`)
return null
}
resolveSubcommand(
group: (CliCommandDefinition | CliCommandGroupDefinition)[],
subCommandName: string,
parentGroupName: string,
): ResolvedCliCommand | null {
if (!subCommandName) {
return null
}
const subCommand = group.find((cmd) => cmd.name === subCommandName)
if (!subCommand) {
throw new Error(getNoSuchCommandText(subCommandName, parentGroupName, this.commandGroups))
}
return {
command: subCommand,
commandName: subCommandName,
parentName: parentGroupName,
isGroup: false,
isCommand: true,
}
}
resolveHelpForGroup(): ResolvedCliCommand {
const command = this.commandGroups.default.find((cmd) => cmd.name === 'help')
if (!command) {
throw new Error('Failed to find default `help` command')
}
return {
command,
commandName: 'help',
isGroup: false,
isCommand: true,
}
}
}
export function getCliRunner(commands: CommandOrGroup[]): CommandRunner {
return new CommandRunner(
{
outputter: cliOutputter,
prompter: cliPrompter,
},
commands,
)
}
function getVersionedContextParams(
cliConfig: CliConfigResult | null,
):
| {sanityMajorVersion: 2; cliConfig?: SanityJson; cliConfigPath?: string}
| {sanityMajorVersion: 3; cliConfig?: CliConfig; cliConfigPath?: string} {
return cliConfig?.version === 2
? {
sanityMajorVersion: 2,
cliConfig: cliConfig?.config || undefined,
cliConfigPath: cliConfig?.path || undefined,
}
: {
sanityMajorVersion: 3,
cliConfig: cliConfig?.config || undefined,
cliConfigPath: cliConfig?.path || undefined,
}
}