@elsikora/bean

中文文档
1.0.0发布于 2026/02/24MITTypeScriptESM + CJS零依赖近 30 天下载 6501

Zero-dependency terminal interaction library for prompts, spinners, and rich CLI output.

原始使用文档
文档目录31 节
  1. 💡 Highlights
  2. 📚 Table of Contents
  3. 📖 Description
  4. Real-World Use Cases
  5. Architecture
  6. 🛠️ Tech Stack
  7. 🚀 Features
  8. 🏗 Architecture
  9. System Architecture
  10. Data Flow
  11. 📁 Project Structure
  12. 📋 Prerequisites
  13. 🛠 Installation
  14. 💡 Usage
  15. Quick Start
  16. Interactive Prompts
  17. Spinners & Progress
  18. Task Runner
  19. Fluent API
  20. Styling
  21. Output Formatting
  22. 🛣 Roadmap
  23. ❓ FAQ
  24. Why zero dependencies?
  25. How does Bean compare to chalk + ora + inquirer?
  26. Can I use Bean in non-interactive environments (CI/CD)?
  27. How do I test code that uses Bean?
  28. Does Bean support Windows terminals?
  29. Can I customize the visual style of prompts?
  30. 🔒 License
  31. 🙏 Acknowledgments

project-logo

🫘 Bean

Zero-dependency terminal interaction library for prompts, spinners, progress bars, task runners, and rich CLI output — built with clean architecture in TypeScript.

ElsiKora TypeScript Node.js npm Rollup Vitest ESLint Prettier GitHub Actions Git

💡 Highlights

  • 🪶 Truly zero runtime dependencies — everything from ANSI rendering to grapheme segmentation is built in-house, keeping your node_modules lean
  • 🎨 All-in-one CLI toolkit replacing chalk + ora + inquirer + listr2 + ink with a single, coherent API surface
  • 🏗️ Clean Architecture with injectable ports — swap clock, input, output, editor, and theme adapters for deterministic testing or custom runtimes
  • ⚡ Fluent API for chaining prompts, spinners, and output into elegant multi-step workflows with a single expression

📚 Table of Contents

📖 Description

Bean is a comprehensive, zero-dependency terminal interaction library that gives CLI developers everything they need to build beautiful, interactive command-line experiences — without pulling in a single runtime dependency.

Unlike traditional CLI toolkits that stitch together chalk, ora, inquirer, listr2, and ink (often totaling 200+ transitive dependencies), Bean consolidates prompts, spinners, progress bars, task runners, styled output, tables, trees, diffs, and more into a single, cohesive package built from the ground up in TypeScript.

Real-World Use Cases

  • CLI Scaffolding Tools: Build interactive project generators with multi-step prompt flows, spinners for file operations, and progress bars for dependency installation — all from one import.
  • CI/CD Pipeline Runners: Use the task runner with retries, rollback, subtasks, and structured lifecycle events to orchestrate build pipelines with rich terminal feedback.
  • Developer Tooling: Create linters, formatters, and code generators that provide beautiful, informative output with tables, trees, diffs, and colored log levels.
  • Internal DevOps Scripts: Replace ad-hoc console.log statements with structured output including boxes, notes, dividers, hyperlinks, and JSON formatting.

Architecture

Bean follows Clean Architecture principles with clearly separated domain, application, infrastructure, and presentation layers. Every port is injectable, making the library fully testable with fake adapters — no real TTY required for unit tests.

🛠️ Tech Stack

CategoryTechnologies
LanguageTypeScript
RuntimeNode.js
Build ToolRollup
TestingVitest
LintingESLint, Prettier
CI/CDGitHub Actions, Semantic Release
Package Managernpm

🚀 Features

  • ✨ **Interactive Prompts — text, password, confirm, toggle, number, rating, select, multiselect, group-multiselect, rawlist, expand, autocomplete, date, list, tree-select, editor, and schema-driven prompts with validation, abort signals, timeouts, and fallback values**
  • ✨ **Spinner System — manual spinners with succeed/fail/warn/info/stop states, prefix support, elapsed time tracking, state change callbacks, spinnerPromise for auto-managed task spinners, and a spinner manager for concurrent hierarchical spinners**
  • ✨ **Progress Bars — static snapshots, animated progress with custom characters, render callbacks, multi-progress with concurrent bars, and hierarchical parent-child progress tracking**
  • ✨ **Task Runner — sequential and concurrent task execution with nested subtasks, retries, conditional when guards, rollback on failure, structured lifecycle events, configurable log prefixes, and optional persisted log entries**
  • ✨ **Rich Output Formatting — tables with ANSI-aware column alignment, tree rendering, JSON pretty/compact output, LCS-based text diffs, notes, boxes, dividers, columns, hyperlinks, and log levels (info/success/warn/error)**
  • ✨ **Full Styling Engine — named colors, hex, RGB, 256-color palette, bright variants, background colors, bold/italic/underline/strikethrough/overline/inverse/dim decorations, fluent style chaining, tagged template literals, custom themes, gradient text, and automatic color level detection with graceful downgrading**
  • ✨ **Fluent API — chain intro → prompts → spinners → output → outro into a single expressive workflow with bean.fluent()**
  • ✨ **Zero-Dependency Image Renderer — render pixel arrays as ASCII/Unicode art directly in the terminal without external image libraries**
  • ✨ **Dual Module Output — ships both ESM and CJS builds with full TypeScript declarations and source maps**
  • ✨ **Fully Injectable Architecture — every I/O port (clock, input, output, editor, theme, prompt style) is abstracted behind interfaces for deterministic testing with fake adapters**

🏗 Architecture

System Architecture

flowchart TD
    Factory["createBeanAdapterFactory"]
    BeanAdapter["BeanAdapter (Facade)"]
    FluentApi["BeanFluentApi"]
    PromptService["BeanPromptService"]
    SpinnerService["BeanSpinnerService"]
    ProgressService["BeanProgressService"]
    TaskRunner["BeanTaskRunnerService"]
    StyleService["BeanStyleService"]
    UseCases["Use Cases (Text/Select/Multiselect/Spinner)"]
    DomainEntities["Domain Entities & Value Objects"]
    InputPort["IInputPortInterface"]
    OutputPort["IOutputPortInterface"]
    ClockPort["IClockPortInterface"]
    ThemePort["IThemePortInterface"]
    NodeAdapters["Node.js Adapters (stdin/stdout/readline)"]

    Factory --> BeanAdapter
    Factory --> NodeAdapters
    BeanAdapter --> FluentApi
    BeanAdapter --> PromptService
    BeanAdapter --> SpinnerService
    BeanAdapter --> ProgressService
    BeanAdapter --> TaskRunner
    BeanAdapter --> StyleService
    PromptService --> UseCases
    SpinnerService --> UseCases
    UseCases --> DomainEntities
    UseCases --> InputPort
    UseCases --> OutputPort
    UseCases --> ClockPort
    BeanAdapter --> ThemePort
    NodeAdapters --> InputPort
    NodeAdapters --> OutputPort
    NodeAdapters --> ClockPort

Data Flow

sequenceDiagram
    participant User
    participant BeanAdapter
    participant PromptService
    participant TextUseCase
    participant InputPort
    participant OutputPort
    participant TextBuffer

    User->>BeanAdapter: bean.text({ message, validate })
    BeanAdapter->>PromptService: text(input)
    PromptService->>TextUseCase: execute(args)
    TextUseCase->>InputPort: enableRawMode()
    TextUseCase->>OutputPort: hideCursor()
    TextUseCase->>OutputPort: writeFrame(questionLine)
    loop Each Keystroke
        InputPort->>TextUseCase: onKeyEvent(event)
        TextUseCase->>TextBuffer: applyInsert/applyBackspace
        TextUseCase->>OutputPort: writeFrame(updatedLine)
    end
    TextUseCase->>InputPort: disableRawMode()
    TextUseCase->>OutputPort: showCursor()
    TextUseCase->>OutputPort: writeLine(completedLine)
    TextUseCase-->>PromptService: "user input"
    PromptService-->>BeanAdapter: "user input"
    BeanAdapter-->>User: "user input"

📁 Project Structure

Click to expand
bean/
├── .github/
│   ├── workflows/
│   │   ├── mirror-to-codecommit.yml
│   │   ├── qodana-quality-scan.yml
│   │   ├── release.yml
│   │   ├── snyk-security-scan.yml
│   │   └── test.yml
│   └── dependabot.yml
├── docs/
│   ├── api-reference/
│   │   ├── _meta.js
│   │   └── page.mdx
│   ├── architecture/
│   │   ├── _meta.js
│   │   ├── bean-adapter-modularization.plan.md
│   │   └── page.mdx
│   ├── core-concepts/
│   │   ├── _meta.js
│   │   └── page.mdx
│   ├── getting-started/
│   │   ├── _meta.js
│   │   └── page.mdx
│   ├── guides/
│   │   ├── _meta.js
│   │   └── page.mdx
│   ├── migration/
│   │   ├── _meta.js
│   │   ├── page.mdx
│   │   └── runtime-parity-breaking-changes.md
│   ├── _meta.js
│   └── page.mdx
├── examples/
│   ├── 01-getting-started/
│   │   └── main.ts
│   ├── 02-styling/
│   │   └── main.ts
│   ├── 03-prompts-basic/
│   │   └── main.ts
│   ├── 04-prompts-select/
│   │   └── main.ts
│   ├── 05-prompts-advanced/
│   │   └── main.ts
│   ├── 06-spinners/
│   │   └── main.ts
│   ├── 07-progress/
│   │   └── main.ts
│   ├── 08-task-runner/
│   │   └── main.ts
│   ├── 09-output-formatting/
│   │   └── main.ts
│   ├── 10-fluent-api/
│   │   └── main.ts
│   ├── README.md
│   └── tsconfig.json
├── scripts/
│   └── validate-docs.mjs
├── src/
│   ├── application/
│   │   ├── constant/
│   │   ├── interface/
│   │   ├── service/
│   │   ├── type/
│   │   ├── use-case/
│   │   └── index.ts
│   ├── composition/
│   │   ├── factory/
│   │   └── interface/
│   ├── domain/
│   │   ├── constant/
│   │   ├── entity/
│   │   ├── enum/
│   │   ├── interface/
│   │   ├── type/
│   │   ├── value-object/
│   │   └── index.ts
│   ├── infrastructure/
│   │   ├── adapter/
│   │   ├── di/
│   │   └── index.ts
│   ├── presentation/
│   │   ├── bean/
│   │   ├── interface/
│   │   ├── type/
│   │   └── index.ts
│   └── index.ts
├── test/
│   ├── e2e/
│   │   ├── fixtures/
│   │   └── integration/
│   └── unit/
│       ├── application/
│       ├── domain/
│       ├── fixtures/
│       ├── infrastructure/
│       └── presentation/
├── eslint.config.js
├── generated-logo.png
├── lint-staged.config.js
├── package-lock.json
├── package.json
├── prettier.config.js
├── README.md
├── release.config.js
├── rollup.config.js
├── rollup.test.config.js
├── tsconfig.build.json
├── tsconfig.json
├── vitest.end-to-end.config.js
└── vitest.unit.config.js

📋 Prerequisites

  • Node.js >= 18.0.0
  • npm >= 9.0.0

🛠 Installation

# Install with npm
npm install @elsikora/bean

# Install with yarn
yarn add @elsikora/bean

# Install with pnpm
pnpm add @elsikora/bean

💡 Usage

Quick Start

import { createBeanAdapterFactory } from "@elsikora/bean";

const bean = createBeanAdapterFactory();

bean.intro({ message: "Welcome to my CLI!" });
bean.message({ message: "Setting things up..." });
bean.outro({ message: "All done!" });
bean.dispose();

Interactive Prompts

import { SelectOptionValueObject, createBeanAdapterFactory } from "@elsikora/bean";

const bean = createBeanAdapterFactory();

// Text input with validation
const name = await bean.text({
    message: "What is your project name?",
    placeholder: "my-awesome-app",
    validate: (value) => (value.length < 2 ? "Name too short" : null),
});

// Select with search
const framework = await bean.select({
    message: "Pick a framework:",
    isSearchEnabled: true,
    options: [new SelectOptionValueObject({ label: "React", value: "react" }), new SelectOptionValueObject({ label: "Vue", value: "vue" }), new SelectOptionValueObject({ label: "Svelte", value: "svelte" })],
});

// Multiselect with groups
const features = await bean.groupMultiselect({
    message: "Select features:",
    options: [new SelectOptionValueObject({ group: "Auth", label: "OAuth 2.0", value: "oauth" }), new SelectOptionValueObject({ group: "Auth", label: "JWT", value: "jwt" }), new SelectOptionValueObject({ group: "DB", label: "PostgreSQL", value: "postgres" }), new SelectOptionValueObject({ group: "DB", label: "Redis", value: "redis" })],
});

// Confirm
const shouldInstall = await bean.confirm({
    message: "Install dependencies now?",
    isDefaultValue: true,
});

bean.dispose();

Spinners & Progress

// Manual spinner
const spinner = bean.spinner({ text: "Compiling..." });
await doWork();
spinner.succeed("Compilation complete");

// Auto-managed spinner
const result = await bean.spinnerPromise({
    text: "Installing dependencies...",
    successText: "Dependencies installed",
    failText: "Installation failed",
    task: async () => {
        await installDeps();
        return "142 packages";
    },
});

// Concurrent spinner manager
const manager = bean.spinnerManager();
const api = manager.create({ id: "api", text: "Starting API..." });
const db = manager.create({ id: "db", text: "Connecting DB..." });
await connectDb();
db.succeed("Database connected");
await startApi();
api.succeed("API listening on :3000");

// Animated progress bar
const bar = bean.createProgress({ label: "Downloading", total: 100 });
for (let i = 0; i < 100; i++) {
    await delay(50);
    bar.increment();
}
bar.stop();

Task Runner

const summary = await bean.runTasks({
    isPersistLogs: true,
    logPrefix: "[release]",
    onEvent: (event) => console.log(event.status, event.taskPath),
    tasks: [
        {
            title: "Build",
            run: async () => {
                /* build logic */
            },
            subtasks: [
                {
                    title: "Lint",
                    run: async () => {
                        /* lint */
                    },
                },
                {
                    title: "Test",
                    run: async () => {
                        /* test */
                    },
                    retries: 2,
                },
            ],
        },
        {
            title: "Deploy",
            run: async () => {
                /* deploy */
            },
            rollback: async () => {
                /* rollback */
            },
            when: () => process.env.CI === "true",
        },
    ],
});
// summary: { succeeded: 3, failed: 0, skipped: 1, logs: [...] }

Fluent API

let projectName = "";
let framework = "";

await bean
    .fluent()
    .intro("Project Scaffolding Wizard")
    .text({
        message: "Project name?",
        onResolved: (v) => {
            projectName = v ?? "my-project";
        },
    })
    .select({
        message: "Framework?",
        onResolved: (v) => {
            framework = v ?? "none";
        },
        options: [new SelectOptionValueObject({ label: "React", value: "react" }), new SelectOptionValueObject({ label: "Vue", value: "vue" })],
    })
    .spinner({
        text: "Scaffolding...",
        task: async (handle) => {
            handle.update("Creating files...");
            await createProject();
        },
    })
    .outro(`Project "${projectName}" created with ${framework}!`)
    .run();

Styling

// Named colors and decorations
bean.message({ message: bean.style({ color: "red", isBold: true, text: "Error!" }) });

// Hex, RGB, 256-color
bean.message({ message: bean.styleHex({ color: "#FF6B6B", text: "coral" }) });
bean.message({ message: bean.styleRgb({ rgb: { r: 50, g: 200, b: 150 }, text: "teal" }) });

// Fluent chain
bean.message({ message: bean.styleChain({ color: "yellow" }).bold().render("⚠ Warning") });

// Tagged template
const danger = bean.styleTemplate({ color: "red", isBold: true });
bean.message({ message: danger`Error: ${3} files failed` });

// Gradient text
bean.message({ message: bean.styleGradient({ from: "#ff0000", to: "#0000ff", text: "Rainbow" }) });

Output Formatting

// Table
bean.table({ columns: ["Package", "Version"], rows: [["bean", "0.1.0"]] });

// Tree
bean.tree({ label: "src/", children: [{ label: "domain/" }, { label: "application/" }] });

// Diff
bean.diff({ before: "version: 0.1.0", after: "version: 0.2.0" });

// JSON
bean.json({ value: { name: "bean", deps: 0 }, isPretty: true });

// Box, Note, Divider
bean.box({ title: "Success", message: "Your CLI is ready!" });
bean.note({ title: "Tip", message: "Run `npm start` to begin." });
bean.divider();

🛣 Roadmap

Click to expand
Task / FeatureStatus
Core prompt runtime (text, select, multiselect, confirm)✅ Done
Spinner system with manager and hierarchical labels✅ Done
Progress bars with multi-progress and hierarchy✅ Done
Task runner with subtasks, retries, and rollback✅ Done
Full ANSI styling engine with gradient support✅ Done
Fluent API for chained workflows✅ Done
Rich output (table, tree, diff, JSON, box, note)✅ Done
Schema-driven prompt generation (promptFromSchema)✅ Done
Dual ESM/CJS builds with TypeScript declarations✅ Done
Extract BeanAdapter into fully modular sub-services✅ Done
Complete BeanAdapter modularization (prompt strategies)🚧 In Progress
Architecture tests enforcing layer boundaries🚧 In Progress
Plugin system for custom prompt types🚧 In Progress
Internationalization (i18n) for prompt messages🚧 In Progress

❓ FAQ

Click to expand

Why zero dependencies?

Every dependency is a potential security vulnerability, version conflict, and bundle size increase. Bean implements everything from ANSI escape code generation to grapheme-aware text segmentation internally, so your project gets a complete CLI toolkit without inheriting anyone else's dependency tree.

How does Bean compare to chalk + ora + inquirer?

Bean replaces all three (and more) with a single import. It provides the styling capabilities of chalk, the spinner functionality of ora, the prompt system of inquirer, the task runner of listr2, and the output formatting of boxen/cli-table — all with a unified, type-safe API.

Can I use Bean in non-interactive environments (CI/CD)?

Yes! Every prompt method accepts a fallbackValue that is used automatically when no TTY is available. Spinners gracefully degrade to static line output. This makes Bean safe for both interactive and non-interactive use.

How do I test code that uses Bean?

Bean's architecture is built around injectable ports. In your tests, create instances using createBeanAdapterFactory with fake inputPort, outputPort, and clockPort adapters. The test suite demonstrates this pattern extensively — see test/e2e/fixtures/ for reusable fake adapters.

Does Bean support Windows terminals?

Yes. Bean detects Unicode support and automatically falls back to ASCII equivalents for spinner frames, box characters, tree prefixes, and more. The BEAN_FORCE_ASCII=1 environment variable can force ASCII mode. Color level detection respects NO_COLOR, FORCE_COLOR, COLORTERM, and TERM environment variables.

Can I customize the visual style of prompts?

Absolutely. Pass a custom promptStylePort to change pointer symbols, checkmarks, separators, and more. Pass a custom themePort to override accent, success, danger, muted, info, and strong text styling. Use bean.createTheme() for partial overrides.

🔒 License

This project is licensed under MIT.

🙏 Acknowledgments

  • Inspired by the excellent work of clack, inquirer, ora, chalk, and listr2
  • Clean Architecture principles from Robert C. Martin's writings
  • ANSI escape code handling informed by the ECMA-48 standard
  • Grapheme segmentation powered by the Intl.Segmenter API with graceful fallback
  • Built with ❤️ by Elsikora

Back to Top

版本与发布记录

@elsikora/bean 共发布 2 个版本,首次发布于 2026/02/24;过去 12 个月发布了 2 个版本。

版本发布日期距今
1.0.0latest2026/02/248 个月前
1.0.0-dev.2beta2026/02/248 个月前

关于 @elsikora/bean 的常见问题

@elsikora/bean 是什么?

Zero-dependency terminal interaction library for prompts, spinners, and rich CLI output.

如何安装 @elsikora/bean?

在项目目录运行 npm install @elsikora/bean,或使用 pnpm add / yarn add / bun add。

@elsikora/bean 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@elsikora/bean 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@elsikora/bean 可以商用吗?

@elsikora/bean 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@elsikora/bean 的下载量有多少?

根据 npm 官方下载统计,@elsikora/bean 在 2026/10/14 – 2026/11/12 这 30 天内被下载 6,501 次,日均约 217 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@elsikora/bean 还在维护吗?

最新版本 1.0.0 发布于 2026/02/24(8 个月前);至今共发布 2 个版本,其中过去 12 个月发布了 2 个;首次发布于 2026/02/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。