本教程是 Claude Code 从零复刻系列的第一篇,将带你从零搭建一个 TypeScript CLI 项目框架。


学习目标

  • 创建 Node.js + TypeScript 项目
  • 配置 TypeScript 编译选项
  • 使用 Commander.js 创建 CLI 命令
  • 实现基本的帮助信息和版本命令
  • 添加彩色终端输出

核心概念

什么是 CLI?

CLI(Command Line Interface)是通过终端命令与程序交互的界面。相比 GUI,CLI 具有以下优势:

优势 说明
自动化友好 易于脚本化和集成
资源占用低 无需图形界面
远程友好 通过 SSH 使用
可组合 Unix 哲学,管道和重定向

为什么选择 TypeScript?

TypeScript 为 JavaScript 添加了类型系统,对 CLI 工具尤为重要:

// JavaScript:运行时才能发现错误
function greet(name) {
  console.log(`Hello, ${name.toUpperCase()}`)
}

// TypeScript:编译时即发现错误
function greet(name: string) {
  console.log(`Hello, ${name.toUpperCase()}`)
}

Commander.js 简介

Commander.js 是 Node.js 最流行的 CLI 框架:

import { Command } from 'commander'

const program = new Command()

program
  .name('my-claude')
  .description('AI 编程助手')
  .version('1.0.0')

program.parse()

代码实现

1. 创建项目目录

mkdir my-claude && cd my-claude
npm init -y

2. 安装依赖

npm install commander picocolors
npm install -D typescript @types/node tsx

依赖说明:

用途
commander CLI 参数解析
picocolors 彩色终端输出(比 chalk 更轻量)
typescript TypeScript 编译器
@types/node Node.js 类型定义
tsx 直接运行 TypeScript(无需编译)

3. 创建 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4. 配置 package.json

{
  "name": "my-claude",
  "version": "1.0.0",
  "description": "AI 编程助手 CLI",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "my-claude": "./dist/index.js"
  },
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "keywords": ["cli", "ai", "anthropic"],
  "license": "MIT"
}

关键配置说明:

  • "type": "module":启用 ES Modules
  • "bin":注册全局命令 my-claude
  • "tsx":开发时直接运行 TypeScript

5. 创建入口文件

src/index.ts - 入口文件
#!/usr/bin/env node

import { main } from './main.js'

main().catch((error) => {
  console.error('Fatal error:', error)
  process.exit(1)
})
src/main.ts - 主程序
import { Command } from 'commander'
import pc from 'picocolors'

export async function main() {
  const program = new Command()

  // 配置程序信息
  program
    .name('my-claude')
    .description(pc.green('🤖 AI 编程助手 - Claude Code 开源复刻版'))
    .version('1.0.0')
    .option('-v, --verbose', '输出详细日志')
    .hook('preAction', (thisCommand) => {
      const opts = thisCommand.opts()
      if (opts.verbose) {
        console.log(pc.gray('Verbose mode enabled'))
      }
    })

  // 注册命令
  registerCommands(program)

  // 解析参数
  program.parse()
}

function registerCommands(program: Command) {
  // 默认交互命令
  program
    .command('chat')
    .description('启动交互式对话')
    .action(async () => {
      console.log(pc.blue('🔵 启动对话模式...'))
      console.log(pc.gray('(后续篇目实现)'))
    })

  // 发送单条消息
  program
    .command('ask <message>')
    .description('发送单条消息给 AI')
    .option('-m, --model <model>', '指定模型', 'claude-sonnet-4-20250514')
    .action(async (message: string, options) => {
      console.log(pc.blue(`📤 发送: ${message}`))
      console.log(pc.gray(`模型: ${options.model}`))
      console.log(pc.gray('(后续篇目实现)'))
    })

  // 配置命令
  program
    .command('config')
    .description('管理配置')
    .addCommand(
      new Command('get')
        .description('获取配置项')
        .argument('<key>', '配置键名')
        .action((key: string) => {
          console.log(pc.yellow(`config.get("${key}")`))
        })
    )
    .addCommand(
      new Command('set')
        .description('设置配置项')
        .argument('<key>', '配置键名')
        .argument('<value>', '配置值')
        .action((key: string, value: string) => {
          console.log(pc.yellow(`config.set("${key}", "${value}")`))
        })
    )
}

6. 完整项目结构

my-claude/
├── src/
│   ├── index.ts      # 入口文件(bin 入口)
│   └── main.ts       # 主程序逻辑
├── dist/             # 编译输出(git 忽略)
├── package.json
├── tsconfig.json
└── README.md

运行演示

方式一:开发模式(推荐)

npm run dev -- --help

输出:

🤖 AI 编程助手 - Claude Code 开源复刻版

Usage: my-claude [options] [command]

Options:
  -v, --verbose   输出详细日志
  -V, --version    output the version number
  -h, --help       display help for command

Commands:
  chat             启动交互式对话
  ask <message>    发送单条消息给 AI
  config           管理配置
  help            display help for command

方式二:构建后运行

npm run build
npm start -- --help

方式三:全局安装

npm install -g
my-claude --help

原理深入

1. Shebang 行

#!/usr/bin/env node

这行告诉操作系统使用 node 来执行此脚本。注意:

  • 必须放在文件第一行
  • 需要文件有执行权限

2. ES Modules vs CommonJS

我们选择 ES Modules("type": "module"):

// ES Modules
import { main } from './main.js'

// CommonJS
const { main } = require('./main')

ES Modules 优势:

  • 更好的 tree-shaking
  • 原生异步支持
  • 符合浏览器标准

3. Commander.js 工作原理

用户输入
    │
    ▼
┌─────────────────────────────────┐
│  Commander 解析参数              │
│  - 识别命令 (chat/ask/config)   │
│  - 解析选项 (-v/--verbose)     │
│  - 提取参数 (<message>)        │
└─────────────────────────────────┘
    │
    ▼
匹配 command 或 action
    │
    ▼
执行对应的 action 回调

4. picocolors 原理

picocolors 使用 ANSI 转义码实现彩色输出:

import pc from 'picocolors'

console.log(pc.red('Error'))     // \x1b[31mError\x1b[0m
console.log(pc.green('Success')) // \x1b[32mSuccess\x1b[0m
console.log(pc.blue('Info'))     // \x1b[34mInfo\x1b[0m

常用颜色:

函数 颜色 用途
pc.black() 调试信息
pc.red() 错误
pc.green() 绿 成功
pc.yellow() 警告
pc.blue() 信息
pc.cyan() 强调
pc.white() 常规

样式修饰:

函数 效果
pc.bold() 加粗
pc.dim() 暗淡
pc.italic() 斜体
pc.underline() 下划线

5. 模块解析规则

TypeScript 的 moduleResolution: "bundler" 使用类似 Vite 的解析策略:

// 导入时
import { main } from './main.js'

// TypeScript 会查找
// 1. ./main.ts
// 2. ./main/index.ts
// 3. ./main.tsx

练习作业

基础练习

  1. 添加作者信息
    package.json 中添加 authorrepository 字段

    答案
    {
      "name": "my-claude",
      "version": "1.0.0",
      "description": "AI 编程助手 CLI",
      "author": "Your Name <your.email@example.com>",
      "repository": {
        "type": "git",
        "url": "https://github.com/yourusername/my-claude.git"
      }
    }
    
  2. 添加新命令
    实现 my-claude version 命令,输出更详细的版本信息:

    my-claude version
    # 输出:
    # my-claude v1.0.0
    # Node.js v20.0.0
    # TypeScript 5.x
    
    答案
    // 在 main.ts 中添加
    program
      .command('version')
      .description('显示详细版本信息')
      .action(() => {
        console.log(pc.cyan(`my-claude v1.0.0`))
        console.log(pc.gray(`Node.js ${process.version}`))
        console.log(pc.gray('TypeScript 5.x'))
      })
    
  3. 自定义颜色主题
    修改 main.ts,将默认的绿色提示改为蓝色

    答案
    // 修改前
    .description(pc.green('🤖 AI 编程助手...'))
    
    // 修改后
    .description(pc.blue('🤖 AI 编程助手...'))
    

进阶练习

  1. 实现 --json 选项
    添加全局 --json 选项,当启用时命令输出 JSON 格式:

    my-claude --json chat
    # 输出:{"status": "ok", "message": "启动对话模式..."}
    
    答案
    program
      .option('--json', '输出 JSON 格式')
      .hook('preAction', (thisCommand) => {
        const opts = thisCommand.opts()
        if (opts.json) {
          // 重写 console.log 为 JSON 输出
          const originalLog = console.log
          console.log = (...args) => {
            originalLog(JSON.stringify({ message: args.join(' ') }))
          }
        }
      })
    
  2. 添加交互式提示
    实现 my-claude setup 命令,引导用户配置 API Key:

    my-claude setup
    # 请输入 Anthropic API Key: sk-ant-xxx
    # 配置已保存到 ~/.my-claude/settings.json
    
    答案
    import * as readline from 'readline'
    import * as fs from 'fs/promises'
    import * as path from 'path'
    import * as os from 'os'
    
    program
      .command('setup')
      .description('初始化配置')
      .action(async () => {
        const rl = readline.createInterface({
          input: process.stdin,
          output: process.stdout
        })
    
        const ask = (prompt: string): Promise<string> => {
          return new Promise((resolve) => {
            rl.question(prompt, resolve)
          })
        }
    
        const apiKey = await ask('请输入 Anthropic API Key: ')
        rl.close()
    
        const configDir = path.join(os.homedir(), '.my-claude')
        await fs.mkdir(configDir, { recursive: true })
        
        const config = { apiKey }
        await fs.writeFile(
          path.join(configDir, 'settings.json'),
          JSON.stringify(config, null, 2)
        )
    
        console.log(pc.green('✓ 配置已保存到 ~/.my-claude/settings.json'))
      })
    

完整文件

package.json

{
  "name": "my-claude",
  "version": "1.0.0",
  "description": "AI 编程助手 CLI",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "my-claude": "./dist/index.js"
  },
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "keywords": ["cli", "ai", "anthropic"],
  "license": "MIT",
  "author": "Your Name <your.email@example.com>",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/my-claude.git"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

src/index.ts

#!/usr/bin/env node

import { main } from './main.js'

main().catch((error) => {
  console.error('Fatal error:', error)
  process.exit(1)
})

src/main.ts(包含所有练习答案)

import { Command } from 'commander'
import pc from 'picocolors'
import * as readline from 'readline'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'

export async function main() {
  const program = new Command()

  // 练习 3:使用蓝色主题
  program
    .name('my-claude')
    .description(pc.blue('🤖 AI 编程助手 - Claude Code 开源复刻版'))
    .version('1.0.0')
    .option('-v, --verbose', '输出详细日志')
    .option('--json', '输出 JSON 格式')
    .hook('preAction', (thisCommand) => {
      const opts = thisCommand.opts()
      if (opts.verbose) {
        console.log(pc.gray('Verbose mode enabled'))
      }
      if (opts.json) {
        const originalLog = console.log
        console.log = (...args) => {
          originalLog(JSON.stringify({ message: args.join(' ') }))
        }
      }
    })

  // 练习 2:version 命令
  program
    .command('version')
    .description('显示详细版本信息')
    .action(() => {
      console.log(pc.cyan(`my-claude v1.0.0`))
      console.log(pc.gray(`Node.js ${process.version}`))
      console.log(pc.gray('TypeScript 5.x'))
    })

  // 练习 5:setup 命令
  program
    .command('setup')
    .description('初始化配置')
    .action(async () => {
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      })

      const ask = (prompt: string): Promise<string> => {
        return new Promise((resolve) => {
          rl.question(prompt, resolve)
        })
      }

      const apiKey = await ask('请输入 Anthropic API Key: ')
      rl.close()

      const configDir = path.join(os.homedir(), '.my-claude')
      await fs.mkdir(configDir, { recursive: true })
      
      const config = { apiKey }
      await fs.writeFile(
        path.join(configDir, 'settings.json'),
        JSON.stringify(config, null, 2)
      )

      console.log(pc.green('✓ 配置已保存到 ~/.my-claude/settings.json'))
    })

  // 默认交互命令
  program
    .command('chat')
    .description('启动交互式对话')
    .action(async () => {
      console.log(pc.cyan('🔵 启动对话模式...'))
      console.log(pc.gray('(后续篇目实现)'))
    })

  // 发送单条消息
  program
    .command('ask <message>')
    .description('发送单条消息给 AI')
    .option('-m, --model <model>', '指定模型', 'claude-sonnet-4-20250514')
    .action(async (message: string, options) => {
      console.log(pc.cyan(`📤 发送: ${message}`))
      console.log(pc.gray(`模型: ${options.model}`))
      console.log(pc.gray('(后续篇目实现)'))
    })

  // 配置命令
  program
    .command('config')
    .description('管理配置')
    .addCommand(
      new Command('get')
        .description('获取配置项')
        .argument('<key>', '配置键名')
        .action((key: string) => {
          console.log(pc.yellow(`config.get("${key}")`))
        })
    )
    .addCommand(
      new Command('set')
        .description('设置配置项')
        .argument('<key>', '配置键名')
        .argument('<value>', '配置值')
        .action((key: string, value: string) => {
          console.log(pc.yellow(`config.set("${key}", "${value}")`))
        })
    )

  program.parse()
}

参考资料

官方文档

相关工具

工具 用途
ts-node 另一个 TS 运行时
esbuild 极速打包工具
rollup ESM 打包器
oclif 企业级 CLI 框架

下篇预告

在下一篇文章「REPL 循环实现」中,我们将:

  • 实现真正的交互式输入循环
  • 处理多行输入(如代码块)
  • 添加历史记录功能
  • 实现基本的输出格式化

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐