Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.34.4",
"@astrojs/starlight": "^0.37.1",
"@bomb.sh/args": "^0.3.1",
"@clack/core": "^0.4.2",
"@clack/prompts": "1.0.0-alpha.0",
"@types/node": "^22.13.11",
"astro": "^5.10.0",
"expressive-code-twoslash": "^0.4.0",
"@clack/core": "^1.0.0-alpha.7",
"@clack/prompts": "1.0.0-alpha.9",
"@types/node": "^22.19.3",
"astro": "^5.16.6",
"expressive-code-twoslash": "^0.5.3",
"sharp": "^0.33.5",
"starlight-sidebar-topics": "^0.6.0"
"starlight-sidebar-topics": "^0.6.2"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
2,294 changes: 1,257 additions & 1,037 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions src/content/docs/clack/basics/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Getting Started
description: Learn how to get started with Clack
---

import { Tabs, TabItem } from '@astrojs/starlight/components';
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';

Clack is a modern, flexible and powerful CLI library that helps you build beautiful command-line interfaces with ease. It provides a set of high-level components and low-level primitives that make it simple to create interactive command-line applications.

Expand All @@ -17,6 +17,7 @@ Clack is a modern, flexible and powerful CLI library that helps you build beauti
- 🎭 Form validation
- 🎯 Error handling
- 🎨 Consistent styling
- 📦 ESM-first distribution

## Installation

Expand Down Expand Up @@ -87,11 +88,19 @@ async function main() {
Clack provides several high-level components that make it easy to build interactive CLIs:

- `text()` - For text input with validation
- `password()` - For secure password input with masking
- `select()` - For selection menus
- `confirm()` - For yes/no confirmations
- `multiselect()` - For multiple selections
- `groupMultiselect()` - For grouped multiple selections
- `autocomplete()` - For searchable selection menus
- `path()` - For file/directory path selection with autocomplete
- `note()` - For displaying information
- `box()` - For boxed text display
- `spinner()` - For loading states
- `progress()` - For progress bar display
- `tasks()` - For sequential task execution
- `taskLog()` - For log output that clears on success

### Low-Level Primitives

Expand All @@ -102,7 +111,7 @@ import { TextPrompt, isCancel } from '@clack/core';

const p = new TextPrompt({
render() {
return `What's your name?\n${this.valueWithCursor}`;
return `What's your name?\n${this.value ?? ''}`;
},
});

Expand All @@ -129,7 +138,8 @@ import { text } from '@clack/prompts';
// TypeScript will ensure the validation function returns the correct type
const age = await text({
message: 'Enter your age:',
validate: (value: string) => {
validate: (value) => {
if (!value) return 'Please enter a value';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 0 || num > 120) return 'Age must be between 0 and 120';
Expand Down
12 changes: 8 additions & 4 deletions src/content/docs/clack/guides/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ Implement proper validation for user inputs:
import { text } from '@clack/prompts';

// Define validation function outside for reusability
function validateAge(value: string): string | undefined {
function validateAge(value: string | undefined): string | undefined {
if (!value) return 'Please enter a value';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 0 || num > 120) return 'Age must be between 0 and 120';
Expand Down Expand Up @@ -134,8 +135,8 @@ interface ProjectConfig {
}

// Define validation functions
function validateProjectName(value: string): string | undefined {
if (value.length === 0) return 'Name is required';
function validateProjectName(value: string | undefined): string | undefined {
if (!value || value.length === 0) return 'Name is required';
if (!/^[a-z0-9-]+$/.test(value)) return 'Name can only contain lowercase letters, numbers, and hyphens';
return undefined;
}
Expand Down Expand Up @@ -289,7 +290,8 @@ import { text } from '@clack/prompts';

const serverPort = await text({
message: 'Enter port number:',
validate: (value: string) => {
validate: (value) => {
if (!value) return 'Please enter a value';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
Expand Down Expand Up @@ -349,6 +351,7 @@ async function setupDatabase() {
message: 'Database port:',
defaultValue: dbType === 'postgres' ? '5432' : '3306',
validate: (value) => {
if (!value) return 'Please enter a value';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
Expand Down Expand Up @@ -381,6 +384,7 @@ async function promptForPort(defaultPort: string) {
message: 'Enter port number:',
defaultValue: defaultPort,
validate: (value) => {
if (!value) return 'Please enter a value';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
Expand Down
220 changes: 218 additions & 2 deletions src/content/docs/clack/guides/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async function setupProject() {
const name = await text({
message: 'Project name:',
validate: (value) => {
if (value.length === 0) return 'Name is required';
if (!value || value.length === 0) return 'Name is required';
if (!/^[a-z0-9-]+$/.test(value)) return 'Name can only contain lowercase letters, numbers, and hyphens';
return undefined;
},
Expand Down Expand Up @@ -248,6 +248,7 @@ async function setupConfig(): Promise<Config | null> {
message: 'Enter port number:',
placeholder: '3000',
validate: (value) => {
if (!value) return 'Please enter a value';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
Expand Down Expand Up @@ -570,7 +571,7 @@ async function collectUserData(): Promise<UserData | null> {
const name = await text({
message: 'Full name:',
validate: (value) => {
if (value.length < 2) return 'Name must be at least 2 characters';
if (!value || value.length < 2) return 'Name must be at least 2 characters';
if (!/^[a-zA-Z\s]*$/.test(value)) return 'Name can only contain letters and spaces';
return undefined;
},
Expand All @@ -585,6 +586,7 @@ async function collectUserData(): Promise<UserData | null> {
const email = await text({
message: 'Email address:',
validate: (value) => {
if (!value) return 'Email is required';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) return 'Please enter a valid email address';
return undefined;
Expand All @@ -600,6 +602,7 @@ async function collectUserData(): Promise<UserData | null> {
const ageInput = await text({
message: 'Age:',
validate: (value) => {
if (!value) return 'Age is required';
const num = parseInt(value);
if (isNaN(num)) return 'Please enter a valid number';
if (num < 18 || num > 100) return 'Age must be between 18 and 100';
Expand Down Expand Up @@ -715,4 +718,217 @@ async function collectUserData(): Promise<UserData | null> {
}
```

## New Features Examples

### Path Selection

The `path` prompt provides file system navigation with autocomplete:

```ts twoslash
import { path, isCancel } from '@clack/prompts';

async function selectConfigFile() {
const configPath = await path({
message: 'Select a configuration file:',
root: process.cwd(),
validate: (value) => {
if (!value?.endsWith('.json') && !value?.endsWith('.yaml')) {
return 'Please select a .json or .yaml file';
}
return undefined;
},
});

if (isCancel(configPath)) {
console.log('Operation cancelled');
process.exit(0);
}

console.log(`Selected config: ${configPath}`);
}
```

### Progress Bar

Display progress for long-running operations:

```ts twoslash
import { progress, intro, outro } from '@clack/prompts';

async function downloadFiles() {
intro('File Downloader');

const files = ['package.json', 'README.md', 'src/index.ts', 'tsconfig.json', 'LICENSE'];

const prog = progress({
style: 'heavy',
max: files.length,
size: 30,
});

prog.start('Downloading files');

for (let i = 0; i < files.length; i++) {
// Simulate download
await new Promise(resolve => setTimeout(resolve, 500));
prog.advance(1, `Downloaded ${files[i]}`);
}

prog.stop('All files downloaded');
outro('Download complete!');
}
```

### Spinner with Cancellation Handling

Handle graceful cancellation with the new spinner API:

```ts twoslash
import { spinner, confirm, isCancel } from '@clack/prompts';

async function longRunningTask() {
const shouldStart = await confirm({
message: 'Start the long-running task?',
});

if (isCancel(shouldStart) || !shouldStart) {
console.log('Task not started');
return;
}

const spin = spinner({
onCancel: () => {
console.log('\nCleaning up resources...');
},
cancelMessage: 'Task was cancelled by user',
errorMessage: 'Task failed unexpectedly',
});

spin.start('Processing data');

try {
// Simulate work in stages
await new Promise(resolve => setTimeout(resolve, 1000));
spin.message('Analyzing results');

await new Promise(resolve => setTimeout(resolve, 1000));
spin.message('Generating report');

await new Promise(resolve => setTimeout(resolve, 1000));

// Check if cancelled during processing
if (spin.isCancelled) {
return;
}

spin.stop('Task completed successfully');
} catch (error) {
spin.error('An error occurred during processing');
}
}
```

### Task Log with Groups

Organize complex operations into groups:

```ts twoslash
// @errors: 2345
import { taskLog, intro, outro } from '@clack/prompts';

async function buildProject() {
intro('Project Builder');

const log = taskLog({
title: 'Building project',
limit: 5, // Show last 5 lines per section
});

// TypeScript compilation group
const tsGroup = log.group('TypeScript Compilation');
tsGroup.message('Checking types...');
await new Promise(resolve => setTimeout(resolve, 500));
tsGroup.message('Compiling src/index.ts');
await new Promise(resolve => setTimeout(resolve, 300));
tsGroup.message('Compiling src/utils.ts');
await new Promise(resolve => setTimeout(resolve, 300));
tsGroup.success('TypeScript compiled successfully');

// Bundling group
const bundleGroup = log.group('Bundling');
bundleGroup.message('Reading entry points...');
await new Promise(resolve => setTimeout(resolve, 400));
bundleGroup.message('Resolving dependencies...');
await new Promise(resolve => setTimeout(resolve, 600));
bundleGroup.message('Creating bundle...');
await new Promise(resolve => setTimeout(resolve, 500));
bundleGroup.message('Minifying output...');
await new Promise(resolve => setTimeout(resolve, 400));
bundleGroup.success('Bundle created: dist/index.js (45kb)');

// Final success
log.success('Build completed in 2.5s');

outro('Ready to deploy!');
}
```

### Programmatic Cancellation with AbortController

Cancel prompts after a timeout or from external events:

```ts twoslash
import { text, select, isCancel } from '@clack/prompts';

async function timedPrompt() {
// Auto-cancel after 30 seconds
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);

try {
const name = await text({
message: 'Quick! Enter your name (30 second limit):',
signal: controller.signal,
});

clearTimeout(timeoutId);

if (isCancel(name)) {
console.log('Time ran out or cancelled');
return;
}

console.log(`Hello, ${name}!`);
} catch (error) {
console.log('Prompt was aborted');
}
}

// Racing prompts - first response wins
async function racePrompts() {
const controller = new AbortController();

const result = await Promise.race([
// Option 1: Manual selection
select({
message: 'Choose a framework:',
options: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
],
signal: controller.signal,
}),
// Option 2: Auto-detect (simulated)
new Promise<string>(resolve => {
setTimeout(() => {
resolve('detected-framework');
controller.abort();
}, 5000);
}),
]);

console.log(`Using: ${String(result)}`);
}
```

For more examples and best practices, check out our [GitHub repository](https://github.com/bombshell-dev/clack/tree/main/examples/basic).
Loading