CLI构建器Skill cli-builder

用于使用 Bun 和 TypeScript 构建命令行界面(CLI)的技能,涵盖参数解析、子命令模式、输出格式化、文件系统操作、shell 执行和错误处理,适用于开发工具、自动化脚本和项目特定命令,关键词包括 CLI、TypeScript、Bun、命令行工具、开发工具、自动化。

DevOps 0 次安装 0 次浏览 更新于 3/19/2026

name: cli-builder description: 使用 Bun 构建 TypeScript CLI 的指南。适用于创建命令行工具、向现有 CLI 添加子命令或构建开发者工具。涵盖参数解析、子命令模式、输出格式化和分发。 tags:

  • cli
  • typescript
  • bun
  • tooling

CLI 构建器

使用 Bun 构建 TypeScript 命令行工具。

何时构建 CLI

CLI 非常适合:

  • 开发者工具和自动化
  • 项目特定命令(如 swarmbd 等)
  • 需要参数/标志的脚本
  • 可与 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

最佳实践

  1. 始终提供 --help - 用户期望它
  2. 退出代码重要 - 0 表示成功,非零表示错误
  3. 支持 --json - 用于可脚本化和管道
  4. 快速失败 - 尽早验证输入
  5. 默认保持安静 - 使用 --verbose 获取详细信息
  6. 尊重 NO_COLOR - 检查 process.env.NO_COLOR
  7. 流式处理大输出 - 不要将所有内容缓冲在内存中