Showing preview only (734K chars total). Download the full file or copy to clipboard to get everything.
Repository: bombshell-dev/clack
Branch: main
Commit: 8a96e2dcd7f8
Files: 148
Total size: 691.3 KB
Directory structure:
gitextract_95ui2zim/
├── .changeset/
│ ├── README.md
│ ├── afraid-donkeys-sin.md
│ ├── big-pants-invite.md
│ ├── config.json
│ ├── dirty-actors-find.md
│ ├── tangy-mirrors-hug.md
│ └── tricky-states-tease.md
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ ├── ci.yml
│ ├── detect-agent.yml
│ ├── format.yml
│ ├── issue.yml
│ ├── preview.yml
│ ├── publish.yml
│ └── require-allow-edits.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode/
│ └── settings.json
├── CONTRIBUTING.md
├── README.md
├── biome.json
├── build.preset.ts
├── examples/
│ ├── basic/
│ │ ├── autocomplete-multiselect.ts
│ │ ├── autocomplete.ts
│ │ ├── date.ts
│ │ ├── default-value.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── path.ts
│ │ ├── progress.ts
│ │ ├── spinner-cancel-advanced.ts
│ │ ├── spinner-cancel.ts
│ │ ├── spinner-ci.ts
│ │ ├── spinner-timer.ts
│ │ ├── spinner.ts
│ │ ├── stream.ts
│ │ ├── task-log.ts
│ │ ├── text-validation.ts
│ │ └── tsconfig.json
│ └── changesets/
│ ├── index.ts
│ ├── package.json
│ └── tsconfig.json
├── knip.json
├── package.json
├── packages/
│ ├── core/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── build.config.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── prompts/
│ │ │ │ ├── autocomplete.ts
│ │ │ │ ├── confirm.ts
│ │ │ │ ├── date.ts
│ │ │ │ ├── group-multiselect.ts
│ │ │ │ ├── multi-select.ts
│ │ │ │ ├── password.ts
│ │ │ │ ├── prompt.ts
│ │ │ │ ├── select-key.ts
│ │ │ │ ├── select.ts
│ │ │ │ └── text.ts
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── cursor.ts
│ │ │ ├── index.ts
│ │ │ ├── settings.ts
│ │ │ └── string.ts
│ │ ├── test/
│ │ │ ├── mock-readable.ts
│ │ │ ├── mock-writable.ts
│ │ │ ├── prompts/
│ │ │ │ ├── autocomplete.test.ts
│ │ │ │ ├── confirm.test.ts
│ │ │ │ ├── date.test.ts
│ │ │ │ ├── multi-select.test.ts
│ │ │ │ ├── password.test.ts
│ │ │ │ ├── prompt.test.ts
│ │ │ │ ├── select.test.ts
│ │ │ │ └── text.test.ts
│ │ │ └── utils.test.ts
│ │ └── tsconfig.json
│ └── prompts/
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── __mocks__/
│ │ └── fs.cjs
│ ├── build.config.ts
│ ├── package.json
│ ├── src/
│ │ ├── autocomplete.ts
│ │ ├── box.ts
│ │ ├── common.ts
│ │ ├── confirm.ts
│ │ ├── date.ts
│ │ ├── group-multi-select.ts
│ │ ├── group.ts
│ │ ├── index.ts
│ │ ├── limit-options.ts
│ │ ├── log.ts
│ │ ├── messages.ts
│ │ ├── multi-select.ts
│ │ ├── note.ts
│ │ ├── password.ts
│ │ ├── path.ts
│ │ ├── progress-bar.ts
│ │ ├── select-key.ts
│ │ ├── select.ts
│ │ ├── spinner.ts
│ │ ├── stream.ts
│ │ ├── task-log.ts
│ │ ├── task.ts
│ │ └── text.ts
│ ├── test/
│ │ ├── __snapshots__/
│ │ │ ├── autocomplete.test.ts.snap
│ │ │ ├── box.test.ts.snap
│ │ │ ├── confirm.test.ts.snap
│ │ │ ├── date.test.ts.snap
│ │ │ ├── group-multi-select.test.ts.snap
│ │ │ ├── log.test.ts.snap
│ │ │ ├── multi-select.test.ts.snap
│ │ │ ├── note.test.ts.snap
│ │ │ ├── password.test.ts.snap
│ │ │ ├── path.test.ts.snap
│ │ │ ├── progress-bar.test.ts.snap
│ │ │ ├── select-key.test.ts.snap
│ │ │ ├── select.test.ts.snap
│ │ │ ├── spinner.test.ts.snap
│ │ │ ├── task-log.test.ts.snap
│ │ │ └── text.test.ts.snap
│ │ ├── autocomplete.test.ts
│ │ ├── box.test.ts
│ │ ├── confirm.test.ts
│ │ ├── date.test.ts
│ │ ├── group-multi-select.test.ts
│ │ ├── limit-options.test.ts
│ │ ├── log.test.ts
│ │ ├── multi-select.test.ts
│ │ ├── note.test.ts
│ │ ├── password.test.ts
│ │ ├── path.test.ts
│ │ ├── progress-bar.test.ts
│ │ ├── select-key.test.ts
│ │ ├── select.test.ts
│ │ ├── spinner.test.ts
│ │ ├── task-log.test.ts
│ │ ├── test-utils.ts
│ │ └── text.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── pnpm-workspace.yaml
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
You will be responsible for writing a **changeset** for your Astro PR. This is a Markdown file that states:
- which repository has changed
- the kind of change according to [Astro's semantic versioning](https://docs.astro.build/en/upgrade-astro/#semantic-versioning)
- a message describing the change that will be publicly displayed on the repo's CHANGELOG
```md title=".changeset/my-super-cool-changeset.md" wrap
---
"astro": patch
---
Fixes unexpected `undefined` value when calling an action from the client without a return value
```
You can generate a changeset using the `pnpm changeset` command, which will prompt you for the necessary information and create a randomly-named file in the `.changeset` folder. You can then edit the generated file to add more detail, or write your changeset from scratch.
## Format
Begin your changeset message with **a present tense verb** that completes a sentence in the style of: "This PR . . ." or "This contribution ..."
- Adds
- Removes
- Fixes
- Updates
- Refactors
- Improves
- Deprecates
Finish the introductory sentence with a message focusing on what has changed **about the codebase** (not what your feature itself does) that is meaningful to a **user** of Astro. It is usually more helpful to describe the change **as someone building an Astro site will experience it**, instead of describing **how you fixed it** or **what the code in the PR does**:
```markdown title="changeset.md" del={2} ins={5}
// What the code now does
Logs helpful errors if content is invalid
// The nature of the change to the Astro code base
Adds logging for content collections configuration errors.
```
You may then include additional paragraphs if necessary to describe the change in more detail. This may not be needed for a small bug fix, but is often helpful if the reader must make a change to their own code, or needs to understand how the change might affect them.
The level of detail of a changeset depends on the type of change (e.g. `patch` vs `minor`, breaking or not, only affects integration authors etc.) You may also use [`<h4>` - `<h6>` Markdown headings](#using-markdown-section-headings) to break your longer content into sections.
:::tip[Don't hide the good stuff in the changeset!]
CHANGELOGS are often read only once, when someone is updating to the latest version of a package. Documentation is a constant reference that will be revisited and consulted frequently.
Make sure that the helpful explanations and examples for your fellow developers in both your changeset and your PR description are captured in the actual feature documentation, too!
:::
### Patch updates
These updates are often fixes, refactors or other small improvements. They are typically not user-facing, and do not require someone to update their own project code.
Verbs like "fixes" and "refactors" are helpful to let readers know this is an internal or implementation change that they do not need to worry about. At the same time, these messages are helpful to someone who is interested in keeping up with small changes to the codebase.
Often one line is enough to describe your change meaningfully to an Astro user:
```md title="my-patch-changeset.md" wrap
---
"astro": patch
---
Fixes a bug where the toolbar audit would incorrectly flag images as above the fold
```
```md title="my-patch-changeset.md" wrap
---
"astro": patch
---
Refactors internal handling of styles and scripts for content collections to improve build performance
```
```md title="my-patch-changeset.md" wrap
---
"astro": patch
---
Updates the `HTMLAttributes` type exported from `astro` to allow data attributes
```
These do not need to be full sentences and do not need end punctuation unless you write multiple sentences.
:::tip[Help your reader figure out if this is change important to them]
Even though these are small changes described by very short sentences, they still need to communicate a lot!
Check that you have clearly stated not just **what has changed** but also **who needs to know**. If your reader can identify that a change is important to them, they can always seek out further information if they need to know more.
:::
#### Tips and Examples
For a great changeset message:
Include the specific API changed (using inline code highlighting as appropriate) when your change might not be easy to identify so that your reader can easily tell whether it's something they are using and need to care about:
> Improves automatic fallbacks generation
>
> vs
>
> ✅ Improves automatic `fallbacks` generation **for the experimental Fonts API**
When the specific API change is not user-facing (e.g. a type not publicly exposed) and/or your reader will not recognize it by name, describe the use case or end result that will be meaningful to the reader instead:
> Adds `| (string & {})` for better autocomplete of `App.SessionData`
>
> vs
>
> ✅ Improves autocompletion for session keys
### New features
Begin your changeset with "Adds" to alert readers that there is something new and mention the names of any new items (options, functions) that have been added directly in the first sentence:
```md title="my-minor-changeset.md" wrap
---
"astro": minor
---
Adds a new `flamethrow` view transitions animation
<!-- -->
```
Additionally, describe what people are now able to do because of these additions that they could not before.
The changeset is an opportunity to call people's attention to new things they might wish to try in their Astro project, and may include a code example showing basic usage of the new feature:
````md wrap title="my-minor-changeset.md"
---
"astro": minor
---
Adds a new, optional property `timeout` for the `client:idle` directive
This value allows you to specify a maximum time to wait, in milliseconds, before hydrating a UI framework component, even if the page is not yet done with its initial load.
This means you can delay hydration for lower-priority UI elements with more control to ensure your element is interactive within a specified time frame.
```
<Button client:idle={{ timeout: 500 }} />
```
````
You can also use [`<h4>` - `<h6>` Markdown headings](#using-markdown-section-headings) to break your longer content into sections.
### Breaking changes
Changesets focus on what has changed, and **must include any breaking changes**, including changes to default behavior. Most users will have several default settings configured (often by not setting any value themselves), so changes to defaults can have a significant impact on someone's project!
Verbs like "removes", "changes", and "deprecates" call attention to something that might require attention. Unlike a new feature someone can choose not to use, changing default or expected behavior cannot be ignored.
You can also use [`<h4>` - `<h6>` Markdown headings](#using-markdown-section-headings) to break your content into sections, for example, to add headings like, "What should I do?" or "Migrating from the previous version."
```md title="my-major-changeset.md" wrap
---
"astro": major
---
Removes support for returning simple objects from endpoints. You must now return a `Response` instead.
```
Changeset messages for breaking changes must also provide clear guidance for updating. Diff code samples are encouraged when appropriate:
````md title="my-major-changeset.md" wrap
---
"astro": major
---
Removes support for Shiki custom language's `path` property. The language JSON file must now be imported and passed to the option instead.
```diff
// astro.config.js
+ import customLang from './custom.tmLanguage.json'
export default defineConfig({
markdown: {
shikiConfig: {
langs: [
- { path: './custom.tmLanguage.json' },
+ customLang,
],
},
},
})
```
````
If your contribution changes a default value, then it is helpful to describe how to revert back to the previous behavior. Readers may also appreciate being informed that they can remove configuration that is no longer needed.
````md title="my-major-changeset.md" wrap
---
"astro": major
---
Changes the default value of `security.checkOrigin` to true, which now enables Cross-Site Request Forgery (CSRF) protection by default for pages rendered on demand.
If you had previously configured `security.checkOrigin: true`, you no longer need this set in your Astro config. This is now the default and it is safe to remove.
To revert to the previous default behavior and opt out of automatically checking that the “origin” header matches the URL sent by each request, you must now explicitly set `security.checkOrigin: false`:
```diff
export default defineConfig({
+ security: {
+ checkOrigin: false
+ }
})
```
````
## Using Markdown section headings
For longer descriptions that you want to split into sections, always start at the `<h4>` level, using `####` where you would normally use `##` in a regular Markdown post. This ensures readability when your message is incorporated into the final CHANGELOG:
```md wrap title="my-long-changeset-with-sections.md"
---
"astro": minor
---
Adds a new Sessions API to store user state between requests for on-demand rendered pages.
#### Configuring session storage
<!-- ... -->
#### Using sessions
<!-- ... -->
##### In `.astro` pages
<!-- ... -->
##### In API endpoints
<!-- ... -->
#### Upgrading from the experimental API
<!-- ... -->
```
================================================
FILE: .changeset/afraid-donkeys-sin.md
================================================
---
"@clack/prompts": minor
"@clack/core": minor
---
Externalize `fast-string-width` and `fast-wrap-ansi` to avoid double dependencies
================================================
FILE: .changeset/big-pants-invite.md
================================================
---
"@clack/prompts": patch
---
Fix the `path` prompt so `directory: true` correctly enforces directory-only selection while still allowing directory navigation, and add regression tests for both directory and default file selection behavior.
================================================
FILE: .changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@example/*"]
}
================================================
FILE: .changeset/dirty-actors-find.md
================================================
---
"@clack/prompts": patch
"@clack/core": patch
---
Adds `placeholder` option to `autocomplete`. When the placeholder is set and the input is empty, pressing `tab` will set the value to `placeholder`.
================================================
FILE: .changeset/tangy-mirrors-hug.md
================================================
---
"@clack/prompts": minor
"@clack/core": minor
---
Adds `date` prompt with `format` support (YMD, MDY, DMY)
================================================
FILE: .changeset/tricky-states-tease.md
================================================
---
"@clack/prompts": patch
---
Fix `path` directory mode so pressing Enter with an existing directory `initialValue` submits that current directory instead of the first child option, and add regression coverage for immediate submit and child-directory navigation.
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,json,yaml}]
indent_style = space
indent_size = 2
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: '[Bug] '
labels: bug
assignees: ''
---
**Environment**
- OS: [e.g. macOS, Windows]
- Node Version: [e.g. v18.14.0]
- Package: [e.g. `@clack/prompts`, `@clack/core`]
- Package Version: [e.g. v0.2.0]
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Include a link to a minimal reproduction using [`node.new`](https://node.new/)
Steps to reproduce the behavior:
- Include reproduction steps
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional Information**
If applicable, add screenshots to help explain your problem.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: '[Request]'
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
# Automatically cancel in-progress actions on the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
scripts:
if: github.repository_owner == 'bombshell-dev'
uses: bombshell-dev/automation/.github/workflows/run.yml@main
secrets: inherit
with:
commands: >
[
"build",
"types",
"test",
"deps"
]
================================================
FILE: .github/workflows/detect-agent.yml
================================================
name: Detect Agent
on:
pull_request_target:
types: [opened]
workflow_dispatch: {}
permissions:
issues: write
pull-requests: write
jobs:
detect:
if: github.event_name != 'workflow_dispatch'
uses: bombshell-dev/automation/.github/workflows/detect-agent.yml@main
backfill:
if: github.event_name == 'workflow_dispatch'
uses: bombshell-dev/automation/.github/workflows/detect-agent-backfill.yml@main
================================================
FILE: .github/workflows/format.yml
================================================
name: Format
on:
workflow_dispatch:
push:
branches:
- main
jobs:
format:
if: github.repository_owner == 'bombshell-dev'
uses: bombshell-dev/automation/.github/workflows/format.yml@main
permissions:
contents: write
pull-requests: write
secrets: inherit
================================================
FILE: .github/workflows/issue.yml
================================================
name: issue
on:
issues:
types: [opened, edited, labeled, reopened]
jobs:
backlog:
if: github.event.action == 'edited' || github.event.action == 'labeled'
uses: bombshell-dev/automation/.github/workflows/move-issue-to-backlog.yml@main
secrets: inherit
project:
if: github.event.action == 'opened' || github.event.action == 'reopened'
uses: bombshell-dev/automation/.github/workflows/add-issue-to-project.yml@main
secrets: inherit
================================================
FILE: .github/workflows/preview.yml
================================================
name: Preview
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
preview:
if: github.repository_owner == 'bombshell-dev'
uses: bombshell-dev/automation/.github/workflows/preview.yml@main
permissions:
contents: write
pull-requests: write
id-token: write
with:
publish: "./packages/*"
template: "./examples/*"
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
push:
branches: [main, v0]
workflow_dispatch:
permissions:
id-token: write
contents: write
pull-requests: write
packages: write
jobs:
publish:
if: github.repository_owner == 'bombshell-dev'
uses: bombshell-dev/automation/.github/workflows/publish.yml@main
secrets: inherit
================================================
FILE: .github/workflows/require-allow-edits.yml
================================================
name: Require “Allow Edits”
on: [pull_request_target]
permissions:
contents: read
jobs:
_:
permissions:
pull-requests: read
name: "Require “Allow Edits”"
runs-on: ubuntu-latest
steps:
- uses: ljharb/require-allow-edits@v2
================================================
FILE: .gitignore
================================================
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# JetBrains IDEA
.idea
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
================================================
FILE: .npmrc
================================================
# Important! Never install from registry even when new version is available
prefer-workspace-packages=true
link-workspace-packages=true
save-workspace-protocol=false
auto-install-peers=false
================================================
FILE: .nvmrc
================================================
20.18.1
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules/typescript/lib"
}
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Clack
Thank you for your interest in contributing to Clack! This document provides detailed instructions for setting up your development environment, navigating the codebase, making changes, and submitting contributions.
> [!Tip]
> **For new contributors:** Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing to open source projects.
## Getting Started
### Prerequisites
- [Node.js](https://nodejs.org/) (version specified in `.nvmrc`, currently v20.18.1)
- [pnpm](https://pnpm.io/) (version 9.14.2 or higher)
If you use [volta](https://volta.sh/) or [nvm](https://github.com/nvm-sh/nvm), the correct Node.js version will be automatically selected based on the project's `.nvmrc` file.
### Local Development Setup
1. **Fork the repository**:
- Visit [https://github.com/bombshell-dev/clack](https://github.com/bombshell-dev/clack)
- Click the "Fork" button in the top right
- Clone your fork to your local machine
```bash
git clone https://github.com/YOUR_USERNAME/clack.git
cd clack
```
2. **Set up the upstream remote**:
```bash
git remote add upstream https://github.com/bombshell-dev/clack.git
```
3. **Install dependencies**:
```bash
pnpm install
```
4. **Build the packages**:
```bash
pnpm build
```
5. **Run the development server**:
```bash
pnpm dev
```
### Using Clack Packages in Your Own Projects During Development
If you want to test changes to Clack packages in your own project, you can use pnpm's linking capabilities:
1. **Build the Clack packages locally first**:
```bash
# In the clack repository
cd /path/to/clack
pnpm build
```
2. **Link the packages to your project using one of these methods**:
**Method 1: Using pnpm link**
```bash
# In your project
cd /path/to/your-project
# Link @clack/core
pnpm link --global /path/to/clack/packages/core
# Link @clack/prompts
pnpm link --global /path/to/clack/packages/prompts
```
**Method 2: Using local path in package.json**
In your project's package.json, reference the local paths:
```json
{
"dependencies": {
"@clack/core": "file:/path/to/clack/packages/core",
"@clack/prompts": "file:/path/to/clack/packages/prompts"
}
}
```
Then run `pnpm install` in your project.
3. **Watch for changes** (optional):
```bash
# In the clack repository
cd /path/to/clack
pnpm build --watch
```
4. **Refresh after changes**:
If you're making changes to Clack while testing in your project, you'll need to rebuild Clack and potentially reinstall in your project:
```bash
# In the clack repository
cd /path/to/clack
pnpm build
# In your project (if using Method 2)
cd /path/to/your-project
pnpm install
```
With this setup, you can develop and test your changes to Clack within the context of your own project. This is especially useful for implementing new features like filtering.
## Repository Structure
Clack is organized as a monorepo using pnpm workspaces. Understanding the structure will help you navigate and contribute effectively:
```
clack/
├── .changeset/ # Changeset files for versioning
├── .github/ # GitHub workflows and templates
├── examples/ # Example implementations of Clack
├── packages/ # Core packages
│ ├── core/ # Unstyled primitives (@clack/core)
│ └── prompts/ # Ready-to-use components (@clack/prompts)
├── biome.json # Biome configuration
├── package.json # Root package.json
├── pnpm-workspace.yaml # Workspace configuration
└── tsconfig.json # TypeScript configuration
```
### Key Packages
1. **@clack/core** (`packages/core/`):
- Contains the unstyled, extensible primitives for building CLI applications
- The foundation layer that provides the core functionality
2. **@clack/prompts** (`packages/prompts/`):
- Built on top of @clack/core
- Provides beautiful, ready-to-use CLI prompt components
- What most users will interact with directly
### Examples
The `examples/` directory contains sample projects that demonstrate how to use Clack. Examining these examples is a great way to understand how the library works in practice.
## Development Workflow
### Common Commands
- **Build all packages**:
```bash
pnpm build
```
- **Start development environment**:
```bash
pnpm dev
```
- **Run tests**:
```bash
pnpm test
```
- **Lint code**:
```bash
pnpm lint
```
- **Format code**:
```bash
pnpm format
```
- **Type check**:
```bash
pnpm run types
```
- **Build stubbed packages** (for faster development):
```bash
pnpm stub
```
### Making Changes
1. **Create a new branch**:
```bash
git checkout -b my-feature-branch
```
2. **Implement your changes**:
- For bug fixes, start by reproducing the issue
- For new features, consider how it fits into the existing architecture
- Maintain type safety with TypeScript
- Add or update tests as necessary
3. **Run local verification**:
```bash
# Ensure everything builds
pnpm build
# Check formatting and lint issues
pnpm format
pnpm lint
# Verify type correctness
pnpm types
# Run tests
pnpm test
```
4. **Create a changeset** (for changes that need versioning):
```bash
pnpm changeset
```
- Follow the prompts to select which packages have changed
- Choose the appropriate semver increment (patch, minor, major)
- Write a concise but descriptive message explaining the changes
### Testing Your Changes
For testing changes to the core functionality:
1. **Use the examples**:
```bash
# Run an example to test your changes
pnpm --filter @example/changesets run start
```
2. **Create a test-specific example** (if needed):
- Add a new directory in the `examples/` folder
- Implement a minimal reproduction that uses your new feature/fix
- Run it with `pnpm --filter @example/your-example run start`
### Debugging Tips
When encountering issues during development:
1. **Check for errors in the console** - Clack will often output helpful error messages
2. **Review the API documentation** - Ensure you're using components and functions as intended
3. **Look at existing examples** - See how similar features are implemented
4. **Inspect the packages individually** - Sometimes issues are isolated to either `core` or `prompts`
## Pull Request Process
1. **Commit your changes**:
```bash
git add .
git commit -m "feat: add new awesome feature"
```
2. **Push to your fork**:
```bash
git push origin my-feature-branch
```
3. **Create a pull request**:
- Go to your fork on GitHub
- Click "New pull request"
- Select your branch and add a descriptive title
- Fill in the PR template with details about your changes
- Reference any related issues
4. **Wait for the automated checks**:
- GitHub Actions will run tests, type checking, and lint validation
- Fix any issues that arise
5. **Address review feedback**:
- Make requested changes
- Push additional commits to your branch
- The PR will update automatically
### PR Previews
Clack uses [pkg.pr.new](https://pkg.pr.new) (provided by [bolt.new](https://bolt.new)) to create continuous preview releases of all PRs. This simplifies testing and makes verifying bug fixes easier for our dependents.
The workflow that builds a preview version and adds instructions for installation as a comment on your PR should run automatically if you have contributed to Clack before. First-time contributors may need to wait until a maintainer manually approves GitHub Actions running on your PR.
## Release Process
Clack uses [Changesets](https://github.com/changesets/changesets) to manage versions and releases.
1. **For contributors**:
- Create a changeset file with your PR as described above
- Maintainers will handle the actual release process
2. **For maintainers**:
- Merging PRs with changesets will queue them for the next release
- When ready to release, merge the `[ci] release` PR
### Backporting to v0 Branch
Clack maintains a stable `v0` branch alongside the main development branch. For maintainers who need to backport changes:
1. Label PRs that should be backported with the `backport` label
2. After the PR is merged to `main`, manually cherry-pick the squashed commit into the `v0` branch:
```bash
# Ensure you have the latest v0 branch
git checkout v0
git pull upstream v0
# Cherry-pick the squashed commit from main
git cherry-pick <commit-hash>
# Push the changes
git push upstream v0
```
3. CI is configured to run changesets from the `v0` branch, so release PRs will be opened automatically
The GitHub Actions are configured to cut releases from both the `main` and `v0` branches.
## Filing Issues
When reporting bugs or requesting features:
1. **Check existing issues** to avoid duplicates
2. **Use the issue templates** to provide all necessary information
3. **Be specific and clear** about what's happening and what you expect
4. **Provide reproduction steps** - ideally a minimal example that demonstrates the issue
5. **Include environment details** like OS, Node.js version, etc.
### Issue Types
When opening an issue, consider which category it falls into:
- **Bug Report**: Something isn't working as documented or expected
- **Feature Request**: A suggestion for new functionality
- **Documentation Issue**: Improvements or corrections to documentation
- **Performance Issue**: Problems with speed or resource usage
## Style Guide
We use [Biome](https://biomejs.dev/) for linting and formatting. Your code should follow these standards:
```bash
# To check formatting
pnpm format
# To lint and fix issues automatically where possible
pnpm lint
```
The project follows standard TypeScript practices. If you're new to TypeScript:
- Use precise types whenever possible
- Avoid `any` unless absolutely necessary
- Look at existing code for patterns to follow
### Commit Message Format
We follow conventional commits for commit messages:
- `feat:` - A new feature
- `fix:` - A bug fix
- `docs:` - Documentation changes
- `style:` - Changes that don't affect code functionality (formatting, etc)
- `refactor:` - Code changes that neither fix bugs nor add features
- `perf:` - Performance improvements
- `test:` - Adding or correcting tests
- `chore:` - Changes to the build process, tools, etc
## License
By contributing, you agree that your contributions will be licensed under the project's MIT License.
Thank you for taking the time to contribute to Clack! Feel free to join our community Discord at [bomb.sh/chat](https://bomb.sh/chat). It's a great place to connect with other project contributors—we're chill!
## Acknowledgments
This contributing guide was inspired by and adapted from the [Astro Contributing Manual](https://github.com/withastro/astro/blob/main/CONTRIBUTING.md). We appreciate their excellent documentation and open source practices.
================================================
FILE: README.md
================================================
<br />
<br />
<div align="center">
<img alt="Clack logo" src="/.github/assets/clack.png?sanitize=true" width="320">
</div>
<h2 align="center">stylish interactive prompts for JavaScript CLIs</h3>
<h4 align="center"><a href="packages/prompts#readme"><code>@clack/prompts</code></a>: opinionated, ready-to-use prompt components</h4>
<h4 align="center"><a href="packages/core#readme"><code>@clack/core</code></a>: headless, unstyled prompt primitives</h4>
<br />
<br />
<h3 align="center"><a href="https://bomb.sh/docs/clack/basics/getting-started/">Read the docs</a></h3>
================================================
FILE: biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": { "ignoreUnknown": false, "includes": ["**", "!**/dist/**"] },
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto",
"bracketSpacing": true,
"includes": [
"**",
"!**/.github/workflows/**/*.yml",
"!**/.changeset/**/*.md",
"!**/pnpm-lock.yaml",
"!**/package.json"
]
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": { "recommended": true, "suspicious": { "noExplicitAny": "off" } }
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"overrides": [
{
"includes": ["**/*.json", "**/*.toml", "**/*.yml"],
"formatter": { "indentStyle": "space" }
}
]
}
================================================
FILE: build.preset.ts
================================================
import { definePreset } from 'unbuild';
// @see https://github.com/unjs/unbuild
export default definePreset({
clean: true,
declaration: 'node16',
sourcemap: true,
rollup: {
emitCJS: false,
inlineDependencies: true,
esbuild: {
minify: true,
},
},
});
================================================
FILE: examples/basic/autocomplete-multiselect.ts
================================================
import * as p from '@clack/prompts';
import color from 'picocolors';
/**
* Example demonstrating the integrated autocomplete multiselect component
* Which combines filtering and selection in a single interface
*/
async function main() {
console.clear();
p.intro(`${color.bgCyan(color.black(' Integrated Autocomplete Multiselect Example '))}`);
p.note(
`
${color.cyan('Filter and select multiple items in a single interface:')}
- ${color.yellow('Type')} to filter the list in real-time
- Use ${color.yellow('up/down arrows')} to navigate with improved stability
- Press ${color.yellow('Space')} to select/deselect the highlighted item ${color.green('(multiple selections allowed)')}
- Use ${color.yellow('Backspace')} to modify your filter text when searching for different options
- Press ${color.yellow('Enter')} when done selecting all items
- Press ${color.yellow('Ctrl+C')} to cancel
`,
'Instructions'
);
// Frameworks in alphabetical order
const frameworks = [
{ value: 'angular', label: 'Angular', hint: 'Frontend/UI' },
{ value: 'django', label: 'Django', hint: 'Python Backend' },
{ value: 'dotnet', label: '.NET Core', hint: 'C# Backend' },
{ value: 'electron', label: 'Electron', hint: 'Desktop' },
{ value: 'express', label: 'Express', hint: 'Node.js Backend' },
{ value: 'flask', label: 'Flask', hint: 'Python Backend' },
{ value: 'flutter', label: 'Flutter', hint: 'Mobile' },
{ value: 'laravel', label: 'Laravel', hint: 'PHP Backend' },
{ value: 'nestjs', label: 'NestJS', hint: 'Node.js Backend' },
{ value: 'nextjs', label: 'Next.js', hint: 'React Framework' },
{ value: 'nuxt', label: 'Nuxt.js', hint: 'Vue Framework' },
{ value: 'rails', label: 'Ruby on Rails', hint: 'Ruby Backend' },
{ value: 'react', label: 'React', hint: 'Frontend/UI' },
{ value: 'reactnative', label: 'React Native', hint: 'Mobile' },
{ value: 'spring', label: 'Spring Boot', hint: 'Java Backend' },
{ value: 'svelte', label: 'Svelte', hint: 'Frontend/UI' },
{ value: 'tauri', label: 'Tauri', hint: 'Desktop' },
{ value: 'vue', label: 'Vue.js', hint: 'Frontend/UI' },
];
// Use the new integrated autocompleteMultiselect component
const result = await p.autocompleteMultiselect<string>({
message: 'Select frameworks (type to filter)',
options: frameworks,
placeholder: 'Type to filter...',
maxItems: 8,
});
if (p.isCancel(result)) {
p.cancel('Operation cancelled.');
process.exit(0);
}
// Type guard: if not a cancel symbol, result must be a string array
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
// We can now use the type guard to ensure type safety
if (!isStringArray(result)) {
throw new Error('Unexpected result type');
}
const selectedFrameworks = result;
// If no items selected, show a message
if (selectedFrameworks.length === 0) {
p.note('No frameworks were selected', 'Empty Selection');
process.exit(0);
}
// Display selected frameworks with detailed information
p.note(
`You selected ${color.green(selectedFrameworks.length)} frameworks:`,
'Selection Complete'
);
// Show each selected framework with its details
const selectedDetails = selectedFrameworks
.map((value) => {
const framework = frameworks.find((f) => f.value === value);
return framework
? `${color.cyan(framework.label)} ${color.dim(`- ${framework.hint}`)}`
: value;
})
.join('\n');
p.log.message(selectedDetails);
p.outro(`Successfully selected ${color.green(selectedFrameworks.length)} frameworks.`);
}
main().catch(console.error);
================================================
FILE: examples/basic/autocomplete.ts
================================================
import * as p from '@clack/prompts';
import color from 'picocolors';
async function main() {
console.clear();
p.intro(`${color.bgCyan(color.black(' Autocomplete Example '))}`);
p.note(
`
${color.cyan('This example demonstrates the type-ahead autocomplete feature:')}
- ${color.yellow('Type')} to filter the list in real-time
- Use ${color.yellow('up/down arrows')} to navigate the filtered results
- Press ${color.yellow('Enter')} to select the highlighted option
- Press ${color.yellow('Ctrl+C')} to cancel
`,
'Instructions'
);
const countries = [
{ value: 'us', label: 'United States', hint: 'NA' },
{ value: 'ca', label: 'Canada', hint: 'NA' },
{ value: 'mx', label: 'Mexico', hint: 'NA' },
{ value: 'br', label: 'Brazil', hint: 'SA' },
{ value: 'ar', label: 'Argentina', hint: 'SA' },
{ value: 'uk', label: 'United Kingdom', hint: 'EU' },
{ value: 'fr', label: 'France', hint: 'EU' },
{ value: 'de', label: 'Germany', hint: 'EU' },
{ value: 'it', label: 'Italy', hint: 'EU' },
{ value: 'es', label: 'Spain', hint: 'EU' },
{ value: 'pt', label: 'Portugal', hint: 'EU' },
{ value: 'ru', label: 'Russia', hint: 'EU/AS' },
{ value: 'cn', label: 'China', hint: 'AS' },
{ value: 'jp', label: 'Japan', hint: 'AS' },
{ value: 'in', label: 'India', hint: 'AS' },
{ value: 'kr', label: 'South Korea', hint: 'AS' },
{ value: 'au', label: 'Australia', hint: 'OC' },
{ value: 'nz', label: 'New Zealand', hint: 'OC' },
{ value: 'za', label: 'South Africa', hint: 'AF' },
{ value: 'eg', label: 'Egypt', hint: 'AF' },
];
const result = await p.autocomplete({
message: 'Select a country',
options: countries,
placeholder: 'Type to search countries...',
maxItems: 8,
});
if (p.isCancel(result)) {
p.cancel('Operation cancelled.');
process.exit(0);
}
const selected = countries.find((c) => c.value === result);
p.outro(`You selected: ${color.cyan(selected?.label)} (${color.yellow(selected?.hint)})`);
}
main().catch(console.error);
================================================
FILE: examples/basic/date.ts
================================================
import * as p from '@clack/prompts';
import color from 'picocolors';
async function main() {
const result = (await p.date({
message: color.magenta('Pick a date'),
format: 'YMD',
minDate: new Date('2026-01-01'),
maxDate: new Date('2026-12-31'),
})) as Date;
if (p.isCancel(result)) {
p.cancel('Operation cancelled.');
process.exit(0);
}
const fmt = (d: Date) => d.toISOString().slice(0, 10);
p.outro(`Selected date: ${color.cyan(fmt(result))}`);
}
main().catch(console.error);
================================================
FILE: examples/basic/default-value.ts
================================================
import * as p from '@clack/prompts';
import color from 'picocolors';
async function main() {
const defaultPath = 'my-project';
const result = await p.text({
message: 'Enter the directory to bootstrap the project',
placeholder: ` (hit Enter to use '${defaultPath}')`,
defaultValue: defaultPath,
validate: (value) => {
if (!value) {
return 'Directory is required';
}
if (value.includes(' ')) {
return 'Directory cannot contain spaces';
}
return undefined;
},
});
if (p.isCancel(result)) {
p.cancel('Operation cancelled.');
process.exit(0);
}
p.outro(`Let's bootstrap the project in ${color.cyan(result)}`);
}
main().catch(console.error);
================================================
FILE: examples/basic/index.ts
================================================
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
import color from 'picocolors';
async function main() {
console.clear();
await setTimeout(1000);
p.updateSettings({
aliases: {
w: 'up',
s: 'down',
a: 'left',
d: 'right',
},
});
p.intro(`${color.bgCyan(color.black(' create-app '))}`);
const project = await p.group(
{
path: () =>
p.text({
message: 'Where should we create your project?',
placeholder: './sparkling-solid',
validate: (value) => {
if (!value) return 'Please enter a path.';
if (value[0] !== '.') return 'Please enter a relative path.';
},
}),
password: () =>
p.password({
message: 'Provide a password',
validate: (value) => {
if (!value) return 'Please enter a password.';
if (value.length < 5) return 'Password should have at least 5 characters.';
},
}),
type: ({ results }) =>
p.select({
message: `Pick a project type within "${results.path}"`,
initialValue: 'ts',
maxItems: 5,
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'js', label: 'JavaScript' },
{ value: 'rust', label: 'Rust' },
{ value: 'go', label: 'Go' },
{ value: 'python', label: 'Python' },
{ value: 'coffee', label: 'CoffeeScript', hint: 'oh no' },
],
}),
tools: () =>
p.multiselect({
message: 'Select additional tools.',
initialValues: ['prettier', 'eslint'],
options: [
{ value: 'prettier', label: 'Prettier', hint: 'recommended' },
{ value: 'eslint', label: 'ESLint', hint: 'recommended' },
{ value: 'stylelint', label: 'Stylelint' },
{ value: 'gh-action', label: 'GitHub Action' },
],
}),
install: () =>
p.confirm({
message: 'Install dependencies?',
initialValue: false,
}),
},
{
onCancel: () => {
p.cancel('Operation cancelled.');
process.exit(0);
},
}
);
if (project.install) {
const s = p.spinner();
s.start('Installing via pnpm');
await setTimeout(2500);
s.stop('Installed via pnpm');
}
const nextSteps = `cd ${project.path} \n${project.install ? '' : 'pnpm install\n'}pnpm dev`;
p.note(nextSteps, 'Next steps.');
p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`);
}
main().catch(console.error);
================================================
FILE: examples/basic/package.json
================================================
{
"name": "@example/basic",
"private": true,
"version": "0.0.0",
"type": "module",
"dependencies": {
"@clack/prompts": "workspace:*",
"picocolors": "^1.0.0",
"jiti": "^1.17.0"
},
"scripts": {
"start": "jiti ./index.ts",
"stream": "jiti ./stream.ts",
"progress": "jiti ./progress.ts",
"spinner": "jiti ./spinner.ts",
"path": "jiti ./path.ts",
"date": "jiti ./date.ts",
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts",
"spinner-timer": "jiti ./spinner-timer.ts",
"task-log": "jiti ./task-log.ts"
},
"devDependencies": {
"cross-env": "^7.0.3"
}
}
================================================
FILE: examples/basic/path.ts
================================================
import * as p from '@clack/prompts';
async function demo() {
p.intro('path start...');
const _path = await p.path({
message: 'Read file',
});
p.outro('path stop...');
}
void demo();
================================================
FILE: examples/basic/progress.ts
================================================
import { setTimeout } from 'node:timers/promises';
import type { ProgressResult } from '@clack/prompts';
import * as p from '@clack/prompts';
async function fakeProgress(progressbar: ProgressResult): Promise<void> {
await setTimeout(1000);
for (const _i in Array(10).fill(1)) {
progressbar.advance();
await setTimeout(100 + Math.random() * 500);
}
}
async function demo() {
p.intro('progress start...');
const download = p.progress({ style: 'block', max: 10, size: 30 });
download.start('Downloading package');
await fakeProgress(download);
download.stop('Download completed');
const unarchive = p.progress({ style: 'heavy', max: 10, size: 30, indicator: undefined });
unarchive.start('Un-archiving');
await fakeProgress(unarchive);
unarchive.stop('Un-archiving completed');
const linking = p.progress({ style: 'light', max: 10, size: 30, indicator: 'timer' });
linking.start('Linking');
await fakeProgress(linking);
linking.stop('Package linked');
p.outro('progress stop...');
}
void demo();
================================================
FILE: examples/basic/spinner-cancel-advanced.ts
================================================
import { setTimeout as sleep } from 'node:timers/promises';
import * as p from '@clack/prompts';
async function main() {
p.intro('Advanced Spinner Cancellation Demo');
// First demonstrate a visible spinner with no user input needed
p.note('First, we will show a basic spinner (press CTRL+C to cancel)', 'Demo Part 1');
const demoSpinner = p.spinner({
indicator: 'dots',
onCancel: () => {
p.note('Initial spinner was cancelled with CTRL+C', 'Demo Cancelled');
},
});
demoSpinner.start('Loading demo resources');
// Update spinner message a few times to show activity
for (let i = 0; i < 5; i++) {
if (demoSpinner.isCancelled) break;
await sleep(1000);
demoSpinner.message(`Loading demo resources (${i + 1}/5)`);
}
if (!demoSpinner.isCancelled) {
demoSpinner.stop('Demo resources loaded successfully');
}
// Only continue with the rest of the demo if the initial spinner wasn't cancelled
if (!demoSpinner.isCancelled) {
// Stage 1: Get user input with multiselect
p.note("Now let's select some languages to process", 'Demo Part 2');
const languages = await p.multiselect({
message: 'Select programming languages to process:',
options: [
{ value: 'typescript', label: 'TypeScript' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'python', label: 'Python' },
{ value: 'rust', label: 'Rust' },
{ value: 'go', label: 'Go' },
],
required: true,
});
// Handle cancellation of the multiselect
if (p.isCancel(languages)) {
p.cancel('Operation cancelled during language selection.');
process.exit(0);
}
// Stage 2: Show a spinner that can be cancelled
const processSpinner = p.spinner({
indicator: 'dots',
onCancel: () => {
p.note(
'You cancelled during processing. Any completed work will be saved.',
'Processing Cancelled'
);
},
});
processSpinner.start('Starting to process selected languages...');
// Process each language with individual progress updates
let completedCount = 0;
const totalLanguages = languages.length;
for (const language of languages) {
// Skip the rest if cancelled
if (processSpinner.isCancelled) break;
// Update spinner message with current language
processSpinner.message(`Processing ${language} (${completedCount + 1}/${totalLanguages})`);
try {
// Simulate work - longer pause to give time to test CTRL+C
await sleep(2000);
completedCount++;
} catch (error) {
// Handle errors but continue if not cancelled
if (!processSpinner.isCancelled) {
p.note(`Error processing ${language}: ${error.message}`, 'Error');
}
}
}
// Stage 3: Handle completion based on cancellation status
if (!processSpinner.isCancelled) {
processSpinner.stop(`Processed ${completedCount}/${totalLanguages} languages successfully`);
// Stage 4: Additional user input based on processing results
if (completedCount > 0) {
const action = await p.select({
message: 'What would you like to do with the processed data?',
options: [
{ value: 'save', label: 'Save results', hint: 'Write to disk' },
{ value: 'share', label: 'Share results', hint: 'Upload to server' },
{ value: 'analyze', label: 'Further analysis', hint: 'Generate reports' },
],
});
if (p.isCancel(action)) {
p.cancel('Operation cancelled at final stage.');
process.exit(0);
}
// Stage 5: Final action with a timer spinner
p.note('Now demonstrating a timer-style spinner', 'Final Stage');
const finalSpinner = p.spinner({
indicator: 'timer', // Use timer indicator for variety
onCancel: () => {
p.note(
'Final operation was cancelled, but processing results are still valid.',
'Final Stage Cancelled'
);
},
});
finalSpinner.start(`Performing ${action} operation...`);
try {
// Simulate final action with incremental updates
for (let i = 0; i < 3; i++) {
if (finalSpinner.isCancelled) break;
await sleep(1500);
finalSpinner.message(`Performing ${action} operation... Step ${i + 1}/3`);
}
if (!finalSpinner.isCancelled) {
finalSpinner.stop(`${action} operation completed successfully`);
}
} catch (error) {
if (!finalSpinner.isCancelled) {
finalSpinner.stop(`Error during ${action}: ${error.message}`);
}
}
}
}
}
p.outro('Advanced demo completed. Thanks for trying out the spinner cancellation features!');
}
main().catch((error) => {
console.error('Unexpected error:', error);
process.exit(1);
});
================================================
FILE: examples/basic/spinner-cancel.ts
================================================
import * as p from '@clack/prompts';
p.intro('Spinner with cancellation detection');
// Example 1: Using onCancel callback
const spin1 = p.spinner({
indicator: 'dots',
onCancel: () => {
p.note('You cancelled the spinner with CTRL-C!', 'Callback detected');
},
});
spin1.start('Press CTRL-C to cancel this spinner (using callback)');
// Sleep for 10 seconds, allowing time for user to press CTRL-C
await sleep(10000).then(() => {
// Only show success message if not cancelled
if (!spin1.isCancelled) {
spin1.stop('Spinner completed without cancellation');
}
});
// Example 2: Checking the isCancelled property
p.note('Starting second example...', 'Example 2');
const spin2 = p.spinner({ indicator: 'timer' });
spin2.start('Press CTRL-C to cancel this spinner (polling isCancelled)');
await sleep(10000).then(() => {
if (spin2.isCancelled) {
p.note('Spinner was cancelled by the user!', 'Property check');
} else {
spin2.stop('Spinner completed without cancellation');
}
});
p.outro('Example completed');
// Helper function
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
================================================
FILE: examples/basic/spinner-ci.ts
================================================
/**
* This example addresses a issue reported in GitHub Actions where `spinner` was excessively writing messages,
* leading to confusion and cluttered output.
* To enhance the CI workflow and provide a smoother experience,
* the following changes have been made only for CI environment:
* - Messages will now only be written when a `spinner` method is called and the message updated, preventing unnecessary message repetition.
* - There will be no loading dots animation, instead it will be always `...`
* - Instead of erase the previous message, action that is blocked during CI, it will just write a new one.
*
* Issue: https://github.com/bombshell-dev/clack/issues/168
*/
import * as p from '@clack/prompts';
const s = p.spinner();
let progress = 0;
let counter = 0;
let loop: NodeJS.Timer;
p.intro('Running spinner in CI environment');
s.start('spinner.start');
new Promise((resolve) => {
loop = setInterval(() => {
if (progress % 1000 === 0) {
counter++;
}
progress += 100;
s.message(`spinner.message [${counter}]`);
if (counter > 6) {
clearInterval(loop);
resolve(true);
}
}, 100);
}).then(() => {
s.stop('spinner.stop');
p.outro('Done');
});
================================================
FILE: examples/basic/spinner-timer.ts
================================================
import * as p from '@clack/prompts';
p.intro('spinner start...');
async function main() {
const spin = p.spinner({ indicator: 'timer' });
spin.start('First spinner');
await sleep(3_000);
spin.stop('Done first spinner');
spin.start('Second spinner');
await sleep(5_000);
spin.stop('Done second spinner');
p.outro('spinner stop.');
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
main();
================================================
FILE: examples/basic/spinner.ts
================================================
import * as p from '@clack/prompts';
p.intro('spinner start...');
const spin = p.spinner();
const total = 6000;
let progress = 0;
spin.start();
new Promise((resolve) => {
const timer = setInterval(() => {
progress = Math.min(total, progress + 100);
if (progress >= total) {
clearInterval(timer);
resolve(true);
}
spin.message(`Loading packages [${progress}/${total}]`); // <===
}, 100);
}).then(() => {
spin.stop('Done');
p.outro('spinner stop...');
});
================================================
FILE: examples/basic/stream.ts
================================================
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
import color from 'picocolors';
async function main() {
console.clear();
await setTimeout(1000);
p.intro(`${color.bgCyan(color.black(' create-app '))}`);
await p.stream.step(
(async function* () {
for (const line of lorem) {
for (const word of line.split(' ')) {
yield word;
yield ' ';
await setTimeout(200);
}
yield '\n';
if (line !== lorem.at(-1)) {
await setTimeout(1000);
}
}
})()
);
p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`);
}
const lorem = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
];
main().catch(console.error);
================================================
FILE: examples/basic/task-log.ts
================================================
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
async function main() {
p.intro('task log start...');
const log = p.taskLog({
title: 'Running npm install',
limit: 5,
});
for await (const line of fakeCommand()) {
log.message(line);
}
log.success('Done!');
p.outro('task log stop...');
}
async function* fakeCommand() {
for (let i = 0; i < 100; i++) {
yield `line \x1b[32m${i}\x1b[39m...`;
await setTimeout(80);
}
}
main();
================================================
FILE: examples/basic/text-validation.ts
================================================
import { setTimeout } from 'node:timers/promises';
import { isCancel, note, text } from '@clack/prompts';
async function main() {
console.clear();
// Example demonstrating the issue with initial value validation
const name = await text({
message: 'Enter your name (letters and spaces only)',
initialValue: 'John123', // Invalid initial value with numbers
validate: (value) => {
if (!value || !/^[a-zA-Z\s]+$/.test(value)) return 'Name can only contain letters and spaces';
return undefined;
},
});
if (!isCancel(name)) {
note(`Valid name: ${name}`, 'Success');
}
await setTimeout(1000);
// Example with a valid initial value for comparison
const validName = await text({
message: 'Enter another name (letters and spaces only)',
initialValue: 'John Doe', // Valid initial value
validate: (value) => {
if (!value || !/^[a-zA-Z\s]+$/.test(value)) return 'Name can only contain letters and spaces';
return undefined;
},
});
if (!isCancel(validName)) {
note(`Valid name: ${validName}`, 'Success');
}
await setTimeout(1000);
}
main().catch(console.error);
================================================
FILE: examples/basic/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: examples/changesets/index.ts
================================================
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
import color from 'picocolors';
function onCancel() {
p.cancel('Operation cancelled.');
process.exit(0);
}
async function main() {
console.clear();
await setTimeout(1000);
p.intro(`${color.bgCyan(color.black(' changesets '))}`);
const _changeset = await p.group(
{
packages: () =>
p.groupMultiselect({
message: 'Which packages would you like to include?',
options: {
'changed packages': [
{ value: '@scope/a' },
{ value: '@scope/b' },
{ value: '@scope/c' },
],
'unchanged packages': [
{ value: '@scope/x' },
{ value: '@scope/y' },
{ value: '@scope/z' },
],
},
}),
major: ({ results }) => {
const packages = results.packages ?? [];
return p.multiselect({
message: `Which packages should have a ${color.red('major')} bump?`,
options: packages.map((value) => ({ value })),
required: false,
});
},
minor: ({ results }) => {
const packages = results.packages ?? [];
const major = Array.isArray(results.major) ? results.major : [];
const possiblePackages = packages.filter((pkg) => !major.includes(pkg));
if (possiblePackages.length === 0) return;
return p.multiselect({
message: `Which packages should have a ${color.yellow('minor')} bump?`,
options: possiblePackages.map((value) => ({ value })),
required: false,
});
},
patch: async ({ results }) => {
const packages = results.packages ?? [];
const major = Array.isArray(results.major) ? results.major : [];
const minor = Array.isArray(results.minor) ? results.minor : [];
const possiblePackages = packages.filter(
(pkg) => !major.includes(pkg) && !minor.includes(pkg)
);
if (possiblePackages.length === 0) return;
const note = possiblePackages.join(color.dim(', '));
p.log.step(`These packages will have a ${color.green('patch')} bump.\n${color.dim(note)}`);
return possiblePackages;
},
},
{
onCancel,
}
);
const message = await p.text({
placeholder: 'Summary',
message: 'Please enter a summary for this change',
});
if (p.isCancel(message)) {
return onCancel();
}
p.outro(`Changeset added! ${color.underline(color.cyan('.changeset/orange-crabs-sing.md'))}`);
}
main().catch(console.error);
================================================
FILE: examples/changesets/package.json
================================================
{
"name": "@example/changesets",
"private": true,
"version": "0.0.0",
"type": "module",
"dependencies": {
"jiti": "^1.17.0",
"@clack/prompts": "workspace:*",
"picocolors": "^1.0.0"
},
"scripts": {
"start": "jiti ./index.ts"
},
"devDependencies": {}
}
================================================
FILE: examples/changesets/tsconfig.json
================================================
{
"extends": "../../tsconfig.json"
}
================================================
FILE: knip.json
================================================
{
"workspaces": {
".": {
"ignore": ["build.preset.ts"]
},
"examples/*": {
"entry": "*.ts!",
"project": "*.ts"
},
"packages/*": {
"entry": "src/index.ts!",
"project": ["src/**/*.ts!", "test/**/*.ts"]
}
}
}
================================================
FILE: package.json
================================================
{
"name": "@clack/root",
"private": true,
"type": "module",
"scripts": {
"stub": "pnpm -r run build --stub",
"build": "pnpm --filter \"@clack/*\" run build",
"start": "pnpm run dev",
"dev": "pnpm --filter @example/changesets run start",
"format": "biome check --write",
"lint": "biome lint --write --unsafe",
"types": "tsc --noEmit",
"deps": "pnpm exec knip --production",
"test": "pnpm --color -r run test",
"pretest": "pnpm run build"
},
"devDependencies": {
"@biomejs/biome": "^2.1.2",
"@changesets/cli": "^2.29.5",
"@types/node": "^24.1.0",
"jsr": "^0.13.4",
"knip": "^5.62.0",
"typescript": "^5.8.3",
"unbuild": "^3.6.0"
},
"packageManager": "pnpm@9.14.2",
"volta": {
"node": "20.18.1"
}
}
================================================
FILE: packages/core/CHANGELOG.md
================================================
# @clack/core
## 1.1.0
### Minor Changes
- e3333fb: Replaces `picocolors` with Node.js built-in `styleText`.
## 1.0.1
### Patch Changes
- 6404dc1: Disallows selection of `disabled` options in autocomplete.
- 2533180: Updates the documentation to mention `userInputWithCursor` when using the `TextPrompt` primitive.
## 1.0.0
### Major Changes
- c713fd5: The package is now distributed as ESM-only. In `v0` releases, the package was dual-published as CJS and ESM.
For existing CJS projects using Node v20+, please see Node's guide on [Loading ECMAScript modules using `require()`](https://nodejs.org/docs/latest-v20.x/api/modules.html#loading-ecmascript-modules-using-require).
### Minor Changes
- 7bc3301: Prompts now have a `userInput` stored separately from their `value`.
- 2837845: Adds suggestion and path prompts
- 729bbb6: Add support for customizable spinner cancel and error messages. Users can now customize these messages either per spinner instance or globally via the `updateSettings` function to support multilingual CLIs.
This update also improves the architecture by exposing the core settings to the prompts package, enabling more consistent default message handling across the codebase.
```ts
// Per-instance customization
const spinner = prompts.spinner({
cancelMessage: "Operación cancelada", // "Operation cancelled" in Spanish
errorMessage: "Se produjo un error", // "An error occurred" in Spanish
});
// Global customization via updateSettings
prompts.updateSettings({
messages: {
cancel: "Operación cancelada", // "Operation cancelled" in Spanish
error: "Se produjo un error", // "An error occurred" in Spanish
},
});
// Settings can now be accessed directly
console.log(prompts.settings.messages.cancel); // "Operación cancelada"
// Direct options take priority over global settings
const spinner = prompts.spinner({
cancelMessage: "Cancelled", // This will be used instead of the global setting
});
```
- 55645c2: Support wrapping autocomplete and select prompts.
- f2c2b89: Adds `AutocompletePrompt` to core with comprehensive tests and implement both `autocomplete` and `autocomplete-multiselect` components in prompts package.
- df4eea1: Remove `suggestion` prompt and change `path` prompt to be an autocomplete prompt.
- 1604f97: Add `clearOnError` option to password prompt to automatically clear input when validation fails
### Patch Changes
- 0718b07: fix: export `*Options` types for prompts.
- bfe0dd3: Prevents placeholder from being used as input value in text prompts
- 6868c1c: Adds a new `selectableGroups` boolean to the group multi-select prompt. Using `selectableGroups: false` will disable the ability to select a top-level group, but still allow every child to be selected individually.
- 7df841d: Removed all trailing space in prompt output and fixed various padding rendering bugs.
- a4f5034: Fixes an edge case for placeholder values. Previously, when pressing `enter` on an empty prompt, placeholder values would be ignored. Now, placeholder values are treated as the prompt value.
- b103ad3: Allow disabled options in multi-select and select prompts.
- 71b5029: Add missing nullish checks around values.
- a36292b: Fix "TTY initialization failed: uv_tty_init returned EBADF (bad file descriptor)" error happening on Windows for non-tty terminals.
- 1a45f93: Switched from wrap-ansi to fast-wrap-ansi
- 4ba2d78: Support short terminal windows when re-rendering by accounting for off-screen lines
- 34f52fe: Validates initial values immediately when using text prompts with initialValue and validate props.
- 94fee2a: Changes `placeholder` to be a visual hint rather than a tabbable value.
- 4f6b3c2: Set initial values of auto complete prompt to first option when multiple is false.
- 8ead5d3: Avoid passing initial values to core when using auto complete prompt
- acc4c3a: Add a new `withGuide` option to all prompts to disable the default clack border
- 68dbf9b: select-key: Fixed wrapping and added new `caseSensitive` option
- 2310b43: Allow custom writables as output stream.
- d98e033: add invert selection for multiselect prompt
## 0.4.1
### Patch Changes
- 8093f3c: Adds `Error` support to the `validate` function
- e5ba09a: Fixes a cursor display bug in terminals that do not support the "hidden" escape sequence. See [Issue #127](https://github.com/bombshell-dev/clack/issues/127).
- 8cba8e3: Fixes a rendering bug with cursor positions for `TextPrompt`
## 0.4.0
### Minor Changes
- a83d2f8: Adds a new `updateSettings()` function to support new global keybindings.
`updateSettings()` accepts an `aliases` object that maps custom keys to an action (`up | down | left | right | space | enter | cancel`).
```ts
import { updateSettings } from "@clack/core";
// Support custom keybindings
updateSettings({
aliases: {
w: "up",
a: "left",
s: "down",
d: "right",
},
});
```
> [!WARNING]
> In order to enforce consistent, user-friendly defaults across the ecosystem, `updateSettings` does not support disabling Clack's default keybindings.
- 801246b: Adds a new `signal` option to support programmatic prompt cancellation with an [abort controller](https://kettanaito.com/blog/dont-sleep-on-abort-controller).
- a83d2f8: Updates default keybindings to support Vim motion shortcuts and map the `escape` key to cancel (`ctrl+c`).
| alias | action |
| ----- | ------ |
| `k` | up |
| `l` | right |
| `j` | down |
| `h` | left |
| `esc` | cancel |
### Patch Changes
- 51e12bc: Improves types for events and interaction states.
## 0.3.5
### Patch Changes
- 4845f4f: Fixes a bug which kept the terminal cursor hidden after a prompt is cancelled
- d7b2fb9: Adds missing `LICENSE` file. Since the `package.json` file has always included `"license": "MIT"`, please consider this a licensing clarification rather than a licensing change.
## 0.3.4
### Patch Changes
- a04e418: fix(@clack/core): keyboard input not working after await in spinner
- 4f6fcf5: feat(@clack/core): allow tab completion for placeholders
## 0.3.3
### Patch Changes
- cd79076: fix: restore raw mode on unblock
## 0.3.2
### Patch Changes
- c96eda5: Enable hard line-wrapping behavior for long words without spaces
## 0.3.1
### Patch Changes
- 58a1df1: Fix line duplication bug by automatically wrapping prompts to `process.stdout.columns`
## 0.3.0
### Minor Changes
- 8a4a12f: Add `GroupMultiSelect` prompt
### Patch Changes
- 8a4a12f: add `groupMultiselect` prompt
## 0.2.1
### Patch Changes
- ec812b6: fix `readline` hang on Windows
## 0.2.0
### Minor Changes
- d74dd05: Adds a `selectKey` prompt type
- 54c1bc3: **Breaking Change** `multiselect` has renamed `initialValue` to `initialValues`
## 0.1.9
### Patch Changes
- 1251132: Multiselect: return `Value[]` instead of `Option[]`.
- 8994382: Add a password prompt to `@clack/prompts`
## 0.1.8
### Patch Changes
- d96071c: Don't mutate `initialValue` in `multiselect`, fix parameter type for `validate()`.
Credits to @banjo for the bug report and initial PR!
## 0.1.7
### Patch Changes
- 6d9e675: Add support for neovim cursor motion (`hjkl`)
Thanks [@esau-morais](https://github.com/esau-morais) for the assist!
## 0.1.6
### Patch Changes
- 7fb5375: Adds a new `defaultValue` option to the text prompt, removes automatic usage of the placeholder value.
## 0.1.5
### Patch Changes
- de1314e: Support `required` option for multi-select
## 0.1.4
### Patch Changes
- ca77da1: Fix multiselect initial value logic
- 8aed606: Fix `MaxListenersExceededWarning` by detaching `stdin` listeners on close
## 0.1.3
### Patch Changes
- a99c458: Support `initialValue` option for text prompt
## 0.1.2
### Patch Changes
- Allow isCancel to type guard any unknown value
- 7dcad8f: Allow placeholder to be passed to TextPrompt
- 2242f13: Fix multiselect returning undefined
- b1341d6: Improved placeholder handling
## 0.1.1
### Patch Changes
- 4be7dbf: Ensure raw mode is unset on submit
- b480679: Preserve value if validation fails
## 0.1.0
### Minor Changes
- 7015ec9: Create new prompt: multi-select
## 0.0.12
### Patch Changes
- 9d371c3: Fix rendering bug when using y/n to confirm
## 0.0.11
### Patch Changes
- 441d5b7: fix select return undefined
- d20ef2a: Update keywords, URLs
- fe13c2f: fix cursor missing after submit
## 0.0.10
### Patch Changes
- a0cb382: Add `main` entrypoint
## 0.0.9
### Patch Changes
- Fix node@16 issue (cannot read "createInterface" of undefined)
## 0.0.8
### Patch Changes
- a4b5e13: Bug fixes, exposes `block` utility
## 0.0.7
### Patch Changes
- Fix cursor bug
## 0.0.6
### Patch Changes
- Fix error with character check
## 0.0.5
### Patch Changes
- 491f9e0: update readme
## 0.0.4
### Patch Changes
- 7372d5c: Fix bug with line deletion
## 0.0.3
### Patch Changes
- 5605d28: Do not bundle dependencies (take II)
## 0.0.2
### Patch Changes
- 2ee67cb: don't bundle deps
## 0.0.1
### Patch Changes
- 306598e: Initial publish, still WIP
================================================
FILE: packages/core/LICENSE
================================================
MIT License
Copyright (c) Nate Moore
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: packages/core/README.md
================================================
# `@clack/core`
Clack contains low-level primitives for implementing your own command-line applications.
Currently, `TextPrompt`, `SelectPrompt`, and `ConfirmPrompt` are exposed as well as the base `Prompt` class.
Each `Prompt` accepts a `render` function.
```js
import { TextPrompt, isCancel } from '@clack/core';
const p = new TextPrompt({
render() {
return `What's your name?\n${this.userInputWithCursor}`;
},
});
const name = await p.prompt();
if (isCancel(name)) {
process.exit(0);
}
```
================================================
FILE: packages/core/build.config.ts
================================================
import { defineBuildConfig } from 'unbuild';
// @see https://github.com/unjs/unbuild
export default defineBuildConfig({
preset: '../../build.preset',
entries: ['src/index'],
});
================================================
FILE: packages/core/package.json
================================================
{
"name": "@clack/core",
"version": "1.1.0",
"type": "module",
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"repository": {
"type": "git",
"url": "git+https://github.com/bombshell-dev/clack.git",
"directory": "packages/core"
},
"bugs": {
"url": "https://github.com/bombshell-dev/clack/issues"
},
"homepage": "https://github.com/bombshell-dev/clack/tree/main/packages/core#readme",
"files": [
"dist",
"CHANGELOG.md"
],
"keywords": [
"ask",
"clack",
"cli",
"command-line",
"command",
"input",
"interact",
"interface",
"menu",
"prompt",
"prompts",
"stdin",
"ui"
],
"author": {
"name": "Nate Moore",
"email": "nate@natemoo.re",
"url": "https://twitter.com/n_moore"
},
"license": "MIT",
"packageManager": "pnpm@9.14.2",
"scripts": {
"build": "unbuild",
"prepack": "pnpm build",
"test": "vitest run"
},
"dependencies": {
"fast-wrap-ansi": "^0.1.3",
"sisteransi": "^1.0.5"
},
"devDependencies": {
"vitest": "^3.2.4"
}
}
================================================
FILE: packages/core/src/index.ts
================================================
export type { AutocompleteOptions } from './prompts/autocomplete.js';
export { default as AutocompletePrompt } from './prompts/autocomplete.js';
export type { ConfirmOptions } from './prompts/confirm.js';
export { default as ConfirmPrompt } from './prompts/confirm.js';
export type { DateFormat, DateOptions, DateParts } from './prompts/date.js';
export { default as DatePrompt } from './prompts/date.js';
export type { GroupMultiSelectOptions } from './prompts/group-multiselect.js';
export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect.js';
export type { MultiSelectOptions } from './prompts/multi-select.js';
export { default as MultiSelectPrompt } from './prompts/multi-select.js';
export type { PasswordOptions } from './prompts/password.js';
export { default as PasswordPrompt } from './prompts/password.js';
export type { PromptOptions } from './prompts/prompt.js';
export { default as Prompt } from './prompts/prompt.js';
export type { SelectOptions } from './prompts/select.js';
export { default as SelectPrompt } from './prompts/select.js';
export type { SelectKeyOptions } from './prompts/select-key.js';
export { default as SelectKeyPrompt } from './prompts/select-key.js';
export type { TextOptions } from './prompts/text.js';
export { default as TextPrompt } from './prompts/text.js';
export type { ClackState as State } from './types.js';
export { block, getColumns, getRows, isCancel, wrapTextWithPrefix } from './utils/index.js';
export type { ClackSettings } from './utils/settings.js';
export { settings, updateSettings } from './utils/settings.js';
================================================
FILE: packages/core/src/prompts/autocomplete.ts
================================================
import type { Key } from 'node:readline';
import { styleText } from 'node:util';
import { findCursor } from '../utils/cursor.js';
import Prompt, { type PromptOptions } from './prompt.js';
interface OptionLike {
value: unknown;
label?: string;
disabled?: boolean;
}
type FilterFunction<T extends OptionLike> = (search: string, opt: T) => boolean;
function getCursorForValue<T extends OptionLike>(
selected: T['value'] | undefined,
items: T[]
): number {
if (selected === undefined) {
return 0;
}
const currLength = items.length;
// If filtering changed the available options, update cursor
if (currLength === 0) {
return 0;
}
// Try to maintain the same selected item
const index = items.findIndex((item) => item.value === selected);
return index !== -1 ? index : 0;
}
function defaultFilter<T extends OptionLike>(input: string, option: T): boolean {
const label = option.label ?? String(option.value);
return label.toLowerCase().includes(input.toLowerCase());
}
function normalisedValue<T>(multiple: boolean, values: T[] | undefined): T | T[] | undefined {
if (!values) {
return undefined;
}
if (multiple) {
return values;
}
return values[0];
}
export interface AutocompleteOptions<T extends OptionLike>
extends PromptOptions<T['value'] | T['value'][], AutocompletePrompt<T>> {
options: T[] | ((this: AutocompletePrompt<T>) => T[]);
filter?: FilterFunction<T>;
multiple?: boolean;
/**
* When set (non-empty), pressing Tab with no input fills the field with this value
* and runs the normal filter/selection logic so the user can confirm with Enter.
* Tab only fills the input when the placeholder matches at least one option under
* the prompt's filter (so the value remains selectable).
*/
placeholder?: string;
}
export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
T['value'] | T['value'][]
> {
filteredOptions: T[];
multiple: boolean;
isNavigating = false;
selectedValues: Array<T['value']> = [];
focusedValue: T['value'] | undefined;
#cursor = 0;
#lastUserInput = '';
#filterFn: FilterFunction<T>;
#options: T[] | (() => T[]);
#placeholder: string | undefined;
get cursor(): number {
return this.#cursor;
}
get userInputWithCursor() {
if (!this.userInput) {
return styleText(['inverse', 'hidden'], '_');
}
if (this._cursor >= this.userInput.length) {
return `${this.userInput}█`;
}
const s1 = this.userInput.slice(0, this._cursor);
const [s2, ...s3] = this.userInput.slice(this._cursor);
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
}
get options(): T[] {
if (typeof this.#options === 'function') {
return this.#options();
}
return this.#options;
}
constructor(opts: AutocompleteOptions<T>) {
super(opts);
this.#options = opts.options;
this.#placeholder = opts.placeholder;
const options = this.options;
this.filteredOptions = [...options];
this.multiple = opts.multiple === true;
this.#filterFn = opts.filter ?? defaultFilter;
let initialValues: unknown[] | undefined;
if (opts.initialValue && Array.isArray(opts.initialValue)) {
if (this.multiple) {
initialValues = opts.initialValue;
} else {
initialValues = opts.initialValue.slice(0, 1);
}
} else {
if (!this.multiple && this.options.length > 0) {
initialValues = [this.options[0].value];
}
}
if (initialValues) {
for (const selectedValue of initialValues) {
const selectedIndex = options.findIndex((opt) => opt.value === selectedValue);
if (selectedIndex !== -1) {
this.toggleSelected(selectedValue);
this.#cursor = selectedIndex;
}
}
}
this.focusedValue = this.options[this.#cursor]?.value;
this.on('key', (char, key) => this.#onKey(char, key));
this.on('userInput', (value) => this.#onUserInputChanged(value));
}
protected override _isActionKey(char: string | undefined, key: Key): boolean {
return (
char === '\t' ||
(this.multiple &&
this.isNavigating &&
key.name === 'space' &&
char !== undefined &&
char !== '')
);
}
#onKey(_char: string | undefined, key: Key): void {
const isUpKey = key.name === 'up';
const isDownKey = key.name === 'down';
const isReturnKey = key.name === 'return';
// Tab with empty input and placeholder: fill input with placeholder to trigger autocomplete
// Only when the placeholder matches at least one (non-disabled) option so the value remains selectable
const isEmptyOrOnlyTab = this.userInput === '' || this.userInput === '\t';
const placeholder = this.#placeholder;
const options = this.options;
const placeholderMatchesOption =
placeholder !== undefined &&
placeholder !== '' &&
options.some((opt) => !opt.disabled && this.#filterFn(placeholder, opt));
if (key.name === 'tab' && isEmptyOrOnlyTab && placeholderMatchesOption) {
if (this.userInput === '\t') {
this._clearUserInput();
}
this._setUserInput(placeholder, true);
this.isNavigating = false;
return;
}
// Start navigation mode with up/down arrows
if (isUpKey || isDownKey) {
this.#cursor = findCursor(this.#cursor, isUpKey ? -1 : 1, this.filteredOptions);
this.focusedValue = this.filteredOptions[this.#cursor]?.value;
if (!this.multiple) {
this.selectedValues = [this.focusedValue];
}
this.isNavigating = true;
} else if (isReturnKey) {
this.value = normalisedValue(this.multiple, this.selectedValues);
} else {
if (this.multiple) {
if (
this.focusedValue !== undefined &&
(key.name === 'tab' || (this.isNavigating && key.name === 'space'))
) {
this.toggleSelected(this.focusedValue);
} else {
this.isNavigating = false;
}
} else {
if (this.focusedValue) {
this.selectedValues = [this.focusedValue];
}
this.isNavigating = false;
}
}
}
deselectAll() {
this.selectedValues = [];
}
toggleSelected(value: T['value']) {
if (this.filteredOptions.length === 0) {
return;
}
if (this.multiple) {
if (this.selectedValues.includes(value)) {
this.selectedValues = this.selectedValues.filter((v) => v !== value);
} else {
this.selectedValues = [...this.selectedValues, value];
}
} else {
this.selectedValues = [value];
}
}
#onUserInputChanged(value: string): void {
if (value !== this.#lastUserInput) {
this.#lastUserInput = value;
const options = this.options;
if (value) {
this.filteredOptions = options.filter((opt) => this.#filterFn(value, opt));
} else {
this.filteredOptions = [...options];
}
const valueCursor = getCursorForValue(this.focusedValue, this.filteredOptions);
this.#cursor = findCursor(valueCursor, 0, this.filteredOptions);
const focusedOption = this.filteredOptions[this.#cursor];
if (focusedOption && !focusedOption.disabled) {
this.focusedValue = focusedOption.value;
} else {
this.focusedValue = undefined;
}
if (!this.multiple) {
if (this.focusedValue !== undefined) {
this.toggleSelected(this.focusedValue);
} else {
this.deselectAll();
}
}
}
}
}
================================================
FILE: packages/core/src/prompts/confirm.ts
================================================
import { cursor } from 'sisteransi';
import Prompt, { type PromptOptions } from './prompt.js';
export interface ConfirmOptions extends PromptOptions<boolean, ConfirmPrompt> {
active: string;
inactive: string;
initialValue?: boolean;
}
export default class ConfirmPrompt extends Prompt<boolean> {
get cursor() {
return this.value ? 0 : 1;
}
private get _value() {
return this.cursor === 0;
}
constructor(opts: ConfirmOptions) {
super(opts, false);
this.value = !!opts.initialValue;
this.on('userInput', () => {
this.value = this._value;
});
this.on('confirm', (confirm) => {
this.output.write(cursor.move(0, -1));
this.value = confirm;
this.state = 'submit';
this.close();
});
this.on('cursor', () => {
this.value = !this.value;
});
}
}
================================================
FILE: packages/core/src/prompts/date.ts
================================================
import type { Key } from 'node:readline';
import { settings } from '../utils/settings.js';
import Prompt, { type PromptOptions } from './prompt.js';
interface SegmentConfig {
type: 'year' | 'month' | 'day';
len: number;
}
export interface DateParts {
year: string;
month: string;
day: string;
}
export type DateFormat = 'YMD' | 'MDY' | 'DMY';
const SEGMENTS: Record<string, SegmentConfig> = {
Y: { type: 'year', len: 4 },
M: { type: 'month', len: 2 },
D: { type: 'day', len: 2 },
} as const;
function segmentsFor(fmt: DateFormat): SegmentConfig[] {
return [...fmt].map((c) => SEGMENTS[c as keyof typeof SEGMENTS]);
}
function detectLocaleFormat(locale?: string): { segments: SegmentConfig[]; separator: string } {
const fmt = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const parts = fmt.formatToParts(new Date(2000, 0, 15));
const segments: SegmentConfig[] = [];
let separator = '/';
for (const p of parts) {
if (p.type === 'literal') {
separator = p.value.trim() || p.value;
} else if (p.type === 'year' || p.type === 'month' || p.type === 'day') {
segments.push({ type: p.type, len: p.type === 'year' ? 4 : 2 });
}
}
return { segments, separator };
}
/** Parse string segment values to numbers, treating blanks as 0 */
function parseSegmentToNum(s: string): number {
return Number.parseInt((s || '0').replace(/_/g, '0'), 10) || 0;
}
function parse(parts: DateParts): { year: number; month: number; day: number } {
return {
year: parseSegmentToNum(parts.year),
month: parseSegmentToNum(parts.month),
day: parseSegmentToNum(parts.day),
};
}
function daysInMonth(year: number, month: number): number {
return new Date(year || 2001, month || 1, 0).getDate();
}
/** Validate and return calendar parts, or undefined if invalid */
function validParts(parts: DateParts): { year: number; month: number; day: number } | undefined {
const { year, month, day } = parse(parts);
if (!year || year < 0 || year > 9999) return undefined;
if (!month || month < 1 || month > 12) return undefined;
if (!day || day < 1) return undefined;
const d = new Date(Date.UTC(year, month - 1, day));
if (d.getUTCFullYear() !== year || d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day)
return undefined;
return { year, month, day };
}
function toDate(parts: DateParts): Date | undefined {
const p = validParts(parts);
return p ? new Date(Date.UTC(p.year, p.month - 1, p.day)) : undefined;
}
function segmentBounds(
type: 'year' | 'month' | 'day',
ctx: { year: number; month: number },
minDate: Date | undefined,
maxDate: Date | undefined
): { min: number; max: number } {
const minP = minDate
? {
year: minDate.getUTCFullYear(),
month: minDate.getUTCMonth() + 1,
day: minDate.getUTCDate(),
}
: null;
const maxP = maxDate
? {
year: maxDate.getUTCFullYear(),
month: maxDate.getUTCMonth() + 1,
day: maxDate.getUTCDate(),
}
: null;
if (type === 'year') {
return { min: minP?.year ?? 1, max: maxP?.year ?? 9999 };
}
if (type === 'month') {
return {
min: minP && ctx.year === minP.year ? minP.month : 1,
max: maxP && ctx.year === maxP.year ? maxP.month : 12,
};
}
return {
min: minP && ctx.year === minP.year && ctx.month === minP.month ? minP.day : 1,
max:
maxP && ctx.year === maxP.year && ctx.month === maxP.month
? maxP.day
: daysInMonth(ctx.year, ctx.month),
};
}
export interface DateOptions extends PromptOptions<Date, DatePrompt> {
format?: DateFormat;
locale?: string;
separator?: string;
defaultValue?: Date;
initialValue?: Date;
minDate?: Date;
maxDate?: Date;
}
export default class DatePrompt extends Prompt<Date> {
#segments: SegmentConfig[];
#separator: string;
#segmentValues: DateParts;
#minDate: Date | undefined;
#maxDate: Date | undefined;
#cursor = { segmentIndex: 0, positionInSegment: 0 };
#segmentSelected = true;
#pendingTensDigit: string | null = null;
inlineError = '';
get segmentCursor() {
return { ...this.#cursor };
}
get segmentValues(): DateParts {
return { ...this.#segmentValues };
}
get segments(): readonly SegmentConfig[] {
return this.#segments;
}
get separator(): string {
return this.#separator;
}
get formattedValue(): string {
return this.#format(this.#segmentValues);
}
#format(parts: DateParts): string {
return this.#segments.map((s) => parts[s.type]).join(this.#separator);
}
#refresh() {
this._setUserInput(this.#format(this.#segmentValues));
this._setValue(toDate(this.#segmentValues) ?? undefined);
}
constructor(opts: DateOptions) {
const detected = opts.format
? { segments: segmentsFor(opts.format), separator: opts.separator ?? '/' }
: detectLocaleFormat(opts.locale);
const sep = opts.separator ?? detected.separator;
const segments = opts.format ? segmentsFor(opts.format) : detected.segments;
const initialDate = opts.initialValue ?? opts.defaultValue;
const segmentValues: DateParts = initialDate
? {
year: String(initialDate.getUTCFullYear()).padStart(4, '0'),
month: String(initialDate.getUTCMonth() + 1).padStart(2, '0'),
day: String(initialDate.getUTCDate()).padStart(2, '0'),
}
: { year: '____', month: '__', day: '__' };
const initialDisplay = segments.map((s) => segmentValues[s.type]).join(sep);
super({ ...opts, initialUserInput: initialDisplay }, false);
this.#segments = segments;
this.#separator = sep;
this.#segmentValues = segmentValues;
this.#minDate = opts.minDate;
this.#maxDate = opts.maxDate;
this.#refresh();
this.on('cursor', (key) => this.#onCursor(key));
this.on('key', (char, key) => this.#onKey(char, key));
this.on('finalize', () => this.#onFinalize(opts));
}
#seg(): { segment: SegmentConfig; index: number } | undefined {
const index = Math.max(0, Math.min(this.#cursor.segmentIndex, this.#segments.length - 1));
const segment = this.#segments[index];
if (!segment) return undefined;
this.#cursor.positionInSegment = Math.max(
0,
Math.min(this.#cursor.positionInSegment, segment.len - 1)
);
return { segment, index };
}
#navigate(direction: 1 | -1) {
this.inlineError = '';
this.#pendingTensDigit = null;
const ctx = this.#seg();
if (!ctx) return;
this.#cursor.segmentIndex = Math.max(
0,
Math.min(this.#segments.length - 1, ctx.index + direction)
);
this.#cursor.positionInSegment = 0;
this.#segmentSelected = true;
}
#adjust(direction: 1 | -1) {
const ctx = this.#seg();
if (!ctx) return;
const { segment } = ctx;
const raw = this.#segmentValues[segment.type];
const isBlank = !raw || raw.replace(/_/g, '') === '';
const num = Number.parseInt((raw || '0').replace(/_/g, '0'), 10) || 0;
const bounds = segmentBounds(
segment.type,
parse(this.#segmentValues),
this.#minDate,
this.#maxDate
);
let next: number;
if (isBlank) {
next = direction === 1 ? bounds.min : bounds.max;
} else {
next = Math.max(Math.min(bounds.max, num + direction), bounds.min);
}
this.#segmentValues = {
...this.#segmentValues,
[segment.type]: next.toString().padStart(segment.len, '0'),
};
this.#segmentSelected = true;
this.#pendingTensDigit = null;
this.#refresh();
}
#onCursor(key?: string) {
if (!key) return;
switch (key) {
case 'right':
return this.#navigate(1);
case 'left':
return this.#navigate(-1);
case 'up':
return this.#adjust(1);
case 'down':
return this.#adjust(-1);
}
}
#onKey(char: string | undefined, key: Key) {
// Backspace
const isBackspace =
key?.name === 'backspace' ||
key?.sequence === '\x7f' ||
key?.sequence === '\b' ||
char === '\x7f' ||
char === '\b';
if (isBackspace) {
this.inlineError = '';
const ctx = this.#seg();
if (!ctx) return;
if (!this.#segmentValues[ctx.segment.type].replace(/_/g, '')) {
this.#navigate(-1);
return;
}
this.#segmentValues[ctx.segment.type] = '_'.repeat(ctx.segment.len);
this.#segmentSelected = true;
this.#cursor.positionInSegment = 0;
this.#refresh();
return;
}
// Tab navigation
if (key?.name === 'tab') {
this.inlineError = '';
const ctx = this.#seg();
if (!ctx) return;
const dir = key.shift ? -1 : 1;
const next = ctx.index + dir;
if (next >= 0 && next < this.#segments.length) {
this.#cursor.segmentIndex = next;
this.#cursor.positionInSegment = 0;
this.#segmentSelected = true;
}
return;
}
// Digit input
if (char && /^[0-9]$/.test(char)) {
const ctx = this.#seg();
if (!ctx) return;
const { segment } = ctx;
const isBlank = !this.#segmentValues[segment.type].replace(/_/g, '');
// Pending tens digit: complete the two-digit entry
if (this.#segmentSelected && this.#pendingTensDigit !== null && !isBlank) {
const newVal = this.#pendingTensDigit + char;
const newParts = { ...this.#segmentValues, [segment.type]: newVal };
const err = this.#validateSegment(newParts, segment);
if (err) {
this.inlineError = err;
this.#pendingTensDigit = null;
this.#segmentSelected = false;
return;
}
this.inlineError = '';
this.#segmentValues[segment.type] = newVal;
this.#pendingTensDigit = null;
this.#segmentSelected = false;
this.#refresh();
if (ctx.index < this.#segments.length - 1) {
this.#cursor.segmentIndex = ctx.index + 1;
this.#cursor.positionInSegment = 0;
this.#segmentSelected = true;
}
return;
}
// Clear-on-type: typing into a selected filled segment clears it first
if (this.#segmentSelected && !isBlank) {
this.#segmentValues[segment.type] = '_'.repeat(segment.len);
this.#cursor.positionInSegment = 0;
}
this.#segmentSelected = false;
this.#pendingTensDigit = null;
const display = this.#segmentValues[segment.type];
const firstBlank = display.indexOf('_');
const pos =
firstBlank >= 0 ? firstBlank : Math.min(this.#cursor.positionInSegment, segment.len - 1);
if (pos < 0 || pos >= segment.len) return;
let newVal = display.slice(0, pos) + char + display.slice(pos + 1);
// Smart digit placement
let shouldStaySelected = false;
if (pos === 0 && display === '__' && (segment.type === 'month' || segment.type === 'day')) {
const digit = Number.parseInt(char, 10);
newVal = `0${char}`;
shouldStaySelected = digit <= (segment.type === 'month' ? 1 : 2);
}
if (segment.type === 'year') {
const digits = display.replace(/_/g, '');
newVal = (digits + char).padStart(segment.len, '_');
}
if (!newVal.includes('_')) {
const newParts = { ...this.#segmentValues, [segment.type]: newVal };
const err = this.#validateSegment(newParts, segment);
if (err) {
this.inlineError = err;
return;
}
}
this.inlineError = '';
this.#segmentValues[segment.type] = newVal;
// Clamp only when the current segment is fully entered
const parsed = !newVal.includes('_') ? validParts(this.#segmentValues) : undefined;
if (parsed) {
const { year, month } = parsed;
const maxDay = daysInMonth(year, month);
this.#segmentValues = {
year: String(Math.max(0, Math.min(9999, year))).padStart(4, '0'),
month: String(Math.max(1, Math.min(12, month))).padStart(2, '0'),
day: String(Math.max(1, Math.min(maxDay, parsed.day))).padStart(2, '0'),
};
}
this.#refresh();
// Advance cursor
const nextBlank = newVal.indexOf('_');
if (shouldStaySelected) {
this.#segmentSelected = true;
this.#pendingTensDigit = char;
} else if (nextBlank >= 0) {
this.#cursor.positionInSegment = nextBlank;
} else if (firstBlank >= 0 && ctx.index < this.#segments.length - 1) {
this.#cursor.segmentIndex = ctx.index + 1;
this.#cursor.positionInSegment = 0;
this.#segmentSelected = true;
} else {
this.#cursor.positionInSegment = Math.min(pos + 1, segment.len - 1);
}
}
}
#validateSegment(parts: DateParts, seg: SegmentConfig): string | undefined {
const { month, day } = parse(parts);
if (seg.type === 'month' && (month < 0 || month > 12)) {
return settings.date.messages.invalidMonth;
}
if (seg.type === 'day' && (day < 0 || day > 31)) {
return settings.date.messages.invalidDay(31, 'any month');
}
return undefined;
}
#onFinalize(opts: DateOptions) {
const { year, month, day } = parse(this.#segmentValues);
if (year && month && day) {
const maxDay = daysInMonth(year, month);
this.#segmentValues = {
...this.#segmentValues,
day: String(Math.min(day, maxDay)).padStart(2, '0'),
};
}
this.value = toDate(this.#segmentValues) ?? opts.defaultValue ?? undefined;
}
}
================================================
FILE: packages/core/src/prompts/group-multiselect.ts
================================================
import Prompt, { type PromptOptions } from './prompt.js';
export interface GroupMultiSelectOptions<T extends { value: any }>
extends PromptOptions<T['value'][], GroupMultiSelectPrompt<T>> {
options: Record<string, T[]>;
initialValues?: T['value'][];
required?: boolean;
cursorAt?: T['value'];
selectableGroups?: boolean;
}
export default class GroupMultiSelectPrompt<T extends { value: any }> extends Prompt<T['value'][]> {
options: (T & { group: string | boolean })[];
cursor = 0;
#selectableGroups: boolean;
getGroupItems(group: string): T[] {
return this.options.filter((o) => o.group === group);
}
isGroupSelected(group: string) {
const items = this.getGroupItems(group);
const value = this.value;
if (value === undefined) {
return false;
}
return items.every((i) => value.includes(i.value));
}
private toggleValue() {
const item = this.options[this.cursor];
if (this.value === undefined) {
this.value = [];
}
if (item.group === true) {
const group = item.value;
const groupedItems = this.getGroupItems(group);
if (this.isGroupSelected(group)) {
this.value = this.value.filter(
(v: string) => groupedItems.findIndex((i) => i.value === v) === -1
);
} else {
this.value = [...this.value, ...groupedItems.map((i) => i.value)];
}
this.value = Array.from(new Set(this.value));
} else {
const selected = this.value.includes(item.value);
this.value = selected
? this.value.filter((v: T['value']) => v !== item.value)
: [...this.value, item.value];
}
}
constructor(opts: GroupMultiSelectOptions<T>) {
super(opts, false);
const { options } = opts;
this.#selectableGroups = opts.selectableGroups !== false;
this.options = Object.entries(options).flatMap(([key, option]) => [
{ value: key, group: true, label: key },
...option.map((opt) => ({ ...opt, group: key })),
]) as any;
this.value = [...(opts.initialValues ?? [])];
this.cursor = Math.max(
this.options.findIndex(({ value }) => value === opts.cursorAt),
this.#selectableGroups ? 0 : 1
);
this.on('cursor', (key) => {
switch (key) {
case 'left':
case 'up': {
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
const currentIsGroup = this.options[this.cursor]?.group === true;
if (!this.#selectableGroups && currentIsGroup) {
this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
}
break;
}
case 'down':
case 'right': {
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
const currentIsGroup = this.options[this.cursor]?.group === true;
if (!this.#selectableGroups && currentIsGroup) {
this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
}
break;
}
case 'space':
this.toggleValue();
break;
}
});
}
}
================================================
FILE: packages/core/src/prompts/multi-select.ts
================================================
import { findCursor } from '../utils/cursor.js';
import Prompt, { type PromptOptions } from './prompt.js';
interface OptionLike {
value: any;
disabled?: boolean;
}
export interface MultiSelectOptions<T extends OptionLike>
extends PromptOptions<T['value'][], MultiSelectPrompt<T>> {
options: T[];
initialValues?: T['value'][];
required?: boolean;
cursorAt?: T['value'];
}
export default class MultiSelectPrompt<T extends OptionLike> extends Prompt<T['value'][]> {
options: T[];
cursor = 0;
private get _value(): T['value'] {
return this.options[this.cursor].value;
}
private get _enabledOptions(): T[] {
return this.options.filter((option) => option.disabled !== true);
}
private toggleAll() {
const enabledOptions = this._enabledOptions;
const allSelected = this.value !== undefined && this.value.length === enabledOptions.length;
this.value = allSelected ? [] : enabledOptions.map((v) => v.value);
}
private toggleInvert() {
const value = this.value;
if (!value) {
return;
}
const notSelected = this._enabledOptions.filter((v) => !value.includes(v.value));
this.value = notSelected.map((v) => v.value);
}
private toggleValue() {
if (this.value === undefined) {
this.value = [];
}
const selected = this.value.includes(this._value);
this.value = selected
? this.value.filter((value) => value !== this._value)
: [...this.value, this._value];
}
constructor(opts: MultiSelectOptions<T>) {
super(opts, false);
this.options = opts.options;
this.value = [...(opts.initialValues ?? [])];
const cursor = Math.max(
this.options.findIndex(({ value }) => value === opts.cursorAt),
0
);
this.cursor = this.options[cursor].disabled ? findCursor<T>(cursor, 1, this.options) : cursor;
this.on('key', (char) => {
if (char === 'a') {
this.toggleAll();
}
if (char === 'i') {
this.toggleInvert();
}
});
this.on('cursor', (key) => {
switch (key) {
case 'left':
case 'up':
this.cursor = findCursor<T>(this.cursor, -1, this.options);
break;
case 'down':
case 'right':
this.cursor = findCursor<T>(this.cursor, 1, this.options);
break;
case 'space':
this.toggleValue();
break;
}
});
}
}
================================================
FILE: packages/core/src/prompts/password.ts
================================================
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';
export interface PasswordOptions extends PromptOptions<string, PasswordPrompt> {
mask?: string;
}
export default class PasswordPrompt extends Prompt<string> {
private _mask = '•';
get cursor() {
return this._cursor;
}
get masked() {
return this.userInput.replaceAll(/./g, this._mask);
}
get userInputWithCursor() {
if (this.state === 'submit' || this.state === 'cancel') {
return this.masked;
}
const userInput = this.userInput;
if (this.cursor >= userInput.length) {
return `${this.masked}${styleText(['inverse', 'hidden'], '_')}`;
}
const masked = this.masked;
const s1 = masked.slice(0, this.cursor);
const s2 = masked.slice(this.cursor);
return `${s1}${styleText('inverse', s2[0])}${s2.slice(1)}`;
}
clear() {
this._clearUserInput();
}
constructor({ mask, ...opts }: PasswordOptions) {
super(opts);
this._mask = mask ?? '•';
this.on('userInput', (input) => {
this._setValue(input);
});
}
}
================================================
FILE: packages/core/src/prompts/prompt.ts
================================================
import { stdin, stdout } from 'node:process';
import readline, { type Key, type ReadLine } from 'node:readline';
import type { Readable, Writable } from 'node:stream';
import { wrapAnsi } from 'fast-wrap-ansi';
import { cursor, erase } from 'sisteransi';
import type { ClackEvents, ClackState } from '../types.js';
import type { Action } from '../utils/index.js';
import {
CANCEL_SYMBOL,
diffLines,
getRows,
isActionKey,
setRawMode,
settings,
} from '../utils/index.js';
export interface PromptOptions<TValue, Self extends Prompt<TValue>> {
render(this: Omit<Self, 'prompt'>): string | undefined;
initialValue?: any;
initialUserInput?: string;
validate?: ((value: TValue | undefined) => string | Error | undefined) | undefined;
input?: Readable;
output?: Writable;
debug?: boolean;
signal?: AbortSignal;
}
export default class Prompt<TValue> {
protected input: Readable;
protected output: Writable;
private _abortSignal?: AbortSignal;
private rl: ReadLine | undefined;
private opts: Omit<PromptOptions<TValue, Prompt<TValue>>, 'render' | 'input' | 'output'>;
private _render: (context: Omit<Prompt<TValue>, 'prompt'>) => string | undefined;
private _track = false;
private _prevFrame = '';
private _subscribers = new Map<string, { cb: (...args: any) => any; once?: boolean }[]>();
protected _cursor = 0;
public state: ClackState = 'initial';
public error = '';
public value: TValue | undefined;
public userInput = '';
constructor(options: PromptOptions<TValue, Prompt<TValue>>, trackValue = true) {
const { input = stdin, output = stdout, render, signal, ...opts } = options;
this.opts = opts;
this.onKeypress = this.onKeypress.bind(this);
this.close = this.close.bind(this);
this.render = this.render.bind(this);
this._render = render.bind(this);
this._track = trackValue;
this._abortSignal = signal;
this.input = input;
this.output = output;
}
/**
* Unsubscribe all listeners
*/
protected unsubscribe() {
this._subscribers.clear();
}
/**
* Set a subscriber with opts
* @param event - The event name
*/
private setSubscriber<T extends keyof ClackEvents<TValue>>(
event: T,
opts: { cb: ClackEvents<TValue>[T]; once?: boolean }
) {
const params = this._subscribers.get(event) ?? [];
params.push(opts);
this._subscribers.set(event, params);
}
/**
* Subscribe to an event
* @param event - The event name
* @param cb - The callback
*/
public on<T extends keyof ClackEvents<TValue>>(event: T, cb: ClackEvents<TValue>[T]) {
this.setSubscriber(event, { cb });
}
/**
* Subscribe to an event once
* @param event - The event name
* @param cb - The callback
*/
public once<T extends keyof ClackEvents<TValue>>(event: T, cb: ClackEvents<TValue>[T]) {
this.setSubscriber(event, { cb, once: true });
}
/**
* Emit an event with data
* @param event - The event name
* @param data - The data to pass to the callback
*/
public emit<T extends keyof ClackEvents<TValue>>(
event: T,
...data: Parameters<ClackEvents<TValue>[T]>
) {
const cbs = this._subscribers.get(event) ?? [];
const cleanup: (() => void)[] = [];
for (const subscriber of cbs) {
subscriber.cb(...data);
if (subscriber.once) {
cleanup.push(() => cbs.splice(cbs.indexOf(subscriber), 1));
}
}
for (const cb of cleanup) {
cb();
}
}
public prompt() {
return new Promise<TValue | symbol | undefined>((resolve) => {
if (this._abortSignal) {
if (this._abortSignal.aborted) {
this.state = 'cancel';
this.close();
return resolve(CANCEL_SYMBOL);
}
this._abortSignal.addEventListener(
'abort',
() => {
this.state = 'cancel';
this.close();
},
{ once: true }
);
}
this.rl = readline.createInterface({
input: this.input,
tabSize: 2,
prompt: '',
escapeCodeTimeout: 50,
terminal: true,
});
this.rl.prompt();
if (this.opts.initialUserInput !== undefined) {
this._setUserInput(this.opts.initialUserInput, true);
}
this.input.on('keypress', this.onKeypress);
setRawMode(this.input, true);
this.output.on('resize', this.render);
this.render();
this.once('submit', () => {
this.output.write(cursor.show);
this.output.off('resize', this.render);
setRawMode(this.input, false);
resolve(this.value);
});
this.once('cancel', () => {
this.output.write(cursor.show);
this.output.off('resize', this.render);
setRawMode(this.input, false);
resolve(CANCEL_SYMBOL);
});
});
}
protected _isActionKey(char: string | undefined, _key: Key): boolean {
return char === '\t';
}
protected _setValue(value: TValue | undefined): void {
this.value = value;
this.emit('value', this.value);
}
protected _setUserInput(value: string | undefined, write?: boolean): void {
this.userInput = value ?? '';
this.emit('userInput', this.userInput);
if (write && this._track && this.rl) {
this.rl.write(this.userInput);
this._cursor = this.rl.cursor;
}
}
protected _clearUserInput(): void {
this.rl?.write(null, { ctrl: true, name: 'u' });
this._setUserInput('');
}
private onKeypress(char: string | undefined, key: Key) {
if (this._track && key.name !== 'return') {
if (key.name && this._isActionKey(char, key)) {
this.rl?.write(null, { ctrl: true, name: 'h' });
}
this._cursor = this.rl?.cursor ?? 0;
this._setUserInput(this.rl?.line);
}
if (this.state === 'error') {
this.state = 'active';
}
if (key?.name) {
if (!this._track && settings.aliases.has(key.name)) {
this.emit('cursor', settings.aliases.get(key.name));
}
if (settings.actions.has(key.name as Action)) {
this.emit('cursor', key.name as Action);
}
}
if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) {
this.emit('confirm', char.toLowerCase() === 'y');
}
// Call the key event handler and emit the key event
this.emit('key', char?.toLowerCase(), key);
if (key?.name === 'return') {
if (this.opts.validate) {
const problem = this.opts.validate(this.value);
if (problem) {
this.error = problem instanceof Error ? problem.message : problem;
this.state = 'error';
this.rl?.write(this.userInput);
}
}
if (this.state !== 'error') {
this.state = 'submit';
}
}
if (isActionKey([char, key?.name, key?.sequence], 'cancel')) {
this.state = 'cancel';
}
if (this.state === 'submit' || this.state === 'cancel') {
this.emit('finalize');
}
this.render();
if (this.state === 'submit' || this.state === 'cancel') {
this.close();
}
}
protected close() {
this.input.unpipe();
this.input.removeListener('keypress', this.onKeypress);
this.output.write('\n');
setRawMode(this.input, false);
this.rl?.close();
this.rl = undefined;
this.emit(`${this.state}`, this.value);
this.unsubscribe();
}
private restoreCursor() {
const lines =
wrapAnsi(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split('\n')
.length - 1;
this.output.write(cursor.move(-999, lines * -1));
}
private render() {
const frame = wrapAnsi(this._render(this) ?? '', process.stdout.columns, {
hard: true,
trim: false,
});
if (frame === this._prevFrame) return;
if (this.state === 'initial') {
this.output.write(cursor.hide);
} else {
const diff = diffLines(this._prevFrame, frame);
const rows = getRows(this.output);
this.restoreCursor();
if (diff) {
const diffOffsetAfter = Math.max(0, diff.numLinesAfter - rows);
const diffOffsetBefore = Math.max(0, diff.numLinesBefore - rows);
let diffLine = diff.lines.find((line) => line >= diffOffsetAfter);
if (diffLine === undefined) {
this._prevFrame = frame;
return;
}
// If a single line has changed, only update that line
if (diff.lines.length === 1) {
this.output.write(cursor.move(0, diffLine - diffOffsetBefore));
this.output.write(erase.lines(1));
const lines = frame.split('\n');
this.output.write(lines[diffLine]);
this._prevFrame = frame;
this.output.write(cursor.move(0, lines.length - diffLine - 1));
return;
// If many lines have changed, rerender everything past the first line
} else if (diff.lines.length > 1) {
if (diffOffsetAfter < diffOffsetBefore) {
diffLine = diffOffsetAfter;
} else {
const adjustedDiffLine = diffLine - diffOffsetBefore;
if (adjustedDiffLine > 0) {
this.output.write(cursor.move(0, adjustedDiffLine));
}
}
this.output.write(erase.down());
const lines = frame.split('\n');
const newLines = lines.slice(diffLine);
this.output.write(newLines.join('\n'));
this._prevFrame = frame;
return;
}
}
this.output.write(erase.down());
}
this.output.write(frame);
if (this.state === 'initial') {
this.state = 'active';
}
this._prevFrame = frame;
}
}
================================================
FILE: packages/core/src/prompts/select-key.ts
================================================
import Prompt, { type PromptOptions } from './prompt.js';
export interface SelectKeyOptions<T extends { value: string }>
extends PromptOptions<T['value'], SelectKeyPrompt<T>> {
options: T[];
caseSensitive?: boolean;
}
export default class SelectKeyPrompt<T extends { value: string }> extends Prompt<T['value']> {
options: T[];
cursor = 0;
constructor(opts: SelectKeyOptions<T>) {
super(opts, false);
this.options = opts.options;
const caseSensitive = opts.caseSensitive === true;
const keys = this.options.map(({ value: [initial] }) => {
return caseSensitive ? initial : initial?.toLowerCase();
});
this.cursor = Math.max(keys.indexOf(opts.initialValue), 0);
this.on('key', (key, keyInfo) => {
if (!key) {
return;
}
const casedKey = caseSensitive && keyInfo.shift ? key.toUpperCase() : key;
if (!keys.includes(casedKey)) {
return;
}
const value = this.options.find(({ value: [initial] }) => {
return caseSensitive ? initial === casedKey : initial?.toLowerCase() === key;
});
if (value) {
this.value = value.value;
this.state = 'submit';
this.emit('submit');
}
});
}
}
================================================
FILE: packages/core/src/prompts/select.ts
================================================
import { findCursor } from '../utils/cursor.js';
import Prompt, { type PromptOptions } from './prompt.js';
export interface SelectOptions<T extends { value: any; disabled?: boolean }>
extends PromptOptions<T['value'], SelectPrompt<T>> {
options: T[];
initialValue?: T['value'];
}
export default class SelectPrompt<T extends { value: any; disabled?: boolean }> extends Prompt<
T['value']
> {
options: T[];
cursor = 0;
private get _selectedValue() {
return this.options[this.cursor];
}
private changeValue() {
this.value = this._selectedValue.value;
}
constructor(opts: SelectOptions<T>) {
super(opts, false);
this.options = opts.options;
const initialCursor = this.options.findIndex(({ value }) => value === opts.initialValue);
const cursor = initialCursor === -1 ? 0 : initialCursor;
this.cursor = this.options[cursor].disabled ? findCursor<T>(cursor, 1, this.options) : cursor;
this.changeValue();
this.on('cursor', (key) => {
switch (key) {
case 'left':
case 'up':
this.cursor = findCursor<T>(this.cursor, -1, this.options);
break;
case 'down':
case 'right':
this.cursor = findCursor<T>(this.cursor, 1, this.options);
break;
}
this.changeValue();
});
}
}
================================================
FILE: packages/core/src/prompts/text.ts
================================================
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';
export interface TextOptions extends PromptOptions<string, TextPrompt> {
placeholder?: string;
defaultValue?: string;
}
export default class TextPrompt extends Prompt<string> {
get userInputWithCursor() {
if (this.state === 'submit') {
return this.userInput;
}
const userInput = this.userInput;
if (this.cursor >= userInput.length) {
return `${this.userInput}█`;
}
const s1 = userInput.slice(0, this.cursor);
const [s2, ...s3] = userInput.slice(this.cursor);
return `${s1}${styleText('inverse', s2)}${s3.join('')}`;
}
get cursor() {
return this._cursor;
}
constructor(opts: TextOptions) {
super({
...opts,
initialUserInput: opts.initialUserInput ?? opts.initialValue,
});
this.on('userInput', (input) => {
this._setValue(input);
});
this.on('finalize', () => {
if (!this.value) {
this.value = opts.defaultValue;
}
if (this.value === undefined) {
this.value = '';
}
});
}
}
================================================
FILE: packages/core/src/types.ts
================================================
import type { Key } from 'node:readline';
import type { Action } from './utils/settings.js';
/**
* The state of the prompt
*/
export type ClackState = 'initial' | 'active' | 'cancel' | 'submit' | 'error';
/**
* Typed event emitter for clack
*/
export interface ClackEvents<TValue> {
initial: (value?: any) => void;
active: (value?: any) => void;
cancel: (value?: any) => void;
submit: (value?: any) => void;
error: (value?: any) => void;
cursor: (key?: Action) => void;
key: (key: string | undefined, info: Key) => void;
value: (value?: TValue) => void;
userInput: (value: string) => void;
confirm: (value?: boolean) => void;
finalize: () => void;
beforePrompt: () => void;
}
================================================
FILE: packages/core/src/utils/cursor.ts
================================================
export function findCursor<T extends { disabled?: boolean }>(
cursor: number,
delta: number,
options: T[]
) {
const hasEnabledOptions = options.some((opt) => !opt.disabled);
if (!hasEnabledOptions) {
return cursor;
}
const newCursor = cursor + delta;
const maxCursor = Math.max(options.length - 1, 0);
const clampedCursor = newCursor < 0 ? maxCursor : newCursor > maxCursor ? 0 : newCursor;
const newOption = options[clampedCursor];
if (newOption.disabled) {
return findCursor(clampedCursor, delta < 0 ? -1 : 1, options);
}
return clampedCursor;
}
================================================
FILE: packages/core/src/utils/index.ts
================================================
import { stdin, stdout } from 'node:process';
import type { Key } from 'node:readline';
import * as readline from 'node:readline';
import type { Readable, Writable } from 'node:stream';
import { ReadStream } from 'node:tty';
import { wrapAnsi } from 'fast-wrap-ansi';
import { cursor } from 'sisteransi';
import { isActionKey } from './settings.js';
export * from './settings.js';
export * from './string.js';
const isWindows = globalThis.process.platform.startsWith('win');
export const CANCEL_SYMBOL = Symbol('clack:cancel');
export function isCancel(value: unknown): value is symbol {
return value === CANCEL_SYMBOL;
}
export function setRawMode(input: Readable, value: boolean) {
const i = input as typeof stdin;
if (i.isTTY) i.setRawMode(value);
}
interface BlockOptions {
input?: Readable;
output?: Writable;
overwrite?: boolean;
hideCursor?: boolean;
}
export function block({
input = stdin,
output = stdout,
overwrite = true,
hideCursor = true,
}: BlockOptions = {}) {
const rl = readline.createInterface({
input,
output,
prompt: '',
tabSize: 1,
});
readline.emitKeypressEvents(input, rl);
if (input instanceof ReadStream && input.isTTY) {
input.setRawMode(true);
}
const clear = (data: Buffer, { name, sequence }: Key) => {
const str = String(data);
if (isActionKey([str, name, sequence], 'cancel')) {
if (hideCursor) output.write(cursor.show);
process.exit(0);
return;
}
if (!overwrite) return;
const dx = name === 'return' ? 0 : -1;
const dy = name === 'return' ? -1 : 0;
readline.moveCursor(output, dx, dy, () => {
readline.clearLine(output, 1, () => {
input.once('keypress', clear);
});
});
};
if (hideCursor) output.write(cursor.hide);
input.once('keypress', clear);
return () => {
input.off('keypress', clear);
if (hideCursor) output.write(cursor.show);
// Prevent Windows specific issues: https://github.com/bombshell-dev/clack/issues/176
if (input instanceof ReadStream && input.isTTY && !isWindows) {
input.setRawMode(false);
}
// @ts-expect-error fix for https://github.com/nodejs/node/issues/31762#issuecomment-1441223907
rl.terminal = false;
rl.close();
};
}
export const getColumns = (output: Writable): number => {
if ('columns' in output && typeof output.columns === 'number') {
return output.columns;
}
return 80;
};
export const getRows = (output: Writable): number => {
if ('rows' in output && typeof output.rows === 'number') {
return output.rows;
}
return 20;
};
export function wrapTextWithPrefix(
output: Writable | undefined,
text: string,
prefix: string,
startPrefix: string = prefix
): string {
const columns = getColumns(output ?? stdout);
const wrapped = wrapAnsi(text, columns - prefix.length, {
hard: true,
trim: false,
});
const lines = wrapped
.split('\n')
.map((line, index) => {
return `${index === 0 ? startPrefix : prefix}${line}`;
})
.join('\n');
return lines;
}
================================================
FILE: packages/core/src/utils/settings.ts
================================================
const actions = ['up', 'down', 'left', 'right', 'space', 'enter', 'cancel'] as const;
export type Action = (typeof actions)[number];
const DEFAULT_MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
/** Global settings for Clack programs, stored in memory */
interface InternalClackSettings {
actions: Set<Action>;
aliases: Map<string, Action>;
messages: {
cancel: string;
error: string;
};
withGuide: boolean;
date: {
monthNames: string[];
messages: {
invalidMonth: string;
required: string;
invalidDay: (days: number, month: string) => string;
afterMin: (min: Date) => string;
beforeMax: (max: Date) => string;
};
};
}
export const settings: InternalClackSettings = {
actions: new Set(actions),
aliases: new Map<string, Action>([
// vim support
['k', 'up'],
['j', 'down'],
['h', 'left'],
['l', 'right'],
['\x03', 'cancel'],
// opinionated defaults!
['escape', 'cancel'],
]),
messages: {
cancel: 'Canceled',
error: 'Something went wrong',
},
withGuide: true,
date: {
monthNames: [...DEFAULT_MONTH_NAMES],
messages: {
required: 'Please enter a valid date',
invalidMonth: 'There are only 12 months in a year',
invalidDay: (days, month) => `There are only ${days} days in ${month}`,
afterMin: (min) => `Date must be on or after ${min.toISOString().slice(0, 10)}`,
beforeMax: (max) => `Date must be on or before ${max.toISOString().slice(0, 10)}`,
},
},
};
export interface ClackSettings {
/**
* Set custom global aliases for the default actions.
* This will not overwrite existing aliases, it will only add new ones!
*
* @param aliases - An object that maps aliases to actions
* @default { k: 'up', j: 'down', h: 'left', l: 'right', '\x03': 'cancel', 'escape': 'cancel' }
*/
aliases?: Record<string, Action>;
/**
* Custom messages for prompts
*/
messages?: {
/**
* Custom message to display when a spinner is cancelled
* @default "Canceled"
*/
cancel?: string;
/**
* Custom message to display when a spinner encounters an error
* @default "Something went wrong"
*/
error?: string;
};
withGuide?: boolean;
/**
* Date prompt localization
*/
date?: {
/** Month names for validation messages (January, February, ...) */
monthNames?: string[];
messages?: {
/** Shown when date is missing */
required?: string;
/** Shown when month > 12 */
invalidMonth?: string;
/** (days, monthName) => message for invalid day */
invalidDay?: (days: number, month: string) => string;
/** (min) => message when date is before minDate */
afterMin?: (min: Date) => string;
/** (max) => message when date is after maxDate */
beforeMax?: (max: Date) => string;
};
};
}
export function updateSettings(updates: ClackSettings) {
// Handle each property in the updates
if (updates.aliases !== undefined) {
const aliases = updates.aliases;
for (const alias in aliases) {
if (!Object.hasOwn(aliases, alias)) continue;
const action = aliases[alias];
if (!settings.actions.has(action)) continue;
if (!settings.aliases.has(alias)) {
settings.aliases.set(alias, action);
}
}
}
if (updates.messages !== undefined) {
const messages = updates.messages;
if (messages.cancel !== undefined) {
settings.messages.cancel = messages.cancel;
}
if (messages.error !== undefined) {
settings.messages.error = messages.error;
}
}
if (updates.withGuide !== undefined) {
settings.withGuide = updates.withGuide !== false;
}
if (updates.date !== undefined) {
const date = updates.date;
if (date.monthNames !== undefined) {
settings.date.monthNames = [...date.monthNames];
}
if (date.messages !== undefined) {
if (date.messages.required !== undefined) {
settings.date.messages.required = date.messages.required;
}
if (date.messages.invalidMonth !== undefined) {
settings.date.messages.invalidMonth = date.messages.invalidMonth;
}
if (date.messages.invalidDay !== undefined) {
settings.date.messages.invalidDay = date.messages.invalidDay;
}
if (date.messages.afterMin !== undefined) {
settings.date.messages.afterMin = date.messages.afterMin;
}
if (date.messages.beforeMax !== undefined) {
settings.date.messages.beforeMax = date.messages.beforeMax;
}
}
}
}
/**
* Check if a key is an alias for a default action
* @param key - The raw key which might match to an action
* @param action - The action to match
* @returns boolean
*/
export function isActionKey(key: string | Array<string | undefined>, action: Action) {
if (typeof key === 'string') {
return settings.aliases.get(key) === action;
}
for (const value of key) {
if (value === undefined) continue;
if (isActionKey(value, action)) {
return true;
}
}
return false;
}
================================================
FILE: packages/core/src/utils/string.ts
================================================
export function diffLines(a: string, b: string) {
if (a === b) return;
const aLines = a.split('\n');
const bLines = b.split('\n');
const numLines = Math.max(aLines.length, bLines.length);
const diff: number[] = [];
for (let i = 0; i < numLines; i++) {
if (aLines[i] !== bLines[i]) diff.push(i);
}
return {
lines: diff,
numLinesBefore: aLines.length,
numLinesAfter: bLines.length,
numLines,
};
}
================================================
FILE: packages/core/test/mock-readable.ts
================================================
import { Readable } from 'node:stream';
export class MockReadable extends Readable {
protected _buffer: unknown[] | null = [];
_read() {
if (this._buffer === null) {
this.push(null);
return;
}
for (const val of this._buffer) {
this.push(val);
}
this._buffer = [];
}
pushValue(val: unknown): void {
this._buffer?.push(val);
}
close(): void {
this._buffer = null;
}
}
================================================
FILE: packages/core/test/mock-writable.ts
================================================
import { Writable } from 'node:stream';
export class MockWritable extends Writable {
public buffer: string[] = [];
_write(
chunk: any,
_encoding: BufferEncoding,
callback: (error?: Error | null | undefined) => void
): void {
this.buffer.push(chunk.toString());
callback();
}
}
================================================
FILE: packages/core/test/prompts/autocomplete.test.ts
================================================
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as AutocompletePrompt } from '../../src/prompts/autocomplete.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('AutocompletePrompt', () => {
let input: MockReadable;
let output: MockWritable;
const testOptions = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
{ value: 'grape', label: 'Grape' },
{ value: 'orange', label: 'Orange' },
];
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
test('initial options match provided options', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
instance.prompt();
// Initial state should have all options
expect(instance.filteredOptions.length).to.equal(testOptions.length);
expect(instance.cursor).to.equal(0);
});
test('cursor navigation with event emitter', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
instance.prompt();
// Initial cursor should be at 0
expect(instance.cursor).to.equal(0);
// Directly trigger the cursor event with 'down'
instance.emit('key', '', { name: 'down' });
// After down event, cursor should be 1
expect(instance.cursor).to.equal(1);
// Trigger cursor event with 'up'
instance.emit('key', '', { name: 'up' });
// After up event, cursor should be back to 0
expect(instance.cursor).to.equal(0);
});
test('initialValue selects correct option', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
initialValue: ['cherry'],
});
// The cursor should be initialized to the cherry index
const cherryIndex = testOptions.findIndex((opt) => opt.value === 'cherry');
expect(instance.cursor).to.equal(cherryIndex);
// The selectedValue should be cherry
expect(instance.selectedValues).to.deep.equal(['cherry']);
});
test('initialValue defaults to first option when non-multiple', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
expect(instance.cursor).to.equal(0);
expect(instance.selectedValues).to.deep.equal(['apple']);
});
test('initialValue is empty when multiple', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
multiple: true,
});
expect(instance.cursor).to.equal(0);
expect(instance.selectedValues).to.deep.equal([]);
});
test('filtering through user input', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
instance.prompt();
// Initial state should have all options
expect(instance.filteredOptions.length).to.equal(testOptions.length);
// Simulate typing 'a' by emitting keypress event
input.emit('keypress', 'a', { name: 'a' });
// Check that filtered options are updated to include options with 'a'
expect(instance.filteredOptions.length).to.be.lessThan(testOptions.length);
// Check that 'apple' is in the filtered options
const hasApple = instance.filteredOptions.some((opt) => opt.value === 'apple');
expect(hasApple).to.equal(true);
});
test('default filter function works correctly', () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
instance.prompt();
input.emit('keypress', 'a', { name: 'a' });
input.emit('keypress', 'p', { name: 'p' });
expect(instance.filteredOptions).toEqual([
{ value: 'apple', label: 'Apple' },
{ value: 'grape', label: 'Grape' },
]);
input.emit('keypress', 'z', { name: 'z' });
expect(instance.filteredOptions).toEqual([]);
});
test('submit without nav resolves to first option in non-multiple', async () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
});
const promise = instance.prompt();
input.emit('keypress', '', { name: 'return' });
const result = await promise;
expect(instance.selectedValues).to.deep.equal(['apple']);
expect(result).to.equal('apple');
});
test('submit without nav resolves to [] in multiple', async () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
multiple: true,
});
const promise = instance.prompt();
input.emit('keypress', '', { name: 'return' });
const result = await promise;
expect(instance.selectedValues).to.deep.equal([]);
expect(result).to.deep.equal([]);
});
test('Tab with empty input and placeholder fills input and submit returns matching option', async () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
placeholder: 'apple',
});
const promise = instance.prompt();
input.emit('keypress', '\t', { name: 'tab' });
input.emit('keypress', '', { name: 'return' });
const result = await promise;
expect(instance.userInput).to.equal('apple');
expect(result).to.equal('apple');
});
test('Tab with non-matching placeholder does not fill input', async () => {
const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: testOptions,
placeholder: 'Type to search...',
});
instance.prompt();
input.emit('keypress', '\t', { name: 'tab' });
// Placeholder does not match any option, so input must not be filled with placeholder
expect(instance.userInput).not.to.equal('Type to search...');
});
});
================================================
FILE: packages/core/test/prompts/confirm.test.ts
================================================
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as ConfirmPrompt } from '../../src/prompts/confirm.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('ConfirmPrompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new ConfirmPrompt({
input,
output,
render: () => 'foo',
active: 'yes',
inactive: 'no',
});
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
test('sets value and submits on confirm (y)', () => {
const instance = new ConfirmPrompt({
input,
output,
render: () => 'foo',
active: 'yes',
inactive: 'no',
initialValue: true,
});
instance.prompt();
input.emit('keypress', 'y', { name: 'y' });
expect(instance.value).to.equal(true);
expect(instance.state).to.equal('submit');
});
test('sets value and submits on confirm (n)', () => {
const instance = new ConfirmPrompt({
input,
output,
render: () => 'foo',
active: 'yes',
inactive: 'no',
initialValue: true,
});
instance.prompt();
input.emit('keypress', 'n', { name: 'n' });
expect(instance.value).to.equal(false);
expect(instance.state).to.equal('submit');
});
describe('cursor', () => {
test('cursor is 1 when inactive', () => {
const instance = new ConfirmPrompt({
input,
output,
render: () => 'foo',
active: 'yes',
inactive: 'no',
initialValue: false,
});
instance.prompt();
input.emit('keypress', '', { name: 'return' });
expect(instance.cursor).to.equal(1);
});
test('cursor is 0 when active', () => {
const instance = new ConfirmPrompt({
input,
output,
render: () => 'foo',
active: 'yes',
inactive: 'no',
initialValue: true,
});
instance.prompt();
input.emit('keypress', '', { name: 'return' });
expect(instance.cursor).to.equal(0);
});
});
});
================================================
FILE: packages/core/test/prompts/date.test.ts
================================================
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as DatePrompt } from '../../src/prompts/date.js';
import { isCancel } from '../../src/utils/index.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
const d = (iso: string) => {
const [y, m, day] = iso.slice(0, 10).split('-').map(Number);
return new Date(Date.UTC(y, m - 1, day));
};
describe('DatePrompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
test('initial value displays correctly', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
expect(instance.userInput).to.equal('2025/01/15');
expect(instance.value).toBeInstanceOf(Date);
expect(instance.value!.toISOString().slice(0, 10)).to.equal('2025-01-15');
});
test('left/right navigates between segments', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 });
input.emit('keypress', undefined, { name: 'right' });
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 });
input.emit('keypress', undefined, { name: 'right' });
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 2, positionInSegment: 0 });
input.emit('keypress', undefined, { name: 'left' });
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 });
});
test('up/down increments and decrements segment', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
input.emit('keypress', undefined, { name: 'right' }); // move to month
input.emit('keypress', undefined, { name: 'up' });
expect(instance.userInput).to.equal('2025/02/15');
input.emit('keypress', undefined, { name: 'down' });
expect(instance.userInput).to.equal('2025/01/15');
});
test('up/down on one segment leaves other segments blank', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
expect(instance.userInput).to.equal('____/__/__');
input.emit('keypress', undefined, { name: 'up' }); // up on year (first segment)
expect(instance.userInput).to.equal('0001/__/__');
input.emit('keypress', undefined, { name: 'right' }); // move to month
input.emit('keypress', undefined, { name: 'up' });
expect(instance.userInput).to.equal('0001/01/__');
});
test('with minDate/maxDate, up on blank segment starts at min', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
minDate: d('2025-03-10'),
maxDate: d('2025-11-20'),
});
instance.prompt();
expect(instance.userInput).to.equal('____/__/__');
input.emit('keypress', undefined, { name: 'up' });
expect(instance.userInput).to.equal('2025/__/__');
input.emit('keypress', undefined, { name: 'right' });
input.emit('keypress', undefined, { name: 'up' });
expect(instance.userInput).to.equal('2025/03/__');
input.emit('keypress', undefined, { name: 'right' });
input.emit('keypress', undefined, { name: 'up' });
expect(instance.userInput).to.equal('2025/03/10');
});
test('with minDate/maxDate, down on blank segment starts at max', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
minDate: d('2025-03-10'),
maxDate: d('2025-11-20'),
});
instance.prompt();
input.emit('keypress', undefined, { name: 'down' });
expect(instance.userInput).to.equal('2025/__/__');
input.emit('keypress', undefined, { name: 'right' });
input.emit('keypress', undefined, { name: 'down' });
expect(instance.userInput).to.equal('2025/11/__');
input.emit('keypress', undefined, { name: 'right' });
input.emit('keypress', undefined, { name: 'down' });
expect(instance.userInput).to.equal('2025/11/20');
});
test('digit-by-digit editing from left to right', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 });
// Type 2,0,2,3 to change year to 2023 (right-to-left fill)
input.emit('keypress', '2', { name: undefined, sequence: '2' });
expect(instance.userInput).to.equal('___2/01/15');
input.emit('keypress', '0', { name: undefined, sequence: '0' });
expect(instance.userInput).to.equal('__20/01/15');
input.emit('keypress', '2', { name: undefined, sequence: '2' });
expect(instance.userInput).to.equal('_202/01/15');
input.emit('keypress', '3', { name: undefined, sequence: '3' });
expect(instance.userInput).to.equal('2023/01/15');
});
test('backspace clears entire segment at any cursor position', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-12-21'),
});
instance.prompt();
expect(instance.userInput).to.equal('2025/12/21');
input.emit('keypress', undefined, { name: 'backspace', sequence: '\x7f' });
expect(instance.userInput).to.equal('____/12/21');
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 });
});
test('backspace clears segment when cursor at first char (2___)', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
input.emit('keypress', '2', { name: undefined, sequence: '2' });
expect(instance.userInput).to.equal('___2/__/__');
input.emit('keypress', '\x7f', { name: undefined, sequence: '\x7f' });
expect(instance.userInput).to.equal('____/__/__');
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 0, positionInSegment: 0 });
});
test('digit input updates segment and jumps to next when complete', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
for (const c of '2025') {
input.emit('keypress', c, { name: undefined, sequence: c });
}
expect(instance.userInput).to.equal('2025/__/__');
expect(instance.segmentCursor).to.deep.equal({ segmentIndex: 1, positionInSegment: 0 });
});
test('submit returns Date for valid date', async () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-31'),
});
const resultPromise = instance.prompt();
input.emit('keypress', undefined, { name: 'return' });
const result = await resultPromise;
expect(result).toBeInstanceOf(Date);
expect((result as Date).toISOString().slice(0, 10)).to.equal('2025-01-31');
});
test('can cancel', async () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
const resultPromise = instance.prompt();
input.emit('keypress', 'escape', { name: 'escape' });
const result = await resultPromise;
expect(isCancel(result)).toBe(true);
});
test('defaultValue used when invalid date submitted', async () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
defaultValue: d('2025-06-15'),
});
const resultPromise = instance.prompt();
input.emit('keypress', undefined, { name: 'return' });
const result = await resultPromise;
expect(result).toBeInstanceOf(Date);
expect((result as Date).toISOString().slice(0, 10)).to.equal('2025-06-15');
});
test('supports MDY format', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'MDY',
initialValue: d('2025-01-15'),
});
instance.prompt();
expect(instance.userInput).to.equal('01/15/2025');
});
test('supports DMY format', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'DMY',
initialValue: d('2025-01-15'),
});
instance.prompt();
expect(instance.userInput).to.equal('15/01/2025');
});
test('rejects invalid month via pending tens digit', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
// Navigate to month
input.emit('keypress', undefined, { name: 'right' });
// Type '1' → '01' with pending tens digit (since 1 <= 1)
input.emit('keypress', '1', { name: undefined, sequence: '1' });
expect(instance.segmentValues.month).to.equal('01');
// Type '3' → tries '13' which is > 12 → inline error
input.emit('keypress', '3', { name: undefined, sequence: '3' });
expect(instance.inlineError).to.equal('There are only 12 months in a year');
});
test('rejects invalid day via pending tens digit', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
// Navigate to day
input.emit('keypress', undefined, { name: 'right' });
input.emit('keypress', undefined, { name: 'right' });
// Type '2' → '02' with pending (2 <= 2)
input.emit('keypress', '2', { name: undefined, sequence: '2' });
input.emit('keypress', '0', { name: undefined, sequence: '0' });
expect(instance.inlineError).to.equal('');
});
describe('segmentValues and segmentCursor', () => {
test('segmentValues reflects current input', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
const segmentValues = instance.segmentValues;
expect(segmentValues.year).to.equal('2025');
expect(segmentValues.month).to.equal('01');
expect(segmentValues.day).to.equal('15');
});
test('segmentCursor tracks cursor position', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
input.emit('keypress', undefined, { name: 'right' }); // move to month
const cursor = instance.segmentCursor;
expect(cursor.segmentIndex).to.equal(1);
expect(cursor.positionInSegment).to.equal(0);
});
test('segmentValues updates on submit', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
initialValue: d('2025-01-15'),
});
instance.prompt();
input.emit('keypress', undefined, { name: 'return' });
const segmentValues = instance.segmentValues;
expect(segmentValues.year).to.equal('2025');
expect(segmentValues.month).to.equal('01');
expect(segmentValues.day).to.equal('15');
});
});
describe('formattedValue and segments', () => {
test('formattedValue returns formatted string', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'MDY',
initialValue: d('2025-03-15'),
});
instance.prompt();
expect(instance.formattedValue).to.equal('03/15/2025');
});
test('segments exposes segment config', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'DMY',
});
instance.prompt();
expect(instance.segments).to.deep.equal([
{ type: 'day', len: 2 },
{ type: 'month', len: 2 },
{ type: 'year', len: 4 },
]);
});
test('separator defaults to / for explicit format', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
});
instance.prompt();
expect(instance.separator).to.equal('/');
});
});
describe('locale detection', () => {
test('locale auto-detects format from Intl', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
locale: 'en-US',
initialValue: d('2025-03-15'),
});
instance.prompt();
// en-US is MDY
expect(instance.segments[0].type).to.equal('month');
expect(instance.segments[1].type).to.equal('day');
expect(instance.segments[2].type).to.equal('year');
});
test('explicit format overrides locale', () => {
const instance = new DatePrompt({
input,
output,
render: () => 'foo',
format: 'YMD',
locale: 'en-US', // would be MDY, but format takes precedence
initialValue: d('2025-03-15'),
});
instance.prompt();
expect(instance.segments[0].type).to.equal('year');
});
});
});
================================================
FILE: packages/core/test/prompts/multi-select.test.ts
================================================
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as MultiSelectPrompt } from '../../src/prompts/multi-select.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('MultiSelectPrompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
describe('cursor', () => {
test('cursor is index of selected item', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
expect(instance.cursor).to.equal(0);
input.emit('keypress', 'down', { name: 'down' });
expect(instance.cursor).to.equal(1);
});
test('cursor loops around', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }],
});
instance.prompt();
expect(instance.cursor).to.equal(0);
input.emit('keypress', 'up', { name: 'up' });
expect(instance.cursor).to.equal(2);
input.emit('keypress', 'down', { name: 'down' });
expect(instance.cursor).to.equal(0);
});
test('left behaves as up', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }],
});
instance.prompt();
input.emit('keypress', 'left', { name: 'left' });
expect(instance.cursor).to.equal(2);
});
test('right behaves as down', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
input.emit('keypress', 'left', { name: 'left' });
expect(instance.cursor).to.equal(1);
});
test('initial values is selected', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
initialValues: ['bar'],
});
instance.prompt();
expect(instance.value).toEqual(['bar']);
});
test('select all when press "a" key', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
input.emit('keypress', 'down', { name: 'down' });
input.emit('keypress', 'space', { name: 'space' });
input.emit('keypress', 'a', { name: 'a' });
expect(instance.value).toEqual(['foo', 'bar']);
});
test('select invert when press "i" key', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
input.emit('keypress', 'down', { name: 'down' });
input.emit('keypress', 'space', { name: 'space' });
input.emit('keypress', 'i', { name: 'i' });
expect(instance.value).toEqual(['foo']);
});
test('disabled options are skipped', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
});
instance.prompt();
expect(instance.cursor).to.equal(0);
input.emit('keypress', 'down', { name: 'down' });
expect(instance.cursor).to.equal(2);
input.emit('keypress', 'up', { name: 'up' });
expect(instance.cursor).to.equal(0);
});
test('initial cursorAt on disabled option', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
cursorAt: 'bar',
});
instance.prompt();
expect(instance.cursor).to.equal(2);
});
});
describe('toggleAll', () => {
test('selects all enabled options', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
});
instance.prompt();
input.emit('keypress', 'a', { name: 'a' });
expect(instance.value).toEqual(['foo', 'baz']);
});
test('unselects all enabled options if all selected', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
initialValues: ['foo', 'baz'],
});
instance.prompt();
input.emit('keypress', 'a', { name: 'a' });
expect(instance.value).toEqual([]);
});
});
describe('toggleInvert', () => {
test('inverts selection of enabled options', () => {
const instance = new MultiSelectPrompt({
input,
output,
render: () => 'foo',
options: [
{ value: 'foo' },
{ value: 'bar', disabled: true },
{ value: 'baz' },
{ value: 'qux' },
],
initialValues: ['foo', 'baz'],
});
instance.prompt();
input.emit('keypress', 'i', { name: 'i' });
expect(instance.value).toEqual(['qux']);
});
});
});
================================================
FILE: packages/core/test/prompts/password.test.ts
================================================
import { styleText } from 'node:util';
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as PasswordPrompt } from '../../src/prompts/password.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('PasswordPrompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new PasswordPrompt({
input,
output,
render: () => 'foo',
});
// leave the promise hanging since we don't want to submit in this test
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
describe('cursor', () => {
test('can get cursor', () => {
const instance = new PasswordPrompt({
input,
output,
render: () => 'foo',
});
expect(instance.cursor).to.equal(0);
});
});
describe('userInputWithCursor', () => {
test('returns masked value on submit', () => {
const instance = new PasswordPrompt({
input,
output,
render: () => 'foo',
});
instance.prompt();
const keys = 'foo';
for (let i = 0; i < keys.length; i++) {
input.emit('keypress', keys[i], { name: keys[i] });
}
input.emit('keypress', '', { name: 'return' });
expect(instance.userInputWithCursor).to.equal('•••');
});
test('renders marker at end', () => {
const instance = new PasswordPrompt({
input,
output,
render: () => 'foo',
});
instance.prompt();
input.emit('keypress', 'x', { name: 'x' });
expect(instance.userInputWithCursor).to.equal(`•${styleText(['inverse', 'hidden'], '_')}`);
});
test('renders cursor inside value', () => {
const instance = new PasswordPrompt({
input,
output,
render: () => 'foo',
});
instance.prompt();
input.emit('keypress', 'x', { name: 'x' });
input.emit('keypress', 'y', { name: 'y' });
input.emit('keypress', 'z', { name: 'z' });
input.emit('keypress', undefined, { name: 'left' });
input.emit('keypress', undefined, { name: 'left' });
expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`);
});
test('renders custom mask', () => {
const instance = new PasswordPrompt({
input,
output,
render: () => 'foo',
mask: 'X',
});
instance.prompt();
input.emit('keypress', 'x', { name: 'x' });
expect(instance.userInputWithCursor).to.equal(`X${styleText(['inverse', 'hidden'], '_')}`);
});
});
});
================================================
FILE: packages/core/test/prompts/prompt.test.ts
================================================
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as Prompt } from '../../src/prompts/prompt.js';
import { isCancel } from '../../src/utils/index.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('Prompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
// leave the promise hanging since we don't want to submit in this test
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
test('submits on return key', async () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
const resultPromise = instance.prompt();
input.emit('keypress', '', { name: 'return' });
const result = await resultPromise;
expect(result).to.equal(undefined);
expect(isCancel(result)).to.equal(false);
expect(instance.state).to.equal('submit');
expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]);
});
test('cancels on ctrl-c', async () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
const resultPromise = instance.prompt();
input.emit('keypress', '\x03', { name: 'c' });
const result = await resultPromise;
expect(isCancel(result)).to.equal(true);
expect(instance.state).to.equal('cancel');
expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]);
});
test('does not write initialValue to value', () => {
const eventSpy = vi.fn();
const instance = new Prompt({
input,
output,
render: () => 'foo',
initialValue: 'bananas',
});
instance.on('value', eventSpy);
instance.prompt();
expect(instance.value).to.equal(undefined);
expect(eventSpy).not.toHaveBeenCalled();
});
test('re-renders on resize', () => {
const renderFn = vi.fn().mockImplementation(() => 'foo');
const instance = new Prompt({
input,
output,
render: renderFn,
});
instance.prompt();
expect(renderFn).toHaveBeenCalledTimes(1);
output.emit('resize');
expect(renderFn).toHaveBeenCalledTimes(2);
});
test('state is active after first render', async () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
expect(instance.state).to.equal('initial');
instance.prompt();
expect(instance.state).to.equal('active');
});
test('emits truthy confirm on y press', () => {
const eventFn = vi.fn();
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
instance.on('confirm', eventFn);
instance.prompt();
input.emit('keypress', 'y', { name: 'y' });
expect(eventFn).toBeCalledWith(true);
});
test('emits falsey confirm on n press', () => {
const eventFn = vi.fn();
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
instance.on('confirm', eventFn);
instance.prompt();
input.emit('keypress', 'n', { name: 'n' });
expect(eventFn).toBeCalledWith(false);
});
test('emits key event for unknown chars', () => {
const eventSpy = vi.fn();
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
instance.on('key', eventSpy);
instance.prompt();
input.emit('keypress', 'z', { name: 'z' });
expect(eventSpy).toBeCalledWith('z', { name: 'z' });
});
test('emits cursor events for movement keys', () => {
const keys = ['up', 'down', 'left', 'right'];
const eventSpy = vi.fn();
const instance = new Prompt({
input,
output,
render: () => 'foo',
});
instance.on('cursor', eventSpy);
instance.prompt();
for (const key of keys) {
input.emit('keypress', key, { name: key });
expect(eventSpy).toBeCalledWith(key);
}
});
test('emits cursor events for movement key aliases when not tracking', () => {
const keys = [
['k', 'up'],
['j', 'down'],
['h', 'left'],
['l', 'right'],
];
const eventSpy = vi.fn();
const instance = new Prompt(
{
input,
output,
render: () => 'foo',
},
false
);
instance.on('cursor', eventSpy);
instance.prompt();
for (const [alias, key] of keys) {
input.emit('keypress', alias, { name: alias });
expect(eventSpy).toBeCalledWith(key);
}
});
test('aborts on abort signal', () => {
const abortController = new AbortController();
const instance = new Prompt({
input,
output,
render: () => 'foo',
signal: abortController.signal,
});
instance.prompt();
expect(instance.state).to.equal('active');
abortController.abort();
expect(instance.state).to.equal('cancel');
});
test('returns immediately if signal is already aborted', () => {
const abortController = new AbortController();
abortController.abort();
const instance = new Prompt({
input,
output,
render: () => 'foo',
signal: abortController.signal,
});
instance.prompt();
expect(instance.state).to.equal('cancel');
});
test('accepts invalid initial value', () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
initialValue: 'invalid',
validate: (value) => (value === 'valid' ? undefined : 'must be valid'),
});
instance.prompt();
expect(instance.state).to.equal('active');
expect(instance.error).to.equal('');
});
test('validates value on return', () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
validate: (value) => (value === 'valid' ? undefined : 'must be valid'),
});
instance.prompt();
instance.value = 'invalid';
input.emit('keypress', '', { name: 'return' });
expect(instance.state).to.equal('error');
expect(instance.error).to.equal('must be valid');
});
test('validates value with Error object', () => {
const instance = new Prompt({
input,
output,
render: () => 'foo',
validate: (value) => (value === 'valid' ? undefined : new Error('must be valid')),
});
instance.prompt();
instance.value = 'invalid';
input.emit('keypress', '', { name: 'return' });
expect(instance.state).to.equal('error');
expect(instance.error).to.equal('must be valid');
});
test('validates value with regex validation', () => {
const instance = new Prompt<string>({
input,
output,
render: () => 'foo',
validate: (value) => (/^[A-Z]+$/.test(value ?? '') ? undefined : 'Invalid value'),
});
instance.prompt();
instance.value = 'Invalid Value $$$';
input.emit('keypress', '', { name: 'return' });
expect(instance.state).to.equal('error');
expect(instance.error).to.equal('Invalid value');
});
test('accepts valid value with regex validation', () => {
const instance = new Prompt<string>({
input,
output,
render: () => 'foo',
validate: (value) => (/^[A-Z]+$/.test(value ?? '') ? undefined : 'Invalid value'),
});
instance.prompt();
instance.value = 'VALID';
input.emit('keypress', '', { name: 'return' });
expect(instance.state).to.equal('submit');
expect(instance.error).to.equal('');
});
});
================================================
FILE: packages/core/test/prompts/select.test.ts
================================================
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as SelectPrompt } from '../../src/prompts/select.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('SelectPrompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
describe('cursor', () => {
test('cursor is index of selected item', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
expect(instance.cursor).to.equal(0);
input.emit('keypress', 'down', { name: 'down' });
expect(instance.cursor).to.equal(1);
});
test('cursor loops around', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }],
});
instance.prompt();
expect(instance.cursor).to.equal(0);
input.emit('keypress', 'up', { name: 'up' });
expect(instance.cursor).to.equal(2);
input.emit('keypress', 'down', { name: 'down' });
expect(instance.cursor).to.equal(0);
});
test('left behaves as up', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }],
});
instance.prompt();
input.emit('keypress', 'left', { name: 'left' });
expect(instance.cursor).to.equal(2);
});
test('right behaves as down', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
});
instance.prompt();
input.emit('keypress', 'left', { name: 'left' });
expect(instance.cursor).to.equal(1);
});
test('initial value is selected', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar' }],
initialValue: 'bar',
});
instance.prompt();
expect(instance.cursor).to.equal(1);
});
test('cursor skips disabled options (down)', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
});
instance.prompt();
expect(instance.cursor).to.equal(0);
input.emit('keypress', 'down', { name: 'down' });
expect(instance.cursor).to.equal(2);
});
test('cursor skips disabled options (up)', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
initialValue: 'baz',
options: [{ value: 'foo' }, { value: 'bar', disabled: true }, { value: 'baz' }],
});
instance.prompt();
expect(instance.cursor).to.equal(2);
input.emit('keypress', 'up', { name: 'up' });
expect(instance.cursor).to.equal(0);
});
test('cursor skips initial disabled option', () => {
const instance = new SelectPrompt({
input,
output,
render: () => 'foo',
options: [{ value: 'foo', disabled: true }, { value: 'bar' }, { value: 'baz' }],
});
instance.prompt();
expect(instance.cursor).to.equal(1);
});
});
});
================================================
FILE: packages/core/test/prompts/text.test.ts
================================================
import { styleText } from 'node:util';
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as TextPrompt } from '../../src/prompts/text.js';
import { MockReadable } from '../mock-readable.js';
import { MockWritable } from '../mock-writable.js';
describe('TextPrompt', () => {
let input: MockReadable;
let output: MockWritable;
beforeEach(() => {
input = new MockReadable();
output = new MockWritable();
});
afterEach(() => {
vi.restoreAllMocks();
});
test('renders render() result', () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
});
// leave the promise hanging since we don't want to submit in this test
instance.prompt();
expect(output.buffer).to.deep.equal([cursor.hide, 'foo']);
});
test('sets default value on finalize if no value', async () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
defaultValue: 'bleep bloop',
});
const resultPromise = instance.prompt();
input.emit('keypress', '', { name: 'return' });
const result = await resultPromise;
expect(result).to.equal('bleep bloop');
});
test('keeps value on finalize', async () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
defaultValue: 'bleep bloop',
});
const resultPromise = instance.prompt();
input.emit('keypress', 'x', { name: 'x' });
input.emit('keypress', '', { name: 'return' });
const result = await resultPromise;
expect(result).to.equal('x');
});
describe('cursor', () => {
test('can get cursor', () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
});
expect(instance.cursor).to.equal(0);
});
});
describe('userInputWithCursor', () => {
test('returns value on submit', () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
});
instance.prompt();
input.emit('keypress', 'x', { name: 'x' });
input.emit('keypress', '', { name: 'return' });
expect(instance.userInputWithCursor).to.equal('x');
});
test('highlights cursor position', () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
});
instance.prompt();
const keys = 'foo';
for (let i = 0; i < keys.length; i++) {
input.emit('keypress', keys[i], { name: keys[i] });
}
input.emit('keypress', undefined, { name: 'left' });
expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`);
});
test('shows cursor at end if beyond value', () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
});
instance.prompt();
const keys = 'foo';
for (let i = 0; i < keys.length; i++) {
input.emit('keypress', keys[i], { name: keys[i] });
}
input.emit('keypress', undefined, { name: 'right' });
expect(instance.userInputWithCursor).to.equal('foo█');
});
test('does not use placeholder as value when pressing enter', async () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
placeholder: ' (hit Enter to use default)',
defaultValue: 'default-value',
});
const resultPromise = instance.prompt();
input.emit('keypress', '', { name: 'return' });
const result = await resultPromise;
expect(result).to.equal('default-value');
});
test('returns empty string when no value and no default', async () => {
const instance = new TextPrompt({
input,
output,
render: () => 'foo',
placeholder: ' (hit Enter to use default)',
});
const resultPromise = instance.prompt();
input.emit('keypress', '', { name: 'return' });
const result = await resultPromise;
expect(result).to.equal('');
});
});
});
================================================
FILE: packages/core/test/utils.test.ts
================================================
import type { Key } from 'node:readline';
import { cursor } from 'sisteransi';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { block } from '../src/utils/index.js';
import { MockReadable } from './mock-readable.js';
import { MockWritable } from './mock-writable.js';
describe('utils', () => {
afterEach(() => {
vi.restoreAllMocks();
});
describe('block', () => {
test('clears output on keypress', () => {
const input = new MockReadable();
const output = new MockWritable();
const callback = block({ input, output });
const event: Key = {
name: 'x',
};
const eventData = Buffer.from('bloop');
input.emit('keypress', eventData, event);
callback();
expect(output.buffer).to.deep.equal([cursor.hide, cursor.move(-1, 0), cursor.show]);
});
test('clears output vertically when return pressed', () => {
const input = new MockReadable();
const output = new MockWritable();
const callback = block({ input, output });
const event: Key = {
name: 'return',
};
const eventData = Buffer.from('bloop');
input.emit('keypress', eventData, event);
callback();
expect(output.buffer).to.deep.equal([cursor.hide, cursor.move(0, -1), cursor.show]);
});
test('ignores additional keypresses after dispose', () => {
const input = new MockReadable();
const output = new MockWritable();
const callback = block({ input, output });
const event: Key = {
name: 'x',
};
const eventData = Buffer.from('bloop');
input.emit('keypress', eventData, event);
callback();
input.emit('keypress', eventData, event);
expect(output.buffer).to.deep.equal([cursor.hide, cursor.move(-1, 0), cursor.show]);
});
test('exits on ctrl-c', () => {
const input = new MockReadable();
const output = new MockWritable();
// purposely don't keep the callback since we would exit the process
block({ input, output });
const spy = vi.spyOn(process, 'exit').mockImplementation((() => {
return;
}) as () => never);
const event: Key = {
name: 'c',
};
const eventData = Buffer.from('\x03');
input.emit('keypress', eventData, event);
expect(spy).toHaveBeenCalled();
expect(output.buffer).to.deep.equal([cursor.hide, cursor.show]);
});
test('does not clear if overwrite=false', () => {
const input = new MockReadable();
const output = new MockWritable();
const callback = block({ input, output, overwrite: false });
const event: Key = {
name: 'c',
};
const eventData = Buffer.from('bloop');
input.emit('keypress', eventData, event);
callback();
expect(output.buffer).to.deep.equal([cursor.hide, cursor.show]);
});
});
});
================================================
FILE: packages/core/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"include": ["src", "test"]
}
================================================
FILE: packages/prompts/CHANGELOG.md
================================================
# @clack/prompts
## 1.1.0
### Minor Changes
- e3333fb: Replaces `picocolors` with Node.js built-in `styleText`.
### Patch Changes
- c3666e2: destruct `limitOption` param for better code readability, tweak types definitions
- ba3df8e: Fixes withGuide support in intro, outro, and cancel messages.
- Updated dependencies [e3333fb]
- @clack/core@1.1.0
## 1.0.1
### Patch Changes
- 6404dc1: Disallows selection of `disabled` options in autocomplete.
- 86e36d8: Adds `withGuide` support to select prompt.
- c697439: Fixes line wrapping behavior in autocomplete.
- 0ded19c: Simplifies `withGuide` option checks.
- 0e4ddc9: Fixes `withGuide` support in password and path prompts.
- 76550d6: Adds `withGuide` support to selectKey prompt.
- f9b9953: Adds `withGuide` support to password prompt.
- 0e93ccb: Adds `vertical` arrangement option to `confirm` prompt.
- 4e9ae13: Adds `withGuide` support to confirm prompt.
- 0256238: Adds `withGuide` support to spinner prompt.
- Updated dependencies [6404dc1]
- Updated dependencies [2533180]
- @clack/core@1.0.1
## 1.0.0
### Major Changes
- c713fd5: The package is now distributed as ESM-only. In `v0` releases, the package was dual-published as CJS and ESM.
For existing CJS projects using Node v20+, please see Node's guide on [Loading ECMAScript modules using `require()`](https://nodejs.org/docs/latest-v20.x/api/modules.html#loading-ecmascript-modules-using-require).
### Minor Changes
- 415410b: This adds a custom filter function to autocompleteMultiselect. It could be used, for example, to support fuzzy searching logic.
- 7bc3301: Prompts now have a `userInput` stored separately from their `value`.
- 8409f2c: feat: add styleFrame option for spinner
- 2837845: Adds suggestion and path prompts
- 99c3530: Adds `format` option to the note prompt to allow formatting of individual lines
- 0aaee4c: Added new `taskLog` prompt for log output which is cleared on success
- 729bbb6: Add support for customizable spinner cancel and error messages. Users can now customize these messages either per spinner instance or globally via the `updateSettings` function to support multilingual CLIs.
This update also improves the architecture by exposing the core settings to the prompts package, enabling more consistent default message handling across the codebase.
```ts
// Per-instance customization
const spinner = prompts.spinner({
cancelMessage: "Operación cancelada", // "Operation cancelled" in Spanish
errorMessage: "Se produjo un error", // "An error occurred" in Spanish
});
// Global customization via updateSettings
prompts.updateSettings({
messages: {
cancel: "Operación cancelada", // "Operation cancelled" in Spanish
error: "Se produjo un error", // "An error occurred" in Spanish
},
});
// Settings can now be accessed directly
console.log(prompts.settings.messages.cancel); // "Operación cancelada"
// Direct options take priority over global settings
const spinner = prompts.spinner({
cancelMessage: "Cancelled", // This will be used instead of the global setting
});
```
- 44df9af: Adds a new `groupSpacing` option to grouped multi-select prompts. If set to an integer greater than 0, it will add that number of new lines between each group.
- 55645c2: Support wrapping autocomplete and select prompts.
- 9e5bc6c: Add support for signals in prompts, allowing them to be aborted.
- f2c2b89: Adds `AutocompletePrompt` to core with comprehensive tests and implement both `autocomplete` and `autocomplete-multiselect` components in prompts package.
- 38019c7: Updates the API for stopping spinners and progress bars to be clearer
Previously, both the spinner and progress bar components used a single `stop` method that accepted a code to indicate success, cancellation, or error. This update separates these into distinct methods: `stop()`, `cancel()`, and `error()`:
```diff
const spinner = prompts.spinner();
spinner.start();
// Cancelling a spinner
- spinner.stop(undefined, 1);
+ spinner.cancel();
// Stopping with an error
- spinner.stop(undefined, 2);
+ spinner.error();
```
As before, you can pass a message to each method to customize the output displayed:
```js
spinner.cancel("Operation cancelled by user");
progressBar.error("An error occurred during processing");
```
- c45b9fb: Adds support for detecting spinner cancellation via CTRL+C. This allows for graceful handling of user interruptions during long-running operations.
- f10071e: Using the `group` method, task logs can now have groups which themselves can have scrolling windows of logs.
- df4eea1: Remove `suggestion` prompt and change `path` prompt to be an autocomplete prompt.
- 76fd17f: Added new `box` prompt for rendering boxed text, similar a note.
- 9a09318: Adds new `progress` prompt to display a progess-bar
- 1604f97: Add `clearOnError` option to password prompt to automatically clear input when validation fails
- 9bd8072: Add a `required` option to autocomplete multiselect.
- 19558b9: Added support for custom frames in spinner prompt
### Patch Changes
- 46dc0a4: Fixes multiselect only shows hints on the first item in the options list. Now correctly shows hints for all selected options with hint property.
- aea4573: Clamp scrolling windows to 5 rows.
- bfe0dd3: Prevents placeholder from being used as input value in text prompts
- 55eb280: Fix placeholder rendering when using autocomplete.
- 4d1d83b: Fixes rendering of multi-line messages and options in select prompt.
- 6176ced: Add withGuide support to note prompt
- 7b009df: Fix spinner clearing too many lines upwards when non-wrapping.
- 43aed55: Change styling of disabled multi-select options to have strikethrough.
- 17342d2: Exposes a new `SpinnerResult` type to describe the return type of `spinner`
- 282b39e: Wrap spinner output to allow for multi-line/wrapped messages.
- 2feaebb: Fix duplicated logs when scrolling through options with multiline messages by calculating `rowPadding` dynamically based on actual rendered lines instead of using a hardcoded value.
- 69681ea: Strip destructive ANSI codes from task log messages.
- b0fa7d8: Add support for wrapped messages in multi line prompts
- 9999adf: fix note component overflow bug
- 6868c1c: Adds a new `selectableGroups` boolean to the group multi-select prompt. Using `selectableGroups: false` will disable the ability to select a top-level group, but still allow every child to be selected individually.
- 7df841d: Removed all trailing space in prompt output and fixed various padding rendering bugs.
- 2839c66: fix(note): hard wrap text to column limit
- 7a556ad: Updates all prompts to accept a custom `output` and `input` stream
- 17d3650: Use a default import for picocolors to avoid run time errors in some environments.
- 7cc8a55: Messages passed to the `stop` method of a spinner no longer have dots stripped.
- b103ad3: Allow disabled options in multi-select and select prompts.
- 71b5029: Add missing nullish checks around values.
- 1a45f93: Switched from wrap-ansi to fast-wrap-ansi
- f952592: Fixes missing guide when rendering empty log lines.
- 372b526: Add `clear` method to spinner for stopping and clearing.
- d25f6d0: fix(note, box): handle CJK correctly
- 94fee2a: Changes `placeholder` to be a visual hint rather than a tabbable value.
- 7530af0: Fixes wrapping of cancelled and success messages of select prompt
- 4c89dd7: chore: use more accurate type to replace any in group select
- 0b852e1: Handle `stop` calls on spinners which have not yet been started.
- 42adff8: fix: add missing guide line in autocomplete-multiselect
- 8e2e30a: fix: fix autocomplete bar color when validate
- 2048eb1: Fix spinner's dots behavior with custom frames
- acc4c3a: Add a new `withGuide` option to all prompts to disable the default clack border
- 9b92161: Show symbol when withGuide is true for log messages
- 68dbf9b: select-key: Fixed wrapping and added new `caseSensitive` option
- 09e596c: refactor(progress): remove unnecessary return statement in start function
- 2310b43: Allow custom writables as output stream.
- ae84dd0: Update key binding text to show tab/space when navigating, and tab otherwise.
- Updated dependency on `@clack/core` to `1.0.0`
## 0.10.0
### Minor Changes
- 613179d: Adds a new `indicator` option to `spinner`, which supports the original `"dots"` loading animation or a new `"timer"` loading animation.
```ts
import * as p from "@clack/prompts";
const spin = p.spinner({ indicator: "timer" });
spin.start("Loading");
await sleep(3000);
spin.stop("Loaded");
```
- a38b2bc: Adds `stream` API which provides the same methods as `log`, but for iterable (even async) message streams. This is particularly useful for AI responses which are dynamically generated by LLMs.
```ts
import * as p from "@clack/prompts";
await p.stream.step(
(async function* () {
yield* generateLLMResponse(question);
})()
);
```
## 0.9.1
### Patch Changes
- 8093f3c: Adds `Error` support to the `validate` function
- 98925e3: Exports the `Option` type and improves JSDocannotations
- 1904e57: Replace custom utility for stripping ANSI control sequences with Node's built-in [`stripVTControlCharacters`](https://nodejs.org/docs/latest/api/util.html#utilstripvtcontrolcharactersstr) utility.
- Updated dependencies [8093f3c]
- Updated dependencies [e5ba09a]
- Updated dependencies [8cba8e3]
- @clack/core@0.4.1
## 0.9.0
### Minor Changes
- a83d2f8: Adds a new `updateSettings()` function to support new global keybindings.
`updateSettings()` accepts an `aliases` object that maps custom keys to an action (`up | down | left | right | space | enter | cancel`).
```ts
import { updateSettings } from "@clack/prompts";
// Support custom keybindings
updateSettings({
aliases: {
w: "up",
a: "left",
s: "down",
d: "right",
},
});
```
> [!WARNING]
> In order to enforce consistent, user-friendly defaults across the ecosystem, `updateSettings` does not support disabling Clack's default keybindings.
- 801246b: Adds a new `signal` option to support programmatic prompt cancellation with an [abort controller](https://kettanaito.com/blog/dont-sleep-on-abort-controller).
One example use case is automatically cancelling a prompt after a timeout.
```ts
const shouldContinue = await confirm({
message: "This message will self destruct in 5 seconds",
signal: AbortSignal.timeout(5000),
});
```
Another use case is racing a long running task with a manual prompt.
```ts
const abortController = new AbortController();
const projectType = await Promise.race([
detectProjectType({
signal: abortController.signal,
}),
select({
message: "Pick a project type.",
options: [
{ value: "ts", label: "TypeScript" },
{ value: "js", label: "JavaScript" },
{ value: "coffee", label: "CoffeeScript", hint: "oh no" },
],
signal: abortController.signal,
}),
]);
abortController.abort();
```
- a83d2f8: Updates default keybindings to support Vim motion shortcuts and map the `escape` key to cancel (`ctrl+c`).
| alias | action |
| ----- | ------ |
| `k` | up |
| `l` | right |
| `j` | down |
| `h` | left |
| `esc` | cancel |
### Patch Changes
- f9f139d: Adapts `spinner` output for static CI environments
- Updated dependencies [a83d2f8]
- Updated dependencies [801246b]
- Updated dependencies [a83d2f8]
- Updated dependencies [51e12bc]
- @clack/core@0.4.0
## 0.8.2
### Patch Changes
- Updated dependencies [4845f4f]
- Updated dependencies [d7b2fb9]
- @clack/core@0.3.5
## 0.8.1
### Patch Changes
- 360afeb: feat: adaptative max items
## 0.8.0
### Minor Changes
- 9acccde: Add tasks function for executing tasks in spinners
### Patch Changes
- b5c6b9b: Feat multiselect maxItems option
- 50ed94a: fix: clear `spinner` hooks on `spinner.stop`
- Updated dependencies [a04e418]
- Updated dependencies [4f6fcf5]
- @clack/core@0.3.4
## 0.7.0
### Minor Changes
- b27a701: add maxItems option to select prompt
- 89371be: added a new method called `spinner.message(msg: string)`
### Patch Changes
- 52183c4: Fix `spinner` conflict with terminal on error between `spinner.start()` and `spinner.stop()`
- ab51d29: Fixes cases where the note title length was miscalculated due to ansi characters
- Updated dependencies [cd79076]
- @clack/core@0.3.3
## 0.6.3
### Patch Changes
- c96eda5: Enable hard line-wrapping behavior for long words without spaces
- Updated dependencies [c96eda5]
- @clack/core@0.3.2
## 0.6.2
### Patch Changes
- 58a1df1: Fix line duplication bug by automatically wrapping prompts to `process.stdout.columns`
- Updated dependencies [58a1df1]
- @clack/core@0.3.1
## 0.6.1
### Patch Changes
- ca08fb6: Support complex value types for `select`, `multiselect` and `groupMultiselect`.
## 0.6.0
### Minor Changes
- 8a4a12f: add `groupMultiselect` prompt
- 165a1b3: Add `log` APIs. Supports `log.info`, `log.success`, `log.warn`, and `log.error`. For low-level control, `log.message` is also exposed.
### Patch Changes
- Updated dependencies [8a4a12f]
- Updated dependencies [8a4a12f]
- @clack/core@0.3.0
## 0.5.1
### Patch Changes
- cc11917: Update default `password` mask
- Updated dependencies [ec812b6]
- @clack/core@0.2.1
## 0.5.0
### Minor Changes
- d74dd05: Adds a `selectKey` prompt type
- 54c1bc3: **Breaking Change** `multiselect` has renamed `initialValue` to `initialValues`
### Patch Changes
- Updated dependencies [d74dd05]
- Updated dependencies [54c1bc3]
- @clack/core@0.2.0
## 0.4.5
### Patch Changes
- 1251132: Multiselect: return `Value[]` instead of `Option[]`.
- 8994382: Add a password prompt to `@clack/prompts`
- Updated dependencies [1251132]
- Updated dependencies [8994382]
- @clack/core@0.1.9
## 0.4.4
### Patch Changes
- d96071c: Don't mutate `initialValue` in `multiselect`, fix parameter type for `validate()`.
Credits to @banjo for the bug report and initial PR!
- Updated dependencies [d96071c]
- @clack/core@0.1.8
## 0.4.3
### Patch Changes
- 83d890e: Fix text cancel display bug
## 0.4.2
### Patch Changes
- Update README
## 0.4.1
### Patch Changes
- 7fb5375: Adds a new `defaultValue` option to the text prompt, removes automatic usage of the placeholder value.
- Updated dependencies [7fb5375]
- @clack/core@0.1.6
## 0.4.0
### Minor Changes
- 61b88b6: Add `group` construct to group many prompts together
### Patch Changes
- de1314e: Support `required` option for multi-select
- Updated dependencies [de1314e]
- @clack/core@0.1.5
## 0.3.0
### Minor Changes
- 493c592: Improve types for select/multiselect prompts. Numbers and booleans are now supported as the `value` option.
- 15558e3: Improved Windows/non-unicode support
### Patch Changes
- ca77da1: Fix multiselect initial value logic
- Updated dependencies [ca77da1]
- Updated dependencies [8aed606]
- @clack/core@0.1.4
## 0.2.2
### Patch Changes
- 94b24d9: Fix CJS `ansi-regex` interop
## 0.2.1
### Patch Changes
- a99c458: Support `initialValue` option for text prompt
- Updated dependencies [a99c458]
- @clack/core@0.1.3
## 0.2.0
### Minor Changes
- Improved type safety
- b1341d6: Updated styles, new note component
### Patch Changes
- Updated dependencies [7dcad8f]
- Updated dependencies [2242f13]
- Updated dependencies [b1341d6]
- @clack/core@0.1.2
## 0.1.1
### Patch Changes
- fa09bf5: Use circle for radio, square for checkbox
- Updated dependencies [4be7dbf]
- Updated dependencies [b480679]
- @clack/core@0.1.1
## 0.1.0
### Minor Changes
- 7015ec9: Create new prompt: multi-select
### Patch Changes
- Updated dependencies [7015ec9]
- @clack/core@0.1.0
## 0.0.10
### Patch Changes
- e0b49e5: Update spinner so it actually spins
## 0.0.9
### Patch Changes
- Update README
## 0.0.8
### Patch Changes
- Updated dependencies [9d371c3]
- @clack/core@0.0.12
## 0.0.7
### Patch Changes
- Update README
## 0.0.6
### Patch Changes
- d20ef2a: Update keywords, URLs
- Updated dependencies [441d5b7]
- Updated dependencies [d20ef2a]
- Updated dependencies [fe13c2f]
- @clack/core@0.0.11
## 0.0.5
### Patch Changes
- Update README
## 0.0.4
### Patch Changes
- 80404ab: Update README
## 0.0.3
### Patch Changes
- a0cb382: Add `main` entrypoint
- Updated dependencies [a0cb382]
- @clack/core@0.0.10
## 0.0.2
### Patch Changes
- Updated dependencies
- @clack/core@0.0.9
## 0.0.1
### Patch Changes
- a4b5e13: Initial release
- Updated dependencies [a4b5e13]
- @clack/core@0.0.8
================================================
FILE: packages/prompts/LICENSE
================================================
MIT License
Copyright (c) Nate Moore
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: packages/prompts/README.md
================================================
# `@clack/prompts`
Effortlessly build beautiful command-line apps 🪄 [Try the demo](https://stackblitz.com/edit/clack-prompts?file=index.js)

---
`@clack/prompts` is an opinionated, pre-styled wrapper around [`@clack/core`](https://www.npmjs.com/package/@clack/core).
- 🤏 80% smaller than other options
- 💎 Beautiful, minimal UI
- ✅ Simple API
- 🧱 Comes with `text`, `confirm`, `select`, `multiselect`, and `spinner` components
## Basics
### Setup
The `intro` and `outro` functions will print a message to begin or end a prompt session, respectively.
```js
import { intro, outro } from '@clack/prompts';
intro(`create-my-app`);
// Do stuff
outro(`You're all set!`);
```
### Cancellation
The `isCancel` function is a guard that detects when a user cancels a question with `CTRL + C`. You should handle this situation for each prompt, optionally providing a nice cancellation message with the `cancel` utility.
```js
import { isCancel, cancel, text } from '@clack/prompts';
const value = await text({
message: 'What is the meaning of life?',
});
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(0);
}
```
## Components
### Text
The text component accepts a single line of text.
```js
import { text } from '@clack/prompts';
const meaning = await text({
message: 'What is the meaning of life?',
placeholder: 'Not sure',
initialValue: '42',
validate(value) {
if (value.length === 0) return `Value is required!`;
},
});
```
### Confirm
The confirm component accepts a yes or no answer. The result is a boolean value of `true` or `false`.
```js
import { confirm } from '@clack/prompts';
const shouldContinue = await confirm({
message: 'Do you want to continue?',
});
```
### Select
The select component allows a user to choose one value from a list of options. The result is the `value` prop of a given option.
```js
import { select } from '@clack/prompts';
const projectType = await select({
message: 'Pick a project type.',
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'js', label: 'JavaScript', disabled: true },
{ value: 'coffee', label: 'CoffeeScript', hint: 'oh no' },
],
});
```
### Multi-Select
The `multiselect` component allows a user to choose many values from a list of options. The result is an array with all selected `value` props.
```js
import { multiselect } from '@clack/prompts';
const additionalTools = await multiselect({
message: 'Select additional tools.',
options: [
{ value: 'eslint', label: 'ESLint', hint: 'recommended' },
{ value: 'prettier', label: 'Prettier', disabled: true },
{ value: 'gh-action', label: 'GitHub Action' },
],
required: false,
});
```
It is also possible to select multiple items arranged into hierarchy by using `groupMultiselect`:
```js
import { groupMultiselect } from '@clack/prompts';
const basket = await groupMultiselect({
message: 'Select your favorite fruits and vegetables:',
options: {
fruits: [
{ value: 'apple', label: 'apple' },
{ value: 'banana', label: 'banana' },
{ value: 'cherry', label: 'cherry' },
],
vegetables: [
{ value: 'carrot', label: 'carrot' },
{ value: 'spinach', label: 'spinach' },
{ value: 'potato', label: 'potato' },
]
}
});
```
### Spinner
The spinner component surfaces a pending action, such as a long-running download or dependency installation.
```js
import { spinner } from '@clack/prompts';
const s = spinner();
s.start('Installing via npm');
// Do installation here
s.stop('Installed via npm');
```
### Progress
The progress component extends the spinner component to add a progress bar to visualize the progression of an action.
```js
import { progress } from '@clack/prompts';
const p = progress({ max: 10 });
p.start('Downloading archive');
// Do download here
p.advance(3, 'Downloading (30%)');
// ...
p.advance(8, 'Downloading (80%)');
// ...
p.stop('Archive downloaded');
```
## Utilities
### Grouping
Grouping prompts together is a great way to keep your code organized. This accepts a JSON object with a name that can be used to reference the group later. The second argument is an optional but has a `onCancel` callback that will be called if the user cancels one of the prompts in the group.
```js
import * as p from '@clack/prompts';
const group = await p.group(
{
name: () => p.text({ message: 'What is your name?' }),
age: () => p.text({ message: 'What is
gitextract_95ui2zim/ ├── .changeset/ │ ├── README.md │ ├── afraid-donkeys-sin.md │ ├── big-pants-invite.md │ ├── config.json │ ├── dirty-actors-find.md │ ├── tangy-mirrors-hug.md │ └── tricky-states-tease.md ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── ci.yml │ ├── detect-agent.yml │ ├── format.yml │ ├── issue.yml │ ├── preview.yml │ ├── publish.yml │ └── require-allow-edits.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode/ │ └── settings.json ├── CONTRIBUTING.md ├── README.md ├── biome.json ├── build.preset.ts ├── examples/ │ ├── basic/ │ │ ├── autocomplete-multiselect.ts │ │ ├── autocomplete.ts │ │ ├── date.ts │ │ ├── default-value.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── path.ts │ │ ├── progress.ts │ │ ├── spinner-cancel-advanced.ts │ │ ├── spinner-cancel.ts │ │ ├── spinner-ci.ts │ │ ├── spinner-timer.ts │ │ ├── spinner.ts │ │ ├── stream.ts │ │ ├── task-log.ts │ │ ├── text-validation.ts │ │ └── tsconfig.json │ └── changesets/ │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── knip.json ├── package.json ├── packages/ │ ├── core/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── build.config.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── prompts/ │ │ │ │ ├── autocomplete.ts │ │ │ │ ├── confirm.ts │ │ │ │ ├── date.ts │ │ │ │ ├── group-multiselect.ts │ │ │ │ ├── multi-select.ts │ │ │ │ ├── password.ts │ │ │ │ ├── prompt.ts │ │ │ │ ├── select-key.ts │ │ │ │ ├── select.ts │ │ │ │ └── text.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── cursor.ts │ │ │ ├── index.ts │ │ │ ├── settings.ts │ │ │ └── string.ts │ │ ├── test/ │ │ │ ├── mock-readable.ts │ │ │ ├── mock-writable.ts │ │ │ ├── prompts/ │ │ │ │ ├── autocomplete.test.ts │ │ │ │ ├── confirm.test.ts │ │ │ │ ├── date.test.ts │ │ │ │ ├── multi-select.test.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── prompt.test.ts │ │ │ │ ├── select.test.ts │ │ │ │ └── text.test.ts │ │ │ └── utils.test.ts │ │ └── tsconfig.json │ └── prompts/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── __mocks__/ │ │ └── fs.cjs │ ├── build.config.ts │ ├── package.json │ ├── src/ │ │ ├── autocomplete.ts │ │ ├── box.ts │ │ ├── common.ts │ │ ├── confirm.ts │ │ ├── date.ts │ │ ├── group-multi-select.ts │ │ ├── group.ts │ │ ├── index.ts │ │ ├── limit-options.ts │ │ ├── log.ts │ │ ├── messages.ts │ │ ├── multi-select.ts │ │ ├── note.ts │ │ ├── password.ts │ │ ├── path.ts │ │ ├── progress-bar.ts │ │ ├── select-key.ts │ │ ├── select.ts │ │ ├── spinner.ts │ │ ├── stream.ts │ │ ├── task-log.ts │ │ ├── task.ts │ │ └── text.ts │ ├── test/ │ │ ├── __snapshots__/ │ │ │ ├── autocomplete.test.ts.snap │ │ │ ├── box.test.ts.snap │ │ │ ├── confirm.test.ts.snap │ │ │ ├── date.test.ts.snap │ │ │ ├── group-multi-select.test.ts.snap │ │ │ ├── log.test.ts.snap │ │ │ ├── multi-select.test.ts.snap │ │ │ ├── note.test.ts.snap │ │ │ ├── password.test.ts.snap │ │ │ ├── path.test.ts.snap │ │ │ ├── progress-bar.test.ts.snap │ │ │ ├── select-key.test.ts.snap │ │ │ ├── select.test.ts.snap │ │ │ ├── spinner.test.ts.snap │ │ │ ├── task-log.test.ts.snap │ │ │ └── text.test.ts.snap │ │ ├── autocomplete.test.ts │ │ ├── box.test.ts │ │ ├── confirm.test.ts │ │ ├── date.test.ts │ │ ├── group-multi-select.test.ts │ │ ├── limit-options.test.ts │ │ ├── log.test.ts │ │ ├── multi-select.test.ts │ │ ├── note.test.ts │ │ ├── password.test.ts │ │ ├── path.test.ts │ │ ├── progress-bar.test.ts │ │ ├── select-key.test.ts │ │ ├── select.test.ts │ │ ├── spinner.test.ts │ │ ├── task-log.test.ts │ │ ├── test-utils.ts │ │ └── text.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-workspace.yaml └── tsconfig.json
SYMBOL INDEX (237 symbols across 52 files)
FILE: examples/basic/autocomplete-multiselect.ts
function main (line 9) | async function main() {
FILE: examples/basic/autocomplete.ts
function main (line 4) | async function main() {
FILE: examples/basic/date.ts
function main (line 4) | async function main() {
FILE: examples/basic/default-value.ts
function main (line 4) | async function main() {
FILE: examples/basic/index.ts
function main (line 5) | async function main() {
FILE: examples/basic/path.ts
function demo (line 3) | async function demo() {
FILE: examples/basic/progress.ts
function fakeProgress (line 5) | async function fakeProgress(progressbar: ProgressResult): Promise<void> {
function demo (line 13) | async function demo() {
FILE: examples/basic/spinner-cancel-advanced.ts
function main (line 4) | async function main() {
FILE: examples/basic/spinner-cancel.ts
function sleep (line 40) | function sleep(ms: number) {
FILE: examples/basic/spinner-timer.ts
function main (line 5) | async function main() {
function sleep (line 22) | function sleep(ms: number) {
FILE: examples/basic/stream.ts
function main (line 5) | async function main() {
FILE: examples/basic/task-log.ts
function main (line 4) | async function main() {
FILE: examples/basic/text-validation.ts
function main (line 4) | async function main() {
FILE: examples/changesets/index.ts
function onCancel (line 5) | function onCancel() {
function main (line 10) | async function main() {
FILE: packages/core/src/prompts/autocomplete.ts
type OptionLike (line 6) | interface OptionLike {
type FilterFunction (line 12) | type FilterFunction<T extends OptionLike> = (search: string, opt: T) => ...
function getCursorForValue (line 14) | function getCursorForValue<T extends OptionLike>(
function defaultFilter (line 34) | function defaultFilter<T extends OptionLike>(input: string, option: T): ...
function normalisedValue (line 39) | function normalisedValue<T>(multiple: boolean, values: T[] | undefined):...
type AutocompleteOptions (line 49) | interface AutocompleteOptions<T extends OptionLike>
class AutocompletePrompt (line 63) | class AutocompletePrompt<T extends OptionLike> extends Prompt<
method cursor (line 78) | get cursor(): number {
method userInputWithCursor (line 82) | get userInputWithCursor() {
method options (line 94) | get options(): T[] {
method constructor (line 101) | constructor(opts: AutocompleteOptions<T>) {
method _isActionKey (line 139) | protected override _isActionKey(char: string | undefined, key: Key): b...
method #onKey (line 150) | #onKey(_char: string | undefined, key: Key): void {
method deselectAll (line 202) | deselectAll() {
method toggleSelected (line 206) | toggleSelected(value: T['value']) {
method #onUserInputChanged (line 222) | #onUserInputChanged(value: string): void {
FILE: packages/core/src/prompts/confirm.ts
type ConfirmOptions (line 4) | interface ConfirmOptions extends PromptOptions<boolean, ConfirmPrompt> {
class ConfirmPrompt (line 10) | class ConfirmPrompt extends Prompt<boolean> {
method cursor (line 11) | get cursor() {
method _value (line 15) | private get _value() {
method constructor (line 19) | constructor(opts: ConfirmOptions) {
FILE: packages/core/src/prompts/date.ts
type SegmentConfig (line 5) | interface SegmentConfig {
type DateParts (line 10) | interface DateParts {
type DateFormat (line 16) | type DateFormat = 'YMD' | 'MDY' | 'DMY';
constant SEGMENTS (line 18) | const SEGMENTS: Record<string, SegmentConfig> = {
function segmentsFor (line 24) | function segmentsFor(fmt: DateFormat): SegmentConfig[] {
function detectLocaleFormat (line 28) | function detectLocaleFormat(locale?: string): { segments: SegmentConfig[...
function parseSegmentToNum (line 48) | function parseSegmentToNum(s: string): number {
function parse (line 52) | function parse(parts: DateParts): { year: number; month: number; day: nu...
function daysInMonth (line 60) | function daysInMonth(year: number, month: number): number {
function validParts (line 65) | function validParts(parts: DateParts): { year: number; month: number; da...
function toDate (line 76) | function toDate(parts: DateParts): Date | undefined {
function segmentBounds (line 81) | function segmentBounds(
type DateOptions (line 120) | interface DateOptions extends PromptOptions<Date, DatePrompt> {
class DatePrompt (line 130) | class DatePrompt extends Prompt<Date> {
method segmentCursor (line 142) | get segmentCursor() {
method segmentValues (line 146) | get segmentValues(): DateParts {
method segments (line 150) | get segments(): readonly SegmentConfig[] {
method separator (line 154) | get separator(): string {
method formattedValue (line 158) | get formattedValue(): string {
method #format (line 162) | #format(parts: DateParts): string {
method #refresh (line 166) | #refresh() {
method constructor (line 171) | constructor(opts: DateOptions) {
method #seg (line 202) | #seg(): { segment: SegmentConfig; index: number } | undefined {
method #navigate (line 213) | #navigate(direction: 1 | -1) {
method #adjust (line 226) | #adjust(direction: 1 | -1) {
method #onCursor (line 256) | #onCursor(key?: string) {
method #onKey (line 270) | #onKey(char: string | undefined, key: Key) {
method #validateSegment (line 409) | #validateSegment(parts: DateParts, seg: SegmentConfig): string | undef...
method #onFinalize (line 420) | #onFinalize(opts: DateOptions) {
FILE: packages/core/src/prompts/group-multiselect.ts
type GroupMultiSelectOptions (line 3) | interface GroupMultiSelectOptions<T extends { value: any }>
class GroupMultiSelectPrompt (line 11) | class GroupMultiSelectPrompt<T extends { value: any }> extends Prompt<T[...
method getGroupItems (line 16) | getGroupItems(group: string): T[] {
method isGroupSelected (line 20) | isGroupSelected(group: string) {
method toggleValue (line 29) | private toggleValue() {
method constructor (line 53) | constructor(opts: GroupMultiSelectOptions<T>) {
FILE: packages/core/src/prompts/multi-select.ts
type OptionLike (line 4) | interface OptionLike {
type MultiSelectOptions (line 9) | interface MultiSelectOptions<T extends OptionLike>
class MultiSelectPrompt (line 16) | class MultiSelectPrompt<T extends OptionLike> extends Prompt<T['value'][...
method _value (line 20) | private get _value(): T['value'] {
method _enabledOptions (line 24) | private get _enabledOptions(): T[] {
method toggleAll (line 28) | private toggleAll() {
method toggleInvert (line 34) | private toggleInvert() {
method toggleValue (line 43) | private toggleValue() {
method constructor (line 53) | constructor(opts: MultiSelectOptions<T>) {
FILE: packages/core/src/prompts/password.ts
type PasswordOptions (line 4) | interface PasswordOptions extends PromptOptions<string, PasswordPrompt> {
class PasswordPrompt (line 7) | class PasswordPrompt extends Prompt<string> {
method cursor (line 9) | get cursor() {
method masked (line 12) | get masked() {
method userInputWithCursor (line 15) | get userInputWithCursor() {
method clear (line 28) | clear() {
method constructor (line 31) | constructor({ mask, ...opts }: PasswordOptions) {
FILE: packages/core/src/prompts/prompt.ts
type PromptOptions (line 17) | interface PromptOptions<TValue, Self extends Prompt<TValue>> {
class Prompt (line 28) | class Prompt<TValue> {
method constructor (line 46) | constructor(options: PromptOptions<TValue, Prompt<TValue>>, trackValue...
method unsubscribe (line 64) | protected unsubscribe() {
method setSubscriber (line 72) | private setSubscriber<T extends keyof ClackEvents<TValue>>(
method on (line 86) | public on<T extends keyof ClackEvents<TValue>>(event: T, cb: ClackEven...
method once (line 95) | public once<T extends keyof ClackEvents<TValue>>(event: T, cb: ClackEv...
method emit (line 104) | public emit<T extends keyof ClackEvents<TValue>>(
method prompt (line 124) | public prompt() {
method _isActionKey (line 178) | protected _isActionKey(char: string | undefined, _key: Key): boolean {
method _setValue (line 182) | protected _setValue(value: TValue | undefined): void {
method _setUserInput (line 187) | protected _setUserInput(value: string | undefined, write?: boolean): v...
method _clearUserInput (line 196) | protected _clearUserInput(): void {
method onKeypress (line 201) | private onKeypress(char: string | undefined, key: Key) {
method close (line 255) | protected close() {
method restoreCursor (line 266) | private restoreCursor() {
method render (line 273) | private render() {
FILE: packages/core/src/prompts/select-key.ts
type SelectKeyOptions (line 3) | interface SelectKeyOptions<T extends { value: string }>
class SelectKeyPrompt (line 8) | class SelectKeyPrompt<T extends { value: string }> extends Prompt<T['val...
method constructor (line 12) | constructor(opts: SelectKeyOptions<T>) {
FILE: packages/core/src/prompts/select.ts
type SelectOptions (line 4) | interface SelectOptions<T extends { value: any; disabled?: boolean }>
class SelectPrompt (line 9) | class SelectPrompt<T extends { value: any; disabled?: boolean }> extends...
method _selectedValue (line 15) | private get _selectedValue() {
method changeValue (line 19) | private changeValue() {
method constructor (line 23) | constructor(opts: SelectOptions<T>) {
FILE: packages/core/src/prompts/text.ts
type TextOptions (line 4) | interface TextOptions extends PromptOptions<string, TextPrompt> {
class TextPrompt (line 9) | class TextPrompt extends Prompt<string> {
method userInputWithCursor (line 10) | get userInputWithCursor() {
method cursor (line 22) | get cursor() {
method constructor (line 25) | constructor(opts: TextOptions) {
FILE: packages/core/src/types.ts
type ClackState (line 7) | type ClackState = 'initial' | 'active' | 'cancel' | 'submit' | 'error';
type ClackEvents (line 12) | interface ClackEvents<TValue> {
FILE: packages/core/src/utils/cursor.ts
function findCursor (line 1) | function findCursor<T extends { disabled?: boolean }>(
FILE: packages/core/src/utils/index.ts
constant CANCEL_SYMBOL (line 15) | const CANCEL_SYMBOL = Symbol('clack:cancel');
function isCancel (line 17) | function isCancel(value: unknown): value is symbol {
function setRawMode (line 21) | function setRawMode(input: Readable, value: boolean) {
type BlockOptions (line 27) | interface BlockOptions {
function block (line 34) | function block({
function wrapTextWithPrefix (line 101) | function wrapTextWithPrefix(
FILE: packages/core/src/utils/settings.ts
type Action (line 2) | type Action = (typeof actions)[number];
constant DEFAULT_MONTH_NAMES (line 4) | const DEFAULT_MONTH_NAMES = [
type InternalClackSettings (line 20) | interface InternalClackSettings {
type ClackSettings (line 69) | interface ClackSettings {
function updateSettings (line 118) | function updateSettings(updates: ClackSettings) {
function isActionKey (line 179) | function isActionKey(key: string | Array<string | undefined>, action: Ac...
FILE: packages/core/src/utils/string.ts
function diffLines (line 1) | function diffLines(a: string, b: string) {
FILE: packages/core/test/mock-readable.ts
class MockReadable (line 3) | class MockReadable extends Readable {
method _read (line 6) | _read() {
method pushValue (line 19) | pushValue(val: unknown): void {
method close (line 23) | close(): void {
FILE: packages/core/test/mock-writable.ts
class MockWritable (line 3) | class MockWritable extends Writable {
method _write (line 6) | _write(
FILE: packages/prompts/src/autocomplete.ts
function getLabel (line 16) | function getLabel<T>(option: Option<T>) {
function getFilteredOption (line 20) | function getFilteredOption<T>(searchText: string, option: Option<T>): bo...
function getSelectedOptions (line 32) | function getSelectedOptions<T>(values: T[], options: Option<T>[]): Optio...
type AutocompleteSharedOptions (line 44) | interface AutocompleteSharedOptions<Value> extends CommonOptions {
type AutocompleteOptions (line 72) | interface AutocompleteOptions<Value> extends AutocompleteSharedOptions<V...
method render (line 98) | render() {
type AutocompleteMultiSelectOptions (line 227) | interface AutocompleteMultiSelectOptions<Value> extends AutocompleteShar...
method render (line 287) | render() {
FILE: packages/prompts/src/box.ts
type BoxAlignment (line 19) | type BoxAlignment = 'left' | 'center' | 'right';
type BoxSymbols (line 21) | type BoxSymbols = [topLeft: string, topRight: string, bottomLeft: string...
type BoxOptions (line 31) | interface BoxOptions extends CommonOptions {
function getPaddingForLine (line 41) | function getPaddingForLine(
FILE: packages/prompts/src/common.ts
constant S_STEP_ACTIVE (line 12) | const S_STEP_ACTIVE = unicodeOr('◆', '*');
constant S_STEP_CANCEL (line 13) | const S_STEP_CANCEL = unicodeOr('■', 'x');
constant S_STEP_ERROR (line 14) | const S_STEP_ERROR = unicodeOr('▲', 'x');
constant S_STEP_SUBMIT (line 15) | const S_STEP_SUBMIT = unicodeOr('◇', 'o');
constant S_BAR_START (line 17) | const S_BAR_START = unicodeOr('┌', 'T');
constant S_BAR (line 18) | const S_BAR = unicodeOr('│', '|');
constant S_BAR_END (line 19) | const S_BAR_END = unicodeOr('└', '—');
constant S_BAR_START_RIGHT (line 20) | const S_BAR_START_RIGHT = unicodeOr('┐', 'T');
constant S_BAR_END_RIGHT (line 21) | const S_BAR_END_RIGHT = unicodeOr('┘', '—');
constant S_RADIO_ACTIVE (line 23) | const S_RADIO_ACTIVE = unicodeOr('●', '>');
constant S_RADIO_INACTIVE (line 24) | const S_RADIO_INACTIVE = unicodeOr('○', ' ');
constant S_CHECKBOX_ACTIVE (line 25) | const S_CHECKBOX_ACTIVE = unicodeOr('◻', '[•]');
constant S_CHECKBOX_SELECTED (line 26) | const S_CHECKBOX_SELECTED = unicodeOr('◼', '[+]');
constant S_CHECKBOX_INACTIVE (line 27) | const S_CHECKBOX_INACTIVE = unicodeOr('◻', '[ ]');
constant S_PASSWORD_MASK (line 28) | const S_PASSWORD_MASK = unicodeOr('▪', '•');
constant S_BAR_H (line 30) | const S_BAR_H = unicodeOr('─', '-');
constant S_CORNER_TOP_RIGHT (line 31) | const S_CORNER_TOP_RIGHT = unicodeOr('╮', '+');
constant S_CONNECT_LEFT (line 32) | const S_CONNECT_LEFT = unicodeOr('├', '+');
constant S_CORNER_BOTTOM_RIGHT (line 33) | const S_CORNER_BOTTOM_RIGHT = unicodeOr('╯', '+');
constant S_CORNER_BOTTOM_LEFT (line 34) | const S_CORNER_BOTTOM_LEFT = unicodeOr('╰', '+');
constant S_CORNER_TOP_LEFT (line 35) | const S_CORNER_TOP_LEFT = unicodeOr('╭', '+');
constant S_INFO (line 37) | const S_INFO = unicodeOr('●', '•');
constant S_SUCCESS (line 38) | const S_SUCCESS = unicodeOr('◆', '*');
constant S_WARN (line 39) | const S_WARN = unicodeOr('▲', '!');
constant S_ERROR (line 40) | const S_ERROR = unicodeOr('■', 'x');
type CommonOptions (line 70) | interface CommonOptions {
FILE: packages/prompts/src/confirm.ts
type ConfirmOptions (line 12) | interface ConfirmOptions extends CommonOptions {
method render (line 29) | render() {
FILE: packages/prompts/src/date.ts
type DateOptions (line 8) | interface DateOptions extends CommonOptions {
method validate (line 23) | validate(value: Date | undefined) {
method render (line 39) | render() {
function renderDate (line 80) | function renderDate(prompt: Omit<InstanceType<typeof DatePrompt>, 'promp...
type SegmentOptions (line 98) | interface SegmentOptions {
function renderSegment (line 102) | function renderSegment(value: string, opts: SegmentOptions): string {
constant DEFAULT_LABELS (line 109) | const DEFAULT_LABELS: Record<'year' | 'month' | 'day', string> = {
FILE: packages/prompts/src/group-multi-select.ts
type GroupMultiSelectOptions (line 14) | interface GroupMultiSelectOptions<Value> extends CommonOptions {
method validate (line 93) | validate(selected: Value[] | undefined) {
method render (line 106) | render() {
FILE: packages/prompts/src/group.ts
type Prettify (line 3) | type Prettify<T> = {
type PromptGroupAwaitedReturn (line 7) | type PromptGroupAwaitedReturn<T> = {
type PromptGroupOptions (line 11) | interface PromptGroupOptions<T> {
type PromptGroup (line 19) | type PromptGroup<T> = {
FILE: packages/prompts/src/limit-options.ts
type LimitOptionsParams (line 6) | interface LimitOptionsParams<TOption> extends CommonOptions {
FILE: packages/prompts/src/log.ts
type LogMessageOptions (line 13) | interface LogMessageOptions extends CommonOptions {
FILE: packages/prompts/src/multi-select.ts
type MultiSelectOptions (line 16) | interface MultiSelectOptions<Value> extends CommonOptions {
method validate (line 82) | validate(selected: Value[] | undefined) {
method render (line 95) | render() {
FILE: packages/prompts/src/note.ts
type FormatFn (line 18) | type FormatFn = (line: string) => string;
type NoteOptions (line 19) | interface NoteOptions extends CommonOptions {
FILE: packages/prompts/src/password.ts
type PasswordOptions (line 5) | interface PasswordOptions extends CommonOptions {
method render (line 18) | render() {
FILE: packages/prompts/src/path.ts
type PathOptions (line 6) | interface PathOptions extends CommonOptions {
method validate (line 21) | validate(value) {
method options (line 34) | options() {
FILE: packages/prompts/src/progress-bar.ts
constant S_PROGRESS_CHAR (line 6) | const S_PROGRESS_CHAR: Record<NonNullable<ProgressOptions['style']>, str...
type ProgressOptions (line 12) | interface ProgressOptions extends SpinnerOptions {
type ProgressResult (line 18) | interface ProgressResult extends SpinnerResult {
function progress (line 22) | function progress({
FILE: packages/prompts/src/select-key.ts
type SelectKeyOptions (line 6) | interface SelectKeyOptions<Value extends string> extends CommonOptions {
method render (line 42) | render() {
FILE: packages/prompts/src/select.ts
type Primitive (line 14) | type Primitive = Readonly<string | boolean | number>;
type Option (line 16) | type Option<Value> = Value extends Primitive
type SelectOptions (line 68) | interface SelectOptions<Value> extends CommonOptions {
method render (line 115) | render() {
FILE: packages/prompts/src/spinner.ts
type SpinnerOptions (line 15) | interface SpinnerOptions extends CommonOptions {
type SpinnerResult (line 25) | interface SpinnerResult {
method isCancelled (line 215) | get isCancelled() {
FILE: packages/prompts/src/task-log.ts
type TaskLogOptions (line 14) | interface TaskLogOptions extends CommonOptions {
type TaskLogMessageOptions (line 21) | interface TaskLogMessageOptions {
type TaskLogCompletionOptions (line 25) | interface TaskLogCompletionOptions {
type BufferEntry (line 29) | interface BufferEntry {
method message (line 186) | message(msg: string, mopts?: TaskLogMessageOptions) {
method group (line 189) | group(name: string) {
method error (line 214) | error(message: string, opts?: TaskLogCompletionOptions): void {
method success (line 225) | success(message: string, opts?: TaskLogCompletionOptions): void {
FILE: packages/prompts/src/task.ts
type Task (line 4) | type Task = {
FILE: packages/prompts/src/text.ts
type TextOptions (line 5) | interface TextOptions extends CommonOptions {
method render (line 22) | render() {
FILE: packages/prompts/test/test-utils.ts
class MockWritable (line 3) | class MockWritable extends Writable {
method _write (line 9) | _write(
class MockReadable (line 19) | class MockReadable extends Readable {
method _read (line 22) | _read() {
method pushValue (line 35) | pushValue(val: unknown): void {
method close (line 39) | close(): void {
Condensed preview — 148 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (868K chars).
[
{
"path": ".changeset/README.md",
"chars": 9877,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/afraid-donkeys-sin.md",
"chars": 136,
"preview": "---\n\"@clack/prompts\": minor\n\"@clack/core\": minor\n---\n\nExternalize `fast-string-width` and `fast-wrap-ansi` to avoid doub"
},
{
"path": ".changeset/big-pants-invite.md",
"chars": 244,
"preview": "---\n\"@clack/prompts\": patch\n---\n\nFix the `path` prompt so `directory: true` correctly enforces directory-only selection "
},
{
"path": ".changeset/config.json",
"chars": 283,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@2.3.0/schema.json\",\n \"changelog\": \"@changesets/cli/changelog\",\n \""
},
{
"path": ".changeset/dirty-actors-find.md",
"chars": 204,
"preview": "---\n\"@clack/prompts\": patch\n\"@clack/core\": patch\n---\n\nAdds `placeholder` option to `autocomplete`. When the placeholder "
},
{
"path": ".changeset/tangy-mirrors-hug.md",
"chars": 111,
"preview": "---\n\"@clack/prompts\": minor\n\"@clack/core\": minor\n---\n\nAdds `date` prompt with `format` support (YMD, MDY, DMY)\n"
},
{
"path": ".changeset/tricky-states-tease.md",
"chars": 266,
"preview": "---\n\"@clack/prompts\": patch\n---\n\nFix `path` directory mode so pressing Enter with an existing directory `initialValue` s"
},
{
"path": ".editorconfig",
"chars": 187,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".gitattributes",
"chars": 19,
"preview": "* text=auto eol=lf\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 681,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: '[Bug] '\nlabels: bug\nassignees: ''\n---\n\n**Environm"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 612,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: '[Request]'\nlabels: enhancement\nassignees: ''\n-"
},
{
"path": ".github/workflows/ci.yml",
"chars": 565,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n pull_request:\n\n# Automatically cancel in-progress actions on the same"
},
{
"path": ".github/workflows/detect-agent.yml",
"chars": 431,
"preview": "name: Detect Agent\n\non:\n pull_request_target:\n types: [opened]\n workflow_dispatch: {}\n\npermissions:\n issues: write"
},
{
"path": ".github/workflows/format.yml",
"chars": 298,
"preview": "name: Format\n\non:\n workflow_dispatch:\n push:\n branches:\n - main\n\njobs:\n format:\n if: github.repository_own"
},
{
"path": ".github/workflows/issue.yml",
"chars": 466,
"preview": "name: issue\n\non:\n issues:\n types: [opened, edited, labeled, reopened]\n\njobs:\n backlog:\n if: github.event.action "
},
{
"path": ".github/workflows/preview.yml",
"chars": 391,
"preview": "name: Preview\n\non:\n push:\n branches:\n - main\n pull_request:\n workflow_dispatch:\n\n\njobs:\n preview:\n if: gi"
},
{
"path": ".github/workflows/publish.yml",
"chars": 324,
"preview": "name: Publish\n\non:\n push:\n branches: [main, v0]\n workflow_dispatch:\n\npermissions:\n id-token: write\n contents: wri"
},
{
"path": ".github/workflows/require-allow-edits.yml",
"chars": 260,
"preview": "name: Require “Allow Edits”\n\non: [pull_request_target]\n\npermissions:\n contents: read\n\njobs:\n _:\n permissions:\n "
},
{
"path": ".gitignore",
"chars": 2082,
"preview": ".DS_Store\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnos"
},
{
"path": ".npmrc",
"chars": 191,
"preview": "# Important! Never install from registry even when new version is available\nprefer-workspace-packages=true\nlink-workspac"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "20.18.1\n"
},
{
"path": ".vscode/settings.json",
"chars": 55,
"preview": "{\n \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 11212,
"preview": "# Contributing to Clack\n\nThank you for your interest in contributing to Clack! This document provides detailed instructi"
},
{
"path": "README.md",
"chars": 578,
"preview": "<br />\n<br />\n\n<div align=\"center\">\n <img alt=\"Clack logo\" src=\"/.github/assets/clack.png?sanitize=true\" width=\"320\">"
},
{
"path": "biome.json",
"chars": 1302,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/2.1.2/schema.json\",\n \"vcs\": { \"enabled\": false, \"clientKind\": \"git\", \"useIg"
},
{
"path": "build.preset.ts",
"chars": 267,
"preview": "import { definePreset } from 'unbuild';\n\n// @see https://github.com/unjs/unbuild\nexport default definePreset({\n\tclean: t"
},
{
"path": "examples/basic/autocomplete-multiselect.ts",
"chars": 3636,
"preview": "import * as p from '@clack/prompts';\nimport color from 'picocolors';\n\n/**\n * Example demonstrating the integrated autoco"
},
{
"path": "examples/basic/autocomplete.ts",
"chars": 1996,
"preview": "import * as p from '@clack/prompts';\nimport color from 'picocolors';\n\nasync function main() {\n\tconsole.clear();\n\n\tp.intr"
},
{
"path": "examples/basic/date.ts",
"chars": 498,
"preview": "import * as p from '@clack/prompts';\nimport color from 'picocolors';\n\nasync function main() {\n\tconst result = (await p.d"
},
{
"path": "examples/basic/default-value.ts",
"chars": 685,
"preview": "import * as p from '@clack/prompts';\nimport color from 'picocolors';\n\nasync function main() {\n\tconst defaultPath = 'my-p"
},
{
"path": "examples/basic/index.ts",
"chars": 2370,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport * as p from '@clack/prompts';\nimport color from 'picocolors';\n"
},
{
"path": "examples/basic/package.json",
"chars": 627,
"preview": "{\n \"name\": \"@example/basic\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"dependencies\": {\n \"@cla"
},
{
"path": "examples/basic/path.ts",
"chars": 192,
"preview": "import * as p from '@clack/prompts';\n\nasync function demo() {\n\tp.intro('path start...');\n\n\tconst _path = await p.path({\n"
},
{
"path": "examples/basic/progress.ts",
"chars": 1021,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport type { ProgressResult } from '@clack/prompts';\nimport * as p f"
},
{
"path": "examples/basic/spinner-cancel-advanced.ts",
"chars": 4579,
"preview": "import { setTimeout as sleep } from 'node:timers/promises';\nimport * as p from '@clack/prompts';\n\nasync function main() "
},
{
"path": "examples/basic/spinner-cancel.ts",
"chars": 1138,
"preview": "import * as p from '@clack/prompts';\n\np.intro('Spinner with cancellation detection');\n\n// Example 1: Using onCancel call"
},
{
"path": "examples/basic/spinner-ci.ts",
"chars": 1186,
"preview": "/**\n * This example addresses a issue reported in GitHub Actions where `spinner` was excessively writing messages,\n * le"
},
{
"path": "examples/basic/spinner-timer.ts",
"chars": 448,
"preview": "import * as p from '@clack/prompts';\n\np.intro('spinner start...');\n\nasync function main() {\n\tconst spin = p.spinner({ in"
},
{
"path": "examples/basic/spinner.ts",
"chars": 475,
"preview": "import * as p from '@clack/prompts';\n\np.intro('spinner start...');\n\nconst spin = p.spinner();\nconst total = 6000;\nlet pr"
},
{
"path": "examples/basic/stream.ts",
"chars": 912,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport * as p from '@clack/prompts';\nimport color from 'picocolors';\n"
},
{
"path": "examples/basic/task-log.ts",
"chars": 488,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport * as p from '@clack/prompts';\n\nasync function main() {\n\tp.intr"
},
{
"path": "examples/basic/text-validation.ts",
"chars": 1103,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport { isCancel, note, text } from '@clack/prompts';\n\nasync functio"
},
{
"path": "examples/basic/tsconfig.json",
"chars": 39,
"preview": "{\n \"extends\": \"../../tsconfig.json\"\n}\n"
},
{
"path": "examples/changesets/index.ts",
"chars": 2382,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport * as p from '@clack/prompts';\nimport color from 'picocolors';\n"
},
{
"path": "examples/changesets/package.json",
"chars": 285,
"preview": "{\n \"name\": \"@example/changesets\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"dependencies\": {\n "
},
{
"path": "examples/changesets/tsconfig.json",
"chars": 39,
"preview": "{\n \"extends\": \"../../tsconfig.json\"\n}\n"
},
{
"path": "knip.json",
"chars": 263,
"preview": "{\n \"workspaces\": {\n \".\": {\n \"ignore\": [\"build.preset.ts\"]\n },\n \"examples/*\": {\n \"entry\": \"*.ts!\",\n "
},
{
"path": "package.json",
"chars": 787,
"preview": "{\n \"name\": \"@clack/root\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"stub\": \"pnpm -r run build --stub\","
},
{
"path": "packages/core/CHANGELOG.md",
"chars": 9094,
"preview": "# @clack/core\n\n## 1.1.0\n\n### Minor Changes\n\n- e3333fb: Replaces `picocolors` with Node.js built-in `styleText`.\n\n## 1.0."
},
{
"path": "packages/core/LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) Nate Moore\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of t"
},
{
"path": "packages/core/README.md",
"chars": 509,
"preview": "# `@clack/core`\n\nClack contains low-level primitives for implementing your own command-line applications.\n\nCurrently, `T"
},
{
"path": "packages/core/build.config.ts",
"chars": 181,
"preview": "import { defineBuildConfig } from 'unbuild';\n\n// @see https://github.com/unjs/unbuild\nexport default defineBuildConfig({"
},
{
"path": "packages/core/package.json",
"chars": 1269,
"preview": "{\n \"name\": \"@clack/core\",\n \"version\": \"1.1.0\",\n \"type\": \"module\",\n \"main\": \"./dist/index.mjs\",\n \"module\": \"./dist/i"
},
{
"path": "packages/core/src/index.ts",
"chars": 1595,
"preview": "export type { AutocompleteOptions } from './prompts/autocomplete.js';\nexport { default as AutocompletePrompt } from './p"
},
{
"path": "packages/core/src/prompts/autocomplete.ts",
"chars": 7062,
"preview": "import type { Key } from 'node:readline';\nimport { styleText } from 'node:util';\nimport { findCursor } from '../utils/cu"
},
{
"path": "packages/core/src/prompts/confirm.ts",
"chars": 790,
"preview": "import { cursor } from 'sisteransi';\nimport Prompt, { type PromptOptions } from './prompt.js';\n\nexport interface Confirm"
},
{
"path": "packages/core/src/prompts/date.ts",
"chars": 12656,
"preview": "import type { Key } from 'node:readline';\nimport { settings } from '../utils/settings.js';\nimport Prompt, { type PromptO"
},
{
"path": "packages/core/src/prompts/group-multiselect.ts",
"chars": 2889,
"preview": "import Prompt, { type PromptOptions } from './prompt.js';\n\nexport interface GroupMultiSelectOptions<T extends { value: a"
},
{
"path": "packages/core/src/prompts/multi-select.ts",
"chars": 2235,
"preview": "import { findCursor } from '../utils/cursor.js';\nimport Prompt, { type PromptOptions } from './prompt.js';\n\ninterface Op"
},
{
"path": "packages/core/src/prompts/password.ts",
"chars": 1045,
"preview": "import { styleText } from 'node:util';\nimport Prompt, { type PromptOptions } from './prompt.js';\n\nexport interface Passw"
},
{
"path": "packages/core/src/prompts/prompt.ts",
"chars": 8932,
"preview": "import { stdin, stdout } from 'node:process';\nimport readline, { type Key, type ReadLine } from 'node:readline';\nimport "
},
{
"path": "packages/core/src/prompts/select-key.ts",
"chars": 1150,
"preview": "import Prompt, { type PromptOptions } from './prompt.js';\n\nexport interface SelectKeyOptions<T extends { value: string }"
},
{
"path": "packages/core/src/prompts/select.ts",
"chars": 1241,
"preview": "import { findCursor } from '../utils/cursor.js';\nimport Prompt, { type PromptOptions } from './prompt.js';\n\nexport inter"
},
{
"path": "packages/core/src/prompts/text.ts",
"chars": 1046,
"preview": "import { styleText } from 'node:util';\nimport Prompt, { type PromptOptions } from './prompt.js';\n\nexport interface TextO"
},
{
"path": "packages/core/src/types.ts",
"chars": 694,
"preview": "import type { Key } from 'node:readline';\nimport type { Action } from './utils/settings.js';\n\n/**\n * The state of the pr"
},
{
"path": "packages/core/src/utils/cursor.ts",
"chars": 565,
"preview": "export function findCursor<T extends { disabled?: boolean }>(\n\tcursor: number,\n\tdelta: number,\n\toptions: T[]\n) {\n\tconst "
},
{
"path": "packages/core/src/utils/index.ts",
"chars": 2945,
"preview": "import { stdin, stdout } from 'node:process';\nimport type { Key } from 'node:readline';\nimport * as readline from 'node:"
},
{
"path": "packages/core/src/utils/settings.ts",
"chars": 4891,
"preview": "const actions = ['up', 'down', 'left', 'right', 'space', 'enter', 'cancel'] as const;\nexport type Action = (typeof actio"
},
{
"path": "packages/core/src/utils/string.ts",
"chars": 417,
"preview": "export function diffLines(a: string, b: string) {\n\tif (a === b) return;\n\n\tconst aLines = a.split('\\n');\n\tconst bLines = "
},
{
"path": "packages/core/test/mock-readable.ts",
"chars": 402,
"preview": "import { Readable } from 'node:stream';\n\nexport class MockReadable extends Readable {\n\tprotected _buffer: unknown[] | nu"
},
{
"path": "packages/core/test/mock-writable.ts",
"chars": 293,
"preview": "import { Writable } from 'node:stream';\n\nexport class MockWritable extends Writable {\n\tpublic buffer: string[] = [];\n\n\t_"
},
{
"path": "packages/core/test/prompts/autocomplete.test.ts",
"chars": 6157,
"preview": "import { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport "
},
{
"path": "packages/core/test/prompts/confirm.test.ts",
"chars": 2185,
"preview": "import { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport "
},
{
"path": "packages/core/test/prompts/date.test.ts",
"chars": 13087,
"preview": "import { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport "
},
{
"path": "packages/core/test/prompts/multi-select.test.ts",
"chars": 5548,
"preview": "import { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport "
},
{
"path": "packages/core/test/prompts/password.test.ts",
"chars": 2654,
"preview": "import { styleText } from 'node:util';\nimport { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, ex"
},
{
"path": "packages/core/test/prompts/prompt.test.ts",
"chars": 7262,
"preview": "import { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport "
},
{
"path": "packages/core/test/prompts/select.test.ts",
"chars": 3660,
"preview": "import { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport "
},
{
"path": "packages/core/test/prompts/text.test.ts",
"chars": 3829,
"preview": "import { styleText } from 'node:util';\nimport { cursor } from 'sisteransi';\nimport { afterEach, beforeEach, describe, ex"
},
{
"path": "packages/core/test/utils.test.ts",
"chars": 2689,
"preview": "import type { Key } from 'node:readline';\nimport { cursor } from 'sisteransi';\nimport { afterEach, describe, expect, tes"
},
{
"path": "packages/core/tsconfig.json",
"chars": 69,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"include\": [\"src\", \"test\"]\n}\n"
},
{
"path": "packages/prompts/CHANGELOG.md",
"chars": 16649,
"preview": "# @clack/prompts\n\n## 1.1.0\n\n### Minor Changes\n\n- e3333fb: Replaces `picocolors` with Node.js built-in `styleText`.\n\n### "
},
{
"path": "packages/prompts/LICENSE",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) Nate Moore\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of t"
},
{
"path": "packages/prompts/README.md",
"chars": 6741,
"preview": "# `@clack/prompts`\n\nEffortlessly build beautiful command-line apps 🪄 [Try the demo](https://stackblitz.com/edit/clack-pr"
},
{
"path": "packages/prompts/__mocks__/fs.cjs",
"chars": 54,
"preview": "const { fs } = require('memfs');\nmodule.exports = fs;\n"
},
{
"path": "packages/prompts/build.config.ts",
"chars": 141,
"preview": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n\tpreset: '../../build.preset',\n\tentries"
},
{
"path": "packages/prompts/package.json",
"chars": 1449,
"preview": "{\n \"name\": \"@clack/prompts\",\n \"version\": \"1.1.0\",\n \"type\": \"module\",\n \"main\": \"./dist/index.mjs\",\n \"module\": \"./dis"
},
{
"path": "packages/prompts/src/autocomplete.ts",
"chars": 11783,
"preview": "import { styleText } from 'node:util';\nimport { AutocompletePrompt, settings } from '@clack/core';\nimport {\n\ttype Common"
},
{
"path": "packages/prompts/src/box.ts",
"chars": 4172,
"preview": "import type { Writable } from 'node:stream';\nimport { getColumns, settings } from '@clack/core';\nimport stringWidth from"
},
{
"path": "packages/prompts/src/common.ts",
"chars": 2525,
"preview": "import type { Readable, Writable } from 'node:stream';\nimport { styleText } from 'node:util';\nimport type { State } from"
},
{
"path": "packages/prompts/src/confirm.ts",
"chars": 2059,
"preview": "import { styleText } from 'node:util';\nimport { ConfirmPrompt, settings } from '@clack/core';\nimport {\n\ttype CommonOptio"
},
{
"path": "packages/prompts/src/date.ts",
"chars": 3971,
"preview": "import { styleText } from 'node:util';\nimport type { DateFormat, State } from '@clack/core';\nimport { DatePrompt, settin"
},
{
"path": "packages/prompts/src/group-multi-select.ts",
"chars": 6948,
"preview": "import { styleText } from 'node:util';\nimport { GroupMultiSelectPrompt } from '@clack/core';\nimport {\n\ttype CommonOption"
},
{
"path": "packages/prompts/src/group.ts",
"chars": 1428,
"preview": "import { isCancel } from '@clack/core';\n\ntype Prettify<T> = {\n\t[P in keyof T]: T[P];\n} & {};\n\nexport type PromptGroupAwa"
},
{
"path": "packages/prompts/src/index.ts",
"chars": 757,
"preview": "export { type ClackSettings, isCancel, settings, updateSettings } from '@clack/core';\n\nexport * from './autocomplete.js'"
},
{
"path": "packages/prompts/src/limit-options.ts",
"chars": 4163,
"preview": "import { styleText } from 'node:util';\nimport { getColumns, getRows } from '@clack/core';\nimport { wrapAnsi } from 'fast"
},
{
"path": "packages/prompts/src/log.ts",
"chars": 2215,
"preview": "import { styleText } from 'node:util';\nimport { settings } from '@clack/core';\nimport {\n\ttype CommonOptions,\n\tS_BAR,\n\tS_"
},
{
"path": "packages/prompts/src/messages.ts",
"chars": 1128,
"preview": "import type { Writable } from 'node:stream';\nimport { styleText } from 'node:util';\nimport { settings } from '@clack/cor"
},
{
"path": "packages/prompts/src/multi-select.ts",
"chars": 5978,
"preview": "import { styleText } from 'node:util';\nimport { MultiSelectPrompt, wrapTextWithPrefix } from '@clack/core';\nimport {\n\tty"
},
{
"path": "packages/prompts/src/note.ts",
"chars": 2356,
"preview": "import process from 'node:process';\nimport type { Writable } from 'node:stream';\nimport { styleText } from 'node:util';\n"
},
{
"path": "packages/prompts/src/password.ts",
"chars": 2104,
"preview": "import { styleText } from 'node:util';\nimport { PasswordPrompt, settings } from '@clack/core';\nimport { type CommonOptio"
},
{
"path": "packages/prompts/src/path.ts",
"chars": 2012,
"preview": "import { existsSync, lstatSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { autoco"
},
{
"path": "packages/prompts/src/progress-bar.ts",
"chars": 2070,
"preview": "import { styleText } from 'node:util';\nimport type { State } from '@clack/core';\nimport { unicodeOr } from './common.js'"
},
{
"path": "packages/prompts/src/select-key.ts",
"chars": 2781,
"preview": "import { styleText } from 'node:util';\nimport { SelectKeyPrompt, settings, wrapTextWithPrefix } from '@clack/core';\nimpo"
},
{
"path": "packages/prompts/src/select.ts",
"chars": 4872,
"preview": "import { styleText } from 'node:util';\nimport { SelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';\nimport "
},
{
"path": "packages/prompts/src/spinner.ts",
"chars": 6058,
"preview": "import { styleText } from 'node:util';\nimport { block, getColumns, settings } from '@clack/core';\nimport { wrapAnsi } fr"
},
{
"path": "packages/prompts/src/stream.ts",
"chars": 2212,
"preview": "import { stripVTControlCharacters as strip, styleText } from 'node:util';\nimport { S_BAR, S_ERROR, S_INFO, S_STEP_SUBMIT"
},
{
"path": "packages/prompts/src/task-log.ts",
"chars": 5885,
"preview": "import type { Writable } from 'node:stream';\nimport { styleText } from 'node:util';\nimport { getColumns } from '@clack/c"
},
{
"path": "packages/prompts/src/task.ts",
"chars": 684,
"preview": "import type { CommonOptions } from './common.js';\nimport { spinner } from './spinner.js';\n\nexport type Task = {\n\t/**\n\t *"
},
{
"path": "packages/prompts/src/text.ts",
"chars": 2337,
"preview": "import { styleText } from 'node:util';\nimport { settings, TextPrompt } from '@clack/core';\nimport { type CommonOptions, "
},
{
"path": "packages/prompts/test/__snapshots__/autocomplete.test.ts.snap",
"chars": 26162,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`autocomplete > can be aborted by a signal 1`] = "
},
{
"path": "packages/prompts/test/__snapshots__/box.test.ts.snap",
"chars": 10139,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`box (isCI = false) > cannot have width larger th"
},
{
"path": "packages/prompts/test/__snapshots__/confirm.test.ts.snap",
"chars": 9139,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`confirm (isCI = false) > can be aborted by a sig"
},
{
"path": "packages/prompts/test/__snapshots__/date.test.ts.snap",
"chars": 5261,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`date (isCI = false) > can cancel 1`] = `\n[\n \"<c"
},
{
"path": "packages/prompts/test/__snapshots__/group-multi-select.test.ts.snap",
"chars": 30659,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`groupMultiselect (isCI = false) > can be aborted"
},
{
"path": "packages/prompts/test/__snapshots__/log.test.ts.snap",
"chars": 4238,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`log (isCI = false) > error > renders error messa"
},
{
"path": "packages/prompts/test/__snapshots__/multi-select.test.ts.snap",
"chars": 37967,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`multiselect (isCI = false) > can be aborted by a"
},
{
"path": "packages/prompts/test/__snapshots__/note.test.ts.snap",
"chars": 20881,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`note (isCI = false) > don't overflow 1`] = `\n[\n "
},
{
"path": "packages/prompts/test/__snapshots__/password.test.ts.snap",
"chars": 10057,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`password (isCI = false) > can be aborted by a si"
},
{
"path": "packages/prompts/test/__snapshots__/path.test.ts.snap",
"chars": 19690,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`text (isCI = false) > can cancel 1`] = `\n[\n \"<c"
},
{
"path": "packages/prompts/test/__snapshots__/progress-bar.test.ts.snap",
"chars": 12706,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`prompts - progress (isCI = false) > message > se"
},
{
"path": "packages/prompts/test/__snapshots__/select-key.test.ts.snap",
"chars": 16330,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`text (isCI = false) > can cancel by pressing esc"
},
{
"path": "packages/prompts/test/__snapshots__/select.test.ts.snap",
"chars": 22097,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`select (isCI = false) > can be aborted by a sign"
},
{
"path": "packages/prompts/test/__snapshots__/spinner.test.ts.snap",
"chars": 18227,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`spinner (isCI = false) > can be aborted by a sig"
},
{
"path": "packages/prompts/test/__snapshots__/task-log.test.ts.snap",
"chars": 59031,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`taskLog (isCI = false) > error > clears output i"
},
{
"path": "packages/prompts/test/__snapshots__/text.test.ts.snap",
"chars": 11855,
"preview": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`text (isCI = false) > can be aborted by a signal"
},
{
"path": "packages/prompts/test/autocomplete.test.ts",
"chars": 13451,
"preview": "import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';\nimport { autocomplete, autocompleteMultisele"
},
{
"path": "packages/prompts/test/box.test.ts",
"chars": 5499,
"preview": "import { styleText } from 'node:util';\nimport { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, befor"
},
{
"path": "packages/prompts/test/confirm.test.ts",
"chars": 4263,
"preview": "import { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, tes"
},
{
"path": "packages/prompts/test/date.test.ts",
"chars": 4074,
"preview": "import { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, tes"
},
{
"path": "packages/prompts/test/group-multi-select.test.ts",
"chars": 9305,
"preview": "import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';\nimport * as prompts fro"
},
{
"path": "packages/prompts/test/limit-options.test.ts",
"chars": 8315,
"preview": "import { styleText } from 'node:util';\nimport { beforeEach, describe, expect, test } from 'vitest';\nimport { type LimitO"
},
{
"path": "packages/prompts/test/log.test.ts",
"chars": 3559,
"preview": "import { styleText } from 'node:util';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi }"
},
{
"path": "packages/prompts/test/multi-select.test.ts",
"chars": 9743,
"preview": "import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';\nimport * as prompts fro"
},
{
"path": "packages/prompts/test/note.test.ts",
"chars": 2751,
"preview": "import { styleText } from 'node:util';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi }"
},
{
"path": "packages/prompts/test/password.test.ts",
"chars": 4151,
"preview": "import { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, tes"
},
{
"path": "packages/prompts/test/path.test.ts",
"chars": 6503,
"preview": "import { vol } from 'memfs';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vit"
},
{
"path": "packages/prompts/test/progress-bar.test.ts",
"chars": 7754,
"preview": "import process from 'node:process';\nimport { EventEmitter } from 'node:stream';\nimport { afterAll, afterEach, beforeAll,"
},
{
"path": "packages/prompts/test/select-key.test.ts",
"chars": 5905,
"preview": "import { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, tes"
},
{
"path": "packages/prompts/test/select.test.ts",
"chars": 8442,
"preview": "import { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, tes"
},
{
"path": "packages/prompts/test/spinner.test.ts",
"chars": 10369,
"preview": "import { EventEmitter } from 'node:stream';\nimport { styleText } from 'node:util';\nimport { getColumns, updateSettings }"
},
{
"path": "packages/prompts/test/task-log.test.ts",
"chars": 9377,
"preview": "import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';\nimport * as prompts fro"
},
{
"path": "packages/prompts/test/test-utils.ts",
"chars": 729,
"preview": "import { Readable, Writable } from 'node:stream';\n\nexport class MockWritable extends Writable {\n\tpublic buffer: string[]"
},
{
"path": "packages/prompts/test/text.test.ts",
"chars": 5387,
"preview": "import { updateSettings } from '@clack/core';\nimport { afterAll, afterEach, beforeAll, beforeEach, describe, expect, tes"
},
{
"path": "packages/prompts/tsconfig.json",
"chars": 69,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"include\": [\"src\", \"test\"]\n}\n"
},
{
"path": "packages/prompts/vitest.config.ts",
"chars": 180,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n\ttest: {\n\t\tenv: {\n\t\t\tFORCE_COLOR: '1',\n\t\t},"
},
{
"path": "pnpm-workspace.yaml",
"chars": 44,
"preview": "packages:\n - 'examples/*'\n - 'packages/*'\n"
},
{
"path": "tsconfig.json",
"chars": 636,
"preview": "{\n \"compilerOptions\": {\n \"noEmit\": true,\n \"module\": \"node16\",\n \"target\": \"ESNext\",\n \"moduleResolution\": \"no"
}
]
About this extraction
This page contains the full source code of the bombshell-dev/clack GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 148 files (691.3 KB), approximately 261.6k tokens, and a symbol index with 237 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.