mcpField
C
cli-framework-oclif-ink

cli-framework-oclif-ink

github.com/agents-inc开发2646 次安装2 天前更新
代码仓库

关于 cli-framework-oclif-ink

cli-framework-oclif-ink 是用于使用 oclif 构建 TypeScript 命令行应用、使用 Ink 编写交互式终端界面的技能,涵盖集成、插件、生命周期处理与测试。

主要功能

  1. 01oclif 命令、标志与参数
  2. 02基于 React 的 Ink 终端界面
  3. 03交互式向导与进度模式
  4. 04插件架构与生命周期钩子
  5. 05命令和组件测试

适用场景

  1. 01构建多命令 CLI
  2. 02添加终端向导或仪表盘
  3. 03将 oclif 命令与 Ink 界面结合
  4. 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.yaml
examples/
advanced.md
core.md
testing.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>


详细资源:


<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>

评论

支持 Markdown