clack-tree-select
TypeScript icon, indicating that this package has built-in type declarations

1.0.1 • Public • Published

🌳 clack-tree-select

npm version npm downloads license

💖 Sponsored by

Stringer CLI

"Make your React & Vue apps multilingual in minutes!"

Stringer CLI Logo
Learn more about Stringer CLI →

Beautiful, interactive tree selection prompts for command-line applications. Built on top of Clack for the best CLI experience.

🎬 Demo

Tree Select Demo

▶️ Watch Demo on Asciinema

See navigation, hierarchical selection, and smart keyboard shortcuts in action.

✨ Features

  • 🌳 Hierarchical Selection - Select parent directories to select all children
  • ⌨️ Smart Keyboard Shortcuts - Intuitive navigation with toggle expand/select all
  • 🎨 Customizable Styling - Beautiful icons and colors that match your brand
  • 🚀 Performance Optimized - Efficient rendering for large directory trees
  • 📱 TypeScript Ready - Full type safety with excellent IntelliSense
  • 🔧 File System Integration - Built-in support for browsing local directories
  • Validation Support - Custom validation with helpful error messages

📦 Installation

npm install clack-tree-select
yarn add clack-tree-select
pnpm add clack-tree-select

🚀 Quick Start

Add tree selection to your existing Clack CLI:

import { treeSelect } from 'clack-tree-select';
import { intro, outro, text, isCancel } from '@clack/prompts';

async function main() {
  intro('🚀 Project Setup');

  // Your existing Clack prompts
  const projectName = await text({
    message: 'Project name?',
    placeholder: 'my-project'
  });

  if (isCancel(projectName)) return;

  // Add tree selection seamlessly
  const files = await treeSelect({
    message: 'Select files to process:',
    tree: [
      {
        value: 'src',
        name: 'src',
        children: [
          { value: 'src/components', name: 'components' },
          { value: 'src/pages', name: 'pages' },
          { value: 'src/utils', name: 'utils' }
        ]
      },
      { value: 'package.json', name: 'package.json' }
    ]
  });

  if (isCancel(files)) return;

  outro(`✅ ${projectName}: Selected ${files.length} files`);
}

main();

Perfect integration - works alongside all your existing Clack prompts! 🎯

🌲 Beyond Files: Any Tree Data Structure

While clack-tree-select works great with file systems, it's designed as a generic tree selection tool. You can create hierarchical selection prompts for any data structure! Here are some powerful examples:

🎨 UI Component Library Selection

const components = await treeSelect({
  message: 'Select UI components to include:',
  tree: [
    {
      value: 'forms',
      name: 'Form Components',
      children: [
        { value: 'forms/input', name: 'Text Input' },
        { value: 'forms/select', name: 'Select Dropdown' },
        { value: 'forms/checkbox', name: 'Checkbox' },
        { value: 'forms/radio', name: 'Radio Button' }
      ]
    },
    {
      value: 'navigation',
      name: 'Navigation',
      children: [
        { value: 'nav/header', name: 'Header' },
        { value: 'nav/sidebar', name: 'Sidebar' },
        { value: 'nav/breadcrumb', name: 'Breadcrumb' },
        { value: 'nav/pagination', name: 'Pagination' }
      ]
    },
    {
      value: 'feedback',
      name: 'Feedback',
      children: [
        { value: 'feedback/alert', name: 'Alert' },
        { value: 'feedback/toast', name: 'Toast' },
        { value: 'feedback/modal', name: 'Modal' },
        { value: 'feedback/tooltip', name: 'Tooltip' }
      ]
    }
  ]
});

🏢 Organization/Department Structure

interface Department {
  id: string;
  name: string;
  manager?: string;
}

const departments = await treeSelect<Department>({
  message: 'Select departments for the training program:',
  tree: [
    {
      value: { id: 'eng', name: 'Engineering', manager: 'Alice' },
      name: 'Engineering',
      children: [
        { value: { id: 'frontend', name: 'Frontend' }, name: 'Frontend Team' },
        { value: { id: 'backend', name: 'Backend' }, name: 'Backend Team' },
        { value: { id: 'devops', name: 'DevOps' }, name: 'DevOps Team' }
      ]
    },
    {
      value: { id: 'product', name: 'Product', manager: 'Bob' },
      name: 'Product',
      children: [
        { value: { id: 'design', name: 'Design' }, name: 'Design Team' },
        { value: { id: 'pm', name: 'Product Management' }, name: 'Product Managers' },
        { value: { id: 'research', name: 'User Research' }, name: 'Research Team' }
      ]
    }
  ]
});

🛍️ E-commerce Category Selection

const categories = await treeSelect({
  message: 'Select product categories to feature:',
  tree: [
    {
      value: 'electronics',
      name: 'Electronics',
      children: [
        {
          value: 'computers',
          name: 'Computers',
          children: [
            { value: 'laptops', name: 'Laptops' },
            { value: 'desktops', name: 'Desktops' },
            { value: 'tablets', name: 'Tablets' }
          ]
        },
        {
          value: 'phones',
          name: 'Mobile Phones',
          children: [
            { value: 'smartphones', name: 'Smartphones' },
            { value: 'accessories', name: 'Phone Accessories' }
          ]
        }
      ]
    },
    {
      value: 'clothing',
      name: 'Clothing',
      children: [
        { value: 'mens', name: "Men's Clothing" },
        { value: 'womens', name: "Women's Clothing" },
        { value: 'kids', name: "Kids' Clothing" }
      ]
    }
  ]
});

📋 Feature/Permission Selection

interface Permission {
  id: string;
  scope: string;
  level: 'read' | 'write' | 'admin';
}

const permissions = await treeSelect<Permission>({
  message: 'Select permissions for this role:',
  tree: [
    {
      value: { id: 'users', scope: 'users', level: 'admin' },
      name: 'User Management',
      children: [
        { 
          value: { id: 'users-read', scope: 'users', level: 'read' }, 
          name: 'View Users' 
        },
        { 
          value: { id: 'users-write', scope: 'users', level: 'write' }, 
          name: 'Edit Users' 
        },
        { 
          value: { id: 'users-admin', scope: 'users', level: 'admin' }, 
          name: 'Manage Users' 
        }
      ]
    },
    {
      value: { id: 'content', scope: 'content', level: 'admin' },
      name: 'Content Management',
      children: [
        { 
          value: { id: 'content-read', scope: 'content', level: 'read' }, 
          name: 'View Content' 
        },
        { 
          value: { id: 'content-write', scope: 'content', level: 'write' }, 
          name: 'Edit Content' 
        }
      ]
    }
  ]
});

🎯 Key Benefits for Non-File Data

  • Type Safety: Full TypeScript support with custom data types
  • Flexible Values: Use strings, objects, numbers, or any data type as values
  • Custom Display: name property controls what users see, value is what you get back
  • Rich Hierarchies: Create deep, meaningful organizational structures
  • Batch Selection: Select entire categories and all children automatically

The generic TreeItem<T> interface makes it perfect for any hierarchical data structure in your applications!

📚 API Reference

treeSelect(options)

Creates an interactive tree selection prompt.

Options

Option Type Default Description
message string required The prompt message to display
tree TreeItem[] required The tree structure to display
multiple boolean true Allow multiple selections
initialValues T[] [] Pre-selected values
required boolean false Require at least one selection
maxItems number undefined Maximum visible items (scrolling)
icons IconOptions default icons Custom icons for tree items
showHelp boolean true Show keyboard shortcuts in validation

TreeItem Interface

interface TreeItem<T = any> {
  value: T;              // Unique identifier
  name?: string;         // Display name (falls back to value)
  open?: boolean;        // Initially expanded
  children?: TreeItem<T>[] | string[] | T[];  // Child items
}

Icon Options

interface IconOptions {
  directory?: string;    // Default: '📁'
  file?: string;         // Default: '📄'  
  expanded?: string;     // Default: '▼'
  collapsed?: string;    // Default: '▶'
}

fileSystemTreeSelect(options)

Creates a tree selection prompt from a file system directory.

const files = await fileSystemTreeSelect({
  message: 'Select files from your project:',
  root: './src',
  includeFiles: true,
  includeHidden: false,
  maxDepth: 3,
  filter: (path) => !path.includes('node_modules')
});

⌨️ Keyboard Shortcuts

Shortcut Action
/ Navigate up/down
/ Collapse/expand directory
Space Toggle selection
Ctrl+E Toggle expand/collapse all
Ctrl+A Toggle select/deselect all
Enter Submit selection
Ctrl+C Cancel prompt

🎯 Advanced Examples

Custom Icons and Styling

const result = await treeSelect({
  message: 'Choose components to generate:',
  tree: components,
  icons: {
    directory: '📂',
    file: '⚛️',
    expanded: '📂', 
    collapsed: '📁'
  },
  multiple: true,
  required: true
});

Single Selection Mode

const configFile = await treeSelect({
  message: 'Choose a configuration file:',
  tree: [
    {
      value: 'config',
      name: 'config',
      open: true,
      children: [
        { value: 'tsconfig.json', name: 'TypeScript Config' },
        { value: 'package.json', name: 'Package Config' },
        { value: 'vite.config.js', name: 'Vite Config' }
      ]
    }
  ],
  multiple: false,
  required: true
});

Note: In single selection mode, only leaf nodes (files without children) can be selected. Parent directories are shown but not selectable, ensuring users select actual items rather than containers.

File System Integration

import { fileSystemTreeSelect } from 'clack-tree-select';

const selectedFiles = await fileSystemTreeSelect({
  message: 'Select files to process:',
  root: process.cwd(),
  includeFiles: true,
  includeHidden: false,
  maxDepth: 3,
  filter: (path) => {
    // Exclude common non-source directories
    return !path.includes('node_modules') && 
           !path.includes('.git') && 
           !path.includes('dist');
  }
});

Custom Validation

const result = await treeSelect({
  message: 'Select at least 2 components:',
  tree: componentTree,
  validate: (selected) => {
    if (!selected || selected.length < 2) {
      return 'Please select at least 2 components to continue.';
    }
  }
});

🔧 Integration Examples

With Package Managers

// Ask user to select packages to install
const packages = await treeSelect({
  message: 'Select packages to install:',
  tree: [
    {
      value: 'react',
      name: 'React',
      children: [
        { value: 'react-dom', name: 'React DOM' },
        { value: 'react-router', name: 'React Router' }
      ]
    },
    {
      value: 'build-tools',
      name: 'Build Tools',
      children: [
        { value: 'vite', name: 'Vite' },
        { value: 'typescript', name: 'TypeScript' }
      ]
    }
  ]
});

With Monorepos

// Select packages in a monorepo
const workspaces = await fileSystemTreeSelect({
  message: 'Select workspaces to build:',
  root: './packages',
  includeFiles: false,  // Only directories
  maxDepth: 1,
  filter: (path) => {
    // Only include directories with package.json
    return fs.existsSync(`${path}/package.json`);
  }
});

🎨 Theming

The tree select automatically adapts to your terminal's color scheme and follows Clack's beautiful styling conventions:

  • Selected items: Green checkboxes (◼)
  • Unselected items: Gray checkboxes (◻)
  • Active item: Cyan highlighting
  • Directories: Folder icons with expand/collapse indicators
  • Files: Document icons

🔗 Integration with Existing Clack CLIs

This package is designed as a drop-in addition to your existing Clack CLI. No changes needed to your current setup!

// Your existing CLI
import { intro, text, select, multiselect, outro } from '@clack/prompts';
// Just add this import ↓
import { treeSelect } from 'clack-tree-select';

async function myCLI() {
  intro('My Existing CLI');
  
  const name = await text({ message: 'Project name?' });
  const framework = await select({ /* ... */ });
  
  // New: Add tree selection anywhere in your flow
  const files = await treeSelect({
    message: 'Select files to process:',
    tree: myFileTree
  });
  
  const tools = await multiselect({ /* ... */ });
  outro('Done!');
}

Zero breaking changes - just add treeSelect where you need hierarchical selection!

🧪 Examples

Check out the examples/ directory for interactive demos:

cd examples
pnpm install

# Basic tree-select functionality
pnpm tree-select

# Complete integration with other Clack prompts
pnpm integration

The integration demo shows a real CLI workflow mixing treeSelect with text, select, multiselect, and confirm prompts.

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📝 License

This project is licensed under the MIT License - see the LICENSE file for details.


Made with ❤️ for the CLI community

GitHubNPMIssues

Package Sidebar

Install

npm i clack-tree-select

Weekly Downloads

84

Version

1.0.1

License

MIT

Unpacked Size

173 kB

Total Files

12

Last publish

Collaborators

  • titusdecali