扒了 create-vite 的源码,我学会了如何写一个优雅的 CLI
昨天突发奇想,想研究一下我们每天都在用的npm create vite到底是怎么写的。
作为一个每天都要敲好几次的命令,它的交互体验真的很顺滑:响应快、提示清晰、色彩舒服。于是我花了点时间扒了一下create-vite的源码(注意,是脚手架本身,不是 Vite 核心库),结果发现它的核心逻辑其实非常简单清晰,而且里面藏着几个非常值得我们写 CLI 工具时借鉴的“魔鬼细节”。
极简的“三件套”
打开src/index.ts,你会发现整个 CLI 的流程基本就是线性的 6 步:
-
问你项目名。 -
检查目录是否为空。 -
检查包名合法性。 -
选框架(Vue/React/…)。 -
问你要不要立马安装依赖( --immediate)。 -
生成文件。
而支撑起这个流畅体验的,主要是这三个库:
-
mri: 处理命令行参数。极简主义者的最爱,比commander轻量得多。 -
@clack/prompts: 这就是那个漂亮的交互界面的功臣。如果你也想让你的 CLI 看起来很现代,用它就对了。 -
picocolors: 给终端输出上色的。

这就告诉我们:写一个好用的 CLI,不需要太重的依赖,选对工具很重要。
那些文档里没写满的参数
我在看代码的时候,发现了一些文档里可能一笔带过的实用参数,特别是对于做自动化运维(DevOps)或者 AI Agent 开发的同学很有用:
-
--no-interactive(或-i模式): 这是我最喜欢的一个设计。如果检测到是在 CI/CD 流水线里运行,或者是由 AI Agent 调用的,它会自动跳过所有问答环节,直接使用默认值(默认是vanilla-ts)。想要一键生成?直接跑:
# 适合脚本调用的方式
create-vite my-app --template react-ts --no-interactive -
--template的隐藏菜单: 除了常见的vue、react,源码里其实藏了一大堆预设,包括solid、svelte、qwik甚至marko。以后想尝鲜新框架,不妨先试试--template能不能直接生成。
源码里的三个“魔鬼细节”
这部分是我觉得最值得借鉴的。代码逻辑不复杂,但就是这些细节决定了一个 CLI 是“能用”还是“好用”。

1. 它是怎么知道以此用什么包管理器的?
你有没有发现,如果你用pnpm create vite,生成的提示里就会让你用pnpm install;如果你用npm,它就提示npm。它是怎么做到的?
秘密在于npm_config_user_agent这个环境变量。
// 伪代码逻辑
const userAgent = process.env.npm_config_user_agent ?? '';
const pkgManager = /pnpm/.test(userAgent) ? 'pnpm' : /yarn/.test(userAgent) ? 'yarn' : 'npm';
当你在终端运行包管理器命令时,Node 进程会被注入这个变量。比如运行pnpm config get user-agent,你会看到类似pnpm/10.20.0 npm/? node/v20.11.1 ...的字符串。
通过解析这个字符串,CLI 就能“智能”地感知用户的偏好,保持体验的一致性。这比让用户自己选“你用什么安装依赖”要高明得多。
2. 管道模式与 TTY 检测
如果我试图把一个文件内容管道传输给 CLI 会发生什么?
cat config.txt | create-vite
这时候交互式问答是没法工作的。create-vite做了一个很标准的检查:
// 只有在标准输入连接到终端(TTY)时,才开启交互模式
const canSkipEmptying = args.overwrite || (!isInteractive && !process.stdin.isTTY);
process.stdin.isTTY是 Node.js 中用来判断当前进程是否直接连接到终端(Terminal)的关键属性。如果是管道或者重定向,它就是false。作为一个健壮的 CLI,必须考虑到非人机交互的场景。
3. 优雅地处理 Ctrl+C
很多自己写的 CLI 工具,在用户按Ctrl+C强行中断时,往往会抛出一大堆丑陋的错误栈信息。
而在create-vite中,每一次交互提示后,都有这样一行代码:
const projectName = await prompts.text({ ... });
// 这一行是关键
if (prompts.isCancel(projectName)) {
cancel('Operation cancelled');
return process.exit(0);
}
@clack/prompts提供了一个isCancel方法。一旦检测到用户取消操作,它会捕获信号,打印一句友好的 “Operation cancelled” 然后干净利落地退出进程。
不要让用户看到报错信息,除非真的出错了。这是一个 CLI 工具最基本的礼貌。
写在最后
看完create-vite的源码,给我最大的感受是:克制。
它没有花里胡哨的功能,代码量也很少,但每一个交互点(参数处理、环境检测、异常退出)都打磨得很细致。如果你也想写一个给开发者用的 CLI 工具,不妨照着它的src/index.ts抄作业,绝对能少走很多弯路。
夜雨聆风
