Utilities for graceful handling of user cancellation across all prompt types with consistent behavior.
Core utility for detecting when a user cancels a prompt operation.
/**
* Guards against cancellation by detecting the cancellation symbol
* @param value - The value returned from any prompt function
* @returns true if the value represents cancellation, false otherwise
*/
function isCancel(value: any): boolean;Usage Examples:
import { text, isCancel, cancel } from "@clack/prompts";
// Basic cancellation handling
const name = await text({
message: "What's your name?",
});
if (isCancel(name)) {
cancel("Operation cancelled by user");
process.exit(0);
}
// name is now guaranteed to be a string
console.log(`Hello, ${name}!`);
// Multiple prompt cancellation handling
const email = await text({
message: "What's your email?",
});
if (isCancel(email)) {
cancel("Email input cancelled");
process.exit(0);
}
const confirm = await confirm({
message: "Subscribe to newsletter?",
});
if (isCancel(confirm)) {
cancel("Subscription prompt cancelled");
process.exit(0);
}The most common pattern for handling cancellation:
import { text, isCancel, cancel } from "@clack/prompts";
async function getUserInput() {
const result = await text({
message: "Enter a value:",
});
if (isCancel(result)) {
cancel("Input cancelled");
process.exit(0);
}
return result; // TypeScript knows this is a string
}import { isCancel, cancel } from "@clack/prompts";
function handleCancel<T>(value: T | symbol, message: string = "Operation cancelled"): T {
if (isCancel(value)) {
cancel(message);
process.exit(0);
}
return value as T;
}
// Usage
const name = handleCancel(
await text({ message: "Your name:" }),
"Name input cancelled"
);
const framework = handleCancel(
await select({
message: "Choose framework:",
options: [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
],
}),
"Framework selection cancelled"
);import { text, isCancel, cancel, spinner } from "@clack/prompts";
async function setupProject() {
const s = spinner();
try {
const projectName = await text({
message: "Project name:",
});
if (isCancel(projectName)) {
cancel("Project setup cancelled");
return; // Exit gracefully without process.exit
}
s.start("Creating project");
// Setup logic here
await new Promise(resolve => setTimeout(resolve, 1000));
s.stop("Project created successfully");
} catch (error) {
if (s) s.stop("Setup failed", 1);
throw error;
}
}
// Call without process.exit in the cancellation handler
try {
await setupProject();
} catch (error) {
console.error("Setup failed:", error.message);
process.exit(1);
}import * as p from "@clack/prompts";
async function collectUserData() {
const prompts = [
() => p.text({ message: "Name:" }),
() => p.text({ message: "Email:" }),
() => p.select({
message: "Role:",
options: [
{ value: "admin", label: "Administrator" },
{ value: "user", label: "User" },
],
}),
];
const results: any[] = [];
for (const [index, promptFn] of prompts.entries()) {
const result = await promptFn();
if (p.isCancel(result)) {
p.cancel(`Data collection cancelled at step ${index + 1}`);
process.exit(0);
}
results.push(result);
}
return {
name: results[0],
email: results[1],
role: results[2],
};
}import { text, isCancel, cancel, confirm } from "@clack/prompts";
async function robustInput(message: string, maxRetries: number = 3): Promise<string> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const result = await text({
message: attempt === 1 ? message : `${message} (attempt ${attempt}/${maxRetries})`,
validate: (value) => {
if (!value.trim()) return "Value cannot be empty";
}
});
if (isCancel(result)) {
if (attempt < maxRetries) {
const retry = await confirm({
message: "Do you want to try again?",
initialValue: true,
});
if (isCancel(retry) || !retry) {
cancel("Input cancelled");
process.exit(0);
}
continue;
} else {
cancel("Maximum attempts exceeded");
process.exit(0);
}
}
return result;
}
throw new Error("Should not reach here");
}import * as p from "@clack/prompts";
interface UserProfile {
name?: string;
email?: string;
age?: number;
preferences?: string[];
}
async function collectProfile(): Promise<UserProfile> {
const profile: UserProfile = {};
// Name (required)
const name = await p.text({ message: "Name (required):" });
if (p.isCancel(name)) {
p.cancel("Profile creation cancelled");
process.exit(0);
}
profile.name = name;
// Email (optional)
const email = await p.text({ message: "Email (optional):" });
if (p.isCancel(email)) {
p.note("Profile creation cancelled, but name was saved", "Partial Progress");
return profile; // Return partial results
}
profile.email = email;
// Age (optional)
const age = await p.text({
message: "Age (optional):",
validate: (value) => {
if (value && isNaN(Number(value))) return "Please enter a valid number";
}
});
if (p.isCancel(age)) {
p.note("Profile creation cancelled, but name and email were saved", "Partial Progress");
return profile;
}
if (age) profile.age = Number(age);
return profile;
}import * as p from "@clack/prompts";
const results = await p.group(
{
name: () => p.text({ message: "Name:" }),
email: () => p.text({ message: "Email:" }),
framework: () => p.select({
message: "Framework:",
options: [
{ value: "react", label: "React" },
{ value: "vue", label: "Vue" },
],
}),
},
{
// Centralized cancellation handling for the entire group
onCancel: ({ results }) => {
p.cancel("Setup wizard cancelled");
// Log partial results for debugging
if (Object.keys(results).length > 0) {
console.log("Partial results collected:", results);
}
process.exit(0);
},
}
);
// If we reach here, all prompts completed successfully
console.log("Setup completed:", results);import * as p from "@clack/prompts";
// Standard CLI cancellation
process.on('SIGINT', () => {
p.cancel("\nOperation interrupted by user");
process.exit(0);
});
async function main() {
p.intro("My CLI Tool");
try {
const input = await p.text({ message: "Enter command:" });
if (p.isCancel(input)) {
p.cancel("Command cancelled");
process.exit(0);
}
// Process command
p.outro("Command executed successfully");
} catch (error) {
p.cancel(`Error: ${error.message}`);
process.exit(1);
}
}import * as p from "@clack/prompts";
async function setupWizard() {
p.intro("Setup Wizard");
const steps = [
"Basic Information",
"Configuration",
"Confirmation"
];
let currentStep = 0;
const cleanup = () => {
p.cancel(`Setup cancelled at: ${steps[currentStep]}`);
p.note("You can resume setup later by running this command again", "Info");
};
try {
// Step 1
currentStep = 0;
const basicInfo = await p.group({
name: () => p.text({ message: "Project name:" }),
type: () => p.select({
message: "Project type:",
options: [
{ value: "web", label: "Web App" },
{ value: "api", label: "API" },
],
}),
}, { onCancel: cleanup });
// Step 2
currentStep = 1;
const config = await p.multiselect({
message: "Select features:",
options: [
{ value: "auth", label: "Authentication" },
{ value: "db", label: "Database" },
],
});
if (p.isCancel(config)) {
cleanup();
return;
}
// Step 3
currentStep = 2;
const confirmed = await p.confirm({
message: "Proceed with setup?",
});
if (p.isCancel(confirmed)) {
cleanup();
return;
}
if (!confirmed) {
p.cancel("Setup cancelled by user choice");
return;
}
p.outro("Setup completed successfully!");
} catch (error) {
p.cancel(`Setup failed: ${error.message}`);
process.exit(1);
}
}