本教程教你使用 Claude Code 从零构建一个完整的 Node.js CLI 工具,涵盖项目初始化、Commander.js 命令定义、Inquirer.js 交互式提示、文件系统操作、单元测试,最终发布到 npm 注册表。适合有基础 Node.js 经验、想把重复工作流封装为可复用命令行工具的开发者。
| 条件 | 说明 |
|---|---|
| Node.js ≥ 18 | 运行时环境 |
| npm ≥ 9 | 包管理器 |
| Claude Code 已安装 | npm install -g @anthropic-ai/claude-code |
| npm 账号 | 发布到 registry 需要 |
| 基础 TypeScript | 能读懂类型标注即可 |
mkdir fileforge && cd fileforge
npm init -y
启动 Claude Code 并告诉它你的目标:
> 我要创建一个名为 fileforge 的 CLI 工具,用于批量处理文件:
> 1. 扫描目录中的文件
> 2. 按规则重命名(小写、去空格、加前缀)
> 3. 生成目录结构 JSON 报告
> 请帮我初始化 TypeScript 项目,配置 tsconfig.json、package.json,安装必要依赖。
Claude Code 会执行以下操作(你可以逐步确认):
tsconfig.json(target: ES2022, module: NodeNext)package.json(添加 bin 字段、type: "module")commander、chalk、ora、inquirertypescript、@types/node、vitest这是让 Claude Code 持续理解你项目的核心配置:
# fileforge 项目说明
## 项目概述
Node.js CLI 工具,用于批量文件处理(重命名、扫描、报告生成)。
## 技术栈
- TypeScript 5.x + ES Modules
- Commander.js 命令行框架
- Inquirer.js 交互提示
- Chalk 5.x 终端着色
- Ora 进度指示器
- Vitest 单元测试
## 项目结构
src/
├── index.ts # 入口,注册命令
├── commands/ # 各子命令实现
│ ├── scan.ts # 文件扫描
│ ├── rename.ts # 批量重命名
│ └── report.ts # 目录报告
├── utils/ # 工具函数
│ ├── fs.ts # 文件系统封装
│ └── format.ts # 格式化工具
└── types/ # 类型定义
└── index.ts
## 编码规范
- 使用 ESM import/export,不用 require
- 异步操作全部用 async/await
- 文件路径用 path.join() 拼接
- 错误处理用自定义 CLIError 类
- 函数必须有 JSDoc 注释
## 测试要求
- 每个命令至少 3 个测试用例
- 用 vitest mock 文件系统
- 测试文件放在 tests/ 目录
## 构建与发布
- npm run build → tsc 编译到 dist/
- npm run test → vitest 运行测试
- npm publish 发布
为什么这一步很重要:CLAUDE.md 是 Claude Code 的项目上下文。每次对话时它会自动读取,确保所有生成的代码符合你的架构决策和编码规范。没有它,Claude Code 只能猜测你的意图。
// src/index.ts
#!/usr/bin/env node
import { Command } from 'commander';
import { scanCommand } from './commands/scan.js';
import { renameCommand } from './commands/rename.js';
import { reportCommand } from './commands/report.js';
const program = new Command();
program
.name('fileforge')
.description('批量文件处理 CLI 工具')
.version('1.0.0');
program.addCommand(scanCommand);
program.addCommand(renameCommand);
program.addCommand(reportCommand);
program.parse();
注意:ESM 模式下,本地
.ts文件的 import 必须带.js后缀(即使源文件是.ts)。
{
"name": "fileforge",
"version": "1.0.0",
"type": "module",
"bin": {
"fileforge": "./dist/index.js"
},
"files": ["dist"],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run",
"prepublishOnly": "npm run test && npm run build"
},
"engines": {
"node": ">=18.0.0"
}
}
bin 字段是 CLI 工具的核心:它告诉 npm 在全局安装时创建一个符号链接,让用户直接在终端输入 fileforge 命令。
> 请根据 CLAUDE.md 中定义的项目结构,为 scan、rename、report 三个命令创建完整的实现文件。
> 每个命令需要:
> - Commander 命令定义(参数、选项、描述)
> - 核心业务逻辑
> - 错误处理
>
> scan 命令:扫描目录,支持 --ext 过滤文件类型,支持 --recursive 递归扫描
> rename 命令:批量重命名,支持 --dry-run 预览模式,支持 --prefix 添加前缀
> report 命令:生成 JSON 报告,包含文件数量、大小统计、目录树结构
// src/commands/scan.ts
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import { scanDirectory } from '../utils/fs.js';
export const scanCommand = new Command('scan')
.description('扫描目录中的文件')
.argument('[path]', '目标目录', '.')
.option('-e, --ext <extensions>', '过滤文件类型(逗号分隔)', '')
.option('-r, --recursive', '递归扫描子目录', false)
.option('-s, --sort <field>', '排序字段 (name|size|date)', 'name')
.action(async (targetPath: string, options: ScanOptions) => {
const spinner = ora('正在扫描文件...').start();
try {
const extensions = options.ext
? options.ext.split(',').map(e => e.trim().toLowerCase())
: null;
const files = await scanDirectory(targetPath, {
recursive: options.recursive,
extensions,
sortBy: options.sort as SortField,
});
spinner.succeed(`找到 ${files.length} 个文件`);
// 格式化输出
console.log(chalk.bold('\n文件列表:\n'));
for (const file of files) {
const size = chalk.gray(`(${formatSize(file.size)})`);
const date = chalk.gray(file.modifiedAt.toLocaleDateString());
console.log(` ${chalk.cyan(file.name)} ${size} ${date}`);
}
// 统计摘要
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
console.log(chalk.bold(`\n合计:${files.length} 个文件,${formatSize(totalSize)}`));
} catch (error) {
spinner.fail('扫描失败');
throw new CLIError(`扫描错误:${(error as Error).message}`);
}
});
// src/commands/rename.ts
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import { scanDirectory } from '../utils/fs.js';
interface RenameOptions {
dryRun: boolean;
prefix: string;
lowercase: boolean;
noSpaces: boolean;
pattern: string;
interactive: boolean;
}
export const renameCommand = new Command('rename')
.description('批量重命名文件')
.argument('[path]', '目标目录', '.')
.option('--dry-run', '预览模式,不实际重命名', false)
.option('--prefix <prefix>', '添加前缀')
.option('--lowercase', '文件名转小写', false)
.option('--no-spaces', '空格替换为连字符', false)
.option('--pattern <pattern>', '正则匹配替换 (格式: /pattern/replacement/)')
.option('-i, --interactive', '交互式逐个确认', false)
.action(async (targetPath: string, options: RenameOptions) => {
const spinner = ora('正在分析文件...').start();
const files = await scanDirectory(targetPath, { recursive: false });
spinner.stop();
// 生成重命名计划
const plan = buildRenamePlan(files, options);
if (plan.length === 0) {
console.log(chalk.yellow('没有需要重命名的文件'));
return;
}
// 显示预览
console.log(chalk.bold('\n重命名计划:\n'));
for (const item of plan) {
console.log(` ${chalk.red(item.original)} → ${chalk.green(item.renamed)}`);
}
console.log(chalk.bold(`\n共 ${plan.length} 个文件将被重命名\n`));
if (options.dryRun) {
console.log(chalk.yellow('(预览模式,未实际执行)'));
return;
}
// 交互确认或批量执行
if (options.interactive) {
for (const item of plan) {
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: `重命名 ${item.original} → ${item.renamed}?`,
default: true,
}]);
if (confirm) {
await renameFile(item.originalPath, item.renamedPath);
console.log(chalk.green(' ✓ 已重命名'));
}
}
} else {
const { proceed } = await inquirer.prompt([{
type: 'confirm',
name: 'proceed',
message: `确认执行以上 ${plan.length} 个重命名操作?`,
default: false,
}]);
if (!proceed) {
console.log(chalk.yellow('已取消'));
return;
}
const spinner2 = ora('正在重命名...').start();
for (const item of plan) {
await renameFile(item.originalPath, item.renamedPath);
}
spinner2.succeed(`已完成 ${plan.length} 个文件的重命名`);
}
});
// src/commands/report.ts
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import { writeFileSync } from 'fs';
import { join } from 'path';
import { buildDirectoryTree, calculateStats } from '../utils/fs.js';
export const reportCommand = new Command('report')
.description('生成目录结构 JSON 报告')
.argument('[path]', '目标目录', '.')
.option('-o, --output <file>', '输出文件路径', 'fileforge-report.json')
.option('--include-hidden', '包含隐藏文件', false)
.option('--max-depth <n>', '最大递归深度', '10')
.action(async (targetPath: string, options: ReportOptions) => {
const spinner = ora('正在分析目录...').start();
try {
const tree = await buildDirectoryTree(targetPath, {
includeHidden: options.includeHidden,
maxDepth: parseInt(options.maxDepth, 10),
});
const stats = calculateStats(tree);
const report = {
generatedAt: new Date().toISOString(),
rootPath: targetPath,
tree,
stats: {
totalFiles: stats.fileCount,
totalDirs: stats.dirCount,
totalSize: stats.totalSize,
sizeByExtension: stats.sizeByExtension,
largestFiles: stats.largestFiles.slice(0, 10),
},
};
const outputPath = join(targetPath, options.output);
writeFileSync(outputPath, JSON.stringify(report, null, 2));
spinner.succeed('报告已生成');
console.log(chalk.bold('\n目录统计:\n'));
console.log(` 文件数:${chalk.cyan(String(stats.fileCount))}`);
console.log(` 目录数:${chalk.cyan(String(stats.dirCount))}`);
console.log(` 总大小:${chalk.cyan(formatSize(stats.totalSize))}`);
console.log(` 报告路径:${chalk.green(outputPath)}\n`);
// 按类型统计 Top 5
const top5 = Object.entries(stats.sizeByExtension)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
if (top5.length > 0) {
console.log(chalk.bold('按类型分布:'));
for (const [ext, size] of top5) {
const bar = '█'.repeat(Math.ceil(size / stats.totalSize * 30));
console.log(` ${chalk.yellow(`.${ext || '无后缀'}`)} ${chalk.gray(bar)} ${formatSize(size)}`);
}
}
} catch (error) {
spinner.fail('报告生成失败');
throw new CLIError(`报告错误:${(error as Error).message}`);
}
});
// src/utils/fs.ts
import { readdir, stat, rename } from 'fs/promises';
import { join, extname, parse } from 'path';
export async function scanDirectory(
dirPath: string,
options: ScanOptions
): Promise<FileInfo[]> {
const entries = await readdir(dirPath, { withFileTypes: true });
const files: FileInfo[] = [];
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory() && options.recursive) {
const subFiles = await scanDirectory(fullPath, options);
files.push(...subFiles);
} else if (entry.isFile()) {
if (options.extensions) {
const ext = extname(entry.name).toLowerCase().slice(1);
if (!options.extensions.includes(ext)) continue;
}
const stats = await stat(fullPath);
files.push({
name: entry.name,
path: fullPath,
size: stats.size,
modifiedAt: stats.mtime,
extension: extname(entry.name).toLowerCase(),
});
}
}
return sortFiles(files, options.sortBy);
}
export async function renameFile(
oldPath: string,
newPath: string
): Promise<void> {
await rename(oldPath, newPath);
}
export async function buildDirectoryTree(
dirPath: string,
options: { includeHidden: boolean; maxDepth: number },
depth = 0
): Promise<DirectoryNode> {
if (depth > options.maxDepth) {
return { name: '...', type: 'dir', children: [] };
}
const entries = await readdir(dirPath, { withFileTypes: true });
const children: DirectoryNode[] = [];
for (const entry of entries) {
if (!options.includeHidden && entry.name.startsWith('.')) continue;
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
children.push(
await buildDirectoryTree(fullPath, options, depth + 1)
);
} else {
const stats = await stat(fullPath);
children.push({
name: entry.name,
type: 'file',
size: stats.size,
});
}
}
return { name: dirPath, type: 'dir', children };
}
// src/utils/format.ts
export function formatSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}
export function sanitizeFileName(name: string): string {
return name
.replace(/\s+/g, '-')
.replace(/[^\w\-.]/g, '')
.toLowerCase();
}
// src/types/index.ts
export interface FileInfo {
name: string;
path: string;
size: number;
modifiedAt: Date;
extension: string;
}
export type SortField = 'name' | 'size' | 'date';
export interface ScanOptions {
recursive: boolean;
extensions: string[] | null;
sortBy: SortField;
}
export interface RenamePlan {
original: string;
renamed: string;
originalPath: string;
renamedPath: string;
}
export interface DirectoryNode {
name: string;
type: 'file' | 'dir';
size?: number;
children?: DirectoryNode[];
}
export class CLIError extends Error {
constructor(message: string) {
super(message);
this.name = 'CLIError';
}
}
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
include: ['src/**/*.ts'],
exclude: ['src/index.ts'],
},
},
});
// tests/utils/fs.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdir, writeFile, rm } from 'fs/promises';
import { join } from 'path';
import { scanDirectory, buildDirectoryTree } from '../../src/utils/fs.js';
const TEST_DIR = join(import.meta.dirname, '__test_fixtures__');
beforeEach(async () => {
await mkdir(TEST_DIR, { recursive: true });
await writeFile(join(TEST_DIR, 'hello.txt'), 'hello');
await writeFile(join(TEST_DIR, 'data.json'), '{"key":"value"}');
await writeFile(join(TEST_DIR, 'image.png'), Buffer.alloc(1024));
await mkdir(join(TEST_DIR, 'subdir'), { recursive: true });
await writeFile(join(TEST_DIR, 'subdir', 'nested.md'), '# nested');
});
afterEach(async () => {
await rm(TEST_DIR, { recursive: true, force: true });
});
describe('scanDirectory', () => {
it('应返回目录中的所有文件', async () => {
const files = await scanDirectory(TEST_DIR, {
recursive: false,
extensions: null,
sortBy: 'name',
});
expect(files).toHaveLength(3);
expect(files.map(f => f.name).sort()).toEqual(
['data.json', 'hello.txt', 'image.png']
);
});
it('应按扩展名过滤文件', async () => {
const files = await scanDirectory(TEST_DIR, {
recursive: false,
extensions: ['txt'],
sortBy: 'name',
});
expect(files).toHaveLength(1);
expect(files[0].name).toBe('hello.txt');
});
it('应递归扫描子目录', async () => {
const files = await scanDirectory(TEST_DIR, {
recursive: true,
extensions: null,
sortBy: 'name',
});
expect(files).toHaveLength(4); // 3 + 1 nested
});
it('应正确返回文件大小', async () => {
const files = await scanDirectory(TEST_DIR, {
recursive: false,
extensions: null,
sortBy: 'name',
});
const png = files.find(f => f.name === 'image.png');
expect(png?.size).toBe(1024);
});
});
describe('buildDirectoryTree', () => {
it('应构建完整的目录树', async () => {
const tree = await buildDirectoryTree(TEST_DIR, {
includeHidden: false,
maxDepth: 5,
});
expect(tree.type).toBe('dir');
expect(tree.children?.length).toBe(4); // 3 files + 1 subdir
});
it('应排除隐藏文件', async () => {
await writeFile(join(TEST_DIR, '.hidden'), 'secret');
const tree = await buildDirectoryTree(TEST_DIR, {
includeHidden: false,
maxDepth: 5,
});
const hasHidden = tree.children?.some(c => c.name === '.hidden');
expect(hasHidden).toBe(false);
});
it('应遵守最大深度限制', async () => {
const tree = await buildDirectoryTree(TEST_DIR, {
includeHidden: false,
maxDepth: 0,
});
expect(tree.children?.length).toBe(0);
});
});
> 查看 src/commands/rename.ts 的实现,为 buildRenamePlan 函数编写完整的测试用例。
> 需要覆盖:
> 1. 小写转换
> 2. 空格替换
> 3. 前缀添加
> 4. 正则匹配替换
> 5. 组合规则
> 6. 无匹配文件时的空结果
> 测试文件放在 tests/commands/rename.test.ts
npm run build
预期输出:
> tsc
# dist/ 目录生成所有 .js 文件
# 在项目目录下创建全局符号链接
npm link
# 现在可以在任意目录使用
fileforge --help
fileforge scan ./my-project
fileforge rename ./my-project --dry-run --lowercase --no-spaces
fileforge report ./my-project -o analysis.json
| 问题 | 原因 | 解决方案 |
|---|---|---|
Cannot use import statement |
ESM/CJS 混用 | 确保 package.json 有 "type": "module" |
Command not found |
bin 路径错误 | 检查 package.json 的 bin 字段指向 dist/index.js |
ERR_MODULE_NOT_FOUND |
import 缺少 .js 后缀 |
ESM 模式下本地 import 必须带 .js |
#!/usr/bin/env node 无效 |
shebang 在非首行 | 确保 shebang 是文件第一行,前面无空行 |
chmod 权限问题 |
bin 文件不可执行 | npm link 自动处理,手动需 chmod +x dist/index.js |
# 1. 确认构建干净
npm run build
# 2. 运行全部测试
npm run test
# 3. 检查打包内容(确认不会发布源码和测试)
npx npm-packlist
# 应该只看到 dist/ 目录下的文件
# 4. 检查包大小
npm pack --dry-run
# 5. 确认 package.json 字段完整
cat package.json | grep -E '"name"|"version"|"description"|"keywords"|"license"|"repository"'
{
"name": "fileforge",
"version": "1.0.0",
"description": "批量文件处理 CLI 工具 — 扫描、重命名、报告",
"keywords": ["cli", "file", "rename", "batch", "tool"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourname/fileforge"
},
"author": "Your Name",
"prepublishOnly": "npm run test && npm run build"
}
# 首次登录
npm login
# 发布
npm publish
# 如果使用 GitHub Packages
npm publish --registry=https://npm.pkg.github.com
# 修补版本(bug fix)
npm version patch # 1.0.0 → 1.0.1
npm publish
# 功能版本(新功能)
npm version minor # 1.0.0 → 1.1.0
npm publish
# 主版本(破坏性变更)
npm version major # 1.0.0 → 2.0.0
npm publish
> 我需要给 fileforge 添加一个 convert 命令,用于批量转换图片格式。
> 需要支持 --format webp|avif|png 和 --quality 参数。
> 请遵循 CLAUDE.md 中的项目结构规范,创建命令文件和对应的测试。
Claude Code 会:
1. 读取 CLAUDE.md 了解项目规范
2. 在 src/commands/ 创建 convert.ts
3. 在 src/index.ts 注册新命令
4. 在 tests/ 创建对应测试
5. 更新 package.json 如果需要新依赖
-p 模式做一次性任务# 快速检查代码质量
claude -p "检查 src/ 目录下所有文件,找出可能的类型安全问题"
# 批量更新依赖
claude -p "检查 package.json 中所有依赖,列出有新版本可用的,然后逐个更新并确认测试通过"
# 生成 CHANGELOG
claude -p "根据最近 10 个 git commit 生成 CHANGELOG.md,使用 conventional commits 格式"
创建 .claude/skills/add-command.md:
---
name: add-command
description: 为 fileforge 添加新 CLI 命令的标准化流程
---
## 使用场景
当用户要求添加新的 CLI 命令时触发。
## 执行步骤
1. 在 src/commands/ 创建命令文件,命名与命令名一致
2. 使用 Commander.js 的 Command 类定义命令
3. 实现核心逻辑,错误使用 CLIError 类
4. 使用 chalk 着色输出,ora 显示进度
5. 交互式操作使用 inquirer
6. 在 src/index.ts 中 import 并 addCommand
7. 在 tests/commands/ 创建对应测试文件
8. 运行 npm run build && npm run test 确认通过
然后在 Claude Code 中:
> /add-command 添加一个 clean 命令,用于清理空目录和临时文件
# Step 1: 扫描目录,看看有什么
fileforge scan ~/Downloads --recursive --sort size
# 输出:
# ✔ 找到 247 个文件
#
# 文件列表:
# large-video.mp4 (2.3 GB) 2026/5/15
# presentation.pptx (15.2 MB) 2026/5/18
# screenshot-2026-01.png (1.1 MB) 2026/1/12
# ...
# Step 2: 预览重命名效果
fileforge rename ~/Downloads --dry-run --lowercase --no-spaces
# 输出:
# 重命名计划:
# Screen Shot 2026.png → screen-shot-2026.png
# Final Report (1).pdf → final-report-(1).pdf
# ...
# (预览模式,未实际执行)
# Step 3: 交互式确认执行
fileforge rename ~/Downloads --lowercase --no-spaces --interactive
# Step 4: 生成报告
fileforge report ~/Downloads -o downloads-report.json
# 输出:
# ✔ 报告已生成
#
# 目录统计:
# 文件数:247
# 目录数:12
# 总大小:8.5 GB
# 报告路径:/home/user/Downloads/downloads-report.json
#
# 按类型分布:
# .png ██████████████ 2.1 GB
# .mp4 ██████████ 1.8 GB
# .pdf ████ 890.5 MB
graph TB
subgraph CLI入口
A[fileforge command] --> B[Commander.js 路由]
end
subgraph 命令层
B --> C[scan]
B --> D[rename]
B --> E[report]
B --> F[convert...]
end
subgraph 工具层
C --> G[fs.ts<br/>文件系统封装]
D --> G
E --> G
F --> G
C --> H[format.ts<br/>格式化工具]
D --> H
E --> H
end
subgraph 依赖
G --> I[node:fs/promises]
H --> J[chalk / ora]
D --> K[inquirer]
C --> L[commander]
end
subgraph 输出
C --> M[终端表格]
D --> N[文件重命名]
E --> O[JSON 报告文件]
end
style A fill:#7c3aed,color:#fff
style B fill:#6d28d9,color:#fff
style G fill:#059669,color:#fff
style H fill:#059669,color:#fff
flowchart LR
A[需求描述] --> B[Claude Code<br/>生成骨架]
B --> C[本地 npm link]
C --> D[手动测试]
D --> E{发现问题?}
E -->|是| F[描述问题<br/>给 Claude Code]
F --> C
E -->|否| G[npm run test]
G --> H{测试通过?}
H -->|否| I[修复测试]
I --> G
H -->|是| J[npm publish]
J --> K[npm version patch]
K --> L[迭代开发]
L --> A
style A fill:#f59e0b,color:#000
style B fill:#7c3aed,color:#fff
style J fill:#059669,color:#fff
## 新命令模板
每个命令文件必须包含:
1. Commander Command 定义(description、arguments、options)
2. async action 函数
3. ora spinner 包装
4. try/catch + CLIError 错误处理
5. chalk 格式化输出
这样每次添加命令时,Claude Code 会自动遵循这个模式。
# .claude/commands/check-exports.md
检查 src/ 目录下所有文件的 export 是否正确。
特别关注:
- 命令文件的 export 命名是否与 import 一致
- 类型文件是否 re-export
- index.ts 是否注册了所有命令
运行 npm run build 确认无编译错误。
使用方式:/check-exports
在 .claude/settings.json 中配置:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "cd $PROJECT_DIR && npx vitest run --reporter=verbose 2>&1 | tail -5"
}
]
}
}
每次文件编辑后自动运行测试,Claude Code 可以即时看到测试结果。
> 我需要同时开发三个新功能:
> 1. watch 命令 — 监听文件变化
> 2. diff 命令 — 对比两个目录差异
> 3. sync 命令 — 双向同步目录
>
> 请创建 Task subagent 并行开发这三个命令,每个命令包含实现和测试。
> 完成后汇总结果到 src/index.ts。
Claude Code 会启动三个并行 subagent,每个独立开发一个命令,最后汇总。
| 维度 | 评估 |
|---|---|
| 适合训练营类型 | Node.js 进阶训练营、工具链开发训练营、DevOps 自动化训练营 |
| 预计教学时长 | 4-6 小时(含实操) |
| 难度等级 | 中等(需 TypeScript 和 Node.js 基础) |
| 实操占比 | 70% 编码 + 20% 测试调试 + 10% 发布流程 |
| 前置知识 | TypeScript 基础、npm 包管理、命令行操作 |
| 产出物 | 一个可安装、可发布、有完整测试的 npm CLI 工具 |
| 延伸方向 | oclif 框架迁移、GitHub Actions 自动发布、插件化架构 |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
