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) ![Bun](https://img.shields.io/badge/Bun-1.2+-000?logo=bun&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5.8-3178C6?logo=typescript&logoColor=white) ![License](https://img.shields.io/badge/license-MIT-green)
## 项目简介 这是一个针对特定类型项目的起点:**一个封装了没有公开 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 —— 偏偏在你最需要它的地方。现在日志级别是共享的。
## 项目结构 ``` bin/ cli.ts Commander wiring. No logic. setup-mcp.ts Register the MCP server with local IDEs scripts/ init.ts Rename the template (deletes itself) src/ commands/ One file per command: formatting + control flow example.ts Copy this to start login.ts login / whoami / logout (delete if not needed) config/constants.ts ← START HERE lib/ banner.ts Startup banner concurrency.ts mapLimit worker pool errors.ts Typed error hierarchy driving retry behaviour logger.ts Leveled, stderr-only text.ts fold / slugify / stripHtml / wrap services/ api/client.ts The ONLY place that touches the network auth/ Session persistence and expiry cache/ Disk cache with TTL catalog.ts name/alias/id → real id mcp/server.ts MCP tools types/index.ts Raw* + domain types tests/ Offline tests; *.live.test.ts for contract tests discovery/ How to reverse-engineer the API AGENTS.md Conventions for AI coding agents ``` **依赖规则:** `commands → services → lib`,绝不反向。一个 command 不能包含 `fetch`;一个 service 不能包含 `console.log`。 ## 开发约定 | 规则 | 原因 | |---|---| | 将日志输出到 **stderr**,数据输出到 **stdout** | `--json` 保持可通过管道传输;MCP stdio 保持有效 | | 每个命令都支持 `--json` | 可脚本化不是一项可以事后补上的功能 | | `Raw*` 类型保留原始的丑陋字段名 | 类型不匹配会变成编译错误,而不是 `undefined` | | 永远不要缓存实时状态 | 被缓存的预约名额是一种谎言 | | 在数据结构改变时增加 `CACHE_VERSION` | 比进行缓存迁移成本更低 | | 成功时退出码为 `0`,失败时为 `1` | 脚本和 cron 任务依赖于此 | 完整版本见 [`AGENTS.md`](AGENTS.md)。 ## MCP ``` bun run setup-mcp # detect and register with local IDEs bun run setup-mcp -- --remove ``` 工具会在你的 package name 下注册,因此基于此 模板的多个项目可以共存。封装你的命令所使用的相同服务函数 —— 永远不要在工具中重新实现逻辑。 ## 法律声明 此模板用于与你有权使用的服务进行互操作。 在发布任何基于此构建的内容之前: - 尊重该网站的服务条款和 `robots.txt` - 保持请求频率与人类浏览相当 (`SCAN_CONCURRENCY`) - 不要对稀缺的公共资源执行写入自动化 - 不要提交捕获的 session、HAR 文件或个人数据 ## 许可证 MIT
标签:API逆向, Bun, CLI脚手架, MCP, SOC Prime, TypeScript, 安全插件, 开发工具, 自动化攻击