从零搭建 CLI 工具 — brainstorming + TDD + writing-plans 防翻车三件套

21 阅读7分钟

从零搭建 CLI 工具 — brainstorming + TDD + writing-plans 防翻车三件套

🛡️ "帮我写个 CLI 工具" → AI 哗哗输出 500 行代码 → 跑不起来 → 改了 3 小时 → 还是不对。听起来熟悉吗?这篇文章教你用三个 Skills 组合,让 AI 从"冲动型选手"变成"靠谱的工程搭档"。


🎯 场景与挑战

你想开发一个 Markdown 链接检查器 CLI 工具(mdlink-check),它能:

  • 扫描 .md 文件中的所有超链接
  • 并发检测链接是否存活(HTTP 状态码)
  • 输出一份报告:哪些链接是死链

听起来很简单对吧?但如果你直接告诉 AI "帮我写一个 Markdown 链接检查器",大概率会翻车:

❌ 翻车场景一:AI 一口气写了 400 行代码,但没处理相对链接
❌ 翻车场景二:HTTP 请求没有超时控制,遇到慢站点直接卡死
❌ 翻车场景三:输出格式是纯文本,不支持 JSON 输出,后续自动化没法用
❌ 翻车场景四:"完成了"——但根本没跑过测试

核心问题:AI 不是能力不够,而是"没有约束"地写代码。


🧰 Skills 组合

本篇用到 Superpowers 的三个核心 Skills:

Skill职责解决什么问题
🧠 brainstorming在写代码前逐步澄清需求防止"需求没想清楚就动手"
📋 writing-plans把需求拆成原子化的 TDD 任务防止"AI 一口气写完改不动"
🔴 test-driven-developmentRED-GREEN-REFACTOR 循环防止"代码写了但没验证"
工作流:
brainstorming(想清楚)→ writing-plans(拆清楚)→ TDD(做清楚)

🛠️ 实战过程

Phase 1:brainstorming — 在写代码前踩刹车

你对 AI 说出需求:

我想做一个 Markdown 链接检查器 CLI 工具。

→ AI 不会直接写代码。它会自动激活 brainstorming,开始逐个问你问题:

AI 第一个问题:

链接检查的范围包括哪些?

A) 仅 HTTP/HTTPS 外部链接
B) 外部链接 + 本地文件链接(相对路径)
C) 外部链接 + 本地链接 + 锚点链接(#section)

我推荐选 B,理由是:
- A 太窄,文档项目的相对链接失效同样痛苦
- C 过于复杂,锚点检查需要解析目标文件的标题结构
- MVP 先覆盖最常见的场景

你回答: B,同意。

AI 第二个问题:

HTTP 请求的并发策略用哪种?

A) 串行请求(简单但慢)
B) 固定并发池(如最多 10 个并发请求)
C) 自适应并发(根据目标域名限速)

推荐 B,固定并发池。理由:
- 实现简单,够用
- 用 p-limit 库一行代码搞定
- 后续可以升级为 C

你回答: B。

AI 第三个问题:

输出格式支持什么?

A) 纯文本(人类可读)
B) JSON(机器可读)
C) 两者都支持,用 --format 参数切换

推荐 C。理由:
- 终端使用时看纯文本
- CI/CD 集成时解析 JSON
- 用 commander.js 的 option 即可实现

你回答: C。

经过 3 轮问答,AI 生成设计文档并提交到 Git:

🧠 [Brainstorming] 设计文档已保存:
   docs/superpowers/specs/mdlink-check-design.md

   需求确认:
   ✅ 检查 HTTP/HTTPS 外部链接
   ✅ 检查本地文件相对链接
   ✅ 固定并发池(p-limit, 10并发)
   ✅ 双输出格式(text/JSON, --format 参数)
   ✅ 支持 glob 模式指定扫描目录
   ✅ 超时设置(默认 10s, --timeout 参数)

💡 关键价值:3 轮提问花了 5 分钟,但帮你避免了后面至少 2 小时的返工。代价极低,收益极高。


Phase 2:writing-plans — 把设计拆成原子任务

brainstorming 完成后,AI 自动进入 writing-plans,基于设计文档生成实施计划:

# mdlink-check 实施计划

### Task 1: 项目脚手架
- [ ] 初始化 Node.js 项目(TypeScript + Vitest)
- [ ] 安装依赖:commander, p-limit, node-fetch, glob
- [ ] 配置 tsconfig.json 和 vitest.config.ts
- [ ] 验证空项目跑通
- [ ] 提交

### Task 2: Markdown 链接提取器
- [ ] 写测试 — 从 Markdown 文本中提取所有链接
- [ ] 运行测试,确认失败 🔴
- [ ] 实现 extractLinks() 函数
- [ ] 运行测试,确认通过 🟢
- [ ] 写测试 — 区分外部链接和本地链接
- [ ] 运行测试,确认失败 🔴
- [ ] 实现链接分类逻辑
- [ ] 运行测试,确认通过 🟢
- [ ] 提交

### Task 3: HTTP 链接检查器
- [ ] 写测试 — 检查可达链接返回 200
- [ ] 运行测试,确认失败 🔴
- [ ] 实现 checkHttpLink() 函数
- [ ] 运行测试,确认通过 🟢
- [ ] 写测试 — 超时场景返回 timeout 错误
- [ ] 运行测试,确认失败 🔴
- [ ] 实现超时处理
- [ ] 运行测试,确认通过 🟢
- [ ] 提交

### Task 4: 本地链接检查器
- [ ] 写测试 — 检查存在的本地文件返回 ok
- [ ] 运行测试,确认失败 🔴
- [ ] 实现 checkLocalLink() 函数
- [ ] 运行测试,确认通过 🟢
- [ ] 写测试 — 不存在的文件返回 not-found
- [ ] 运行测试,确认失败 🔴
- [ ] 实现文件不存在处理
- [ ] 运行测试,确认通过 🟢
- [ ] 提交

### Task 5: CLI 入口与报告输出
- [ ] 写测试 — 文本格式输出包含状态标记
- [ ] 运行测试,确认失败 🔴
- [ ] 实现 CLI 入口和文本格式报告
- [ ] 运行测试,确认通过 🟢
- [ ] 写测试 — JSON 格式输出可被 JSON.parse
- [ ] 运行测试,确认失败 🔴
- [ ] 实现 JSON 格式输出
- [ ] 运行测试,确认通过 🟢
- [ ] 提交

注意看:每个 Task 都严格遵循 TDD 步骤——先测试(🔴),后实现(🟢)。


Phase 3:TDD 执行 — RED-GREEN-REFACTOR

以 Task 2(链接提取器)为例,展示 TDD 循环的实际过程:

🔴 RED — 写失败测试

// tests/extract-links.test.ts
import { extractLinks } from '../src/extract-links';

test('从 Markdown 文本中提取所有链接', () => {
  const markdown = `
# 测试文档
这是一个 [外部链接](https://example.com)。
这是一个 [本地链接](./README.md)。
这是一个 [图片](https://img.example.com/photo.jpg)。
  `;

  const links = extractLinks(markdown);

  expect(links).toHaveLength(3);
  expect(links[0]).toEqual({
    text: '外部链接',
    url: 'https://example.com',
    type: 'external',
  });
  expect(links[1]).toEqual({
    text: '本地链接',
    url: './README.md',
    type: 'local',
  });
});

确认失败

$ npx vitest run extract-links
FAIL: ReferenceError: extractLinks is not defined

✅ 好——测试因为"函数不存在"而失败,这正是我们期望的。

🟢 GREEN — 写最小实现

// src/extract-links.ts
interface Link {
  text: string;
  url: string;
  type: 'external' | 'local';
}

export function extractLinks(markdown: string): Link[] {
  const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
  const links: Link[] = [];
  let match;

  while ((match = linkRegex.exec(markdown)) !== null) {
    const [, text, url] = match;
    links.push({
      text,
      url,
      type: url.startsWith('http') ? 'external' : 'local',
    });
  }

  return links;
}

确认通过

$ npx vitest run extract-links
PASS: 1/1

🔵 REFACTOR — 整理代码

代码已经很清晰了,暂时不需要重构。继续下一个测试循环。


就这样一个一个 Task、一个一个测试循环地推进。 每几分钟就有一个绿色的✅,像打怪升级一样。

最终产出:

📊 项目统计:
   - 5 个模块  
   - 12 个测试(全部通过)
   - 0 个"应该没问题"的声明
   - 每个功能都有对应的测试守护

📊 效果对比

维度不用 Skills(直让 AI 写)用 brainstorming + TDD + plans
需求遗漏漏了本地链接检查、超时控制3 轮问答覆盖全部需求
代码质量一坨 400 行,改不动5 个模块,单一职责
测试覆盖0 个测试12 个测试全绿
返工次数改了 3 轮还有 Bug一路绿灯
总耗时~3 小时(含反复修改)~1.5 小时
信心"应该能跑吧……"测试全绿,确认能跑

💡 实战心得

1. brainstorming 的 5 分钟值千金

很多人觉得"需求很简单,不用讨论"。但实际上,越简单的需求越容易翻车——因为你觉得"显然"的东西,AI 可能觉得"不确定"。花 5 分钟对齐需求,省 2 小时返工。

2. TDD 不是"写两倍代码"

TDD 的真正价值不在于"有了测试",而在于:

  • 强迫你先想清楚 API 长什么样(因为你要先调用它)
  • 小步快跑,每几分钟就有一个绿色反馈
  • 重构的底气——测试绿色就大胆改

3. writing-plans 让"大象"可以被吃掉

一个 CLI 工具看起来不大,但一口气让 AI 写完就是灾难。拆成 5 个原子任务后,每个任务 15-20 分钟搞定,心理压力和出错概率都直线下降

4. 适用场景

这套三件套组合适合:

  • ✅ 从零开始的新项目
  • ✅ 功能边界清晰的工具类应用
  • ✅ 需要长期维护的代码(有测试才敢改)
  • ⚠️ 探索性原型可以简化(跳过 TDD,只用 brainstorming + plans)

好了,本期的内容到这里就结束了,如果你觉得对你有帮助的话,欢迎点赞、在看、转发,我们下期见!Bye~