clack-prompts
Scannednpx machina-cli add skill nklisch/skilltap/clack-prompts --openclaw@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
- Step 1: Install the library and import the needed functions (e.g., intro, outro, cancel, isCancel, text, password, confirm, select, multiselect).
- Step 2: Build a simple flow using text() (and optionally password(), confirm(), or select()) and guard with isCancel() to handle cancellation.
- 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.