import input from "@inquirer/input";
import select from "@inquirer/select";
import search from "@inquirer/search";
import confirm from "@inquirer/confirm";
import { Context } from "../../../bundler/context.js";
import { logOutput } from "../../../bundler/log.js";
/**
* Handle ExitPromptError thrown by @inquirer/* packages when the user
* presses Ctrl+C. Instead of printing an ugly stack trace, exit cleanly.
* For unexpected errors, use ctx.crash to report to Sentry.
*/
function handlePromptError(ctx: Context) {
return async (error: unknown): Promise<never> => {
if (error instanceof Error && error.name === "ExitPromptError") {
// User pressed Ctrl+C — exit silently with code 130 (standard for SIGINT)
// eslint-disable-next-line no-process-exit
process.exit(130);
}
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Unexpected prompt error: ${String(error)}`,
errForSentry: error instanceof Error ? error : undefined,
});
};
}
export const promptString = async (
ctx: Context,
options: {
message: string;
default?: string;
},
): Promise<string> => {
if (process.stdin.isTTY) {
return input({
message: options.message,
...(options.default !== undefined ? { default: options.default } : {}),
}).catch(handlePromptError(ctx));
} else {
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Cannot prompt for input in non-interactive terminals. (${options.message})`,
});
}
};
export const promptOptions = async <V>(
ctx: Context,
options: {
message: string;
choices: Array<{ name: string; value: V }>;
default?: V;
prefix?: string;
suffix?: string;
},
): Promise<V> => {
if (process.stdin.isTTY) {
return select<V>({
message: options.message + (options.suffix ?? ""),
choices: options.choices,
...(options.default !== undefined ? { default: options.default } : {}),
...(options.prefix !== undefined
? { theme: { prefix: options.prefix } }
: {}),
}).catch(handlePromptError(ctx));
} else {
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Cannot prompt for input in non-interactive terminals. (${options.message})`,
});
}
};
export const promptSearch = async <V>(
ctx: Context,
options: {
message: string;
choices: Array<{ name: string; value: V }>;
default?: V;
},
): Promise<V> => {
if (process.stdin.isTTY) {
return search<V>({
message: options.message,
...(options.default !== undefined ? { default: options.default } : {}),
source: (input: string | undefined) => {
if (!input) return options.choices;
const term = input.toLowerCase();
return options.choices.filter((c) =>
c.name.toLowerCase().includes(term),
);
},
}).catch(handlePromptError(ctx));
} else {
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Cannot prompt for input in non-interactive terminals. (${options.message})`,
});
}
};
export const promptYesNo = async (
ctx: Context,
options: {
message: string;
default?: boolean;
prefix?: string;
nonInteractiveError?: string;
},
): Promise<boolean> => {
if (process.stdin.isTTY) {
return confirm({
message: options.message,
...(options.default !== undefined ? { default: options.default } : {}),
...(options.prefix !== undefined
? { theme: { prefix: options.prefix } }
: {}),
}).catch(handlePromptError(ctx));
} else {
logOutput(options.message);
return ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage:
options.nonInteractiveError ??
`Cannot prompt for input in non-interactive terminals. (${options.message})`,
});
}
};