effect-cli-tui
v2.2.0
Published
Effect-native CLI wrapper with interactive prompts and Ink components for terminal UIs
Downloads
1,047
Maintainers
Readme
effect-cli-tui
Effect-native CLI wrapper with interactive prompts and display utilities for building powerful terminal user interfaces.
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-tuiQuick 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 stylingdisplayLines(lines, options?)- Display multiple lines with consistent formattingdisplayJson(data, options?)- Pretty-print JSON datadisplaySuccess(message)- Convenience for success messagesdisplayError(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 inputselectOption(message, options)- Single selectionmultiSelect(message, options)- Multiple selectionconfirm(message)- Yes/No confirmationdisplay(message, type)- Display styled messages
EffectCLI
Execute CLI commands with Effect error handling.
Methods:
run(command, args?, options?)- Execute and capture outputstream(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/quitor/exit- Exit the current interactive session/clearor/cls- Clear the terminal screen/historyor/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-fvset both boolean flags.
- Example:
- Auto-Completion: While typing a slash command in
prompt()an inline suggestions list appears. PressTabto 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/historyoutput.
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 errorexitSession- 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 colorsthemes.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 prefixJsonDisplayOptions 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:
- Create a feature branch (
git checkout -b feature/your-feature) - Make your changes with tests
- Run validation:
bun run build && bun test && bun run lint - Commit with conventional commits
- 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-checkRunning Examples
Test that examples work:
bun run examples/test-example.tsFor interactive examples, see examples/README.md.
License
MIT © 2025 Paul Philp
Related Projects
- create-effect-agent — CLI tool using effect-cli-tui
- Effect — Effect-TS runtime
- @inquirer/prompts — Interactive CLI prompts
