关于 cli-framework-oclif-ink
cli-framework-oclif-ink 是用于使用 oclif 构建 TypeScript 命令行应用、使用 Ink 编写交互式终端界面的技能,涵盖集成、插件、生命周期处理与测试。
主要功能
- 01oclif 命令、标志与参数
- 02基于 React 的 Ink 终端界面
- 03交互式向导与进度模式
- 04插件架构与生命周期钩子
- 05命令和组件测试
适用场景
- 01构建多命令 CLI
- 02添加终端向导或仪表盘
- 03将 oclif 命令与 Ink 界面结合
- 04测试 CLI 输出和键盘交互
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill cli-framework-oclif-ink安装后在 Claude Code 中输入/cli-framework-oclif-ink确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlexamples/advanced.mdcore.mdtesting.md详细介绍
oclif + Ink CLI 模式
快速指南: 使用 oclif 处理命令路由、标志/参数解析和插件架构。使用 Ink 构建基于 React 的交互式终端 UI,并采用 Flexbox 布局。命令需要丰富的有状态界面时,可将两者结合使用。从 oclif 命令中渲染 Ink 时,务必
await waitUntilExit()。使用this.log()而不是console.log,以保留 JSON 输出模式。
<critical_requirements>
关键:使用此技能前
所有代码都必须遵循项目在 CLAUDE.md 中的约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(在 oclif 命令中,render() 后必须 await waitUntilExit()——否则进程会在 UI 完成前退出)
(在命令中必须使用 this.log() / this.warn() / this.error()——console.log 会破坏 --json 模式和测试捕获)
(在 Ink 中,所有文本都必须放在 <Text> 组件中——裸字符串会导致渲染错误)
(必须使用 useEffect 清理函数取消异步操作——用户按下 Ctrl+C 时,Ink 组件会卸载)
</critical_requirements>
自动检测: oclif、@oclif/core、@oclif/test、Ink、ink、@inkjs/ui、Command 类、Flags、Args、useInput、useApp、useFocus、render()、waitUntilExit、终端 UI、CLI 命令、ink-testing-library
适用场景:
- 构建支持标志/参数解析的多命令 CLI
- 创建交互式终端 UI(向导、仪表板、进度显示)
- 将命令路由与丰富的基于 React 的界面结合起来
- 构建可通过插件扩展的 CLI 架构
不适用场景:
- 简单的一次性脚本(使用纯 Node.js 即可)
- 仅需基本提示的场景(使用轻量级提示库即可)
- 启动时间要求低于 100ms 的性能关键场景(oclif 会增加约 200ms 的开销)
涵盖的关键模式:
- 包含类型化标志、参数和输出方法的 oclif 命令结构
- Ink 组件、Flexbox 布局、键盘输入和焦点管理
- 集成:在 oclif 命令中渲染 Ink,并管理生命周期
- @inkjs/ui 预构建组件(Select、TextInput、Spinner 等)
- 插件架构和生命周期钩子
- 多步骤向导、进度指示器和可取消操作
- 使用
@oclif/test测试命令,使用ink-testing-library测试组件
设计理念
oclif 和 Ink 解决的是彼此独立的问题。oclif 负责那些繁琐但关键的部分:命令路由、标志解析、帮助生成、插件发现和自动更新。Ink 负责交互部分:使用 React 组件模型和 Flexbox 布局构建有状态的终端 UI。
**当命令只需完成工作并打印输出时,单独使用 oclif。**当命令需要实时用户交互(向导、仪表板、进度)时,添加 Ink。集成点很简单:oclif 命令的 run() 调用 render(),然后等待 waitUntilExit()。
关键架构决策:
- 命令使用
.ts文件(而非.tsx)——它们从单独的.tsx文件导入 Ink 组件 - oclif 管理进程生命周期;Ink 在进程内管理 UI 生命周期
- 键盘处理放在 Ink 组件中,通过
useInput实现,而不是放在 oclif 命令中 - 复杂 Ink UI 的状态管理应使用外部存储(而不是逐层传递属性)
核心模式
模式 1:包含类型化标志和参数的 oclif 命令
命令使用静态属性定义元数据和标志/参数。run() 方法是异步的,并返回类型化数据,以支持 JSON 输出。
import { Command, Flags, Args } from "@oclif/core";
const DEFAULT_RETRIES = 3;
export class Deploy extends Command {
static summary = "Deploy to target environment";
static enableJsonFlag = true; // Adds --json flag
static flags = {
env: Flags.string({
char: "e",
required: true,
options: ["staging", "production"] as const,
}),
retries: Flags.integer({
char: "r",
default: DEFAULT_RETRIES,
min: 0,
max: 10,
}),
verbose: Flags.boolean({ char: "v", default: false, allowNo: true }),
apiKey: Flags.string({ env: "MY_CLI_API_KEY" }), // From env var
};
static args = {
target: Args.string({ description: "Deploy target", required: true }),
};
async run(): Promise<{ status: string }> {
const { args, flags } = await this.parse(Deploy);
// Use this.log, this.warn, this.error -- never console.*
this.log(`Deploying ${args.target} to ${flags.env}`);
return { status: "deployed" };
}
}
有关完整的标志类型、参数、输出方法和错误处理,请参阅 examples/core.md 中的模式 1-5。
模式 2:带键盘处理的 Ink 组件
Ink 组件是使用钩子处理输入、应用生命周期和焦点的 React 函数组件。
import React, { useState } from "react";
import { Box, Text, useInput, useApp } from "ink";
interface SelectorProps {
items: string[];
onSelect: (item: string) => void;
}
export const Selector: React.FC<SelectorProps> = ({ items, onSelect }) => {
const [index, setIndex] = useState(0);
const { exit } = useApp();
useInput((input, key) => {
if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
if (key.downArrow) setIndex((i) => Math.min(items.length - 1, i + 1));
if (key.return) onSelect(items[index]);
if (input === "q") exit();
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={item} bold={i === index}>
{i === index ? "> " : " "}
{item}
</Text>
))}
</Box>
);
};
有关样式、布局和 @inkjs/ui 组件,请参阅 examples/core.md 中的模式 6-8。
模式 3:从 oclif 命令渲染 Ink
集成模式:oclif 命令渲染一个 Ink 组件,并等待其完成。
import { Command, Flags } from "@oclif/core";
import { render } from "ink";
import React from "react";
import { SetupWizard } from "../components/setup-wizard.js";
export class Init extends Command {
static summary = "Initialize a new project";
static flags = {
yes: Flags.boolean({ char: "y", description: "Use defaults", default: false }),
};
async run(): Promise<void> {
const { flags } = await this.parse(Init);
if (flags.yes) {
this.log("Initialized with defaults.");
return;
}
// CRITICAL: Destructure waitUntilExit and await it
const { waitUntilExit } = render(<SetupWizard />);
await waitUntilExit();
}
}
完整的集成模式(包括非交互式回退)请参见 examples/core.md 模式 9。
模式 4:多步骤向导
向导使用基于步骤的状态,支持前进、后退导航和数据累积。
const MultiStepWizard: React.FC<WizardProps> = ({ steps, onComplete }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [data, setData] = useState<Record<string, unknown>>({});
const handleNext = (stepData: Record<string, unknown>) => {
const merged = { ...data, ...stepData };
setData(merged);
if (currentIndex === steps.length - 1) onComplete(merged);
else setCurrentIndex((i) => i + 1);
};
const handleBack = () => setCurrentIndex((i) => Math.max(0, i - 1));
// Render steps[currentIndex].component with {onNext, onBack, data} props
};
包含导航功能的完整向导实现请参见 examples/advanced.md 模式 1-2。
模式 5:插件架构
oclif 插件是拥有自己命令和钩子的 npm 包。宿主 CLI 会在 package.json 中注册插件。
{
"oclif": {
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-autocomplete",
"@myorg/cli-plugin-analytics"
]
}
}
有关如何创建插件以及支持用户安装插件,请参见 examples/advanced.md 模式 4。
模式 6:测试命令和组件
命令测试(标志、参数、输出、错误)使用 @oclif/test,Ink 组件测试(渲染、键盘模拟)使用 ink-testing-library。
// Command test
import { runCommand } from "@oclif/test";
const { stdout, error } = await runCommand(["deploy", "--env", "staging", "app"]);
expect(stdout).toContain("Deploying");
// Ink component test
import { render } from "ink-testing-library";
const { lastFrame, stdin } = render(<Selector items={["a", "b"]} onSelect={fn} />);
stdin.write("\u001B[B"); // Down arrow
stdin.write("\r"); // Enter
expect(fn).toHaveBeenCalledWith("b");
完整的测试模式(包括异步操作、模拟和快照测试)请参见 examples/testing.md。
<decision_framework>
决策框架
Building a CLI?
|
+-> Need multiple commands / subcommands?
| +-> YES -> oclif (multi-command mode)
| +-> NO -> oclif (single-command mode) or plain Node.js
|
+-> Need interactive terminal UI?
| +-> Simple prompts (name, confirm)? -> Lightweight prompt library
| +-> Complex stateful UI (wizard, dashboard)? -> Ink
|
+-> Need both routing AND complex UI?
+-> YES -> oclif commands + Ink components
+-> NO -> Use whichever fits the primary need
命令文件组织
src/
commands/ # oclif command classes (.ts files)
init.ts
config/
get.ts # mycli config get <key>
set.ts # mycli config set <key> <value>
components/ # Ink React components (.tsx files)
wizard.tsx
progress.tsx
hooks/ # oclif lifecycle hooks
init.ts # Runs before every command
postrun.ts # Runs after every command
lib/ # Shared utilities
</decision_framework>
详细资源:
- examples/core.md -- 命令、标志、参数、Ink 组件、集成
- examples/advanced.md -- 向导、进度、插件、钩子、错误边界
- examples/testing.md -- 命令测试、组件测试、异步测试
<red_flags>
严重问题
高优先级:
- 缺少
await waitUntilExit()-- 命令会在 Ink UI 完成前退出,用户什么也看不到 - 在命令中使用
console.log-- 会破坏--json输出模式,且不会被@oclif/test捕获 - Ink 中使用裸字符串 -- 所有文本都必须包装在
<Text>中,否则渲染会失败 - 阻塞渲染循环 -- 组件中的同步工作会冻结终端 UI
中优先级:
- 将
.tsx文件用作命令 -- oclif 不会自动发现.tsx文件;请使用导入.tsx组件的.ts命令文件 - 缺少 Ctrl+C 处理 -- 始终通过
useInput或useApp().exit()提供退出机制 - 未在 useEffect 中清理 -- 必须取消异步操作,避免退出后更新状态
useInput钩子冲突 -- 多个处于活动状态的useInput钩子会同时触发;使用isActive选项限定其作用范围
注意事项和边缘情况:
- oclif 钩子会并行运行,而非按顺序运行——不要依赖钩子之间的执行顺序
- 粘贴文本时,
useInput对整段文本只会触发一次,而非每个字符触发一次——请显式处理多字符输入字符串 - Ink v5 需要 React 18+,Ink v6 需要 React 19+——请检查 Ink 版本的 peer dependencies
enableJsonFlag会使run()的返回值成为 JSON 输出——请确保返回类型符合调用方的预期- oclif 的
this.error()会抛出异常(并退出进程)——它不会返回
</red_flags>
<critical_reminders>
重要提醒
所有代码都必须遵循项目在 CLAUDE.md 中的约定(kebab-case、命名导出、导入顺序、
import type、命名常量)
(在 oclif 命令中调用 render() 后,必须 await waitUntilExit()——否则进程会在 UI 完成前退出)
(在命令中必须使用 this.log() / this.warn() / this.error()——console.log 会破坏 --json 模式和测试捕获)
(在 Ink 中必须将所有文本包装在 <Text> 组件中——裸字符串会导致渲染错误)
(必须使用 useEffect 清理来取消异步操作——用户按下 Ctrl+C 时,Ink 组件会卸载)
不遵循这些规则会导致进程静默退出、JSON 输出损坏以及终端渲染崩溃。
</critical_reminders>