豆类
Zero-dependency 终端交互库用于提示器,自旋器,进度栏,任务跑取器,以及丰富的CLI输出——以TypeScript中干净的架构来建造.
重点
- • 真正零运行时间依赖性——从ANSI渲染到石墨分块的所有东西都是在内部建造的,保持了节点-模块的收缩
- 将 " 全能CLI " 工具包替换为 " 粉笔+ " ora " + " 查询 " + " Listr2 " + " 墨水 " ,代之以单一、连贯的API表面
- • 带有可注入端口的清洁建筑——交换时钟、输入、输出、编辑器和主题适配器,用于确定测试或自定义运行时间
- QQ 流利 API 用于链路提示,自旋器,并输出为有单一表达式的优雅多步工作流程
目录
说明
豆子是综合的, 零依赖性 终端交互库,可以让CLI开发人员拥有建立美丽,互动的指令行体验所需的一切——而无需拉入单一运行时间依赖.
不像传统的CLI工具箱那样 chalk, (中文(简体) ), ora, (中文(简体) ), inquirer, (中文(简体) ), listr2,以及 ink (通常总计为200+过渡依赖),豆会将导体,回旋器,进度栏,任务跑取器,有风格的输出,表,树,diff,以及更能将从平地上到TypeScript所构建的单个,有凝聚力的包.
实际世界使用案例
- CLI 脚手架工具: 构建交互式项目生成器,多步即时流出,回旋器用于文件操作,并用于依赖性安装的进度栏——全部来自一个导入.
- CI/CD 管道运行器: 利用任务跑道带有回转,回滚,子任务,以及结构化的生命周期事件来协调搭建有丰富的终端反馈的管道.
- 开发者工具: 创建linters,格式化器,以及代码生成器,提供美丽,信息化的输出,并配有表格,树,diffs,和有色日志等级别.
- 内部 DevOps 脚本: 替换临时
console.log带有结构化输出的语句,包括框、注释、分隔符、超链接和JSON格式.
建筑
豆子跟着 清洁建筑 具有明确分开的域、应用、基础设施和演示层的原则。 每个端口都是可注射的,使库能完全用假适配器进行测试——单位测试不需要真正的TTY.
QQ 技术堆栈
| 类别 | 技术 |
|---|---|
| 语言 | 类型脚本 |
| 运行时间 | 节点.js |
| 构建工具 | 滚动 |
| 测试 | 维特 |
| 线性 | ESLint, 高级 |
| 排雷中心/发改委 | GitHub 动作,语义释放 |
| 软件包管理器 | 无 |
* 特性
- ✨ ** 交互式提示 ——文字,密码,确认,切换,编号,评分,选择,多选取,组多选取,原始列表,扩展,自动完成,日期,列表,树选,编辑器,以及具有验证,中止信号,超时,倒置值的由计划驱动的提示**
- ✨ ** Spinner系统 —— 手动自旋器,带有成功/失败/警告/info/stop状态,前缀支持,已过期时间跟踪,状态更改回调,
spinnerPromise自动管理任务旋转器的旋转器管理器** - ✨ ** 进步栏 ——静态快照,动画化进步与自定义字符,制作回调,多进步与并列栏,分级跟踪亲子进度**
- ✨ **任务运行器 - 与嵌入式子任务、重试、有条件的相接和同时执行任务
when警卫、故障回滚、结构化的生命周期事件、可配置的日志前缀以及可选的持续日志条目** - ✨ ** Rich 输出格式 具有 ANSI- 意识列对齐、 树渲染、 JSON 漂亮/ 紧凑输出、 基于 LCS 的文本 diffs、 注释、 框、 分隔符、 列、 超链接和日志级别( info/success/ warn/error) **
- ✨ ** 装配引擎 ——被命名为颜色,十六进制,RGB,256-彩色调色板,亮色变体,背景颜色,粗体/斜体/下行/斜通/上行/倒行/平分装饰,流畅的风格链条,有标记的模板文字,自定义主题,梯度文字,以及以优雅的下行分级方式自动分色检测**
- ✨ ** 流畅的API ——连锁内线 ——导出 ——自旋器 ——输出 ——外出进入一个单一的表达式工作流程,与
bean.fluent()页:1 - ✨ **零依赖图像渲染器 ——将像素阵列作为ASCII/Unicode艺术在终端直接进行,没有外部图像库**
- ✨ ** 双模块输出 ——无害环境管理和共同司法体系的船舶都配有完整的类型Script申报和源地图**
- ✨ ** Fully可注入结构 ——每个 I/O 端口(钟,输入,输出,编辑,主题,即时样式)被抽象在接口后,用于用假适配器进行决定性测试**
建筑
系统架构
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
数据流动
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"
项目结构
单击以扩展
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
· 先决条件
- 节点.js 18.0.0
- npm >= 9.0.0
* 安装
# Install with npm
npm install @elsikora/bean
# Install with yarn
yarn add @elsikora/bean
# Install with pnpm
pnpm add @elsikora/bean
使用量
快速启动
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();
交互式提示
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();
旋转器进度( P)
// 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();
任务运行器
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: [...] }
流利的 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();
样式
// 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" }) });
输出格式
// 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();
* 路线图
单击以扩展
| 任务/ 特性 | 状态 |
|---|---|
| 核心快速运行时间( 文本、 选择、 多选择、 确认) | 完毕 |
| 带有管理器和分级标签的自旋系统 | 完毕 |
| 多进展和分级的进度栏 | 完毕 |
| 有子任务、 重试和回滚的任务运行器 | 完毕 |
| 具有梯度支持的全 ANSI 样式引擎 | 完毕 |
| 流利的连锁工作流程 API | 完毕 |
| 产出丰富(表、树、diff、JSON、框、注) | 完毕 |
由 Schema 驱动的迅速生成(promptFromSchema(中文(简体) ) | 完毕 |
| 与类型Script申报建立双重无害环境管理/CJS | 完毕 |
| 将BeanAdapter 提取到完全模块化的子服务 | 完毕 |
| 完整的豆泥模块化(即时战略) | 执行中 |
| 执行地层边界的建筑测试 | 执行中 |
| 用于自定义提示类型的插件系统 | 执行中 |
| 快速信息国际化(i18n) | 执行中 |
问 题
单击以扩展
为什么是零依赖?
每一种依赖都是潜在的安全脆弱性、版本冲突以及捆绑大小的增加。 豆子从ANSI的逃生代码生成到石墨感知文本分解内部,将一切实现,因此你的项目得到了完整的CLI工具包而不会继承其他人的依赖树.
豆子和粉笔+ora+询取者如何比较??
豆以一入取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取出出出出出出出出出出出取取出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出 它提供了粉笔的造型能力,ora的自旋器功能,查询器的即时系统,lister2的任务执行器,以及boxen/cli-table的输出格式——都有一个统一,型号安全的API.
我能在非互动环境中用豆(CI/CD)吗?
没错! 每种即时方法都接受 fallbackValue 如果没有 TTY 可用,则自动使用。 旋转器优雅地降解为静行输出。 这使得"豆子"既可以交互使用,也可以不交互使用.
我怎么测试用豆的代码?
豆的建筑围绕可注入口而建. 在测试中,使用 createBeanAdapterFactory 装假 inputPort, (中文(简体) ), outputPort,以及 clockPort 适应器. 测试套间广泛展示这种模式——见 test/e2e/fixtures/ 可重复使用的假适配器.
Bean支持Windows终端吗?
对 Bean检测到Unicode支持,并自动回落到ASCII等效的自旋框,框字符,树前缀等. 这个 BEAN_FORCE_ASCII=1 环境变量可以强制ASCII模式. 颜色级别检测尊重 NO_COLOR, (中文(简体) ), FORCE_COLOR, (中文(简体) ), COLORTERM,以及 TERM 环境变量.
我能定制一下提示的视觉风格吗?
当然 通过自定义 promptStylePort 以更改指针符号、检查标记、分隔符等。 通过自定义 themePort 重音,成功,危险, 哑音,信息, 和强大的文本样式。 使用 bean.createTheme() 用于部分重置.
许可证
这个项目的许可证根据 麻省理工学院。 。 。 .
承认
- 受到出色工作的启发 废话, (中文(简体) ), 询问者, (中文(简体) ), 轨道, (中文(简体) ), 粉笔,以及 列表2
- 罗伯特·C·马丁著作中的清洁建筑原则
- ANSI 逃生密码处理 ECMA-48(欧洲经货市场) 标准
- 图形分块由
Intl.SegmenterAPI 有优雅的后退 - 由 QQ 创建 埃尔西科拉