Claude Code 从零复刻教程 第 1 篇:项目初始化与 CLI 骨架
本教程是 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
练习作业
基础练习
-
添加作者信息
答案
在package.json中添加author和repository字段{ "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" } } -
添加新命令
实现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')) }) -
自定义颜色主题
答案
修改main.ts,将默认的绿色提示改为蓝色// 修改前 .description(pc.green('🤖 AI 编程助手...')) // 修改后 .description(pc.blue('🤖 AI 编程助手...'))
进阶练习
-
实现
--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(' ') })) } } }) -
添加交互式提示
实现my-claude setup命令,引导用户配置 API Key:
答案my-claude setup # 请输入 Anthropic API Key: sk-ant-xxx # 配置已保存到 ~/.my-claude/settings.jsonimport * 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 循环实现」中,我们将:
- 实现真正的交互式输入循环
- 处理多行输入(如代码块)
- 添加历史记录功能
- 实现基本的输出格式化
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)