cli-prompts-clack
Beautiful interactive CLI prompts with @clack/prompts and custom prompts with @clack/core
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/cli-prompts-clack/skills/cli-prompts-clack
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Clack CLI Prompts
Quick Guide: Use
@clack/promptsfor pre-styled interactive CLI prompts (text, select, multiselect, confirm, spinner, progress). CheckisCancel()after EVERY prompt call -- users can Ctrl+C at any point.cancel()only prints; exit after it, with a non-zero code. Usegroup()for multi-step flows with centralized cancellation. Use@clack/coreonly when building fully custom prompt UIs. ESM-only since v1.0.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST check isCancel() after EVERY prompt call -- skipping this causes silent crashes when users press Ctrl+C)
(You MUST exit the process after cancel() -- the cancel message prints but execution continues otherwise)
(You MUST exit with a NON-ZERO code on cancellation, taking the value from the CLI framework's exit-code table where one exists and using 130 where none does -- exiting 0 tells every caller the work succeeded)
(You MUST use group() with onCancel for multi-step flows -- it handles cancellation centrally so you don't check each prompt individually)
(You MUST call spinner.stop() before any other output -- overlapping spinner output with prompts or logs corrupts the terminal)
</critical_requirements>
Auto-detection: @clack/prompts, @clack/core, clack, isCancel, intro, outro, cancel, spinner, group, text prompt, select prompt, confirm prompt, multiselect, groupMultiselect, selectKey, note, log, tasks, progress, taskLog, stream, box, autocomplete, date prompt, path prompt, updateSettings
When to use:
- Building interactive CLI prompts (text input, selection, confirmation)
- Creating multi-step CLI wizards with progress indication
- Adding styled terminal output (notes, logs, boxes, spinners)
- Handling user cancellation gracefully across prompt flows
When NOT to use:
- Full terminal UI applications with persistent layout (use a terminal UI framework)
- Non-interactive scripts where stdin is piped (clack prompts require a TTY)
- Simple
y/nconfirmation that doesn't need styling (plain readline suffices)
Key patterns covered:
- Core prompts: text, password, select, multiselect, confirm, selectKey
- Session lifecycle: intro, outro, cancel, isCancel
- Progress: spinner, progress bar, tasks
- Composition: group with centralized cancellation
- Output: log, note, box, stream, taskLog
- Custom prompts with @clack/core primitives
- Validation, default values, and AbortSignal cancellation
Detailed Resources:
- examples/core.md - All prompt types, cancellation, spinner, progress, tasks, group, validation, output
- examples/advanced.md - Custom prompts with @clack/core, AbortSignal, streams, i18n, date/path/autocomplete
- reference.md - API quick reference, decision framework, prompt type comparison
<decision_framework>
Decision Framework
Need user input?
|
+-> Single value?
| +-> Free text -> text() or password()
| +-> One of N choices -> select() (list) or selectKey() (keyboard shortcut)
| +-> Yes/No -> confirm()
| +-> Date -> date()
| +-> File path -> path()
|
+-> Multiple values?
| +-> Flat list -> multiselect()
| +-> Grouped categories -> groupMultiselect()
| +-> Searchable -> autocomplete() or autocompleteMultiselect()
|
+-> Multiple prompts in sequence?
+-> group() with onCancel for centralized handling
Need to show progress?
|
+-> Indeterminate wait -> spinner()
+-> Known total steps -> progress()
+-> Sequential tasks -> tasks()
+-> Detailed logs per task -> taskLog()
Need styled output?
|
+-> Status message -> log.info/warn/error/success/step()
+-> Important notice -> note() or box()
+-> Streaming content -> stream.info/warn/error/success()
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Missing
isCancel()check after a prompt -- the return value isvalue | symbol, and using the symbol as a string crashes or produces garbage. Always check before using the value. - Missing
process.exit()aftercancel()--cancel()only prints a message, it does not stop execution. The process continues running. - Exiting
0aftercancel()-- a cancelled run reports success to every caller, socli && next-stepruns the next step against a half-finished state. Exit non-zero: the framework's cancellation constant, or130when there is no framework table. - Calling another prompt while spinner is active -- spinner output and prompt output overlap, corrupting the terminal display. Always call
spinner.stop()first. - Using
require()with @clack/prompts v1.0+ -- the package is ESM-only since v1.0. Useimportsyntax.
Medium Priority Issues:
- Not using
group()for multi-step flows -- checkingisCancel()after every single prompt is verbose and error-prone.group()withonCancelcentralizes this. - Ignoring the
validateoption -- prompts accept invalid input by default. Add validation for any input that has constraints. - Using
multiselectwithoutrequired: falsewhen zero selections should be valid -- by default, at least one item must be selected.
Gotchas & Edge Cases:
isCancel()returnstruefor the cancel symbol but also narrows the TypeScript type -- always use it as a type guard before accessing the valuespinner()returns an object, not a promise -- call.start()separatelygroup()prompt functions receive{ results }with all previously collected values, but TypeScript types each value as possibly undefined since earlier prompts might not have run yetconfirm()returnsboolean | symbol, not justboolean-- still needsisCancel()check when used outsidegroup()select()generic type parameter controls the return type --select<"react" | "vue">({...})narrows the resultlog.warnhas an aliaslog.warning-- both work identicallyprogress.advance()with no arguments advances by 1 -- the step parameter is optionalnote()andbox()are synchronous (not prompts) -- they returnvoid, not promises- All prompts accept
signal: AbortSignalfor programmatic cancellation (e.g., timeouts) updateSettings()applies globally -- call it once at startup, not per prompt- v1.1.0 replaced
picocolorswith Node.js built-instyleText-- requires Node.js 20.12+
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST check isCancel() after EVERY prompt call -- skipping this causes silent crashes when users press Ctrl+C)
(You MUST exit the process after cancel() -- the cancel message prints but execution continues otherwise)
(You MUST exit with a NON-ZERO code on cancellation, taking the value from the CLI framework's exit-code table where one exists and using 130 where none does -- exiting 0 tells every caller the work succeeded)
(You MUST use group() with onCancel for multi-step flows -- it handles cancellation centrally so you don't check each prompt individually)
(You MUST call spinner.stop() before any other output -- overlapping spinner output with prompts or logs corrupts the terminal)
Failure to follow these rules will cause silent process hangs, corrupted terminal output, and runtime crashes on user cancellation.
</critical_reminders>
Files (skills)
-
examples
-
advanced.md 8 KB
# Advanced Patterns > Related: [core.md](core.md) for basic prompts, cancellation, spinner, group, validation --- ## Exit codes used below Every snippet on this page assumes the same constant as [core.md](core.md#exit-codes-used-below). Where the CLI framework defines an exit-code table, use that table's cancellation constant instead. ```typescript // 128 + SIGINT(2), the value a shell reports for an interrupted command. export const EXIT_CANCELLED = 130; ``` --- ## Pattern 1: Custom Prompts with @clack/core Use `@clack/core` when you need a prompt UI that `@clack/prompts` doesn't provide. Each core prompt has a `render()` function that returns the terminal output string. ### Custom text prompt ```typescript import { TextPrompt, isCancel } from "@clack/core"; const p = new TextPrompt({ render() { const title = "What is your project name?"; const value = this.userInputWithCursor; switch (this.state) { case "initial": case "active": return `${title}\n> ${value}`; case "error": return `${title}\n> ${value}\n Error: ${this.error}`; case "submit": return `${title}\n ${this.value}`; case "cancel": return `${title}\n (cancelled)`; } }, validate(value) { if (!value) return "Name is required"; }, }); const result = await p.prompt(); if (isCancel(result)) { process.exit(EXIT_CANCELLED); } ``` ### Custom select prompt ```typescript import { SelectPrompt, isCancel } from "@clack/core"; interface Option { value: string; label: string; } const options: Option[] = [ { value: "small", label: "Small (1 CPU, 1GB RAM)" }, { value: "medium", label: "Medium (2 CPU, 4GB RAM)" }, { value: "large", label: "Large (4 CPU, 8GB RAM)" }, ]; const p = new SelectPrompt({ options, render() { const title = "Select instance size"; return `${title}\n${this.options .map((opt, i) => { const cursor = i === this.cursor ? ">" : " "; const selected = i === this.cursor ? "[*]" : "[ ]"; return ` ${cursor} ${selected} ${opt.label}`; }) .join("\n")}`; }, }); const result = await p.prompt(); if (isCancel(result)) { process.exit(EXIT_CANCELLED); } ``` ### Prompt state lifecycle The `this.state` property in the render function cycles through these values: | State | When | Typical Display | | ----------- | ----------------------------------- | ---------------------------- | | `"initial"` | First render, before user input | Show prompt with placeholder | | `"active"` | User is typing or navigating | Show current input/selection | | `"error"` | Validation failed | Show error message | | `"submit"` | User pressed Enter with valid input | Show confirmed value | | `"cancel"` | User pressed Ctrl+C | Show cancellation notice | --- ## Pattern 2: AbortSignal for Programmatic Cancellation All prompts accept `signal: AbortSignal` for timeout-based or programmatic cancellation. ### Timeout ```typescript import * as p from "@clack/prompts"; const PROMPT_TIMEOUT_MS = 30_000; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), PROMPT_TIMEOUT_MS); const name = await p.text({ message: "Project name?", signal: controller.signal, }); clearTimeout(timeout); if (p.isCancel(name)) { p.cancel("Timed out waiting for input."); process.exit(1); } ``` ### Cancellation from external event ```typescript import * as p from "@clack/prompts"; const controller = new AbortController(); // Cancel prompt if parent process sends signal process.on("SIGTERM", () => controller.abort()); const result = await p.select({ message: "Choose environment", options: [ { value: "dev", label: "Development" }, { value: "prod", label: "Production" }, ], signal: controller.signal, }); ``` --- ## Pattern 3: Spinner Cancellation ```typescript import * as p from "@clack/prompts"; const s = p.spinner({ onCancel: () => { // Cleanup resources when user cancels during spinner cleanupTempFiles(); }, cancelMessage: "Build cancelled by user", }); s.start("Building project..."); try { await build(); s.stop("Build complete"); } catch (error) { s.error("Build failed"); throw error; } ``` --- ## Pattern 4: Stream Output Stream content character-by-character or line-by-line for real-time output. ```typescript import * as p from "@clack/prompts"; import { createReadStream } from "node:fs"; // Stream from a file await p.stream.info(createReadStream("./build-log.txt", { encoding: "utf-8" })); // Stream from an async generator async function* generateOutput(): AsyncGenerator<string> { yield "Step 1: Preparing...\n"; await delay(500); yield "Step 2: Processing...\n"; await delay(500); yield "Step 3: Complete!\n"; } await p.stream.success(generateOutput()); ``` --- ## Pattern 5: Internationalization (i18n) ```typescript import * as p from "@clack/prompts"; // Set global messages for non-English CLIs p.updateSettings({ messages: { cancel: "Operacion cancelada", error: "Se produjo un error", }, }); // Now all prompts use translated cancel/error messages const name = await p.text({ message: "Nombre del proyecto?" }); ``` --- ## Pattern 6: Date and Path Prompts ### Date prompt ```typescript import * as p from "@clack/prompts"; const deadline = await p.date({ message: "Project deadline?", format: "YMD", // Also: "MDY", "DMY" }); if (p.isCancel(deadline)) { p.cancel("Cancelled."); process.exit(EXIT_CANCELLED); } // deadline is a Date object ``` ### Path prompt ```typescript import * as p from "@clack/prompts"; const configPath = await p.path({ message: "Path to config file?", root: process.cwd(), }); if (p.isCancel(configPath)) { p.cancel("Cancelled."); process.exit(EXIT_CANCELLED); } // configPath is a string (absolute or relative path) ``` ### Directory-only path ```typescript import * as p from "@clack/prompts"; const outputDir = await p.path({ message: "Output directory?", root: process.cwd(), directory: true, // Only show directories }); ``` --- ## Pattern 7: Autocomplete Prompts ### Single autocomplete ```typescript import * as p from "@clack/prompts"; const country = await p.autocomplete({ message: "Select your country", options: [ { value: "us", label: "United States" }, { value: "uk", label: "United Kingdom" }, { value: "de", label: "Germany" }, { value: "fr", label: "France" }, // ... many more options ], placeholder: "Type to search...", maxItems: 5, }); if (p.isCancel(country)) { p.cancel("Cancelled."); process.exit(EXIT_CANCELLED); } ``` ### Multi autocomplete ```typescript import * as p from "@clack/prompts"; const packages = await p.autocompleteMultiselect({ message: "Select packages to install", options: [ { value: "react", label: "react" }, { value: "vue", label: "vue" }, { value: "svelte", label: "svelte" }, { value: "solid", label: "solid-js" }, { value: "preact", label: "preact" }, ], placeholder: "Type to filter...", }); if (p.isCancel(packages)) { p.cancel("Cancelled."); process.exit(EXIT_CANCELLED); } ``` --- ## Pattern 8: Testing Clack Prompts Clack prompts accept custom `input` and `output` streams, making them testable without TTY. ```typescript import * as p from "@clack/prompts"; import { Readable, Writable } from "node:stream"; // Create mock streams function createMockInput(responses: string[]): Readable { const input = new Readable({ read() {} }); for (const response of responses) { // Simulate user typing + Enter input.push(response); input.push("\r"); } input.push(null); return input; } const output = new Writable({ write(chunk, encoding, callback) { callback(); }, }); // Use in tests const result = await p.text({ message: "Name?", input: createMockInput(["my-project"]), output, }); ``` **Note:** For most testing scenarios, consider mocking the `@clack/prompts` module entirely rather than simulating streams, as stream-based testing can be fragile with timing-sensitive prompts. -
core.md 11.2 KB
# Core Patterns > Related: [advanced.md](advanced.md) for custom prompts with @clack/core, streams, and i18n --- ## Exit codes used below Every snippet on this page assumes this constant. Where the CLI framework defines an exit-code table, use that table's cancellation constant instead -- it is the authority, and this value is the fallback for a standalone script that has no table. ```typescript // 128 + SIGINT(2), the value a shell reports for an interrupted command. export const EXIT_CANCELLED = 130; ``` **Cancelling is not declining.** A cancellation is an interruption -- `isCancel()` is true, `p.cancel()` prints, and the exit is non-zero because the work never happened. A user answering "no" to a confirm has _completed_ the interaction; that path prints `p.outro()` and exits `0`, because nothing went wrong. Both appear below, and the `p.cancel()` / `p.outro()` split is what tells them apart. --- ## Pattern 1: Individual Prompts with Cancellation Every prompt returns `value | symbol`. The symbol indicates the user pressed Ctrl+C. Always check before using the value. ### text ```typescript import * as p from "@clack/prompts"; const name = await p.text({ message: "What is your project name?", placeholder: "my-project", defaultValue: "my-app", }); if (p.isCancel(name)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } // TypeScript now knows name is string p.log.info(`Project name: ${name}`); ``` ### password ```typescript import * as p from "@clack/prompts"; const token = await p.password({ message: "Enter your API token", mask: "*", }); if (p.isCancel(token)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } ``` ### select ```typescript import * as p from "@clack/prompts"; const framework = await p.select({ message: "Pick a framework", options: [ { value: "react", label: "React", hint: "Recommended" }, { value: "vue", label: "Vue" }, { value: "svelte", label: "Svelte" }, ], }); if (p.isCancel(framework)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } // framework is typed as "react" | "vue" | "svelte" ``` ### multiselect ```typescript import * as p from "@clack/prompts"; const features = await p.multiselect({ message: "Select features", options: [ { value: "typescript", label: "TypeScript" }, { value: "eslint", label: "ESLint" }, { value: "prettier", label: "Prettier", hint: "Code formatting" }, ], required: false, // Allow zero selections (default requires at least one) }); if (p.isCancel(features)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } // features is string[] ``` ### confirm ```typescript import * as p from "@clack/prompts"; const shouldContinue = await p.confirm({ message: "Do you want to continue?", }); if (p.isCancel(shouldContinue)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } // shouldContinue is boolean if (!shouldContinue) { p.outro("Goodbye!"); process.exit(0); } ``` ### selectKey ```typescript import * as p from "@clack/prompts"; const action = await p.selectKey({ message: "What do you want to do?", options: [ { value: "create", key: "c", label: "Create a new project" }, { value: "clone", key: "l", label: "Clone existing project" }, { value: "exit", key: "q", label: "Quit" }, ], }); if (p.isCancel(action)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } // User pressed the key and action resolved immediately ``` ### groupMultiselect ```typescript import * as p from "@clack/prompts"; const plugins = await p.groupMultiselect({ message: "Select plugins to install", options: { Linting: [ { value: "eslint", label: "ESLint" }, { value: "prettier", label: "Prettier" }, ], Deployment: [ { value: "docker", label: "Docker" }, { value: "ci", label: "CI Pipeline" }, ], }, }); if (p.isCancel(plugins)) { p.cancel("Operation cancelled."); process.exit(EXIT_CANCELLED); } // plugins is string[] (flat array of selected values across all groups) ``` --- ## Pattern 2: Group Prompts `group()` chains prompts and provides centralized cancellation. Each prompt function receives `{ results }` containing all values collected so far. ### Basic group ```typescript import * as p from "@clack/prompts"; p.intro("Project setup"); const project = await p.group( { name: () => p.text({ message: "Project name?", placeholder: "my-project", validate: (value) => { if (!value) return "Name is required"; if (!/^[a-z0-9-]+$/.test(value)) return "Lowercase alphanumeric and hyphens only"; }, }), framework: ({ results }) => p.select({ message: `Framework for ${results.name}?`, options: [ { value: "react", label: "React" }, { value: "vue", label: "Vue" }, ], }), features: () => p.multiselect({ message: "Additional features?", options: [ { value: "typescript", label: "TypeScript" }, { value: "eslint", label: "ESLint" }, ], required: false, }), confirm: ({ results }) => p.confirm({ message: `Create ${results.name} with ${results.framework}?`, }), }, { onCancel: () => { p.cancel("Setup cancelled."); process.exit(EXIT_CANCELLED); }, }, ); // project is fully typed: // { name: string; framework: "react" | "vue"; features: string[]; confirm: boolean } if (!project.confirm) { p.outro("Aborted."); process.exit(0); } p.outro(`Created ${project.name}!`); ``` **Why good:** centralized onCancel, typed result object, each prompt accesses previous results, validation inline --- ## Pattern 3: Validation Patterns ### Text with validation ```typescript import * as p from "@clack/prompts"; const MIN_NAME_LENGTH = 2; const MAX_NAME_LENGTH = 214; const NPM_NAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; const packageName = await p.text({ message: "Package name?", validate: (value) => { if (!value || value.length < MIN_NAME_LENGTH) { return `Must be at least ${MIN_NAME_LENGTH} characters`; } if (value.length > MAX_NAME_LENGTH) { return `Must be at most ${MAX_NAME_LENGTH} characters`; } if (!NPM_NAME_PATTERN.test(value)) { return "Must be a valid npm package name"; } }, }); ``` ### Password with validation ```typescript import * as p from "@clack/prompts"; const MIN_PASSWORD_LENGTH = 8; const secret = await p.password({ message: "Enter password", validate: (value) => { if (!value || value.length < MIN_PASSWORD_LENGTH) { return `Must be at least ${MIN_PASSWORD_LENGTH} characters`; } if (!/[A-Z]/.test(value)) return "Must contain an uppercase letter"; if (!/[0-9]/.test(value)) return "Must contain a number"; }, }); ``` --- ## Pattern 4: Spinner Lifecycle ### Basic spinner ```typescript import * as p from "@clack/prompts"; const s = p.spinner(); s.start("Installing dependencies..."); try { await installDependencies(); s.stop("Dependencies installed"); } catch { s.error("Failed to install dependencies"); process.exit(1); } ``` ### Spinner with message updates ```typescript import * as p from "@clack/prompts"; const s = p.spinner(); s.start("Setting up project..."); s.message("Copying template files..."); await copyTemplate(); s.message("Installing dependencies..."); await installDeps(); s.message("Configuring TypeScript..."); await configureTs(); s.stop("Project ready!"); ``` ### Spinner with timer indicator ```typescript import * as p from "@clack/prompts"; const s = p.spinner({ indicator: "timer" }); s.start("Building project..."); await build(); s.stop("Build complete"); // Shows elapsed time ``` --- ## Pattern 5: Progress Bar ```typescript import * as p from "@clack/prompts"; const files = ["index.ts", "utils.ts", "config.ts", "main.ts"]; const prog = p.progress({ max: files.length, style: "heavy" }); prog.start("Processing files"); for (const file of files) { await processFile(file); prog.advance(1, `Processed ${file}`); } prog.stop("All files processed"); ``` --- ## Pattern 6: Tasks Runner ### Sequential tasks ```typescript import * as p from "@clack/prompts"; await p.tasks([ { title: "Cloning repository", task: async () => { await cloneRepo(); return "Repository cloned"; }, }, { title: "Installing dependencies", task: async (message) => { message("Resolving packages..."); await resolvePkgs(); message("Linking dependencies..."); await linkDeps(); return "Dependencies installed"; }, }, { title: "Building project", task: async () => { await buildProject(); return "Build complete"; }, }, ]); ``` ### taskLog for detailed output ```typescript import * as p from "@clack/prompts"; const VISIBLE_LINES = 5; const tl = p.taskLog({ title: "Deploying", limit: VISIBLE_LINES }); tl.message("Uploading files..."); tl.message("Configuring routes..."); tl.message("Running health check..."); const group = tl.group("Database migrations"); group.message("Migration 001: create users"); group.message("Migration 002: add indexes"); group.success("Migrations complete"); tl.success("Deployment complete"); ``` --- ## Pattern 7: Output Utilities ### Logging ```typescript import * as p from "@clack/prompts"; p.log.info("Checking project configuration..."); p.log.success("All checks passed"); p.log.warn("Optional dependency missing: prettier"); p.log.error("Failed to read config file"); p.log.step("Step 1 of 3 complete"); p.log.message("Additional details here"); ``` ### note ```typescript import * as p from "@clack/prompts"; p.note("cd my-project\nnpm run dev", "Next steps"); ``` ### box ```typescript import * as p from "@clack/prompts"; p.box("Welcome to My CLI v2.0", "Announcement", { contentAlign: "center", rounded: true, }); ``` --- ## Pattern 8: Complete CLI Flow ```typescript import * as p from "@clack/prompts"; async function main(): Promise<void> { p.intro("create-my-app"); const project = await p.group( { name: () => p.text({ message: "Project name?", placeholder: "my-app", validate: (value) => { if (!value) return "Required"; }, }), template: () => p.select({ message: "Select a template", options: [ { value: "basic", label: "Basic", hint: "Minimal setup" }, { value: "full", label: "Full", hint: "All features" }, ], }), git: () => p.confirm({ message: "Initialize git?" }), }, { onCancel: () => { p.cancel("Setup cancelled."); process.exit(0); }, }, ); const s = p.spinner(); s.start("Creating project..."); await createProject(project.name, project.template); s.stop("Project created"); if (project.git) { s.start("Initializing git..."); await initGit(project.name); s.stop("Git initialized"); } p.note(`cd ${project.name}\nnpm run dev`, "Next steps"); p.outro("You're all set!"); } main().catch(console.error); ``` **Why good:** complete flow with intro/outro session, group for related prompts, spinner for async work, note for follow-up instructions, single cancellation handler
-
-
reference.md 5.6 KB
# Clack CLI Prompts Quick Reference ## Prompt Type Comparison | Prompt | Import | Returns | Use When | | --------------------------- | ---------------- | ------------------- | ---------------------------------- | | `text()` | `@clack/prompts` | `string \| symbol` | Free-form text input | | `password()` | `@clack/prompts` | `string \| symbol` | Sensitive input (masked) | | `select()` | `@clack/prompts` | `Value \| symbol` | Pick one from a list | | `selectKey()` | `@clack/prompts` | `Value \| symbol` | Pick one via keyboard shortcut | | `multiselect()` | `@clack/prompts` | `Value[] \| symbol` | Pick multiple from a list | | `groupMultiselect()` | `@clack/prompts` | `Value[] \| symbol` | Pick multiple, grouped by category | | `confirm()` | `@clack/prompts` | `boolean \| symbol` | Yes/No question | | `autocomplete()` | `@clack/prompts` | `Value \| symbol` | Searchable single select | | `autocompleteMultiselect()` | `@clack/prompts` | `Value[] \| symbol` | Searchable multi select | | `date()` | `@clack/prompts` | `Date \| symbol` | Date input with format | | `path()` | `@clack/prompts` | `string \| symbol` | File/directory path selection | ## Output Functions (Synchronous) | Function | Purpose | | ----------------------------- | --------------------------------- | | `intro(title?)` | Start a prompt session | | `outro(message?)` | End a prompt session (success) | | `cancel(message?)` | End a prompt session (cancelled) | | `note(message, title?)` | Display a boxed note | | `box(message, title?, opts?)` | Styled box with alignment options | | `log.info(message)` | Neutral status message | | `log.success(message)` | Success status message | | `log.warn(message)` | Warning status message | | `log.error(message)` | Error status message | | `log.step(message)` | Completed step message | | `log.message(message)` | Plain message without symbol | ## Progress Functions | Function | Purpose | | ----------------- | ------------------------------------ | | `spinner(opts?)` | Indeterminate progress indicator | | `progress(opts?)` | Determinate progress bar | | `tasks(taskList)` | Sequential task runner with spinners | | `taskLog(opts)` | Detailed log output per task | ## Stream Functions (Async) | Function | Purpose | | -------------------------- | ---------------------- | | `stream.info(iterable)` | Stream neutral content | | `stream.success(iterable)` | Stream success content | | `stream.warn(iterable)` | Stream warning content | | `stream.error(iterable)` | Stream error content | | `stream.step(iterable)` | Stream step content | | `stream.message(iterable)` | Stream plain content | ## Common Options (All Prompts) ```typescript interface CommonOptions { signal?: AbortSignal; // Programmatic cancellation input?: Readable; // Custom input stream (testing) output?: Writable; // Custom output stream (testing) withGuide?: boolean; // Show border guide lines } ``` ## Spinner API ```typescript const s = spinner({ indicator?: "dots" | "timer", onCancel?: () => void, cancelMessage?: string, errorMessage?: string }); s.start(message?: string); // Begin animation s.message(message?: string); // Update message mid-spin s.stop(message?: string); // Complete successfully s.error(message?: string); // Complete with error s.cancel(message?: string); // Complete as cancelled s.isCancelled; // boolean -- check if cancelled ``` ## Progress API ```typescript const prog = progress({ max?: number, style?: "light" | "heavy" | "block", size?: number }); prog.start(message?: string); prog.advance(step?: number, message?: string); // step defaults to 1 prog.message(message?: string); prog.stop(message?: string); prog.error(message?: string); prog.cancel(message?: string); prog.clear(); ``` ## Global Settings ```typescript import { updateSettings } from "@clack/prompts"; updateSettings({ withGuide: false, // Disable guide lines globally messages: { cancel: "Operacion cancelada", // i18n for cancel text error: "Error", // i18n for error text }, }); ``` ## Anti-Pattern Quick Reference | Anti-Pattern | Fix | | --------------------------------------- | -------------------------------------------------------------- | | No `isCancel()` check | Always check after every prompt call | | No `process.exit()` after `cancel()` | Exit after `cancel()` -- it prints and returns, nothing more | | `process.exit(0)` after `cancel()` | Exit non-zero: the framework's cancellation constant, or `130` | | Output while spinner is active | Call `spinner.stop()` before any other output | | `require("@clack/prompts")` | Use ESM `import` -- package is ESM-only since v1.0 | | `multiselect` without `required: false` | Add it when zero selections should be valid | | Inline `isCancel` checks in long flows | Use `group()` with `onCancel` instead | -
SKILL.md 15.3 KB
--- name: cli-prompts-clack description: Beautiful interactive CLI prompts with @clack/prompts and custom prompts with @clack/core --- # Clack CLI Prompts > **Quick Guide:** Use `@clack/prompts` for pre-styled interactive CLI prompts (text, select, multiselect, confirm, spinner, progress). Check `isCancel()` after EVERY prompt call -- users can Ctrl+C at any point. `cancel()` only prints; exit after it, with a non-zero code. Use `group()` for multi-step flows with centralized cancellation. Use `@clack/core` only when building fully custom prompt UIs. ESM-only since v1.0. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST check `isCancel()` after EVERY prompt call -- skipping this causes silent crashes when users press Ctrl+C)** **(You MUST exit the process after `cancel()` -- the cancel message prints but execution continues otherwise)** **(You MUST exit with a NON-ZERO code on cancellation, taking the value from the CLI framework's exit-code table where one exists and using 130 where none does -- exiting 0 tells every caller the work succeeded)** **(You MUST use `group()` with `onCancel` for multi-step flows -- it handles cancellation centrally so you don't check each prompt individually)** **(You MUST call `spinner.stop()` before any other output -- overlapping spinner output with prompts or logs corrupts the terminal)** </critical_requirements> --- **Auto-detection:** @clack/prompts, @clack/core, clack, isCancel, intro, outro, cancel, spinner, group, text prompt, select prompt, confirm prompt, multiselect, groupMultiselect, selectKey, note, log, tasks, progress, taskLog, stream, box, autocomplete, date prompt, path prompt, updateSettings **When to use:** - Building interactive CLI prompts (text input, selection, confirmation) - Creating multi-step CLI wizards with progress indication - Adding styled terminal output (notes, logs, boxes, spinners) - Handling user cancellation gracefully across prompt flows **When NOT to use:** - Full terminal UI applications with persistent layout (use a terminal UI framework) - Non-interactive scripts where stdin is piped (clack prompts require a TTY) - Simple `y/n` confirmation that doesn't need styling (plain readline suffices) **Key patterns covered:** - Core prompts: text, password, select, multiselect, confirm, selectKey - Session lifecycle: intro, outro, cancel, isCancel - Progress: spinner, progress bar, tasks - Composition: group with centralized cancellation - Output: log, note, box, stream, taskLog - Custom prompts with @clack/core primitives - Validation, default values, and AbortSignal cancellation --- <philosophy> ## Philosophy Clack provides beautiful, minimal CLI prompts with zero configuration. The `@clack/prompts` package gives you pre-styled components that look great out of the box. Every prompt returns a value or a cancel symbol -- the core discipline is always checking for cancellation. **Two packages, two purposes:** - **`@clack/prompts`** -- Pre-styled, opinionated prompts. Use this for 95% of cases. - **`@clack/core`** -- Unstyled primitives with a `render()` function. Use only when you need a completely custom prompt UI. **Key design principles:** - Every prompt is async and returns `value | symbol` -- the symbol indicates cancellation - Session boundaries (`intro`/`outro`) create visual grouping in the terminal - `group()` composes multiple prompts with shared cancellation handling - Spinners, progress bars, and task runners handle long-running operations - All prompts accept `signal: AbortSignal` for programmatic cancellation </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Session Lifecycle and Cancellation Every clack CLI flow starts with `intro()` and ends with `outro()`. The critical pattern is checking `isCancel()` after every prompt call. ```typescript import * as p from "@clack/prompts"; // 128 + SIGINT(2), the value a shell reports for an interrupted command. // Use only when no CLI framework owns an exit-code table -- see below. const EXIT_CANCELLED = 130; p.intro("Project setup"); const name = await p.text({ message: "Project name?" }); if (p.isCancel(name)) { p.cancel("Setup cancelled."); process.exit(EXIT_CANCELLED); } // name is now narrowed to string (not symbol) p.outro(`Created ${name}`); ``` **Why good:** isCancel check narrows the type from `string | symbol` to `string`, cancel prints a styled message, the exit stops dangling execution, the non-zero code stops callers from reading an abandoned run as a completed one ```typescript // BAD: Missing isCancel check const name = await p.text({ message: "Project name?" }); console.log(`Created ${name}`); // name could be a symbol -- crashes or prints "[Symbol]" ``` **Why bad:** if user presses Ctrl+C, name is a symbol, not a string -- string operations on it will crash or produce garbage output #### Which code to exit with `cancel()` prints and returns; it never stops the process. What you exit _with_ is a separate decision, and it is not this library's to make: | Situation | Exit with | | -------------------------------------------- | ------------------------------------------------------------------------- | | The CLI framework defines an exit-code table | That table's cancellation constant. It is the authority; do not override. | | A standalone script with no such table | `130` -- 128 + SIGINT(2), what a shell reports for an interrupted command | | Never | `0` | **Why never 0:** `0` means success, and `cli && deploy`, `set -e`, CI steps and every other caller act on exactly that. A user who pressed Ctrl+C halfway through setup did not succeed, so a `0` exit hands the next command a half-configured project and no signal that anything went wrong. Upstream examples showing `process.exit(0)` are illustrating _that you must exit at all_ -- the point they make is about the missing exit, not about the value. --- ### Pattern 2: Group Prompts with Centralized Cancellation `group()` chains multiple prompts and handles cancellation in one place. Each prompt receives previous results. ```typescript import * as p from "@clack/prompts"; const project = await p.group( { name: () => p.text({ message: "Project name?", placeholder: "my-app" }), framework: ({ results }) => p.select({ message: `Framework for ${results.name}?`, options: [ { value: "react", label: "React" }, { value: "vue", label: "Vue" }, { value: "svelte", label: "Svelte" }, ], }), install: () => p.confirm({ message: "Install dependencies?" }), }, { onCancel: () => { p.cancel("Setup cancelled."); process.exit(EXIT_CANCELLED); }, }, ); // project is typed: { name: string; framework: string; install: boolean } ``` **Why good:** centralized onCancel eliminates per-prompt isCancel checks, results are typed as an object, each prompt can reference previous results via `results`, the single exit point means the cancellation code is decided once for the whole flow See [examples/core.md](examples/core.md) for complete group patterns with validation and conditional prompts. --- ### Pattern 3: Spinner and Progress Spinners show activity during async work. Always stop the spinner before printing other output. ```typescript import * as p from "@clack/prompts"; const s = p.spinner(); s.start("Installing dependencies"); await installDeps(); s.stop("Dependencies installed"); ``` **Progress bar** extends spinner with incremental tracking: ```typescript const MAX_STEPS = 100; const prog = p.progress({ max: MAX_STEPS, style: "heavy" }); prog.start("Processing files"); for (const file of files) { await processFile(file); prog.advance(1, `Processed ${file.name}`); } prog.stop("All files processed"); ``` **Why good:** spinner and progress provide visual feedback, stop message replaces the spinner line cleanly See [examples/core.md](examples/core.md) for spinner error handling, cancellation with AbortSignal, and tasks runner. --- ### Pattern 4: Validation All input prompts accept a `validate` function. Return a string to show an error, or `undefined` to accept. ```typescript const MIN_LENGTH = 2; const MAX_LENGTH = 50; const name = await p.text({ message: "Package name?", validate: (value) => { if (!value || value.length < MIN_LENGTH) return `Name must be at least ${MIN_LENGTH} characters`; if (value.length > MAX_LENGTH) return `Name must be at most ${MAX_LENGTH} characters`; if (!/^[a-z0-9-]+$/.test(value)) return "Name must be lowercase alphanumeric with hyphens"; }, }); ``` **Why good:** validation runs inline before the prompt resolves, user sees the error immediately and can retry, named constants for limits See [examples/core.md](examples/core.md) for validation patterns on different prompt types. --- ### Pattern 5: Output Utilities (log, note, box) Clack provides styled output functions that match the prompt theme. ```typescript import * as p from "@clack/prompts"; // Logging with state symbols p.log.info("Checking configuration..."); p.log.success("Configuration valid"); p.log.warn("Missing optional field: description"); p.log.error("Invalid config file"); p.log.step("Step 1 complete"); // Boxed note for important information p.note("Run `npm start` to begin development", "Next steps"); // Styled box p.box("v1.0.0 released!", "Announcement", { contentAlign: "center", rounded: true, }); ``` **Why good:** themed output matches prompt styling, note/box draw attention to important information --- ### Pattern 6: Tasks Runner Sequential tasks with automatic success/failure messaging. ```typescript import * as p from "@clack/prompts"; await p.tasks([ { title: "Downloading template", task: async () => { await downloadTemplate(); return "Template downloaded"; }, }, { title: "Installing dependencies", task: async (message) => { message("Resolving packages..."); await installDeps(); return "Dependencies installed"; }, }, ]); ``` **Why good:** tasks display spinner per item, return value becomes the completion message, `message()` callback updates spinner text mid-task See [examples/core.md](examples/core.md) for error handling in tasks and taskLog for detailed output. </patterns> --- **Detailed Resources:** - [examples/core.md](examples/core.md) - All prompt types, cancellation, spinner, progress, tasks, group, validation, output - [examples/advanced.md](examples/advanced.md) - Custom prompts with @clack/core, AbortSignal, streams, i18n, date/path/autocomplete - [reference.md](reference.md) - API quick reference, decision framework, prompt type comparison --- <decision_framework> ## Decision Framework ``` Need user input? | +-> Single value? | +-> Free text -> text() or password() | +-> One of N choices -> select() (list) or selectKey() (keyboard shortcut) | +-> Yes/No -> confirm() | +-> Date -> date() | +-> File path -> path() | +-> Multiple values? | +-> Flat list -> multiselect() | +-> Grouped categories -> groupMultiselect() | +-> Searchable -> autocomplete() or autocompleteMultiselect() | +-> Multiple prompts in sequence? +-> group() with onCancel for centralized handling Need to show progress? | +-> Indeterminate wait -> spinner() +-> Known total steps -> progress() +-> Sequential tasks -> tasks() +-> Detailed logs per task -> taskLog() Need styled output? | +-> Status message -> log.info/warn/error/success/step() +-> Important notice -> note() or box() +-> Streaming content -> stream.info/warn/error/success() ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - **Missing `isCancel()` check after a prompt** -- the return value is `value | symbol`, and using the symbol as a string crashes or produces garbage. Always check before using the value. - **Missing `process.exit()` after `cancel()`** -- `cancel()` only prints a message, it does not stop execution. The process continues running. - **Exiting `0` after `cancel()`** -- a cancelled run reports success to every caller, so `cli && next-step` runs the next step against a half-finished state. Exit non-zero: the framework's cancellation constant, or `130` when there is no framework table. - **Calling another prompt while spinner is active** -- spinner output and prompt output overlap, corrupting the terminal display. Always call `spinner.stop()` first. - **Using `require()` with @clack/prompts v1.0+** -- the package is ESM-only since v1.0. Use `import` syntax. **Medium Priority Issues:** - **Not using `group()` for multi-step flows** -- checking `isCancel()` after every single prompt is verbose and error-prone. `group()` with `onCancel` centralizes this. - **Ignoring the `validate` option** -- prompts accept invalid input by default. Add validation for any input that has constraints. - **Using `multiselect` without `required: false` when zero selections should be valid** -- by default, at least one item must be selected. **Gotchas & Edge Cases:** - `isCancel()` returns `true` for the cancel symbol but also narrows the TypeScript type -- always use it as a type guard before accessing the value - `spinner()` returns an object, not a promise -- call `.start()` separately - `group()` prompt functions receive `{ results }` with all previously collected values, but TypeScript types each value as possibly undefined since earlier prompts might not have run yet - `confirm()` returns `boolean | symbol`, not just `boolean` -- still needs `isCancel()` check when used outside `group()` - `select()` generic type parameter controls the return type -- `select<"react" | "vue">({...})` narrows the result - `log.warn` has an alias `log.warning` -- both work identically - `progress.advance()` with no arguments advances by 1 -- the step parameter is optional - `note()` and `box()` are synchronous (not prompts) -- they return `void`, not promises - All prompts accept `signal: AbortSignal` for programmatic cancellation (e.g., timeouts) - `updateSettings()` applies globally -- call it once at startup, not per prompt - v1.1.0 replaced `picocolors` with Node.js built-in `styleText` -- requires Node.js 20.12+ </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST check `isCancel()` after EVERY prompt call -- skipping this causes silent crashes when users press Ctrl+C)** **(You MUST exit the process after `cancel()` -- the cancel message prints but execution continues otherwise)** **(You MUST exit with a NON-ZERO code on cancellation, taking the value from the CLI framework's exit-code table where one exists and using 130 where none does -- exiting 0 tells every caller the work succeeded)** **(You MUST use `group()` with `onCancel` for multi-step flows -- it handles cancellation centrally so you don't check each prompt individually)** **(You MUST call `spinner.stop()` before any other output -- overlapping spinner output with prompts or logs corrupts the terminal)** **Failure to follow these rules will cause silent process hangs, corrupted terminal output, and runtime crashes on user cancellation.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.