| name | clack-prompts |
| description | Reference for @clack/prompts terminal UI library. Use this skill whenever building interactive CLI prompts, spinners, select menus, confirmation dialogs, or any terminal UI. Covers text, password, confirm, select, multiselect, spinner, group flows, tasks, logging, and cancel handling. Use this for any file in packages/cli/src/ui/ or any command that needs user interaction. |
@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)
}
Lifecycle
import { intro, outro, cancel } from "@clack/prompts"
intro("skilltap")
outro("Done!")
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",
initialValue: "",
defaultValue: "my-app",
validate(value) {
if (!value) return "Required!"
if (value.length > 50) return "Too long!"
},
})
password — Masked text input
const secret = await password({
message: "Enter your API key:",
mask: "*",
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,
active: "Yes",
inactive: "No",
})
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,
})
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"],
required: false,
cursorAt: "claude-code",
maxItems: 5,
})
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,
})
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" },
],
})
spinner — Loading indicator
const s = spinner()
s.start("Installing dependencies...")
s.message("Almost done...")
s.stop("Dependencies installed.")
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")
log.info("Informational")
log.success("Operation done!")
log.warn("Be careful")
log.error("Something broke")
log.step("Step 1: Initialize")
note — Boxed information block
import { note } from "@clack/prompts"
note(
"Project: my-app\nFramework: next\nFeatures: typescript, eslint",
"Project Summary"
)
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" },
],
}),
confirm: ({ results }) => confirm({
message: `Install "${results.name}" to ${results.scope}?`,
}),
},
{
onCancel({ results }) {
cancel("Operation cancelled.")
process.exit(0)
},
}
)
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...")
await scanSkills(dir)
return "Repository cloned"
},
},
{
title: "Running security scan",
enabled: !skipScan,
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,
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:
Check config['agent-mode'].enabled early and branch to agent output functions instead of interactive prompts.