Effortlessly build beautiful command-line apps - an opinionated, pre-styled wrapper around @clack/core
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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);
}
}