name: cli-builder description: 使用 Bun 构建 TypeScript CLI 的指南。适用于创建命令行工具、向现有 CLI 添加子命令或构建开发者工具。涵盖参数解析、子命令模式、输出格式化和分发。 tags:
- cli
- typescript
- bun
- tooling
CLI 构建器
使用 Bun 构建 TypeScript 命令行工具。
何时构建 CLI
CLI 非常适合:
- 开发者工具和自动化
- 项目特定命令(如
swarm、bd等) - 需要参数/标志的脚本
- 可与 shell 管道组合的工具
快速开始
最小 CLI
#!/usr/bin/env bun
// scripts/my-tool.ts
const args = process.argv.slice(2);
const command = args[0];
if (!command || command === "help") {
console.log(`
Usage: my-tool <command>
Commands:
hello 打招呼
help 显示此消息
`);
process.exit(0);
}
if (command === "hello") {
console.log("Hello, world!");
}
运行:bun scripts/my-tool.ts hello
带参数解析
使用 Node 的 util 模块中的 parseArgs(在 Bun 中可用):
#!/usr/bin/env bun
import { parseArgs } from "util";
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
name: { type: "string", short: "n" },
verbose: { type: "boolean", short: "v", default: false },
help: { type: "boolean", short: "h", default: false },
},
allowPositionals: true,
});
if (values.help) {
console.log(`
Usage: greet [options] <message>
Options:
-n, --name <name> 要打招呼的名字
-v, --verbose 详细输出
-h, --help 显示帮助
`);
process.exit(0);
}
const message = positionals[0] || "Hello";
const name = values.name || "World";
console.log(`${message}, ${name}!`);
if (values.verbose) {
console.log(` (greeted at ${new Date().toISOString()})`);
}
子命令模式
对于具有多个命令的 CLI,使用命令注册表:
#!/usr/bin/env bun
import { parseArgs } from "util";
type Command = {
description: string;
run: (args: string[]) => Promise<void>;
};
const commands: Record<string, Command> = {
init: {
description: "初始化新项目",
run: async (args) => {
const { values } = parseArgs({
args,
options: {
template: { type: "string", short: "t", default: "default" },
},
});
console.log(`使用模板初始化: ${values.template}`);
},
},
build: {
description: "构建项目",
run: async (args) => {
const { values } = parseArgs({
args,
options: {
watch: { type: "boolean", short: "w", default: false },
},
});
console.log(`构建中...${values.watch ? " (监视模式)" : ""}`);
},
},
};
function showHelp() {
console.log(`
Usage: mytool <command> [options]
Commands:`);
for (const [name, cmd] of Object.entries(commands)) {
console.log(` ${name.padEnd(12)} ${cmd.description}`);
}
console.log(`
运行 'mytool <command> --help' 获取特定命令的帮助。
`);
}
// 主程序
const [command, ...args] = process.argv.slice(2);
if (!command || command === "help" || command === "--help") {
showHelp();
process.exit(0);
}
const cmd = commands[command];
if (!cmd) {
console.error(`未知命令: ${command}`);
showHelp();
process.exit(1);
}
await cmd.run(args);
输出格式化
颜色(无依赖)
const colors = {
reset: "\x1b[0m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
dim: "\x1b[2m",
bold: "\x1b[1m",
};
function success(msg: string) {
console.log(`${colors.green}✓${colors.reset} ${msg}`);
}
function error(msg: string) {
console.error(`${colors.red}✗${colors.reset} ${msg}`);
}
function warn(msg: string) {
console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
}
function info(msg: string) {
console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
}
JSON 输出模式
支持 --json 用于可脚本化的输出:
const { values } = parseArgs({
args: process.argv.slice(2),
options: {
json: { type: "boolean", default: false },
},
allowPositionals: true,
});
const result = { status: "ok", items: ["a", "b", "c"] };
if (values.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log("Status:", result.status);
console.log("Items:", result.items.join(", "));
}
进度指示器
function spinner(message: string) {
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let i = 0;
const id = setInterval(() => {
process.stdout.write(`\r${frames[i++ % frames.length]} ${message}`);
}, 80);
return {
stop: (finalMessage?: string) => {
clearInterval(id);
process.stdout.write(`\r${finalMessage || message}
`);
},
};
}
// 用法
const spin = spinner("加载中...");
await someAsyncWork();
spin.stop("✓ 完成!");
文件系统操作
import { readFile, writeFile, mkdir, readdir } from "fs/promises";
import { existsSync } from "fs";
import { join, dirname } from "path";
// 确保目录存在再写入
async function writeFileWithDir(path: string, content: string) {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, content);
}
// 读取 JSON 带默认值
async function readJsonFile<T>(path: string, defaults: T): Promise<T> {
if (!existsSync(path)) return defaults;
const content = await readFile(path, "utf-8");
return { ...defaults, ...JSON.parse(content) };
}
Shell 执行
import { $ } from "bun";
// 简单命令
const result = await $`git status`.text();
// 带错误处理
try {
await $`npm test`.quiet();
console.log("测试通过!");
} catch (error) {
console.error("测试失败");
process.exit(1);
}
// 捕获输出
const branch = await $`git branch --show-current`.text();
console.log(`当前分支: ${branch.trim()}`);
错误处理
class CLIError extends Error {
constructor(message: string, public exitCode = 1) {
super(message);
this.name = "CLIError";
}
}
async function main() {
try {
await runCommand();
} catch (error) {
if (error instanceof CLIError) {
console.error(`Error: ${error.message}`);
process.exit(error.exitCode);
}
throw error; // 重新抛出意外错误
}
}
main();
分发
package.json bin 字段
{
"name": "my-cli",
"bin": {
"mycli": "./dist/cli.js"
},
"scripts": {
"build": "bun build ./src/cli.ts --outfile ./dist/cli.js --target node"
}
}
Shebang 用于直接执行
#!/usr/bin/env bun
// 你的 CLI 脚本的第一行
使其可执行:chmod +x scripts/my-cli.ts
最佳实践
- 始终提供 --help - 用户期望它
- 退出代码重要 - 0 表示成功,非零表示错误
- 支持 --json - 用于可脚本化和管道
- 快速失败 - 尽早验证输入
- 默认保持安静 - 使用 --verbose 获取详细信息
- 尊重 NO_COLOR - 检查
process.env.NO_COLOR - 流式处理大输出 - 不要将所有内容缓冲在内存中