Effect-native CLI wrapper with interactive prompts and Ink components for terminal UIs
npm install effect-cli-tuiEffect-native CLI wrapper with interactive prompts and display utilities for building powerful terminal user interfaces.


- đ¯ 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
``bash`
npm install effect-cli-tuior
pnpm add effect-cli-tuior
yarn add effect-cli-tui
`typescript
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);
`
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:
`typescript
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 });
`
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
Execute CLI commands with Effect error handling.
Methods:
- run(command, args?, options?) - Execute and capture outputstream(command, args?, options?)
- - Stream output directly
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.
`typescript
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();
`
`typescript
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();
`
`typescript
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 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().
`typescript
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=3production
- Positional args: --force
- Boolean flags: --tag=latest
- Key/value flags: , --count=3-f
- Short flags supported and merged: â --force, -t latest â --tag=latest, -c 3 â --count=3. Clusters like -fv set both boolean flags.prompt()
- Auto-Completion: While typing a slash command in 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:
`ts`
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
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)
All effects can fail with typed errors:
`typescript
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();
`
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':
`typescript
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;
});
`
Handle CLI Errors:
`typescriptFailed with exit code ${err.exitCode}
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();`
return Effect.fail(err);
default:
return Effect.fail(err);
}
})
);
Handle Validation Errors with Retry:
`typescriptValidation failed: ${err.message}
const email =
yield *
tui
.prompt("Email:", {
validate: (input) => input.includes("@") || "Invalid email",
})
.pipe(
Effect.catchTag("TUIError", (err) => {
if (err.reason === "ValidationFailed") {
yield * displayError();`
// Retry or use default
return Effect.succeed("default@example.com");
}
return Effect.fail(err);
})
);
Error Recovery with Fallback:
`typescript`
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.
Customize icons, colors, and styles for display types using the theme system.
`typescript
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
});
`
`typescript
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" });
});
`
Use withTheme() to apply a theme temporarily:
`typescript
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");
});
`
- 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 (â
, â, â ī¸, âšī¸)
`typescript
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);
`
#### display(message: string, options?: DisplayOptions): Effect
Display a single-line message with optional styling.
`typescript`
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
Display multiple lines with consistent formatting.
`typescript`
yield *
displayLines(
[
"Project Status",
"ââââââââââââââ",
"â
Database: Connected",
"â
Cache: Ready",
],
{ type: "success" }
);
#### displayJson(data: unknown, options?: JsonDisplayOptions): Effect
Pretty-print JSON data with optional prefix.
`typescript`
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
Convenience function for success messages.
`typescript`
yield * displaySuccess("Operation completed!");
#### displayError(message: string): Effect
Convenience function for error messages.
`typescript`
yield * displayError("Failed to connect");
#### prompt(message: string, options?: PromptOptions): Effect
Display a text input prompt.
`typescript`
const name =
yield *
tui.prompt("Enter your name:", {
default: "User",
});
#### selectOption(message: string, choices: string[]): Effect
Display a single-select dialog.
Controls:
- Arrow keys (â/â) - Navigate up/down
- Enter - Select highlighted option
`typescript`
const choice =
yield * tui.selectOption("Choose one:", ["Option A", "Option B"]);
#### multiSelect(message: string, choices: string[]): Effect
Display a multi-select dialog (checkbox).
Controls:
- Arrow keys (â/â) - Navigate up/down
- Space - Toggle selection (â â â)
- Enter - Submit selections
`typescript`
const choices =
yield * tui.multiSelect("Choose multiple:", ["Feature 1", "Feature 2"]);
#### confirm(message: string): Effect
Display a yes/no confirmation.
`typescript`
const confirmed = yield * tui.confirm("Are you sure?");
#### display(message: string, type: 'info' | 'success' | 'error'): Effect
Display a styled message.
`typescript`
yield * tui.display("Operation successful!", "success");
yield * tui.display("This is an error", "error");
yield * tui.display("For your information", "info");
#### run(command: string, args?: string[], options?: CLIRunOptions): Effect
Execute a command and capture output.
`typescript
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
Execute a command with streaming output (inherited stdio).
`typescript`
yield *
cli.stream("npm", ["install"], {
cwd: "/path/to/project",
});
`typescript`
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.
`typescript`
interface CLIResult {
exitCode: number;
stdout: string;
stderr: string;
}
`typescript`
interface CLIRunOptions {
cwd?: string; // Working directory
env?: Record
timeout?: number; // Timeout in milliseconds
}
`typescript`
interface PromptOptions {
default?: string; // Default value
validate?: (input: string) => boolean | string; // Validation function
}
`typescript
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)
}
`
Thrown by TUIHandler when prompts fail.
`typescript`
type TUIError = {
_tag: "TUIError";
reason: "Cancelled" | "ValidationFailed" | "RenderError";
message: string;
};
Thrown by EffectCLI when commands fail.
`typescript`
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)
}
You can configure a Supermemory API key and use it from the TUI:
`typescript
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))
);
`
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
The API key is stored in ~/.effect-supermemory.json. You can also set the SUPERMEMORY_API_KEY environment variable as a fallback.
`typescript
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))
);
`
- [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.
- [ ] 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.
Contributions welcome! Please:
1. Create a feature branch (git checkout -b feature/your-feature)bun run build && bun test && bun run lint
2. Make your changes with tests
3. Run validation:
4. Commit with conventional commits
5. Push and open a PR
`bashInstall dependencies
bun install
$3
Test that examples work:
`bash
bun run examples/test-example.ts
`examples/README.md`.MIT Š 2025 Paul Philp
- create-effect-agent â CLI tool using effect-cli-tui
- Effect â Effect-TS runtime
- @inquirer/prompts â Interactive CLI prompts
- đ API Documentation
- đī¸ Architecture
- đ Issues
- đŦ Discussions