OpenCLI深度调研

OpenCLI 深度调研

调研时间:2026-05-19
规范版本:OpenCLI Specification v0.1(草案)
官方站点:https://opencli.org
仓库:https://github.com/spectreconsole/open-cli

目录


一、什么是 OpenCLI

OpenCLI Specification(OCS) 是一种平台无关、语言无关的 CLI(命令行)应用接口描述规范。它用一份 JSON 或 YAML 文档来声明一个 CLI 工具的命令树、参数、选项、退出码等元信息——使得人类和机器都可以在不阅读源码、不查 man 文档的情况下理解这个 CLI 工具应该如何被调用。

它的定位类比起来非常清楚:

OpenAPI 之于 HTTP API,就是 OpenCLI 之于 CLI。

OpenCLI 由 Spectre.Console 团队(以 Patrik Svensson 为主要维护者)发起,规范文档明确说"heavily influenced by the OpenAPI specification",整个对象模型、字段命名风格、版本控制策略都在向 OpenAPI 看齐。

目前的状态是 v0.1 提案(draft),仍处于积极征集社区反馈的阶段,未发布稳定版。


二、为什么需要 OpenCLI

CLI 工具的接口长期以来是"野生的"——每个工具的 --help 各搞一套格式,自动补全脚本要为 bash/zsh/fish/powershell 重复写多份,文档要靠手维护,AI Agent 想自动调用一个 CLI 还得反复尝试。OpenCLI 想解决的痛点:

痛点 现状 OpenCLI 带来的可能
文档与实际不一致 --help 输出和官方网站文档分两份维护 单一描述文件作为 single source of truth,文档自动生成
自动补全脚本重复造 bash/zsh/fish/pwsh 各写一份补全 从一份描述生成多种 shell 的补全脚本
AI / MCP 调用 CLI 困难 LLM 必须解析自然语言 help 信息 直接读结构化描述,转成 MCP 工具 schema
API 变更难追踪 选项被改名/删除没人通知 两个版本的描述做 diff,自动检测破坏性变更
跨语言客户端 想用 Python 包装一个 Go CLI 要手写 wrapper 从描述文件生成强类型客户端

官方 README 给出的五大用途,浓缩为一句话就是:用一份机器可读的描述,撬动 CLI 工具周边的所有自动化。


三、规范结构总览

一份 OpenCLI 描述文档以 Document Object 为根,主要包含四大块:

OpenCLI Document
├── opencli        : string       # 规范版本号(如 "0.1")
├── info           : CliInfo      # 工具元信息(标题、版本、license、联系方式)
├── command        : Command      # 根命令(递归嵌套子命令)
└── conventions    : Conventions  # 工具的语法约定(短选项分组、分隔符等)

Command 是递归结构,下面挂着 options、arguments、commands(子命令),从而描述任意深度的命令树。

字段命名遵循 camelCase,全部 大小写敏感。数组中元素的顺序具有规范意义(normative),生成器在渲染帮助/补全时应按声明顺序展示。


四、核心对象详解

4.1 Document Object(根对象)

字段 类型 必填 说明
opencli string ✓ 规范版本号
info CliInfo ✓ CLI 元信息
command Command ✓ 根命令
conventions Conventions ✗ 语法约定

4.2 CliInfo Object

字段 类型 必填 说明
title string ✓ 应用标题
version string ✓ 应用版本
summary string ✗ 一句话简介
description string ✗ 详细描述
contact Contact ✗ 联系人/组织信息
license License ✗ 许可证(支持 SPDX 标识)

4.3 Command Object(递归核心)

字段 类型 默认 说明
name string - 必填,命令名
aliases string[] - 别名
description string - 命令描述
options Option[] - 该命令的选项
arguments Argument[] - 该命令的位置参数
commands Command[] - 子命令(递归)
exitCodes ExitCode[] - 退出码列表
examples string[] - 使用示例(字符串形式)
interactive bool false 是否需要交互输入
hidden bool false 是否隐藏(不显示在帮助中)
metadata Metadata[] - 扩展元数据

4.4 Option Object

字段 类型 默认 说明
name string - 必填,选项主名(如 --verbose)
aliases string[] - 短别名(如 -v)
required bool false 是否必填
arguments Argument[] - 选项后跟的值
group string - 选项分组(用于帮助分类显示)
recursive bool false 是否递归向子命令继承(即 global flag)
description string - 描述
hidden bool false 是否隐藏

recursive: true 是个非常实用的字段——比如 --verbose 在 git 整棵命令树都生效,就只需在根命令声明一次。

4.5 Argument Object

字段 类型 默认 说明
name string - 必填,参数名
required bool false 是否必填
arity Arity {min:1, max:1} 接受的值数量范围
acceptedValues string[] - 枚举可接受的值
group string - 参数分组
description string - 描述

4.6 Arity Object

{ "minimum": 1, "maximum": 1 }

maximum 不指定(nil)表示不限数量,常用于变长参数(vararg)。

4.7 ExitCode Object

字段 类型 必填 说明
code int ✓ 退出码
description string ✗ 含义

4.8 Conventions Object

字段 类型 默认 说明
groupOptions bool true 是否允许短选项合并(如 -rf)
optionArgumentSeparator string " "(空格) 选项与值的分隔符(也可以是 =)

4.9 Metadata Object

{ name, value } 键值对,用于规范未覆盖到的扩展信息——典型用法是写入"该命令对应的 MCP 工具名称"、"权限分级"、"内部工单号"等。


五、实战示例

5.1 最小可用示例:一个 greet 工具

假设我们有一个简单 CLI:greet --name Alice --shout,对应的 OpenCLI 描述:

{
  "$schema": "https://opencli.org/draft.json",
  "opencli": "0.1",
  "info": {
    "title": "greet",
    "version": "1.0.0",
    "summary": "A friendly greeter",
    "license": { "identifier": "MIT" }
  },
  "command": {
    "name": "greet",
    "description": "Print a greeting",
    "options": [
      {
        "name": "--name",
        "aliases": ["-n"],
        "required": true,
        "description": "Whom to greet",
        "arguments": [
          { "name": "NAME", "required": true }
        ]
      },
      {
        "name": "--shout",
        "description": "Shout the greeting in upper case"
      }
    ],
    "exitCodes": [
      { "code": 0, "description": "Success" },
      { "code": 1, "description": "Missing name" }
    ],
    "examples": [
      "greet --name Alice",
      "greet -n Bob --shout"
    ]
  }
}

5.2 带子命令的示例(仿 .NET CLI)

这是 OpenCLI 官方仓库里 examples/dotnet.json 的精简版,演示 根命令 + 子命令 + 选项+值 的完整结构:

{
  "$schema": "https://opencli.org/draft.json",
  "opencli": "0.1",
  "info": {
    "title": "dotnet",
    "version": "9.0.1",
    "description": "The .NET CLI",
    "license": {
      "name": "MIT License",
      "identifier": "MIT",
      "url": "https://opensource.org/license/mit"
    }
  },
  "command": {
    "name": "dotnet",
    "options": [
      { "name": "--help", "aliases": ["-h"], "description": "Display help." },
      { "name": "--info", "description": "Display .NET information." },
      { "name": "--list-sdks", "description": "Display the installed SDKs." },
      { "name": "--list-runtimes", "description": "Display the installed runtimes." }
    ],
    "commands": [
      {
        "name": "build",
        "description": "Builds a project and all of its dependencies.",
        "arguments": [
          {
            "name": "PROJECT | SOLUTION",
            "description": "The project or solution file to operate on. If a file is not specified, the command will search the current directory for one."
          }
        ],
        "options": [
          {
            "name": "--configuration",
            "aliases": ["-c"],
            "description": "The configuration to use for building the project. The default for most projects is 'Debug'.",
            "arguments": [
              {
                "name": "CONFIGURATION",
                "required": true,
                "arity": { "minimum": 1, "maximum": 1 },
                "acceptedValues": ["Debug", "Release"]
              }
            ]
          }
        ]
      }
    ]
  }
}

5.3 复杂示例:仿 git commit 的多分组选项

git commit 选项分了好几组(提交内容、消息、作者)。OpenCLI 通过 group 字段表达这种结构:

{
  "name": "commit",
  "description": "Record changes to the repository",
  "options": [
    {
      "name": "--all",
      "aliases": ["-a"],
      "group": "Content",
      "description": "Stage all modified and deleted files"
    },
    {
      "name": "--message",
      "aliases": ["-m"],
      "required": true,
      "group": "Message",
      "description": "Commit message",
      "arguments": [
        { "name": "MSG", "required": true, "arity": { "minimum": 1, "maximum": 1 } }
      ]
    },
    {
      "name": "--author",
      "group": "Author",
      "description": "Override author for commit",
      "arguments": [
        { "name": "AUTHOR", "required": true }
      ]
    },
    {
      "name": "--amend",
      "group": "Content",
      "description": "Amend previous commit"
    }
  ],
  "exitCodes": [
    { "code": 0, "description": "Commit created" },
    { "code": 1, "description": "Nothing to commit / commit aborted" },
    { "code": 128, "description": "Repository error" }
  ]
}

5.4 全局选项(recursive)示例

模拟 kubectl --namespace foo get pods 这种"在任何子命令下都生效"的全局选项:

{
  "command": {
    "name": "kubectl",
    "options": [
      {
        "name": "--namespace",
        "aliases": ["-n"],
        "recursive": true,
        "description": "Kubernetes namespace (applies to all subcommands)",
        "arguments": [{ "name": "NS", "required": true }]
      },
      {
        "name": "--kubeconfig",
        "recursive": true,
        "description": "Path to kubeconfig file",
        "arguments": [{ "name": "PATH", "required": true }]
      }
    ],
    "commands": [
      {
        "name": "get",
        "commands": [
          { "name": "pods", "description": "List pods" },
          { "name": "services", "description": "List services" }
        ]
      },
      {
        "name": "apply",
        "options": [
          {
            "name": "--filename",
            "aliases": ["-f"],
            "required": true,
            "arguments": [
              { "name": "FILE", "arity": { "minimum": 1, "maximum": null } }
            ]
          }
        ]
      }
    ]
  }
}

注意 --filename 的 arity.maximum 设为 null,表示可以重复多次(-f a.yaml -f b.yaml -f c.yaml)。

5.5 交互式命令示例

声明 interactive: true 让消费者(如 MCP Server、CI 系统)知道这个子命令需要 TTY、不能直接非交互调用:

{
  "name": "login",
  "description": "Sign in to the platform",
  "interactive": true,
  "options": [
    {
      "name": "--token",
      "description": "Skip interactive prompt by providing a token directly",
      "arguments": [{ "name": "TOKEN", "required": true }]
    }
  ]
}

六、应用场景与落地

OpenCLI 的真正价值在于"一份描述,多种产物"。以下是五个目前已经在路线图里、或者社区已经开始尝试的方向。

6.1 自动生成多种 Shell 的补全脚本

一份 mytool.opencli.json,可以喂给生成器分别产出 bash/zsh/fish/powershell 补全。伪代码示意(Node 实现):

import { readFileSync } from "fs";
const doc = JSON.parse(readFileSync("mytool.opencli.json", "utf8"));

function genBashCompletion(cmd, prefix = "") {
  const subs = (cmd.commands || []).map(c => c.name).join(" ");
  const opts = (cmd.options || []).flatMap(o => [o.name, ...(o.aliases || [])]).join(" ");
  let script = `_${cmd.name}_${prefix || "root"}() {\n`;
  script += `  local cur="\${COMP_WORDS[COMP_CWORD]}"\n`;
  script += `  COMPREPLY=( $(compgen -W "${subs} ${opts}" -- "$cur") )\n`;
  script += `}\n`;
  for (const sub of cmd.commands || []) {
    script += genBashCompletion(sub, cmd.name);
  }
  return script;
}

console.log(genBashCompletion(doc.command));
console.log(`complete -F _${doc.command.name}_root ${doc.command.name}`);

zsh、fish、PowerShell 同理替换模板,一份描述就够。

6.2 自动生成 Markdown / HTML 文档

基于 OpenCLI 描述渲染美观的文档,避免 README 与 --help 漂移。最小渲染逻辑(Python):

import json

def render(cmd, depth=2):
    md = f"{'#' * depth} `{cmd['name']}`\n\n"
    if cmd.get("description"):
        md += cmd["description"] + "\n\n"
    if cmd.get("arguments"):
        md += "**Arguments:**\n\n"
        for a in cmd["arguments"]:
            req = " *(required)*" if a.get("required") else ""
            md += f"- `{a['name']}`{req} — {a.get('description','')}\n"
        md += "\n"
    if cmd.get("options"):
        md += "**Options:**\n\n| Option | Aliases | Description |\n|---|---|---|\n"
        for o in cmd["options"]:
            md += f"| `{o['name']}` | {', '.join(o.get('aliases', []))} | {o.get('description','')} |\n"
        md += "\n"
    if cmd.get("examples"):
        md += "**Examples:**\n\n```bash\n" + "\n".join(cmd["examples"]) + "\n```\n\n"
    for sub in cmd.get("commands", []):
        md += render(sub, depth + 1)
    return md

doc = json.load(open("mytool.opencli.json"))
print(f"# {doc['info']['title']} v{doc['info']['version']}\n")
print(doc['info'].get('description', ''))
print(render(doc["command"]))

6.3 转成 MCP(Model Context Protocol)工具,让 AI 直接调

这是 OpenCLI 最被官方强调的一个目标场景。MCP 把外部能力暴露给 LLM,传统做法是为每个 CLI 工具手写一个 MCP server。有了 OpenCLI,可以做一层通用桥:

// 把 OpenCLI 描述里的每个叶子命令,注册成一个 MCP tool
import { spawn } from "node:child_process";

function commandToMcpTool(rootBin: string, path: string[], cmd: any) {
  const properties: Record<string, any> = {};
  const required: string[] = [];

  for (const opt of cmd.options || []) {
    const key = opt.name.replace(/^-+/, "");
    properties[key] = { type: opt.arguments?.length ? "string" : "boolean", description: opt.description };
    if (opt.required) required.push(key);
  }
  for (const arg of cmd.arguments || []) {
    properties[arg.name] = { type: "string", description: arg.description };
    if (arg.required) required.push(arg.name);
  }

  return {
    name: [rootBin, ...path, cmd.name].join("_"),
    description: cmd.description,
    inputSchema: { type: "object", properties, required },
    handler: async (input: Record<string, any>) => {
      const args = [...path, cmd.name];
      for (const opt of cmd.options || []) {
        const key = opt.name.replace(/^-+/, "");
        if (key in input) {
          args.push(opt.name);
          if (opt.arguments?.length) args.push(String(input[key]));
        }
      }
      for (const arg of cmd.arguments || []) {
        if (arg.name in input) args.push(String(input[arg.name]));
      }
      return await runProcess(rootBin, args);
    },
  };
}

效果:一旦给 kubectl、gh、docker 都备好了 OpenCLI 描述,AI Agent 就能"零定制成本"调用它们。

6.4 CLI API 变更检测

把两个版本的 OpenCLI 描述做语义 diff,可以自动告警破坏性变更:

# 伪代码工具
opencli-diff dotnet@8.0.json dotnet@9.0.json

# 输出:
# [BREAKING] Command `dotnet build`: option `--no-restore` was renamed to `--skip-restore`
# [ADDED]    Command `dotnet workload`
# [CHANGED]  Option `--configuration` of `dotnet build`: acceptedValues changed
#            from [Debug, Release] to [Debug, Release, ReleaseAOT]

这对发布 SDK、维护 CI 模板、写自动化脚本的团队非常有价值——以前这些信息要靠 release notes 手工梳理。

6.5 跨语言客户端代码生成

类似 OpenAPI Generator,可以从 OpenCLI 描述生成 Python、Go、Java 等语言的强类型 wrapper:

# 自动生成的 dotnet_client.py 片段
class DotnetClient:
    def __init__(self, executable: str = "dotnet"):
        self._exe = executable

    def build(
        self,
        project: str | None = None,
        configuration: Literal["Debug", "Release"] | None = None,
    ) -> subprocess.CompletedProcess:
        """Builds a project and all of its dependencies."""
        args = [self._exe, "build"]
        if project: args.append(project)
        if configuration: args += ["--configuration", configuration]
        return subprocess.run(args, capture_output=True, text=True, check=False)

调用方就有了 IDE 补全、类型检查、文档悬浮提示——而不是直接拼字符串。


七、与 OpenAPI 的对照

OpenCLI 的设计师明确把 OpenAPI 当模板,二者的概念基本一一对应:

维度 OpenAPI OpenCLI
描述对象 HTTP API CLI 应用
核心概念 Path / Operation Command(递归)
输入 Parameters / RequestBody Options / Arguments
输出 Responses(状态码) ExitCodes(退出码)
错误模型 HTTP status + schema Exit code + description
元信息 info 块(title/version/license/contact) info 块(结构几乎一致)
文档格式 JSON / YAML JSON / YAML
版本号 openapi: 3.1.0 opencli: 0.1
扩展机制 x-* 字段 metadata: [{name, value}]
工具生态 Swagger UI、Redoc、Generator (建设中)补全生成、MCP 转换、文档生成

差别主要在于 CLI 特有的概念:

  • 递归命令树(OpenAPI 是扁平的 path 列表,CLI 是树)
  • arity(一个选项可以接受 N 个值)
  • interactive(标识是否需要 TTY)
  • conventions(描述短选项是否能合并、用什么分隔符)
  • recursive(全局 flag)

八、现状与未来展望

当前状态(截至 2026-05):

  • 规范版本:v0.1,明确标注是 proposal,欢迎社区反馈
  • 维护方:Spectre.Console 组织(Patrik Svensson 等)
  • 仓库:spectreconsole/open-cli,包含规范文档(draft.md)、JSON Schema(schema.json)、TypeSpec 定义(typespec/main.tsp)和 dotnet 示例(examples/dotnet.json)
  • 工具链:基于 TypeSpec 维护规范源,自动生成 JSON Schema;构建用 Cake / .NET 9 SDK
  • 站点:https://opencli.org(Docusaurus 搭建)

最近的规范变更(节选自 changelog):

日期 变更
2026-04-19 根命令也成为 Command Object 的实例(统一了模型)
2026-03-24 arity 默认值改为 {minimum: 1, maximum: 1}
2025-10-04 schema 增加默认值
2025-08-06 Option 新增 recursive 字段;Argument 移除 ordinal 字段
2025-07-15 新增 interactive 字段
2025-07-16 新增 Metadata Object;map 改成数组(保证顺序稳定)

值得期待的方向:

  1. 官方 .NET SDK:示例页面预留了 ".NET SDK" 入口,将来很可能与 System.CommandLine 和 Spectre.Console.Cli 深度集成,让你直接 [OpenCli] 标注就能导出描述
  2. 跨语言生成器:对标 openapi-generator,社区会涌现 opencli-gen-bash、opencli-gen-mcp、opencli-gen-python 等工具
  3. MCP 桥接器:把任意带 OpenCLI 描述的 CLI 一键暴露成 MCP server,是 LLM 应用领域明显的需求
  4. 大型工具的官方描述:gh、kubectl、docker、aws 等如果发布官方 OpenCLI 文件,整个生态会迅速起飞
  5. 稳定 v1.0:目前是 0.1,规范字段还在演进,正式落地生产还需要等版本稳定

目前的局限:

  • 只是草案,字段还会变(生产环境引入需评估锁版本)
  • 工具链稀薄,主要还是 .NET 生态
  • 还没有大型 CLI 工具发布官方描述文件
  • 缺少校验(validation)规则的标准化(比如"互斥选项"、"依赖选项"还没有原生支持,目前只能塞进 metadata)

九、参考资料


一句话总结:OpenCLI 想做"CLI 世界的 OpenAPI"——用一份机器可读的 JSON/YAML 把 CLI 工具描述清楚,让文档、补全脚本、AI 调用、跨语言 wrapper、变更检测全部自动化。它现在还只是 v0.1 提案,但思路对、问题真,是个值得持续关注的规范。

posted @ 2026-05-19 14:00  cwp0  阅读(290)  评论(0)    收藏  举报