npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ls-stack/cli

v0.2.0

Published

CLI input utilities for interactive prompts

Readme

@ls-stack/cli

A TypeScript library for building interactive command-line interfaces with type-safe prompts and ESC-to-cancel support.

Installation

pnpm add @ls-stack/cli
# or
yarn add @ls-stack/cli

Requirements: Node.js >= 21.5.0

Quick Start

import { cliInput } from '@ls-stack/cli';

const name = await cliInput.text('What is your name?');
const proceed = await cliInput.confirm('Continue?', { initial: true });

console.log(`Hello, ${name}!`);

API Reference

Types

type ValidateFn = (
  value: string,
) => boolean | string | Promise<boolean | string>;

type SelectOption<T extends string> = {
  value: T;
  label?: string;
  hint?: string;
};

cliInput.select()

Single selection from a list of options.

const choice = await cliInput.select<'dev' | 'staging' | 'prod'>(
  'Select environment',
  {
    options: [
      { value: 'dev', label: 'Development', hint: 'Local development server' },
      { value: 'staging', label: 'Staging', hint: 'Pre-production testing' },
      { value: 'prod', label: 'Production', hint: 'Live environment' },
    ],
  },
);
// choice: 'dev' | 'staging' | 'prod'

Parameters:

| Name | Type | Description | | ----------------- | ------------------- | --------------------------- | | title | string | The prompt message | | options.options | SelectOption<T>[] | Array of selectable options |

Returns: Promise<T> - The selected option's value

cliInput.multipleSelect()

Multi-select with checkboxes. Requires at least one selection.

const features = await cliInput.multipleSelect<'ts' | 'eslint' | 'prettier'>(
  'Select features to enable',
  {
    options: [
      { value: 'ts', label: 'TypeScript' },
      { value: 'eslint', label: 'ESLint', hint: 'Code linting' },
      { value: 'prettier', label: 'Prettier', hint: 'Code formatting' },
    ],
  },
);
// features: ('ts' | 'eslint' | 'prettier')[]

Parameters:

| Name | Type | Description | | ----------------- | ------------------- | --------------------------- | | title | string | The prompt message | | options.options | SelectOption<T>[] | Array of selectable options |

Returns: Promise<T[]> - Array of selected values

cliInput.text()

Text input with optional validation.

const projectName = await cliInput.text('Enter project name', {
  initial: 'my-project',
  validate: (value) => {
    if (!/^[a-z0-9-]+$/.test(value)) {
      return 'Only lowercase letters, numbers, and hyphens allowed';
    }
    return true;
  },
});

Parameters:

| Name | Type | Description | | ------------------ | ------------- | ------------------- | | title | string | The prompt message | | options.initial | string? | Default value | | options.validate | ValidateFn? | Validation function |

Returns: Promise<string> - The entered text

cliInput.textWithAutocomplete()

Text input with autocomplete suggestions. Searches across value, label, and hint.

const framework = await cliInput.textWithAutocomplete<
  'react' | 'vue' | 'svelte'
>('Select a framework', {
  options: [
    {
      value: 'react',
      label: 'React',
      hint: 'A JavaScript library for building UIs',
    },
    {
      value: 'vue',
      label: 'Vue',
      hint: 'The progressive JavaScript framework',
    },
    {
      value: 'svelte',
      label: 'Svelte',
      hint: 'Cybernetically enhanced web apps',
    },
  ],
  validate: (value) => value.length > 0 || 'Please select a framework',
});

Parameters:

| Name | Type | Description | | ------------------ | ------------------- | --------------------------------- | | title | string | The prompt message | | options.options | SelectOption<T>[] | Array of autocomplete suggestions | | options.validate | ValidateFn? | Validation function |

Returns: Promise<T> - The selected or entered value

cliInput.confirm()

Yes/No boolean prompt.

const shouldDeploy = await cliInput.confirm('Deploy to production?', {
  initial: false,
});
// shouldDeploy: boolean

Parameters:

| Name | Type | Description | | ----------------- | ---------- | ---------------------------------------------- | | title | string | The prompt message | | options.initial | boolean? | Default value (true for yes, false for no) |

Returns: Promise<boolean> - The user's choice

cliInput.number()

Numeric input.

const port = await cliInput.number('Enter port number', {
  initial: 3000,
});
// port: number | null

Parameters:

| Name | Type | Description | | ----------------- | --------- | ------------------ | | title | string | The prompt message | | options.initial | number? | Default value |

Returns: Promise<number | null> - The entered number, or null on error


CLI Framework

Build complete CLI applications with typed commands, automatic help generation, and interactive mode.

Quick Start

import { createCLI, createCmd } from '@ls-stack/cli';

await createCLI(
  { name: 'My CLI', baseCmd: 'my-cli' },
  {
    hello: createCmd({
      short: 'hi',
      description: 'Say hello',
      run: async () => {
        console.log('Hello, World!');
      },
    }),
  },
);

Argument Types

type Arg =
  | { type: 'positional-string'; name: string; description: string; default?: string }
  | { type: 'positional-number'; name: string; description: string; default?: number }
  | { type: 'flag'; name: string; description: string }
  | { type: 'value-string-flag'; name: string; description: string; default?: string }
  | { type: 'value-number-flag'; name: string; description: string; default?: number };

| Type | CLI Usage | TypeScript Type | | -------------------- | ---------------------- | ------------------------------------------------- | | positional-string | my-cli cmd value | string (or string \| undefined if no default) | | positional-number | my-cli cmd 42 | number (or number \| undefined if no default) | | flag | my-cli cmd --verbose | boolean (defaults to false) | | value-string-flag | my-cli cmd --env dev | string \| undefined (or string if default) | | value-number-flag | my-cli cmd --port 80 | number \| undefined (or number if default) |

createCmd()

Creates a type-safe command definition.

const deploy = createCmd({
  short: 'd',
  description: 'Deploy the application',
  args: {
    env: {
      type: 'positional-string',
      name: 'env',
      description: 'Target environment',
    },
    port: {
      type: 'value-number-flag',
      name: 'port',
      description: 'Port number',
      default: 3000,
    },
    verbose: {
      type: 'flag',
      name: 'verbose',
      description: 'Enable verbose logging',
    },
  },
  examples: [
    { args: ['production'], description: 'Deploy to production' },
    { args: ['staging', '--port', '8080'], description: 'Deploy to staging on port 8080' },
  ],
  run: async ({ env, port, verbose }) => {
    // Types are inferred: env: string, port: number, verbose: boolean
    console.log(`Deploying to ${env} on port ${port}`);
    if (verbose) console.log('Verbose mode enabled');
  },
});

Parameters:

| Name | Type | Description | | ------------- | ----------------------------- | ---------------------------------------------- | | description | string | Command description shown in help | | short | string? | Single-character alias (cannot be 'i' or 'h') | | args | Record<string, Arg>? | Typed argument definitions | | run | (args) => void \| Promise | Handler function receiving parsed arguments | | examples | { args, description }[]? | Usage examples for help text |

createCLI()

Creates and runs a CLI application.

await createCLI(
  {
    name: 'My CLI',
    baseCmd: 'my-cli',
    sort: ['deploy', 'build', 'test'], // Optional: custom command order
  },
  {
    deploy: deployCmd,
    build: buildCmd,
    test: testCmd,
  },
);

Parameters:

| Name | Type | Description | | -------------- | ----------------- | ------------------------------------- | | name | string | CLI display name shown in header | | baseCmd | string | Command prefix for help text | | sort | string[]? | Custom command display order | | cmds | Record<C, Cmd> | Commands created with createCmd |

Built-in Commands

| Command | Description | | ------------------ | ---------------------------------------- | | h, --help | Show help with all commands | | i | Interactive mode (select from list) | | <command> -h | Show help for a specific command |

CLI Usage Examples

my-cli                    # Show interactive menu
my-cli h                  # Show help
my-cli --help             # Show help
my-cli i                  # Interactive mode
my-cli deploy prod        # Run deploy with positional arg
my-cli d prod             # Run deploy via short alias
my-cli deploy -h          # Show deploy command help
my-cli deploy prod --port 8080 --verbose

Complete Example

import { createCLI, createCmd } from '@ls-stack/cli';

await createCLI(
  { name: 'Project CLI', baseCmd: 'project' },
  {
    create: createCmd({
      short: 'c',
      description: 'Create a new project',
      args: {
        name: {
          type: 'positional-string',
          name: 'name',
          description: 'Project name',
        },
        template: {
          type: 'value-string-flag',
          name: 'template',
          description: 'Project template',
          default: 'basic',
        },
      },
      examples: [
        { args: ['my-app'], description: 'Create with default template' },
        { args: ['my-app', '--template', 'react'], description: 'Create React project' },
      ],
      run: async ({ name, template }) => {
        console.log(`Creating ${name} with template: ${template}`);
      },
    }),

    build: createCmd({
      short: 'b',
      description: 'Build the project',
      args: {
        watch: {
          type: 'flag',
          name: 'watch',
          description: 'Watch for changes',
        },
      },
      run: async ({ watch }) => {
        console.log(watch ? 'Building in watch mode...' : 'Building...');
      },
    }),

    serve: createCmd({
      short: 's',
      description: 'Start development server',
      args: {
        port: {
          type: 'value-number-flag',
          name: 'port',
          description: 'Port number',
          default: 3000,
        },
      },
      run: async ({ port }) => {
        console.log(`Server running on http://localhost:${port}`);
      },
    }),
  },
);

Features

ESC-to-Cancel

All prompts support pressing ESC to cancel. When cancelled, the process exits cleanly with code 0.

Type Safety

All prompts are fully typed. When using generic type parameters with select, multipleSelect, or textWithAutocomplete, the return type is narrowed to the union of option values.

// Return type is automatically 'small' | 'medium' | 'large'
const size = await cliInput.select<'small' | 'medium' | 'large'>(
  'Select size',
  {
    options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
  },
);

Validation

Text inputs support synchronous or asynchronous validation:

const email = await cliInput.text('Enter email', {
  validate: async (value) => {
    if (!value.includes('@')) {
      return 'Invalid email format';
    }
    const exists = await checkEmailExists(value);
    if (exists) {
      return 'Email already registered';
    }
    return true;
  },
});

Examples

Interactive Setup Wizard

import { cliInput } from '@ls-stack/cli';

async function setupWizard() {
  const projectName = await cliInput.text('Project name', {
    validate: (v) => v.length >= 3 || 'Name must be at least 3 characters',
  });

  const template = await cliInput.select('Select template', {
    options: [
      { value: 'blank', label: 'Blank', hint: 'Empty project' },
      { value: 'react', label: 'React', hint: 'React with Vite' },
      { value: 'next', label: 'Next.js', hint: 'Full-stack React' },
    ],
  });

  const features = await cliInput.multipleSelect('Enable features', {
    options: [
      { value: 'typescript', label: 'TypeScript' },
      { value: 'eslint', label: 'ESLint' },
      { value: 'prettier', label: 'Prettier' },
      { value: 'testing', label: 'Testing (Vitest)' },
    ],
  });

  const installDeps = await cliInput.confirm('Install dependencies?', {
    initial: true,
  });

  return { projectName, template, features, installDeps };
}

Configuration Menu

import { cliInput } from '@ls-stack/cli';

async function configMenu() {
  const action = await cliInput.select('What would you like to configure?', {
    options: [
      { value: 'port', label: 'Server Port' },
      { value: 'host', label: 'Host Address' },
      { value: 'timeout', label: 'Request Timeout' },
    ],
  });

  switch (action) {
    case 'port': {
      const port = await cliInput.number('Enter port', { initial: 3000 });
      console.log(`Port set to ${port}`);
      break;
    }
    case 'host': {
      const host = await cliInput.text('Enter host', { initial: 'localhost' });
      console.log(`Host set to ${host}`);
      break;
    }
    case 'timeout': {
      const timeout = await cliInput.number('Timeout (seconds)', {
        initial: 30,
      });
      console.log(`Timeout set to ${timeout}s`);
      break;
    }
  }
}

Error Handling

All prompts handle errors gracefully:

  • User cancellation (ESC/Ctrl+C): Process exits with code 0
  • Other errors: Error is logged and process exits with code 1

For the number() prompt specifically, non-cancellation errors return null instead of exiting.

License

MIT