如何快速把一个 API 变成 CLI 工具

手上有一个只提供了 REST API 的服务,想让人、脚本和 AI Agent 都能顺手用它,最快的路子是给它包一个 CLI。这篇直接动手,用 Restish 把 API 接成命令行工具,顺手包一个简化版的 gh

Restish 是什么

很多服务其实已经有 OpenAPI 文件了。

路径、参数、请求体、认证方式、返回结构,全写在里面。API 怎么被调用这件事,已经被结构化描述过一遍了。手写一堆 CLI 命令,或者每次现场拼 curl -H "Authorization: Bearer $TOKEN" ... 这种长串参数,都是在重复 OpenAPI 里已经写好的信息。

这里就可以用 Restish。

Restish 做的事情可以粗略理解成把 OpenAPI 变成一个 API-aware CLI。它不只是帮你发 HTTP 请求,还会根据 OpenAPI 里的信息生成更像专属命令的调用方式。

  • operationId 可以变成子命令
  • path/query/body 参数可以变成命令参数
  • auth/profile 可以放进本地配置
  • 输出可以切成 JSON、YAML、table、CSV,甚至 TOON 这种更适合 LLM 读取的格式

Restish 背后的人

作者是 Daniel G. Taylor,GitHub 上叫 danielgtaylor,项目现在挂在 rest-sh 组织下,Go 写的,MIT 协议,一千三百多 star。

他的项目链值得看一眼。aglio、openapi-cli-generator、apisprout、shorthand,一路做下来最后落到 Restish,全是围绕 OpenAPI 打转,前后十来年。同一个人还写了 Huma,Go 的服务端框架,OpenAPI 3.1 优先,四千多 star。一头写 API,一头调 API,两边共用同一套 OpenAPI 世界观。

Restish 里那些看着不起眼的细节,大概就是这么来的。通过 Link Header 自动发现 spec、结构化输入简写、自动跟着 next link 翻页,这些都是常年写服务端的人才会先想到的问题。

生产采用有一个挺实在的信号。Aiven 内部维护了一份 Restish fork,他们自己的说法是 “load-bearing in places”,也就是关键路径上真在用。开源工具最难得的证据就是这一种,有公司愿意把它放在出事就要半夜被叫醒的位置上。

接上 GitHub 的 OpenAPI

GitHub 自己维护了一份完整的 OpenAPI 描述文件(在 github/rest-api-description 仓库里),正好拿来试一下能不能接出一个简化版的 gh

# 安装
brew install restish

# 连接 GitHub 的 OpenAPI
restish api connect gh \
  https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json

# 看看生成了多少命令
restish gh --help

跑完 connectrestish gh --help 给出来的是一棵按资源分组的命令树。repos、pulls、issues、actions,分组跟 GitHub 文档里的分类基本对得上。查一个仓库的 PR 列表,会生成类似这样的命令。

restish gh pulls-list owner:octocat repo:hello-world

这一步省掉的,是原来自己翻 REST 文档、自己拼路径和参数名的时间。OpenAPI 里已经写好的东西,直接变成了能跑的命令。

包一层 wrapper,让它长得像 gh

pulls-list owner:octocat repo:hello-world 这种调用方式,跟真正用顺手的 gh pr list 比,还是差一截。operationId 按接口原样生成,参数也是 OpenAPI 里的原始命名,自己手敲还行,不好记,也不好讲给别人。

所以这一步要包一层更语义化的 wrapper,把底层调用藏起来。

# 底层:Restish 直接生成的命令
restish gh pulls-list owner:octocat repo:hello-world --format json

# 包装后:给人和脚本用的入口
mygh pr list --repo octocat/hello-world

mygh pr list 内部就是拼好 ownerrepo 参数后调一次 restish gh pulls-list,再把 JSON 结果整理成表格或者按需透传。前者是 API 调用层,后者是业务任务层。用的人只需要知道”查 PR 列表”和”哪个仓库”,不用关心底层 operationId 长什么样,也不用记 Restish 的调用语法。

我现在更倾向于这个结构。

OpenAPI -> Restish -> wrapper -> 人 / 脚本 / CI / Agent

Restish 负责把 API 调起来,wrapper 负责把任务讲清楚。既不需要一开始就写完整 SDK,也不用为了接 AI 先搭一套 MCP server。

从 401 到能用

第一次直接调用,大概率会拿到 401。GitHub 的大部分接口都要带 token,匿名调不了。

这时候别急着改命令。去 ~/.restish/apis.json 里把 gh 对应的 profile 配好,填一个有权限的 personal access token。配一次,之后 restish gh ... 的所有命令都能自动带上认证。接内部服务、OpenAPI 文件不在公开地址时,也可以在配置里直接指到本地文件,这点对内部服务更实用。

小结

这套办法最对症的是一种情况,你手上的服务只有 API,没有 CLI

这时候不用专门立项去写一个命令行工具。路径、参数、认证、返回结构,OpenAPI 文件里全都写好了,Restish 直接把它变成能跑的命令,你只要在上面包一层顺手的 wrapper。为一个内部服务从零手写 CLI,再自己维护参数解析、认证、分页、输出格式这一整套,多数时候没这个必要。

Restish 本身也没什么负担。Go 写的单个原生二进制,没有运行时依赖要伺候,brew install restish 一句装完,也可以用 mise 管版本。性能上它官方给过一组对比,普通请求比 HTTPie 快、跟 cURL 很接近,命中缓存的场景反过来能超过 cURL,GitHub 列仓库那一行是 1.21 秒对 0.62 秒,因为它默认就按 Cache-ControlExpires 做本地缓存。写脚本循环调接口时,这点差距会累起来。

今天这个简化版 mygh 只接了几个最常用的读操作,跟真正的 gh 比还差得远。没有交互确认,没有智能默认值(比如自动识别当前目录对应哪个仓库),错误提示也没那么贴心。剩下的功夫都在 wrapper 那一层,帮助可发现、人机输出分离、参数显式、错误可恢复、副作用有边界、接口稳定,这几条挨个补上去它才好用。

但 OpenAPI 到 Restish 再到 wrapper 这条路径已经走通了。先有一个能跑的命令,再谈更标准的协议和更完整的体验。