diff --git a/cli-ux.ts b/cli-ux.ts index 51b9f04..e9ccf7a 100644 --- a/cli-ux.ts +++ b/cli-ux.ts @@ -54,6 +54,55 @@ export class PromptCancelled extends Error { } } +export function handlePromptFailure( + message: string | null | undefined, + error: Error | undefined, + parser: { showHelp: () => void }, +) { + if (error instanceof PromptCancelled) { + throw error + } + parser.showHelp() + if (message) { + throw new Error(message) + } + if (error) { + throw error + } + throw new Error('Unknown error') +} + +function isExitPromptError(error: unknown) { + if (error instanceof Error) { + return ( + error.name === 'ExitPromptError' || + error.message.includes('User force closed the prompt') + ) + } + if (error && typeof error === 'object' && 'name' in error) { + return (error as { name?: unknown }).name === 'ExitPromptError' + } + return false +} + +function handlePromptError(error: unknown): never { + if (error instanceof PromptCancelled) { + throw error + } + if (isExitPromptError(error)) { + throw new PromptCancelled() + } + throw error +} + +async function runPrompt(action: () => Promise): Promise { + try { + return await action() + } catch (error) { + return handlePromptError(error) + } +} + export function resolveOptionalString(value: unknown) { if (typeof value !== 'string') { return undefined @@ -201,22 +250,25 @@ export async function withSpinner( export function createInquirerPrompter(): Prompter { return { async select(message: string, choices: PromptChoice[]) { - const { result } = await inquirer.prompt<{ result: T }>([ - { - type: 'list', - name: 'result', - message, - choices, - }, - ]) - return result + return runPrompt(async () => { + const { result } = await inquirer.prompt<{ result: T }>([ + { + type: 'list', + name: 'result', + message, + choices, + }, + ]) + return result + }) }, async search(message: string, choices: PromptChoice[]) { - const result = await searchPrompt({ - message, - source: async (input) => filterPromptChoices(choices, input), - }) - return result + return runPrompt(() => + searchPrompt({ + message, + source: async (input) => filterPromptChoices(choices, input), + }), + ) }, async input( message: string, @@ -225,27 +277,31 @@ export function createInquirerPrompter(): Prompter { validate?: (value: string) => true | string | Promise }, ) { - const { result } = await inquirer.prompt<{ result: string }>([ - { - type: 'input', - name: 'result', - message, - default: options?.defaultValue, - validate: options?.validate, - }, - ]) - return result + return runPrompt(async () => { + const { result } = await inquirer.prompt<{ result: string }>([ + { + type: 'input', + name: 'result', + message, + default: options?.defaultValue, + validate: options?.validate, + }, + ]) + return result + }) }, async confirm(message: string, options?: { defaultValue?: boolean }) { - const { result } = await inquirer.prompt<{ result: boolean }>([ - { - type: 'confirm', - name: 'result', - message, - default: options?.defaultValue ?? false, - }, - ]) - return result + return runPrompt(async () => { + const { result } = await inquirer.prompt<{ result: boolean }>([ + { + type: 'confirm', + name: 'result', + message, + default: options?.defaultValue ?? false, + }, + ]) + return result + }) }, } } diff --git a/process-course/edits/cli.ts b/process-course/edits/cli.ts index 57ba20a..6b597cd 100644 --- a/process-course/edits/cli.ts +++ b/process-course/edits/cli.ts @@ -8,6 +8,7 @@ import { createInquirerPrompter, createPathPicker, createStepProgressReporter, + handlePromptFailure, isInteractive, pauseActiveSpinner, resolveOptionalString, @@ -373,6 +374,8 @@ export async function runEditsCli(rawArgs = hideBin(process.argv)) { ) .demandCommand(1) .strict() + .fail(handlePromptFailure) + .exitProcess(false) .help() await parser.parseAsync() diff --git a/src/cli.ts b/src/cli.ts index c60c87c..c1393e6 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun import path from 'node:path' -import type { Arguments, CommandBuilder, CommandHandler } from 'yargs' +import type { Argv, Arguments, CommandBuilder, CommandHandler } from 'yargs' import yargs from 'yargs/yargs' import { hideBin } from 'yargs/helpers' import { startAppServer } from './app-server' @@ -28,6 +28,7 @@ import { createInquirerPrompter, createPathPicker, createStepProgressReporter, + handlePromptFailure, isInteractive, pauseActiveSpinner, resumeActiveSpinner, @@ -249,6 +250,8 @@ async function main(rawArgs = hideBin(process.argv)) { ) .demandCommand(1) .strict() + .fail(handlePromptFailure) + .exitProcess(false) .help() await parser.parseAsync()