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

effect-cli-tui

v2.2.0

Published

Effect-native CLI wrapper with interactive prompts and Ink components for terminal UIs

Downloads

1,047

Readme

effect-cli-tui

Effect-native CLI wrapper with interactive prompts and display utilities for building powerful terminal user interfaces.

npm version License: MIT

Features

  • 🎯 Effect-Native — Built on Effect-TS for type-safe, composable effects
  • 🖥️ Interactive Prompts — Powered by @inquirer/prompts with full customization
  • 📢 Display API — Simple, powerful console output utilities
  • ⚙️ CLI Wrapper — Run commands via Effect with error handling
  • 🔄 Composable — Chain operations seamlessly with Effect's yield* syntax
  • 📦 ESM-Native — Modern JavaScript modules for tree-shaking and optimal bundling
  • Fully Tested — Comprehensive test suite with integration tests
  • 📝 Well-Documented — Clear API docs and practical examples

Installation

npm install effect-cli-tui
# or
pnpm add effect-cli-tui
# or
yarn add effect-cli-tui

Quick Start

import { Effect } from "effect";
import {
  display,
  displaySuccess,
  TUIHandler,
  runWithTUI,
} from "effect-cli-tui";

const program = Effect.gen(function* () {
  // Display utilities
  yield* display("Welcome to my CLI app!");
  yield* displaySuccess("Initialization complete");

  const tui = yield* TUIHandler;

  // Interactive prompt
  const name = yield* tui.prompt("What is your name?");

  // Display results
  yield* displaySuccess(`Hello, ${name}!`);
});

runWithTUI(program);

Core Concepts

Display API

Simple, powerful console output utilities for CLI applications.

Functions:

  • display(message, options?) - Display single-line messages with styling
  • displayLines(lines, options?) - Display multiple lines with consistent formatting
  • displayJson(data, options?) - Pretty-print JSON data
  • displaySuccess(message) - Convenience for success messages
  • displayError(message) - Convenience for error messages

Example:

import { display, displayLines, displayJson } from "effect-cli-tui";

// Simple messages
yield * display("Processing files...");
yield * displaySuccess("All files processed!");

// Multi-line output
yield *
  displayLines([
    "Configuration Summary",
    "─────────────────────",
    "Mode: Production",
    "Files: 42 processed",
  ]);

// JSON output
yield * displayJson({ status: "ok", count: 42 });

TUIHandler

Interactive terminal prompts with Effect integration.

Methods:

  • prompt(message, options?) - Text input
  • selectOption(message, options) - Single selection
  • multiSelect(message, options) - Multiple selection
  • confirm(message) - Yes/No confirmation
  • display(message, type) - Display styled messages

EffectCLI

Execute CLI commands with Effect error handling.

Methods:

  • run(command, args?, options?) - Execute and capture output
  • stream(command, args?, options?) - Stream output directly

Modular API

While the core APIs are available from the main effect-cli-tui entry point, more advanced features and services are exposed via secondary entry points for a cleaner API surface:

  • effect-cli-tui/components: React components for interactive prompts (Confirm, Input, Select, etc.).
  • effect-cli-tui/theme: Theming services and presets for customizing the look and feel.
  • effect-cli-tui/services: Low-level services and runtimes (EffectCLIRuntime, Terminal, InkService).
  • effect-cli-tui/constants: A collection of useful constants for icons, symbols, and ANSI codes.

Examples

Interactive Project Setup

import { Effect } from "effect";
import { TUIHandler } from "effect-cli-tui";
import { TUIHandlerRuntime } from "effect-cli-tui/services";

const setupProject = Effect.gen(function* () {
  const tui = yield* TUIHandler;

  // Gather project info
  const name = yield* tui.prompt("Project name:");
  const description = yield* tui.prompt("Description:", {
    default: "My project",
  });

  // Choose template
  const template = yield* tui.selectOption("Choose template:", [
    "Basic",
    "CLI",
  ]);

  // Multi-select features
  const features = yield* tui.multiSelect("Select features:", [
    "Testing",
    "Linting",
    "Type Checking",
  ]);

  // Confirm
  const shouldCreate = yield* tui.confirm(`Create ${name}? (${template})`);

  if (shouldCreate) {
    yield* tui.display("Creating project...", "info");
    // ... project creation logic ...
    yield* tui.display("Project created!", "success");
  } else {
    yield* tui.display("Cancelled", "error");
  }
});

await TUIHandlerRuntime.runPromise(setupProject);
await TUIHandlerRuntime.dispose();

CLI Command Execution

import { Effect } from "effect";
import { EffectCLI } from "effect-cli-tui";
import { EffectCLIOnlyRuntime } from "effect-cli-tui/services";

const buildProject = Effect.gen(function* () {
  const cli = yield* EffectCLI;

  console.log("Building project...");
  const result = yield* cli.run("build", [], { timeout: 30_000 });

  console.log("Build output:");
  console.log(result.stdout);

  if (result.stderr) {
    console.error("Build warnings:");
    console.error(result.stderr);
  }
});

await EffectCLIOnlyRuntime.runPromise(buildProject);
await EffectCLIOnlyRuntime.dispose();

Workflow Composition

import { Effect } from "effect";
import { EffectCLI, TUIHandler } from "effect-cli-tui";
import { EffectCLIRuntime } from "effect-cli-tui/services";

const completeWorkflow = Effect.gen(function* () {
  const tui = yield* TUIHandler;
  const cli = yield* EffectCLI;

  // Step 1: Gather input
  yield* tui.display("Step 1: Gathering input...", "info");
  const values: string[] = [];
  for (let index = 0; index < 3; index += 1) {
    const value = yield* tui.prompt(`Enter value ${index + 1}:`);
    values.push(value);
  }

  // Step 2: Process
  yield* tui.display("Step 2: Processing...", "info");
  // Process values...

  // Step 3: Report
  yield* tui.display("Complete!", "success");
  console.log("Processed values:", values);
});

await EffectCLIRuntime.runPromise(completeWorkflow);
await EffectCLIRuntime.dispose();

Slash Commands with TUIHandler

Slash commands allow users to type /command at any interactive prompt to trigger meta-commands without leaving the prompting flow. Supported in prompt(), password(), selectOption(), and multiSelect().

import { Effect } from "effect";
import {
  DEFAULT_SLASH_COMMANDS,
  EffectCLI,
  TUIHandler,
  createEffectCliSlashCommand,
  configureDefaultSlashCommands,
} from "effect-cli-tui";
import { EffectCLIRuntime } from "effect-cli-tui/services";

// Configure global "/" commands before running your workflow
configureDefaultSlashCommands([
  ...DEFAULT_SLASH_COMMANDS,
  createEffectCliSlashCommand({
    name: "deploy",
    description: "Run project deploy command",
    effect: () =>
      Effect.gen(function* () {
        const cli = yield* EffectCLI;
        const result = yield* cli.run("echo", ["Deploying..."]);
        console.log(result.stdout.trim());
        return { kind: "continue" } as const;
      }),
  }),
]);

const slashWorkflow = Effect.gen(function* () {
  const tui = yield* TUIHandler;

  console.log(
    "\nType /help for commands, /deploy to run deploy, or /quit to exit.\n"
  );

  // Slash commands work in prompt()
  const name = yield* tui.prompt("Project name:");

  // Slash commands work in selectOption() - include /help or /quit as choices
  const template = yield* tui.selectOption("Choose template:", [
    "Basic",
    "CLI",
    "/help", // User can select this to see help
    "/quit", // User can select this to exit
  ]);

  // Slash commands work in multiSelect() - if any selection starts with "/"
  const features = yield* tui.multiSelect("Choose features:", [
    "Testing",
    "Linting",
    "TypeScript",
    "/help", // If selected, will trigger help command
  ]);

  // Slash commands work in password() too
  const password = yield* tui.password("Enter password:");

  const confirmed = yield* tui.confirm(`Create project '${name}'?`);

  if (confirmed) {
    yield* tui.display("Project created!", "success");
  } else {
    yield* tui.display("Cancelled", "error");
  }
});

await EffectCLIRuntime.runPromise(slashWorkflow);
await EffectCLIRuntime.dispose();

Built-in Commands:

  • /help - Show available slash commands
  • /quit or /exit - Exit the current interactive session
  • /clear or /cls - Clear the terminal screen
  • /history or /h - Show session command history
  • /save - Save session history to a JSON file
  • /load - Load and display a previous session from file

Advanced Features:

  • Argument Parsing: Slash commands support positional args and flags:
    • Example: /deploy production --force --tag=latest --count=3
    • Positional args: production
    • Boolean flags: --force
    • Key/value flags: --tag=latest, --count=3
    • Short flags supported and merged: -f--force, -t latest--tag=latest, -c 3--count=3. Clusters like -fv set both boolean flags.
  • Auto-Completion: While typing a slash command in prompt() an inline suggestions list appears. Press Tab to auto-complete the first suggestion.
  • History Navigation: Press / to cycle through previously executed slash commands (only consecutive unique commands stored).
  • Password Safety: Password inputs are automatically masked (********) in /history output.

Slash Command Context Fields:

Each custom command receives an extended context:

interface SlashCommandContext {
  promptMessage: string;
  promptKind: "input" | "password" | "select" | "multiSelect";
  rawInput: string; // Full text e.g. /deploy prod --force
  command: string; // Parsed command name e.g. 'deploy'
  args: string[]; // Positional arguments ['prod']
  flags: Record<string, string | boolean>; // Parsed flags { force: true }
  tokens: string[]; // Tokens after command ['prod','--force']
  registry: SlashCommandRegistry;
}

You can use these to implement richer behaviors in custom commands.

Slash Command Behavior:

  • continue - Command executes, then the prompt re-appears (e.g., /help)
  • abortPrompt - Cancel the current prompt and return an error
  • exitSession - Exit the entire interactive session (e.g., /quit)

Error Handling

All effects can fail with typed errors:

import * as Effect from "effect/Effect";
import { TUIHandler } from "effect-cli-tui";
import { TUIHandlerRuntime } from "effect-cli-tui/services";

const safePrompt = Effect.gen(function* () {
  const tui = yield* TUIHandler;

  const result = yield* tui.prompt("Enter something:").pipe(
    Effect.catchTag("TUIError", (err) => {
      if (err.reason === "Cancelled") {
        console.log("User cancelled the operation");
        return Effect.succeed("default value");
      }
      console.error(`UI Error: ${err.message}`);
      return Effect.succeed("default value");
    })
  );

  return result;
});

await TUIHandlerRuntime.runPromise(safePrompt);
await TUIHandlerRuntime.dispose();

Cancellation Handling

All interactive prompts support cancellation via Ctrl+C (SIGINT). When a user presses Ctrl+C during a prompt, the operation will fail with a TUIError with reason 'Cancelled':

const program = Effect.gen(function* () {
  const tui = yield* TUIHandler;

  const name = yield* tui.prompt("Enter your name:").pipe(
    Effect.catchTag("TUIError", (err) => {
      if (err.reason === "Cancelled") {
        yield * tui.display("Operation cancelled", "warning");
        return Effect.fail(new Error("User cancelled"));
      }
      return Effect.fail(err);
    })
  );

  return name;
});

Error Handling Patterns

Handle CLI Errors:

const result =
  yield *
  cli.run("git", ["status"]).pipe(
    Effect.catchTag("CLIError", (err) => {
      switch (err.reason) {
        case "NotFound":
          yield * displayError("Command not found. Please install Git.");
          return Effect.fail(err);
        case "Timeout":
          yield * displayError("Command timed out. Try again.");
          return Effect.fail(err);
        case "CommandFailed":
          yield * displayError(`Failed with exit code ${err.exitCode}`);
          return Effect.fail(err);
        default:
          return Effect.fail(err);
      }
    })
  );

Handle Validation Errors with Retry:

const email =
  yield *
  tui
    .prompt("Email:", {
      validate: (input) => input.includes("@") || "Invalid email",
    })
    .pipe(
      Effect.catchTag("TUIError", (err) => {
        if (err.reason === "ValidationFailed") {
          yield * displayError(`Validation failed: ${err.message}`);
          // Retry or use default
          return Effect.succeed("[email protected]");
        }
        return Effect.fail(err);
      })
    );

Error Recovery with Fallback:

const template =
  yield *
  tui.selectOption("Template:", ["basic", "cli"]).pipe(
    Effect.catchTag("TUIError", (err) => {
      if (err.reason === "Cancelled") {
        yield * tui.display("Using default: basic", "info");
        return Effect.succeed("basic"); // Fallback value
      }
      return Effect.fail(err);
    })
  );

See examples/error-handling.ts for more comprehensive error handling examples.

Theming

Customize icons, colors, and styles for display types using the theme system.

Using Preset Themes

import { displaySuccess, displayInfo } from "effect-cli-tui";
import { EffectCLIRuntime } from "effect-cli-tui/services";
import { ThemeService, themes } from "effect-cli-tui/theme";

const program = Effect.gen(function* () {
  const theme = yield* ThemeService;

  // Use emoji theme
  yield* theme.setTheme(themes.emoji);
  yield* displaySuccess("Success!"); // Uses ✅ emoji

  // Use minimal theme (no icons)
  yield* theme.setTheme(themes.minimal);
  yield* displaySuccess("Done!"); // No icon, just green text

  // Use dark theme (optimized for dark terminals)
  yield* theme.setTheme(themes.dark);
  yield* displayInfo("Info"); // Uses cyan instead of blue
});

Creating Custom Themes

import { display } from "effect-cli-tui";
import { EffectCLIRuntime } from "effect-cli-tui/services";
import { createTheme, ThemeService } from "effect-cli-tui/theme";

const customTheme = createTheme({
  icons: {
    success: "✅",
    error: "❌",
    warning: "⚠️",
    info: "ℹ️",
  },
  colors: {
    success: "green",
    error: "red",
    warning: "yellow",
    info: "cyan", // Changed from blue
    highlight: "magenta", // Changed from cyan
  },
});

const program = Effect.gen(function* () {
  const theme = yield* ThemeService;
  yield* theme.setTheme(customTheme);

  yield* display("Custom theme!", { type: "success" });
});

Scoped Theme Changes

Use withTheme() to apply a theme temporarily:

import { displaySuccess, displayError } from "effect-cli-tui";
import { ThemeService, themes } from "effect-cli-tui/theme";

const program = Effect.gen(function* () {
  const theme = yield* ThemeService;

  // Set default theme
  yield* theme.setTheme(themes.default);

  // Use emoji theme only for this scope
  yield* theme.withTheme(
    themes.emoji,
    Effect.gen(function* () {
      yield* displaySuccess("Uses emoji theme");
      yield* displayError("Also uses emoji theme");
    })
  );

  // Back to default theme here
  yield* displaySuccess("Uses default theme");
});

Available Preset Themes

  • themes.default - Current behavior (✓, ✗, ⚠, ℹ with green/red/yellow/blue)
  • themes.minimal - No icons, simple colors
  • themes.dark - Optimized for dark terminal backgrounds (cyan for info)
  • themes.emoji - Emoji icons (✅, ❌, ⚠️, ℹ️)

Theme API

import {
  ThemeService,
  setTheme,
  getCurrentTheme,
  withTheme,
} from "effect-cli-tui/theme";

// Get current theme
const theme = yield * ThemeService;
const currentTheme = theme.getTheme();

// Set theme
yield * theme.setTheme(customTheme);

// Scoped theme
yield * theme.withTheme(customTheme, effect);

// Convenience functions
yield * setTheme(customTheme);
const current = yield * getCurrentTheme();
yield * withTheme(customTheme, effect);

API Reference

Display API

display(message: string, options?: DisplayOptions): Effect<void>

Display a single-line message with optional styling.

yield * display("This is an info message");
yield * display("Success!", { type: "success" });
yield * display("Custom prefix>>>", { prefix: ">>>" });
yield * display("No newline", { newline: false });

Options:

  • type?: 'info' | 'success' | 'error' - Message type (default: 'info')
  • prefix?: string - Custom prefix (overrides default)
  • newline?: boolean - Add newline before message (default: true)

displayLines(lines: string[], options?: DisplayOptions): Effect<void>

Display multiple lines with consistent formatting.

yield *
  displayLines(
    [
      "Project Status",
      "──────────────",
      "✅ Database: Connected",
      "✅ Cache: Ready",
    ],
    { type: "success" }
  );

displayJson(data: unknown, options?: JsonDisplayOptions): Effect<void>

Pretty-print JSON data with optional prefix.

yield * displayJson({ name: "project", version: "1.0.0" });
yield * displayJson(data, { spaces: 4, showPrefix: false });
yield * displayJson(data, { customPrefix: ">>>" }); // Custom prefix

JsonDisplayOptions extends DisplayOptions:

  • spaces?: number - Indentation spaces (default: 2)
  • showPrefix?: boolean - Show/hide the default prefix icon (default: true)
  • customPrefix?: string - Custom prefix string (overrides default icon when provided)

displaySuccess(message: string): Effect<void>

Convenience function for success messages.

yield * displaySuccess("Operation completed!");

displayError(message: string): Effect<void>

Convenience function for error messages.

yield * displayError("Failed to connect");

TUIHandler

prompt(message: string, options?: PromptOptions): Effect<string, TUIError>

Display a text input prompt.

const name =
  yield *
  tui.prompt("Enter your name:", {
    default: "User",
  });

selectOption(message: string, choices: string[]): Effect<string, TUIError>

Display a single-select dialog.

Controls:

  • Arrow keys (↑/↓) - Navigate up/down
  • Enter - Select highlighted option
const choice =
  yield * tui.selectOption("Choose one:", ["Option A", "Option B"]);

multiSelect(message: string, choices: string[]): Effect<string[], TUIError>

Display a multi-select dialog (checkbox).

Controls:

  • Arrow keys (↑/↓) - Navigate up/down
  • Space - Toggle selection (☐ ↔ ☑)
  • Enter - Submit selections
const choices =
  yield * tui.multiSelect("Choose multiple:", ["Feature 1", "Feature 2"]);

confirm(message: string): Effect<boolean, TUIError>

Display a yes/no confirmation.

const confirmed = yield * tui.confirm("Are you sure?");

display(message: string, type: 'info' | 'success' | 'error'): Effect<void>

Display a styled message.

yield * tui.display("Operation successful!", "success");
yield * tui.display("This is an error", "error");
yield * tui.display("For your information", "info");

EffectCLI

run(command: string, args?: string[], options?: CLIRunOptions): Effect<CLIResult, CLIError>

Execute a command and capture output.

const result =
  yield *
  cli.run("echo", ["Hello"], {
    cwd: "/path/to/dir",
    env: { NODE_ENV: "production" },
    timeout: 5000,
  });

console.log(result.stdout); // "Hello"

stream(command: string, args?: string[], options?: CLIRunOptions): Effect<void, CLIError>

Execute a command with streaming output (inherited stdio).

yield *
  cli.stream("npm", ["install"], {
    cwd: "/path/to/project",
  });

Types

SelectOption

interface SelectOption {
  label: string; // Display text
  value: string; // Returned value
  description?: string; // Optional help text
}

Note: selectOption() and multiSelect() accept both string[] (for simple cases) and SelectOption[] (for options with descriptions). When using SelectOption[], descriptions are displayed as gray, dimmed text below each option label.

CLIResult

interface CLIResult {
  exitCode: number;
  stdout: string;
  stderr: string;
}

CLIRunOptions

interface CLIRunOptions {
  cwd?: string; // Working directory
  env?: Record<string, string>; // Environment variables
  timeout?: number; // Timeout in milliseconds
}

PromptOptions

interface PromptOptions {
  default?: string; // Default value
  validate?: (input: string) => boolean | string; // Validation function
}

Display Types

type DisplayType = "info" | "success" | "error";

interface DisplayOptions {
  type?: DisplayType; // Message type (default: 'info')
  prefix?: string; // Custom prefix
  newline?: boolean; // Add newline before message (default: true)
}

interface JsonDisplayOptions extends DisplayOptions {
  spaces?: number; // JSON indentation spaces (default: 2)
  prefix?: boolean; // Show type prefix (default: true)
}

Error Types

TUIError

Thrown by TUIHandler when prompts fail.

type TUIError = {
  _tag: "TUIError";
  reason: "Cancelled" | "ValidationFailed" | "RenderError";
  message: string;
};

CLIError

Thrown by EffectCLI when commands fail.

class CLIError extends Data.TaggedError("CLIError") {
  readonly reason: "CommandFailed" | "Timeout" | "NotFound" | "ExecutionError";
  readonly message: string;
  readonly exitCode?: number; // Exit code when command fails (if available)
}

Supermemory Integration (experimental)

You can configure a Supermemory API key and use it from the TUI:

Setup

import { Effect } from "effect";
import { TUIHandler, runWithTUI, withSupermemory } from "effect-cli-tui";

const program = Effect.gen(function* () {
  const tui = yield* TUIHandler;
  
  // Your interactive program here
  const name = yield* tui.prompt("What's your name?");
  yield* tui.display(`Hello, ${name}!`, "success");
});

// Run with Supermemory integration
await Effect.runPromise(
  withSupermemory(runWithTUI(program))
);

Commands

Once configured, you can use these slash commands in any prompt:

  • Set API key:

    /supermemory api-key sk_...
  • Add a memory:

    /supermemory add This is something I want to remember.
  • Search memories:

    /supermemory search onboarding checklist

Configuration

The API key is stored in ~/.effect-supermemory.json. You can also set the SUPERMEMORY_API_KEY environment variable as a fallback.

Programmatic Usage

import { Effect } from "effect";
import { 
  SupermemoryClient, 
  SupermemoryClientLive,
  loadConfig,
  updateApiKey 
} from "effect-cli-tui";

// Direct API usage
const program = Effect.gen(function* () {
  const client = yield* SupermemoryClient;
  
  // Add a memory
  yield* client.addText("Important meeting notes from today");
  
  // Search memories
  const results = yield* client.search("meeting notes");
  console.log(`Found ${results.memories.length} memories`);
});

// Provide the client layer
await Effect.runPromise(
  program.pipe(Effect.provide(SupermemoryClientLive))
);

Roadmap

v2.0 (Current)

  • [x] API Modularization - Core APIs are in the main entry point, with specialized APIs in effect-cli-tui/components, effect-cli-tui/theme, etc.
  • [x] Ink Integration — Rich, component-based TUI elements are a core feature.
  • [x] Theme System — Customize icons, colors, and styles.

Future

  • [ ] Validation Helpers - Add common validation patterns for prompts.
  • [ ] Advanced Progress Indicators - More complex progress bars and multi-step loaders.
  • [ ] Dashboard / Layout System - For building more complex, real-time terminal dashboards.
  • [x] Supermemory Integration — Context-aware prompts and interactions.

Contributing

Contributions welcome! Please:

  1. Create a feature branch (git checkout -b feature/your-feature)
  2. Make your changes with tests
  3. Run validation: bun run build && bun test && bun run lint
  4. Commit with conventional commits
  5. Push and open a PR

Development

# Install dependencies
bun install

# Build
bun run build

# Test
bun test
bun test:watch
bun test:coverage

# Lint & format
bun run lint
bun run format

# Type check
bun run type-check

Running Examples

Test that examples work:

bun run examples/test-example.ts

For interactive examples, see examples/README.md.

License

MIT © 2025 Paul Philp

Related Projects

Support