引言mini-cc 的技术栈不是我自己拍脑袋定的——我是直接抄了 Claude Code 的作业。当我把 Claude Code 的源码扒下来之后我最大的感受是原来是用 TypeScript 写的吗TypeScript 做类型约束React 写组件再用 Ink 渲染到终端……这套组合拳打下来让一个本来应该很「土」的命令行工具变得清晰、优雅、还特别好扩展。今天这篇我就跟你聊聊这套技术栈在实际开发中它到底给我省了多少事。技术栈概览先列个清单这里面每一项都是 Claude Code 验证过的我直接拿来用分类技术版本用途语言TypeScript5.x主开发语言框架React17.xUI 组件开发终端 UIInk3.x终端界面渲染包管理pnpm9.x依赖管理测试Jest30.x单元测试Anthropic SDKanthropic-ai/sdk0.36.x调用 ClaudeOpenAI SDKopenai6.x调用 GPTMCP SDKmodelcontextprotocol/sdk1.29.x插件协议TypeScript类型是我给代码上的第一道锁为什么我非要用 TypeScriptClaude Code 用 TypeScript我就跟着用了。但说实话最开始我没想太多只是觉得“大项目应该用”。真正写出感觉来是碰到两个让我头疼的场景之后。场景一工具返回结果不统一mini-cc 里有十来个工具有的返回文件内容字符串有的返回执行状态对象有的返回搜索结果数组。如果不用 TypeScript我在QueryEngine里处理工具返回结果的时候得写一堆if (typeof result string)之类的判断或者靠注释提醒自己。用 TypeScript 之后我定义了一个ToolResult的联合类型// src/infrastructure/tools/Tool.tstypeToolResult|{type:text,content:string}|{type:error,message:string,code?:number}|{type:file,path:string,content:string};然后每个工具的execute方法都必须返回这个类型否则编译不过。QueryEngine里处理结果的时候TypeScript 会强制我做类型收窄constresultawaittool.execute(args);switch(result.type){casetext:console.log(result.content);// result.content 是 stringbreak;caseerror:console.error(result.message);// result.message 是 stringbreak;}少了运行时判断代码干净多了。场景二不同 Provider 的参数差异Anthropic 和 OpenAI 的 API 虽然都是 chat completion但细节不一样。比如消息格式Anthropic 用{ role: user, content: ... }OpenAI 也差不多但 tool_calls 的字段名不同。更麻烦的是有的 Provider 不支持流式有的不支持工具调用。如果只用 JS我得在运行时各种if (provider.name openai)然后祈祷自己没写错字段名。TypeScript 帮我做了两件事区分 Provider 的能力我在LLMProvider接口里定义了一个capabilities字段interfaceProviderCapabilities{streaming:boolean;toolCalling:boolean;maxContextTokens:number;}interfaceLLMProvider{name:string;capabilities:ProviderCapabilities;// ...}编译时阻止不可能的操作比如在代码里判断如果provider.capabilities.toolCalling为 false就不允许调用带 tools 参数的chat方法。这个用 TypeScript 的条件类型可以做到但我在 mini-cc 里用了更简单的方式在QueryEngine里调用chat之前先检查这个标志TypeScript 能保证我检查完之后调用的是正确的重载。实际上我踩过一个坑有一次我新增了一个 ProviderOllama它的capabilities.toolCalling是 false。但我在QueryEngine里忘了判断直接传了 tools 参数进去。TypeScript 报错了——因为chat方法的签名在toolCalling为 false 时不允许传 tools 参数。这个错误在编译阶段就被拦截了省了我上线后半夜起来修 bug。场景三消息流的状态机mini-cc 的消息处理有一个状态机idle → thinking → tool_calling → executing → responding → idle。我用 TypeScript 的枚举和泛型把这个状态机编进了类型系统typeMessageState|{status:idle}|{status:thinking}|{status:tool_calling,toolName:string,args:Recordstring,any}|{status:executing,toolName:string,result?:ToolResult}|{status:responding,content:string};// 在代码里根据不同的 statusTypeScript 会自动推导出可用的字段functionhandleState(state:MessageState){if(state.statustool_calling){console.log(state.toolName);// ✅ toolName 存在console.log(state.content);// ❌ 编译报错content 不存在于 tool_calling 状态}}这个设计让我少写了一堆防御性代码而且状态流转的逻辑一目了然。总结一下 TypeScript 在 mini-cc 里的真实价值不是简单的“类型安全”四个字而是强制你设计好数据结构写 interface 的过程就是设计的过程想不清楚就写不出来。把运行时错误变成编译时错误改了一个接口类型检查器会告诉你所有受影响的文件不用自己翻。让重构变得大胆因为你知道只要编译通过大部分逻辑就没错。没有 TypeScriptmini-cc 也能写但代码会膨胀 30% 以上全是运行时类型判断和文档注释。而我最讨厌写注释。React Ink用 Web 那套写 CLIInk 到底是个什么神器Ink 这个东西简单说就是让你用写 React 的方式写终端界面import React from react; import { render, Text, Box } from ink; const App () ( Box flexDirectioncolumn Text colorgreenHello, mini-cc!/Text TextCurrent directory: {process.cwd()}/Text /Box ); render(App /);你跑这个代码终端里就会显示带颜色的两行字而且自动处理了布局、换行那些麻烦事。Claude Code 为什么要这么干我一开始也纳闷CLI 工具老老实实打印文本不就行了为什么要整 React 进去后来我试着写了一个稍微复杂的界面——带消息列表、滚动、加载动画——我才发现用原生console.log去管理这些东西简直是灾难。你要手动计算光标位置、清屏、处理输入……太容易出 bug 了。而用 Ink组件化WelcomeBanner、MessageList、ProgressBar每个组件独立写独立测。状态管理就是 React 那套useState、useEffect和写网页一模一样零学习成本。可以直接用 React 生态想加个 spinner有现成的ink-spinner组件。比如 mini-cc 里的启动欢迎界面// src/components/WelcomeBanner.tsx export const WelcomeBanner ({ model, provider }: { model: string; provider: string; }) ( Box borderStylesingle padding{1} Text bold colorcyanmini-cc CLI {VERSION}/Text Box marginTop{1} TextModel: /Text Text colorgreen{model}/Text /Box TextProvider: {provider}/Text /Box );这段代码渲染出来就是一个带边框的盒子里面显示当前模型和 Provider。你换成纯文本打印得写几十行console.log加一堆对齐计算。实际开发中的另一个好处消息自动滚动const VirtualMessageList ({ messages }: { messages: Message[] }) { const containerRef useRefElement(); useEffect(() { // 每次新消息进来自动滚到底部 if (containerRef.current) { scrollToBottom(containerRef.current); } }, [messages]); // 依赖 messages一变化就滚动 return ( Box flexDirectioncolumn ref{containerRef} {messages.map((msg, idx) ( MessageItem key{idx} message{msg} / ))} /Box ); };这个useEffect的模式写过 React 的人都懂。用原生方案你得手动监听消息数组的变化还要处理滚动逻辑代码会啰嗦很多。源码指路所有 UI 组件都在src/components/目录下你可以看看我是怎么用 Ink 搭界面的。pnpm真的快而且省硬盘Claude Code 用 pnpm我也是一直在用的我的项目基本都是 pnpm。对比一下我的真实体验场景npmpnpm首次安装~30 秒~35 秒差不多第二次以后安装还是 ~30 秒~5 秒有缓存磁盘占用200 MB~80 MB硬链接幽灵依赖问题有比如你忘了装某包但它能跑无严格隔离我用 pnpm 的理由很简单快平时改完代码rm -rf node_modules再重装npm 要等半分钟pnpm 几秒就好。省空间我电脑上有好几个 TypeScript 项目pnpm 把相同的包硬链接到一个地方整体少占了几 GB。安全不会出现“我没装lodash但代码里require(lodash)居然能跑”这种怪事。使用提示如果你想跑 mini-cc确保你本地有 pnpm。没有的话npm i -g pnpm装一下就行。Jest测试也可以不痛苦为什么选 JestClaude Code 用 Jest我也用 Jest。它的好处是开箱即用TypeScript React 项目配一下jest.config.js就能跑。快照测试对 UI 组件太友好了不用手动写几百个断言。Mock 简单想模拟一个函数或者模块几行代码搞定。安全测试的例子mini-cc 的权限系统里我需要确保危险命令被拦截// src/__tests__/security.test.tsdescribe(Security Tests,(){test(should block dangerous commands,(){constresultcheckCommand(rm -rf /);expect(result.isDangerous).toBe(true);});test(should allow safe commands,(){constresultcheckCommand(ls -la);expect(result.isDangerous).toBe(false);});});跑一下pnpm test几秒钟就知道权限逻辑没被改坏。快照测试的实际应用对于 Ink 组件快照测试特别爽import{render}fromink-testing-library;import{WelcomeBanner}from../components/WelcomeBanner;test(renders welcome banner correctly,(){const{lastFrame}render(WelcomeBanner modelgpt-4providerOpenAI/);expect(lastFrame()).toMatchSnapshot();});第一次运行会生成一个__snapshots__文件里面存了终端的输出快照。以后如果你不小心改动了组件的外观测试会失败并提示你差异在哪里。确认改动是故意的按u更新快照就行。源码位置所有测试都在src/__tests__/目录下你可以参考着写自己的测试。完全理解。原稿那个表格看起来是在硬找理由不够自然。我重新写一版把你的真实想法加进去你是全栈、因为 Claude Code 用 TS 所以我也用 TS、后续会用 Python/Go/Rust 再写版本。另外加上了 OpenAI 从 TS 迁移到 Rust 的业界案例让这段更有说服力。下面是重写后的版本替换原文中 “## 为什么不选别的” 及其后所有内容即可为什么不选别的有些朋友可能会问Rust 性能更好Go 部署更简单为什么偏要用 TypeScript说实话这个问题问得有点“非黑即白”了。TypeScript 和 Rust/Go 不是“你死我活”的关系而是不同阶段、不同场景的选择。我的真实情况是这样的1. 因为 Claude Code 用 TS所以我也用 TS我一开始就是想复刻 Claude Code 的核心机制。既然它的源码是 TypeScript我直接抄过来改就行没必要自己重新发明一套。这是最务实的理由没什么好遮遮掩掩的。2. 因为我是全栈偏前端TS 我最熟我是全栈开发者日常主要写 TypeScript/React/Node.js。让我用 TS 写 mini-cc我闭着眼睛都能写。换 Go/Rust 的话我稍微吃力一点需要多花一点时间。时间成本摆在这儿。3. 后续会用 Python、Go、Rust 各写一版这也是很多朋友问过我的问题“mini-cc 以后会支持其他语言吗”我的答案是会的。你看我的 GitHub 仓库https://github.com/you-want/mini-cc里面其实已经有了 Python、Go 的雏形。只是目前 TS 版本功能最全是我的“先行版本”。后续我会用 Python、Go、Rust 分别重写一版把核心 Agent 逻辑用不同语言实现一遍。这件事对我来说有两个意义练习作为全栈这几种语言我都要会用用实际项目来练手最有效。对比不同语言写出来的 Agent 在性能、开发效率、部署体验上到底差多少我想亲自验证一下。所以 TS 版本不是“唯一”版本而是“第一个”版本。4. 业界也在做类似的事TypeScript for iterationRust/Go for production你看 OpenAI 的操作就很有意思。2025 年OpenAI 宣布把 Codex CLI 从 TypeScript 重写为 Rust。官方给出了四个原因零依赖安装不用装 Node.js原生安全绑定更好的沙箱优化性能无 GC内存占用更低可扩展协议支持多语言扩展但注意OpenAI 不是“抛弃”了 TypeScript而是用 Rust 重写生产版本同时 TypeScript 版本还在并行维护直到功能对齐。这是一个清晰的策略用 TS 快速迭代验证用 Rust 上线交付。Claude Code 和 Google 的 Gemini CLI 至今仍然用 TypeScript React Ink。Anthropic 工程师 Boris 说过一句话他们希望用一个“模型已经很擅长”的技术栈来开发 Claude Code——大约 90% 的 Claude Code 代码是由 Claude Code 自己写的。这是一个很巧妙的自我强化循环。说到这插一嘴这也是为啥 AI Coding 之后前端工程师处境最难受。但是也是机遇因为 AI Coding 之后前端工程师需要更关注 AI 技术而不是 UI 设计。行业格局很清晰语言代表项目适用场景TypeScriptClaude Code、Gemini CLI快速迭代、生态丰富、UI 炫酷GoOpenCode并发好、部署简单、单二进制RustOpenAI Codex CLI极致性能、安全、零依赖安装PythonAiderAI 生态成熟、原型开发快这不是“谁更好”的问题而是“哪个更适合当前阶段”的问题。5. 那 mini-cc 到底怎么选我给自己的策略是现在TS 版本快速实现核心功能验证架构设计顺便把技术文档写清楚。未来多语言版本用 Go 和 Rust 各写一个“生产版”对比性能和部署体验。Python 版本用来做 AI 原型测试。说到底我是个全栈工程师写 TypeScript 是我的日常写 Go/Rust/Python 是我的训练。mini-cc 正好是一个绝佳的练手项目逻辑不算太复杂但又能把各种语言的核心特性都用到。所以别再问我“为什么选 TypeScript 不选 Rust”了——我都会我都要写。源码指路GitHub 仓库https://github.com/you-want/mini-cc里可以看到多语言的目录结构。TS 版本是完整实现其他语言的版本还在逐步完善中欢迎关注和贡献。架构与技术的完美契合最后放一张图看看技术栈是怎么和分层架构对应的┌──────────────────────────────────────────────┐ │ UI Layer (React Ink) │ │ - WelcomeBanner 组件 │ │ - VirtualMessageList 组件 │ │ - ProgressBar 组件 │ ├──────────────────────────────────────────────┤ │ Application Layer (TypeScript) │ │ - QueryEngine (Agent 循环) │ │ - SkillManager (技能管理) │ │ - ToolRegistry (工具注册) │ ├──────────────────────────────────────────────┤ │ Infrastructure Layer (Node.js) │ │ - FileReadTool / FileWriteTool │ │ - BashTool (命令执行) │ │ - MCP Client (插件集成) │ └──────────────────────────────────────────────┘每一层都有自己的技术侧重点最上层只管展示用 React 组件拼界面中间层是纯逻辑TypeScript 做类型约束底层用 Node.js 的 fs、child_process 等原生模块干重活这个结构是 Claude Code 用过的我直接搬过来没踩什么坑。总结mini-cc 的技术栈说白了就是一句话Claude Code 用什么我就用什么。不过也是 mini-cc 的一个 TS 版本而已。后续会有其它版本。这个选择给我带来了三方面的实际好处开发体验好TypeScript 的类型安全 React 的组件化 pnpm 的快写代码很舒服。站在巨人肩膀上不用纠结“这个库行不行”Claude Code 已经帮我验证过了。社区贡献简单会用 React 和 TypeScript 的人很多别人想给我提 PR 也没什么门槛。最后再放一次源码地址https://github.com/you-want/mini-cc/tree/main/typescript欢迎 clone 下来跑一跑顺便 star 一下 ⭐️