JGaldo-beep/bun-cli-boilerplate
GitHub: JGaldo-beep/bun-cli-boilerplate
一个基于 Bun 和 TypeScript 的 CLI 脚手架,专门帮助开发者快速封装没有公开 API 的 Web 服务接口,并将踩坑经验预先内置。
Stars: 0 | Forks: 0
```
╔╦╗ ╦ ╦ bun-cli-boilerplate
║║║ ╚╦╝ Ship a CLI for an undocumented API in an afternoon.
╩ ╩ ╩ Bun · TypeScript · MCP · agent-browser
```
[快速开始](#quick-start) ·
[包含内容](#what-you-get) ·
[工作流程](#the-workflow) ·
[内置经验](#lessons-baked-in) ·
[项目结构](#structure)



## 项目简介
这是一个针对特定类型项目的起点:**一个封装了没有公开 API 的 Web 服务的 CLI**。你通过逆向工程获取该网站自身前端调用的 endpoint,然后将它们暴露为命令。
它不是一个通用的“CLI 启动模板”。这里的一切之所以存在,是因为构建这类工具每次都会在同样几个地方出错,而这些失败已经被预先解决了:
- 在 `login` 一小时后静默过期的 session
- 硬编码的 id 悄无声息地返回错误实体的数据
- 包含 HTML 的 `200` 响应最终报出 `unexpected token <`
- 不断重试 `401` 直到账户被锁定
- 对于用户唯一关心的内容,缓存却提供陈旧数据
从实际运行的 CLI 中提取而来 —— [`migracion-citas-cli`](https://github.com/JGaldo-beep/migracion-citas-cli),
`transmilenio-cli`, `cine-colombia-cli`。
## 快速开始
```
# 获取没有 git 历史记录的 template
bunx degit JGaldo-beep/bun-cli-boilerplate my-cli
cd my-cli && bun install
# 重命名所有内容
bun run init
# 确认它是绿色的
bun run verify
# 在编写代码前映射 API — 查看 discovery/README.md
```
`bun run init` 会重写 `package.json`、`src/config/constants.ts` 中的 `APP INFO` / `TARGET SERVICE` 块以及 README,然后自动删除。
非交互式形式:
```
bun run init --name transmilenio-cli --bin transmilenio \
--url https://www.transmilenio.gov.co \
--description "Bus routes from your terminal."
```
不写入进行预览:添加 `--dry-run`。
## 包含内容
| | |
|---|---|
| **HTTP 客户端** | 带有 backoff 的重试、超时、`AbortController`、4xx 不重试、HTML 外壳检测、针对 WAF 的 curl 降级方案 |
| **Session 持久化** | 位于 `~/.{app}/session.json` 权限 `0600`,JWT `exp` 解码,主动刷新,cookie 合并,真实的 `whoami` |
| **磁盘缓存** | 按 key 设置 TTL,版本命名空间隔离,损坏的条目会降级为未命中 |
| **目录解析器** | 用户输入 `medellin`、`15` 或 `Medellín` → 转换为真实 id,并且 **在出现歧义时抛出异常,而不是盲目猜测** |
| **并发控制** | `mapLimit` worker 池,保持顺序且处理优雅 |
| **文本工具** | 去除重音、slugify、HTML 转文本、自动换行 |
| **MCP server** | 通过 `bun run setup-mcp` 将你的命令暴露给 Claude / Cursor / Windsurf |
| **发现工具包** | agent-browser 提示词、笔记模板和陷阱清单 |
| **Agent 指令** | `AGENTS.md`,确保编码 agent 遵循规范 |
| **CI** | Lint、类型检查、测试、构建 —— 外加一个用于脚手架创建新项目并进行验证的作业 |
仅有四个运行时依赖:`commander`、`picocolors`、`zod`、
`@modelcontextprotocol/sdk`。没有 axios,没有 dotenv,没有 inquirer。
## 工作流程
### 1. 发现 —— 在编写任何代码之前
```
npm i -g agent-browser && agent-browser install
```
使用 [`discovery/`](discovery/) 映射服务:记录流量,阅读 JS
bundles,将每个 endpoint 分类为公开 / 需认证 / 不存在,并将真实的 payload 写入 `discovery/discovery-notes.md`。
两件人们经常跳过并为此后悔的事情:
**检查哪些是真正公开的。** UI 中的登录墙并不意味着 API 需要认证。在 `migracion-citas-cli` 中,*所有*读取 endpoint 结果都是公开的 —— 这从关键路径中移除了整个登录流程。
```
curl -s -o /dev/null -w "%{http_code}\n" https://site/api/things
```
**解码 session token。** `exp - iat` 是真正的生命周期。你在浏览器中看到的“因不活动而登出”的计时器通常是前端的一个 `setTimeout`,复制它会丢弃有效的 session。
### 2. 构建
1. `src/config/constants.ts` — URL、`SESSION_COOKIE`、缓存 TTL
2. `src/types/index.ts` — 从真实 payload 粘贴的 `Raw*` 类型
3. `src/services/api/client.ts` — endpoint 方法,在这里将 Raw 映射为领域模型
4. `src/services/` — 领域逻辑
5. `src/commands/` — 复制 `example.ts`;仅处理格式化和控制流
6. `src/mcp/server.ts` — 封装*相同*的服务函数
### 3. 验证
```
bun run verify # lint + type-check + test
bun run build # standalone binary, no Bun needed to run it
```
## 内置经验
这些每一个都曾是发布过的 bug,现在你可以在代码中直接继承已修复的版本。
Session 静默过期
原始的 session 管理器在 `expiresAt` 缺失时总是从 `isValid()` 返回 `true` —— 而且从来没有地方设置过它。`login` 运行正常,`whoami` 宣称成功,而一小时后每个真正的命令都会 401。 现在:过期时间在每次写入时都会被打上标记,在可能的情况下从 JWT `exp` 中解码,并且 `client.ensureFreshSession()` 会主动刷新。 ``` // src/services/auth/session-manager.ts if (!expiresAt) return false // never "assume valid when unknown" ```刷新导致你丢失其他 cookie
刷新 endpoint 通常只重新签发凭证 cookie。替换整个 cookie jar 会丢失其余部分,下一次请求会带着本应正常的 token 返回 401。 ``` mergeCookies('session=old; logged=1', 'session=new') // → 'session=new; logged=1' ```硬编码的 id 返回错误数据
硬编码的映射将 `medellin → 14`。Id 14 实际上是 **Manizales**。用户得到了另一个城市的结果,而且完全没有报错。 现在 id 永远不会被硬编码:目录会被获取、缓存,并按名称解析。别名指向的是*名称*而不是 id,因此它们能在重新编号后依然有效。模糊匹配猜错了
`cali` 是 `MAICAO` 的子字符串。有三个城市以 `PUERTO` 开头。 `match()` 使用严格的优先级 —— id、别名、精确 slug、精确名称、唯一前缀、唯一子字符串 —— 并且在出现歧义时 **抛出异常**: ``` "puerto" is ambiguous. Matches: PUERTO CARREÑO, PUERTO INÍRIDA, PUERTO LEGUÍZAMO ```包含 HTML 的 200 响应
SPA 会为未知路由提供其应用外壳(HTML),而不是返回 404。用户看到 `unexpected token <`,却完全不知道 API 已经发生了更改。 ``` if (/^\s*重试 4xx 会锁定账户
重试三次错误的密码并不会让它变正确。`AuthError`、 `ValidationError` 和 `NOT_JSON` 永远不会被重试。读取了两次 body
`Response.text()` 会消耗数据流;随后的 `.json()` 会抛出与实际问题无关的错误。传输层现在只读取一次并基于字符串进行后续操作。--debug 无法触及网络层
`const log = logger.child('API')` 在模块加载时运行,早于标志位的解析。一个在创建时快照了日志级别的子 logger 永远无法被切换到 debug —— 偏偏在你最需要它的地方。现在日志级别是共享的。标签:API逆向, Bun, CLI脚手架, MCP, SOC Prime, TypeScript, 安全插件, 开发工具, 自动化攻击