Get the FREE Ultimate OpenClaw Setup Guide →

clack-prompts

Scanned
npx machina-cli add skill nklisch/skilltap/clack-prompts --openclaw
Files (1)
SKILL.md
9.5 KB

@clack/prompts — Terminal UI Reference

Beautiful, opinionated terminal prompts with a connected vertical-bar UI. Used in packages/cli/ for all interactive user flows.

Install: bun add @clack/prompts

Import: import { intro, outro, cancel, isCancel, text, password, confirm, select, multiselect, spinner, log, note, group, tasks } from "@clack/prompts"

Critical: Cancel Handling

Every prompt returns Promise<Value | symbol>. When the user presses Ctrl+C, the return value is a symbol. Always check with isCancel():

import { text, isCancel, cancel } from "@clack/prompts"

const name = await text({ message: "Project name?" })

if (isCancel(name)) {
  cancel("Operation cancelled.")
  process.exit(0)
}
// After this guard, `name` is narrowed to `string`

Lifecycle

import { intro, outro, cancel } from "@clack/prompts"

intro("skilltap")           // Header bar at start
// ... prompts ...
outro("Done!")              // Footer bar at end

// On cancellation:
cancel("Operation cancelled.")
process.exit(0)

Prompts

text — Single-line text input

const name = await text({
  message: "What is your project name?",
  placeholder: "my-project",       // Dimmed hint when empty
  initialValue: "",                // Pre-filled value
  defaultValue: "my-app",         // Used if user submits empty
  validate(value) {
    if (!value) return "Required!"
    if (value.length > 50) return "Too long!"
    // Return undefined/void = valid
  },
})

password — Masked text input

const secret = await password({
  message: "Enter your API key:",
  mask: "*",                       // Mask character (default "*")
  validate(value) {
    if (value.length < 8) return "Must be at least 8 characters"
  },
})

confirm — Yes/No prompt

const shouldInstall = await confirm({
  message: "Install anyway?",
  initialValue: false,             // Default selection
  active: "Yes",                   // Label for "yes" (default "Yes")
  inactive: "No",                  // Label for "no" (default "No")
})
// Returns boolean | symbol

select — Single selection from a list

const scope = await select({
  message: "Install to:",
  options: [
    { value: "global", label: "Global (~/.agents/skills/)" },
    { value: "project", label: "Project (.agents/skills/)", hint: "recommended" },
  ],
  initialValue: "global",
  maxItems: 5,                     // Max visible before scrolling
})
// Returns the selected value's type | symbol

Option shape: { value: T, label: string, hint?: string, disabled?: boolean }

multiselect — Multiple selection (toggle with Space, confirm with Enter)

const agents = await multiselect({
  message: "Auto-symlink to which agents?",
  options: [
    { value: "claude-code", label: "Claude Code" },
    { value: "cursor", label: "Cursor" },
    { value: "codex", label: "Codex" },
    { value: "gemini", label: "Gemini" },
    { value: "windsurf", label: "Windsurf" },
  ],
  initialValues: ["claude-code"],  // Pre-selected
  required: false,                 // Allow submitting with none selected (default true)
  cursorAt: "claude-code",         // Initial cursor position
  maxItems: 5,
})
// Returns T[] | symbol

groupMultiselect — Grouped multi-selection

const packages = await groupMultiselect({
  message: "Select packages:",
  options: {
    "Frontend": [
      { value: "react", label: "React" },
      { value: "vue", label: "Vue" },
    ],
    "Backend": [
      { value: "express", label: "Express" },
      { value: "fastify", label: "Fastify" },
    ],
  },
  required: true,
})
// Selecting a group header toggles all items in the group

selectKey — Key-based selection (no Enter needed)

const action = await selectKey({
  message: "What to do?",
  options: [
    { value: "install", label: "Install", key: "i" },
    { value: "skip", label: "Skip", key: "s" },
  ],
})
// User presses "i" or "s" to select immediately

spinner — Loading indicator

const s = spinner()
s.start("Installing dependencies...")

// Update message while spinning:
s.message("Almost done...")

// Stop with final message:
s.stop("Dependencies installed.")

// Stop with error (code 1):
s.stop("Installation failed.", 1)

log — Styled inline messages

All log functions maintain the vertical-bar UI flow:

import { log } from "@clack/prompts"

log.message("A plain message")     // Neutral
log.info("Informational")          // Blue
log.success("Operation done!")     // Green
log.warn("Be careful")            // Yellow
log.error("Something broke")      // Red
log.step("Step 1: Initialize")    // Cyan

note — Boxed information block

import { note } from "@clack/prompts"

note(
  "Project: my-app\nFramework: next\nFeatures: typescript, eslint",
  "Project Summary"  // Optional title
)

Renders as a bordered box with the title. Supports \n for multi-line.

group — Sequential prompt wizard

Runs prompts in sequence, collects all results into a typed object. Handles cancellation globally.

import { group, text, select, confirm } from "@clack/prompts"

const result = await group(
  {
    name: () => text({
      message: "Project name?",
      validate(v) { if (!v) return "Required" },
    }),

    scope: () => select({
      message: "Install scope?",
      options: [
        { value: "global", label: "Global" },
        { value: "project", label: "Project" },
      ],
    }),

    // Access previous results:
    confirm: ({ results }) => confirm({
      message: `Install "${results.name}" to ${results.scope}?`,
    }),
  },
  {
    onCancel({ results }) {
      cancel("Operation cancelled.")
      process.exit(0)
    },
  }
)

// result.name   -> string
// result.scope  -> "global" | "project"
// result.confirm -> boolean

Each prompt function receives { results: Partial<T> } with all prior answers. If any prompt is cancelled and no onCancel is provided, the group throws.

tasks — Sequential async task runner

import { tasks } from "@clack/prompts"

await tasks([
  {
    title: "Cloning repository",
    task: async (message) => {
      await cloneRepo(url)
      message("Scanning for skills...")  // Update spinner text
      await scanSkills(dir)
      return "Repository cloned"         // Completion message
    },
  },
  {
    title: "Running security scan",
    enabled: !skipScan,                  // Conditionally skip
    task: async (message) => {
      const warnings = await scanStatic(dir)
      return warnings.length
        ? `${warnings.length} warnings found`
        : "No warnings"
    },
  },
])

Task shape: { title: string, task: (message: (msg: string) => void) => Promise<string | void>, enabled?: boolean }

Visual Output Style

Clack renders a connected vertical-bar UI:

┌  skilltap
│
◆  Install to:
│  ● Global (~/.agents/skills/)
│  ○ Project (.agents/skills/)
│
◇  Auto-symlink to which agents?
│  ◼ Claude Code
│  ◻ Cursor
│
▪───────────────────────╮
│  Skill: commit-helper  │
│  Scope: global         │
├────────────────────────╯
│
◒  Installing...
│
└  Done!

Coloring text in messages

Use picocolors (or similar) for colored text within prompts:

import color from "picocolors"

await text({
  message: `What is your ${color.bold("project")} name?`,
})

log.info(color.green("All checks passed!"))

Pattern: skilltap Prompt Wrappers

The project wraps clack prompts in packages/cli/src/ui/prompts.ts for consistent behavior:

import { select, confirm, isCancel, cancel } from "@clack/prompts"

export async function promptScope(): Promise<"global" | "project"> {
  const scope = await select({
    message: "Install to:",
    options: [
      { value: "global" as const, label: "Global (~/.agents/skills/)" },
      { value: "project" as const, label: "Project (.agents/skills/)" },
    ],
  })
  if (isCancel(scope)) {
    cancel("Operation cancelled.")
    process.exit(2)
  }
  return scope
}

export async function promptInstall(warnings: boolean): Promise<boolean> {
  const result = await confirm({
    message: warnings ? "Install anyway?" : "Install?",
    initialValue: !warnings,  // Default to "no" when there are warnings
    active: warnings ? "Yes, install" : "Yes",
    inactive: "No",
  })
  if (isCancel(result)) {
    cancel("Operation cancelled.")
    process.exit(2)
  }
  return result
}

Pattern: Agent Mode Output

When agent mode is active, skip all interactive prompts and use plain text output instead. The packages/cli/src/ui/agent-out.ts module handles this:

// Agent mode: no colors, no spinners, no prompts
// Success: "OK: Installed commit-helper → ~/.agents/skills/commit-helper/ (v1.2.0)"
// Error:   "ERROR: Repository not found: https://example.com/bad-url.git"
// Security: "SECURITY ISSUE FOUND — INSTALLATION BLOCKED\n..."

Check config['agent-mode'].enabled early and branch to agent output functions instead of interactive prompts.

Source

git clone https://github.com/nklisch/skilltap/blob/main/.agents/skills/clack-prompts/SKILL.mdView on GitHub

Overview

clack-prompts is a reference for building terminal UIs with the @clack/prompts library. It covers text, password, confirm, select, multiselect, spinner, group flows, tasks, logging, and cancel handling, enabling rich interactive prompts in CLI commands. It’s used across packages/cli/src/ui/ and any command that needs user interaction.

How This Skill Works

Prompts export functions (text, password, select, multiselect, etc.) to render terminal UI. They return a Promise of a value or a cancellation symbol; use isCancel() to detect cancellation and handle with cancel(). Lifecycle helpers like intro and outro render header/footer around prompts to create a polished UX.

When to Use It

  • Designing a project setup wizard that asks for name, key, and preferences.
  • Presenting single- or multi-select options for commands, targets, or features.
  • Collecting sensitive data via password prompts with masking and validation.
  • Asking for confirmation before performing destructive or critical actions.
  • Displaying progress with spinners and logging while a task runs.

Quick Start

  1. Step 1: Install the library and import the needed functions (e.g., intro, outro, cancel, isCancel, text, password, confirm, select, multiselect).
  2. Step 2: Build a simple flow using text() (and optionally password(), confirm(), or select()) and guard with isCancel() to handle cancellation.
  3. Step 3: Wrap prompts with intro('skilltap') at the start and outro('Done!') at the end; on cancel, call cancel('Operation cancelled.') and exit.

Best Practices

  • Guard every prompt with clear validation and actionable error messages.
  • Provide initialValue, placeholder, or defaultValue to guide users.
  • Always handle isCancel and terminate gracefully with cancel().
  • Keep a consistent lifecycle using intro/outro for a polished UX.
  • Break complex flows into smaller prompts or grouped prompts to avoid overwhelming users.

Example Use Cases

  • Ask for a project name using text() and handle cancellation with isCancel.
  • Ask for an API key using password() with masking and validation.
  • Confirm installation or destructive actions using confirm().
  • Choose a deployment scope using select() with labeled options.
  • Let users pick multiple features using multiselect(), returning an array.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers