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 是递归结构,下面挂着 optionsargumentscommands(子命令),从而描述任意深度的命令树。

字段命名遵循 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 是个非常实用的字段——比如 --verbosegit 整棵命令树都生效,就只需在根命令声明一次。

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

注意 --filenamearity.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);
    },
  };
}

效果:一旦给 kubectlghdocker 都备好了 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-bashopencli-gen-mcpopencli-gen-python 等工具
  3. MCP 桥接器:把任意带 OpenCLI 描述的 CLI 一键暴露成 MCP server,是 LLM 应用领域明显的需求
  4. 大型工具的官方描述ghkubectldockeraws 等如果发布官方 OpenCLI 文件,整个生态会迅速起飞
  5. 稳定 v1.0:目前是 0.1,规范字段还在演进,正式落地生产还需要等版本稳定

目前的局限

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

九、参考资料


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

posted @ 2026-05-19 13:59  cwp0  阅读(69)  评论(0)    收藏  举报