Claude Skill

cli-prompts-clack

Beautiful interactive CLI prompts with @clack/prompts and custom prompts with @clack/core

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_cli-prompts-clack_skills_cli-prompts-clack-3a51ef5.zip · 14 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/cli-prompts-clack/skills/cli-prompts-clack
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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/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



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 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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related