From 0c4e718d190af2856ff0b48439b7b2327785692b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Jan 2026 23:15:03 +0000 Subject: [PATCH 1/3] fix(cli-ux): handle prompt cancel Co-authored-by: me --- cli-ux.ts | 104 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 33 deletions(-) diff --git a/cli-ux.ts b/cli-ux.ts index 51b9f04..4c1c753 100644 --- a/cli-ux.ts +++ b/cli-ux.ts @@ -54,6 +54,37 @@ export class PromptCancelled extends 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 +232,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 +259,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 + }) }, } } From 1bb4d2ad27c88503102e296e326b177da63c9b2d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Jan 2026 23:21:17 +0000 Subject: [PATCH 2/3] fix(cli): suppress prompt cancel errors Co-authored-by: me --- process-course/edits/cli.ts | 20 ++++++++++++++++++++ src/cli.ts | 22 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/process-course/edits/cli.ts b/process-course/edits/cli.ts index 57ba20a..9173457 100644 --- a/process-course/edits/cli.ts +++ b/process-course/edits/cli.ts @@ -44,6 +44,24 @@ type CliUxOptions = { pathPicker?: PathPicker } +function handlePromptFailure( + message: string | null | undefined, + error: Error | undefined, + parser: Argv, +) { + if (error instanceof PromptCancelled) { + throw error + } + parser.showHelp() + if (message) { + throw new Error(message) + } + if (error) { + throw error + } + throw new Error('Unknown error') +} + export function buildCombinedOutputPath( video1Path: string, video2Path: string, @@ -373,6 +391,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..451610c 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' @@ -43,6 +43,24 @@ type CliUxContext = { pathPicker?: PathPicker } +function handlePromptFailure( + message: string | null | undefined, + error: Error | undefined, + parser: Argv, +) { + if (error instanceof PromptCancelled) { + throw error + } + parser.showHelp() + if (message) { + throw new Error(message) + } + if (error) { + throw error + } + throw new Error('Unknown error') +} + async function main(rawArgs = hideBin(process.argv)) { const context = createCliUxContext() let args = rawArgs @@ -249,6 +267,8 @@ async function main(rawArgs = hideBin(process.argv)) { ) .demandCommand(1) .strict() + .fail(handlePromptFailure) + .exitProcess(false) .help() await parser.parseAsync() From df0c9157cd7cb727e55da72b73e40e63b9efd1d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 26 Jan 2026 23:36:56 +0000 Subject: [PATCH 3/3] refactor: centralize handlePromptFailure function to eliminate duplication Move handlePromptFailure from src/cli.ts and process-course/edits/cli.ts to cli-ux.ts to prevent maintenance issues and ensure consistent behavior across CLI entry points. --- cli-ux.ts | 18 ++++++++++++++++++ process-course/edits/cli.ts | 19 +------------------ src/cli.ts | 19 +------------------ 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/cli-ux.ts b/cli-ux.ts index 4c1c753..e9ccf7a 100644 --- a/cli-ux.ts +++ b/cli-ux.ts @@ -54,6 +54,24 @@ 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 ( diff --git a/process-course/edits/cli.ts b/process-course/edits/cli.ts index 9173457..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, @@ -44,24 +45,6 @@ type CliUxOptions = { pathPicker?: PathPicker } -function handlePromptFailure( - message: string | null | undefined, - error: Error | undefined, - parser: Argv, -) { - if (error instanceof PromptCancelled) { - throw error - } - parser.showHelp() - if (message) { - throw new Error(message) - } - if (error) { - throw error - } - throw new Error('Unknown error') -} - export function buildCombinedOutputPath( video1Path: string, video2Path: string, diff --git a/src/cli.ts b/src/cli.ts index 451610c..c1393e6 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -28,6 +28,7 @@ import { createInquirerPrompter, createPathPicker, createStepProgressReporter, + handlePromptFailure, isInteractive, pauseActiveSpinner, resumeActiveSpinner, @@ -43,24 +44,6 @@ type CliUxContext = { pathPicker?: PathPicker } -function handlePromptFailure( - message: string | null | undefined, - error: Error | undefined, - parser: Argv, -) { - if (error instanceof PromptCancelled) { - throw error - } - parser.showHelp() - if (message) { - throw new Error(message) - } - if (error) { - throw error - } - throw new Error('Unknown error') -} - async function main(rawArgs = hideBin(process.argv)) { const context = createCliUxContext() let args = rawArgs