学习前置准备
在学习本课程之前,请先将 nest-commander 仓库克隆到本地 external 目录:
# 使用 depth=1 减少克隆时间
git clone --depth=1 https://github.com/jmcdo29/nest-commander.git external/nest-commander
当前文档信息:
- 研究分支 (Branch):
master - Git SHA1:
b0493c16e52636fc05edf6dcb7d42c573bbbd0a3 - 外部仓库位置:
external/nest-commander/
Nest Commander 学习资料
1. 项目概述
Nest Commander 是一个为 NestJS 框架设计的 CLI (命令行界面) 构建工具。它允许开发者使用与 NestJS 相同的装饰器模式和依赖注入机制来构建命令行应用程序。该项目基于流行的 Commander.js 包构建。
- GitHub 仓库: https://github.com/jmcdo29/nest-commander
- 当前版本: 3.20.1
- 许可证: MIT
- 作者: Jay McDoniel
2. 项目架构
2.1 Monorepo 结构
该项目使用 Nx 作为 Monorepo 管理工具,包含以下主要包:
| 包名 | 版本 | 描述 |
|---|---|---|
nest-commander | 3.20.1 | 核心包,提供 CLI 构建能力 |
nest-commander-testing | 3.5.1 | 测试工具包 |
nest-commander-schematics | 3.2.0 | Angular Schematics 代码生成工具 |
2.2 目录结构
nest-commander/
├── apps/
│ └── docs/ # Astro 文档站点
├── packages/
│ ├── nest-commander/ # 核心包
│ ├── nest-commander-testing/ # 测试工具
│ └── nest-commander-schematics/ # 代码生成
├── integration/ # 集成测试 (21 个测试用例)
├── package.json # 根 package.json
└── nx.json # Nx 配置
2.3 核心依赖
主要依赖:
commander: 11.1.0 - CLI 参数解析@golevelup/nestjs-discovery: 5.0.0 - NestJS 服务发现inquirer: 8.2.7 - 交互式命令行提示cosmiconfig: 8.3.6 - 配置文件加载@fig/complete-commander: 3.0.0 - 命令补全
对等依赖 (Peer Dependencies):
@nestjs/common: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0@nestjs/core: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
3. 核心功能模块
3.1 命令定义 (@Command() 装饰器)
使用装饰器定义 CLI 命令:
@Command({
name: 'greet',
arguments: '<name>',
description: 'Greet someone',
})
export class GreetCommand implements CommandRunner {
async run([name]: string[], options: Record<string, any>): Promise<void> {
console.log(`Hello, ${name}!`);
}
}
3.2 选项定义 (@Option() 装饰器)
@Option({
flags: '-l, --love',
description: 'Express your love',
defaultValue: false,
})
parseLove(val: string): boolean {
return JSON.parse(val);
}
3.3 命令工厂 (CommandFactory)
import { CommandFactory } from 'nest-commander';
async function bootstrap() {
await CommandFactory.run(RootModule, ['greet', 'bye']);
}
4. 集成测试覆盖
项目包含 21 个集成测试用例,覆盖以下功能场景:
| 测试目录 | 功能描述 |
|---|---|
basic | 基础命令功能 |
multiple | 多命令支持 |
sub-commands | 子命令 |
default-sub-commands | 默认子命令 |
root-command | 根命令 |
this-command | 命令嵌套 |
this-handler | 自定义处理器 |
dot-command | 点命令 |
help-tests | 帮助信息 |
version-option | 版本选项 |
option-choices | 选项可选值 |
output-config | 输出配置 |
pizza | Inquirer 交互式提问 |
with-questions | 交互式问题 |
plugins | 插件系统 |
register-provider | 自定义 Provider |
request-provider-override | Provider 覆盖 |
5. 学习计划
5.1 入门阶段 (预计 2-3 小时)
-
环境准备
-
安装 Node.js (推荐 LTS 版本)
- 克隆项目仓库
-
安装 pnpm 依赖:
pnpm install -
基础概念学习
-
阅读官方文档: https://nest-commander.jaymcdoniel.dev
- 理解
@Command()装饰器 - 理解
@Option()装饰器 -
了解
CommandRunner接口 -
Hello World 示例
-
创建第一个命令
- 使用
CommandFactory启动
5.2 进阶阶段 (预计 3-4 小时)
-
依赖注入
-
在命令中使用 NestJS 依赖注入
-
自定义 Provider 注册
-
子命令与嵌套
#### 5.1 子命令基础用法
使用 @SubCommand() 装饰器定义子命令,通过父命令的 subCommands 数组注册:
import { Command, CommandRunner } from 'nest-commander';
import { SubCommand } from 'nest-commander';
// 定义子命令
@SubCommand({ name: 'mid-1' })
export class Mid1Command extends CommandRunner {
async run() {
console.log('mid-1 command executed');
}
}
// 父命令注册子命令
@Command({
name: 'top',
subCommands: [Mid1Command],
})
export class TopCommand extends CommandRunner {
async run() {
console.log('top command');
}
}
#### 5.2 子命令嵌套
子命令可以拥有自己的子命令,形成多层嵌套结构:
// 底层子命令
@SubCommand({ name: 'bottom' })
export class BottomCommand extends CommandRunner {
async run() {
console.log('bottom command');
}
}
// 中层子命令(拥有自己的子命令)
@SubCommand({ name: 'mid-1', subCommands: [BottomCommand] })
export class Mid1Command extends CommandRunner {
async run() {
console.log('mid-1 command');
}
}
// 顶层命令
@Command({
name: 'top',
subCommands: [Mid1Command],
})
export class TopCommand extends CommandRunner {
async run() {
console.log('top command');
}
}
运行效果:
top→ 执行 TopCommandtop mid-1→ 执行 Mid1Commandtop mid-1 bottom→ 执行 BottomCommand
#### 5.3 默认子命令
使用 options: { isDefault: true } 设置默认子命令,当不指定子命令时自动执行:
@SubCommand({
name: 'mid-1',
subCommands: [BottomCommand],
options: { isDefault: true }, // 设置为默认子命令
})
export class Mid1Command extends CommandRunner {
async run() {
console.log('default sub command');
}
}
运行 top 时会自动执行 mid-1 子命令。
#### 5.4 子命令别名
使用 aliases 为子命令设置简短别名:
@SubCommand({ name: 'mid-2', aliases: ['m'] })
export class Mid2Command extends CommandRunner {
async run() {
console.log('mid-2 command');
}
}
现在可以使用 top mid-2 或 top m 两种方式调用。
#### 5.5 完整示例:多层嵌套命令
结合以上特性,创建一个完整的多层嵌套命令:
// bottom.command.ts
@SubCommand({ name: 'bottom' })
export class BottomCommand extends CommandRunner {
constructor(private readonly log: LogService) { super(); }
async run() { this.log.log('bottom command executed'); }
}
// mid-1.command.ts(带默认子命令)
@SubCommand({
name: 'mid-1',
subCommands: [BottomCommand],
options: { isDefault: true },
})
export class Mid1Command extends CommandRunner {
constructor(private readonly log: LogService) { super(); }
async run() { this.log.log('mid-1 command executed'); }
}
// mid-2.command.ts(带别名)
@SubCommand({ name: 'mid-2', aliases: ['m'] })
export class Mid2Command extends CommandRunner {
constructor(private readonly log: LogService) { super(); }
async run() { this.log.log('mid-2 command executed'); }
}
// top.command.ts
@Command({
name: 'top',
subCommands: [Mid1Command, Mid2Command],
})
export class TopCommand extends CommandRunner {
constructor(private readonly log: LogService) { super(); }
async run(inputs: string[]) { this.log.log('top command'); }
}
命令调用对照表:
| 命令 | 执行结果 |
|---|---|
top | Mid1Command(默认)→ BottomCommand(默认) |
top mid-1 | Mid1Command |
top mid-1 bottom | BottomCommand |
top mid-2 | Mid2Command |
top m | Mid2Command(别名) |
-
选项处理
-
必选选项 vs 可选选项
- 选项类型转换 (boolean, number, array)
5.3 高级阶段 (预计 4-5 小时)
- 交互式命令行
#### 7.1 基础用法 - InquirerService 注入
通过注入 InquirerService 实现交互式命令行:
import { Command, CommandRunner, InquirerService } from 'nest-commander';
@Command({ name: 'hello' })
export class HelloCommand extends CommandRunner {
constructor(private readonly inquirer: InquirerService) {
super();
}
async run(_inputs: string[], options?: HelloOptions): Promise<void> {
// 询问问题集 'hello',传入已解析的选项
options = await this.inquirer.ask('hello', options);
console.log(`Hello ${options.name}`);
}
}
#### 7.2 问题定义 - @QuestionSet 和 @Question 装饰器
使用装饰器定义问题集:
import { Question, QuestionSet } from 'nest-commander';
@QuestionSet({ name: 'hello' })
export class WhoQuestion {
@Question({
message: 'What is your name?',
name: 'name',
})
parseName(val: string) {
return val;
}
}
常用问题类型:
| 类型 | 描述 | 适用场景 |
|---|---|---|
input | 文本输入 | 姓名、地址等 |
confirm | 是/否确认 | 布尔值选项 |
list | 列表选择 | 单选 |
rawlist | 数字索引列表 | 单选(带编号) |
expand | 展开列表 | 单选(带快捷键) |
checkbox | 复选框 | 多选 |
editor | 编辑器 | 长文本输入 |
#### 7.3 问题验证 - @ValidateFor 装饰器
使用 @ValidateFor 添加字段验证:
@ValidateFor({ name: 'phone' })
validatePhone(value: string) {
const pass = value.match(/^\d{10}$/);
if (pass) {
return true;
}
return 'Please enter a valid 10-digit phone number';
}
#### 7.4 条件问题 - @WhenFor 装饰器
根据前一个答案决定是否显示某个问题:
@WhenFor({ name: 'prize' })
whenPrize(answers: { comments: string }): boolean {
// 只有当 comments 不是默认值时才显示 prize 问题
return answers.comments !== 'Nope, all good!';
}
#### 7.5 完整示例:Pizza 订单
综合运用所有特性:
// pizza.interface.ts
export interface PizzaOptions {
toppings?: string;
toBeDelivered?: boolean;
phone?: string;
size?: string;
quantity?: number;
beverage?: string;
comments?: string;
prize?: string;
}
// pizza.question.ts
import { Question, QuestionSet, ValidateFor, WhenFor } from 'nest-commander';
@QuestionSet({ name: 'pizza' })
export class PizzaQuestion {
// 展开列表选择
@Question({
type: 'expand',
name: 'toppings',
message: 'What about the toppings?',
choices: [
{ key: 'p', name: 'Pepperoni and cheese', value: 'PepperoniCheese' },
{ key: 'a', name: 'All dressed', value: 'alldressed' },
{ key: 'w', name: 'Hawaiian', value: 'hawaiian' },
],
})
parseToppings(val: string) { return val; }
// 确认问题
@Question({
type: 'confirm',
name: 'toBeDelivered',
message: 'Is this for delivery?',
default: false,
})
parseToBeConfirmed(val: boolean) { return val; }
// 带验证的输入
@Question({
type: 'input',
name: 'phone',
message: "What's your phone number?",
})
parsePhone(val: string) { return val; }
@ValidateFor({ name: 'phone' })
validatePhone(value: string) {
const pass = value.match(/^\d{10}$/);
return pass ? true : 'Please enter a valid phone number';
}
// 列表选择
@Question({
type: 'list',
name: 'size',
message: 'What size do you need?',
choices: ['Large', 'Medium', 'Small'],
})
parseSize(val: string) { return val.toLowerCase(); }
// 条件问题
@WhenFor({ name: 'prize' })
whenPrize(answers: { comments: string }): boolean {
return answers.comments !== 'Nope, all good!';
}
}
// pizza.command.ts
import { Command, CommandRunner, InquirerService } from 'nest-commander';
import { PizzaOptions } from './pizza.interface';
@Command({ name: 'pizza', options: { isDefault: true } })
export class PizzaCommand extends CommandRunner {
constructor(private readonly inquirerService: InquirerService) {
super();
}
async run(_inputs: string[], options?: PizzaOptions): Promise<void> {
options = await this.inquirerService.ask('pizza', options);
console.log(options); // 输出完整订单
}
}
执行效果:
$ my-cli pizza
? What about the toppings? (p) Pepperoni and cheese, (a) All dressed, (w) Hawaiian
? Is this for delivery? No
? What's your phone number? 1234567890
? What size do you need? Large
? How many do you need? 2
? You also get a free 2L beverage Pepsi
? Any comments on your purchase experience? Great service!
? For leaving a comment, you get a freebie cake
{ toppings: 'PepperoniCheese', toBeDelivered: false, phone: '1234567890', ... }
#### 7.6 在模块中注册问题
确保在 NestJS 模块中注册问题和命令:
import { Module } from '@nestjs/common';
import { PizzaCommand } from './pizza.command';
import { PizzaQuestion } from './pizza.question';
@Module({
providers: [PizzaCommand, PizzaQuestion],
})
export class PizzaModule {}
- 配置管理
#### 8.1 cosmiconfig 集成
Nest Commander 内置支持 cosmiconfig,用于自动搜索和加载配置文件。CommandFactory.run() 会自动调用 cosmiconfig 搜索配置文件。
支持的配置文件格式:
| 文件名 | 格式 |
|---|---|
.myclirc | JavaScript |
.myclirc.json | JSON |
.myclirc.yaml | YAML |
.myclirc.yml | YAML |
myclirc.json | JSON |
myclirc.yaml | YAML |
myclirc.yml | YAML |
package.json | 包含 mycli 字段 |
(假设 CLI 名称为 mycli)
#### 8.2 配置文件位置搜索顺序
cosmiconfig 从项目根目录开始搜索:
- 当前目录
- 逐级向上查找直到根目录
#### 8.3 插件配置
在配置文件中使用 plugins 字段加载自定义插件:
// my-plugin.js - 导出默认 NestJS 模块
import { Module } from '@nestjs/common';
@Module({
providers: [MyCustomProvider],
})
export class MyPluginModule {}
工作原理:
CommandFactory.run()调用cosmiconfig搜索配置文件- 读取配置中的
plugins数组 - 使用
import()动态加载插件模块 - 将插件模块添加到 NestJS 应用上下文中
#### 8.4 环境变量处理
在 @Option 装饰器中使用 env 属性绑定环境变量:
import { Command, CommandRunner, Option } from 'nest-commander';
@Command({ name: 'config' })
export class ConfigCommand extends CommandRunner {
async run() {
console.log('Config loaded from environment');
}
@Option({
flags: '-u, --username <username>',
description: 'Username',
env: 'MYCLI_USERNAME', // 绑定环境变量
})
parseUsername(val: string): string {
return val;
}
@Option({
flags: '-t, --token <token>',
description: 'API Token',
env: 'MYCLI_TOKEN', // 绑定环境变量
})
parseToken(val: string): string {
return val;
}
}
优先级:命令行参数 > 环境变量 > 默认值
#### 8.5 完整配置示例
假设 CLI 名称为 myapp:
// .myapprc.json
{
"plugins": [
"./plugins/auth-plugin",
"./plugins/logger-plugin"
],
"verbose": true,
"logLevel": "debug"
}
// bootstrap.ts
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';
async function bootstrap() {
await CommandFactory.run(AppModule);
}
bootstrap();
执行时,Nest Commander 会自动:
- 搜索
.myapprc.json配置文件 - 加载
auth-plugin和logger-plugin模块 - 将它们注册到 NestJS 应用中
#### 8.6 自定义配置加载位置
如果需要自定义配置加载行为,可以扩展 CommandFactory:
import { CommandFactory } from 'nest-commander';
class CustomCommandFactory extends CommandFactory {
// 自定义逻辑
}
但通常不需要这样做,默认的 cosmiconfig 集成已足够满足大多数场景。
-
测试
-
使用
nest-commander-testing进行单元测试 - 集成测试编写
5.4 生产实践 (预计 2-3 小时)
-
插件系统
- 理解 Commander 插件机制
- 自定义插件开发
-
Schematics
- 使用
nest-commander-schematics生成代码 - 自定义 schematics 开发
- 使用
-
最佳实践
- 错误处理
- 日志记录
- 构建与发布
6. 关键文件索引
| 文件路径 | 描述 |
|---|---|
packages/nest-commander/src/command.decorators.ts | @Command() 和 @Option() 装饰器实现 |
packages/nest-commander/src/command.factory.ts | CommandFactory 核心类 |
packages/nest-commander/src/command-runner.service.ts | 命令执行服务 |
packages/nest-commander/src/inquirer.service.ts | Inquirer 集成服务 |
packages/nest-commander-testing/src/command-test.factory.ts | 测试工具工厂 |
integration/basic/src/basic.command.ts | 基础命令示例 |
7. 相关资源
- 官方文档: https://nest-commander.jaymcdoniel.dev
- GitHub Issues: https://github.com/jmcdo29/nest-commander/issues
- Commander.js 文档: https://github.com/tj/commander.js
- NestJS 文档: https://docs.nestjs.com
Backlinks (1)
flowchart LR
n0["AI"]
n1["Database"]
n2["Dev Tools"]
n3["Emoji"]
n4["Frontend"]
n5["Game Dev"]
n6["Collection"]
n7["Languages"]
n8["Maps"]
n9["Media"]
n10["Monitor"]
n11["Plans"]
n12["对象存储基本用法(Bucket / Object / 常用操作)"]
n13["对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"]
n14["Object Storage"]
n15["挂载 Bucket 为本地文件系统(FUSE Mount)"]
n16["对象存储签名 URL(Signed URL)原理与实战"]
n17["对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"]
n18["Prototypes"]
n19["Research"]
n20["Better Auth 源码阅读指南"]
n21["DuckDB 环境与基本使用"]
n22["DuckDB 实战研究"]
n23["DuckDB 模拟数据"]
n24["PostgreSQL 数据用 DuckDB 加速查询"]
n25["HK 角色阅读"]
n26["HK 台词阅读"]
n27["Hollow Knight 英语主题"]
n28["HK 物品阅读"]
n29["HK 地点阅读"]
n30["HK 世界观 / Lore 阅读"]
n31["HK 英语学习素材清单"]
n32["英语学习 Dashboard"]
n33["English Scraps Archive"]
n34["English Scraps 使用指南"]
n35["Jellyfin 源码阅读指南"]
n36["Lux 资料整理"]
n37["Nest Commander 学习资料"]
n38["NestJS 源码阅读指南"]
n39["Protomaps 自建底图研究"]
n40["自制 PMTiles 地图(最简单例子)"]
n41["MapLibre 集成 Protomaps"]
n42["PMTiles 格式与工具链"]
n43["上海地区底图项目"]
n44["Redash 源码阅读指南"]
n45["Rust 学习计划"]
n46["shadcn/ui 源码阅读指南"]
n47["TRIP 项目核心原理与代码阅读指南"]
n1 --> n22
n6 --> n0
n6 --> n1
n6 --> n2
n6 --> n3
n6 --> n4
n6 --> n5
n6 --> n7
n6 --> n8
n6 --> n9
n6 --> n10
n6 --> n11
n6 --> n19
n12 --> n13
n12 --> n16
n12 --> n17
n12 --> n18
n13 --> n12
n13 --> n15
n13 --> n16
n13 --> n17
n14 --> n12
n14 --> n13
n14 --> n15
n14 --> n16
n14 --> n17
n15 --> n12
n15 --> n16
n15 --> n17
n16 --> n18
n17 --> n12
n17 --> n13
n17 --> n16
n17 --> n18
n18 --> n12
n18 --> n15
n18 --> n16
n18 --> n17
n18 --> n39
n19 --> n20
n19 --> n22
n19 --> n32
n19 --> n35
n19 --> n36
n19 --> n37
n19 --> n38
n19 --> n39
n19 --> n44
n19 --> n45
n19 --> n46
n19 --> n47
n21 --> n23
n22 --> n1
n22 --> n19
n22 --> n21
n22 --> n23
n22 --> n24
n23 --> n21
n23 --> n24
n24 --> n22
n24 --> n23
n27 --> n25
n27 --> n26
n27 --> n28
n27 --> n29
n27 --> n30
n27 --> n31
n27 --> n34
n31 --> n26
n32 --> n27
n32 --> n33
n32 --> n34
n34 --> n32
n34 --> n33
n39 --> n8
n39 --> n40
n39 --> n41
n39 --> n42
n39 --> n43
n40 --> n42
n41 --> n18
n41 --> n43
n42 --> n40
n42 --> n43
n43 --> n41
n43 --> n42
click n0 "../../../collection/ai/" "AI"
click n1 "../../../collection/database/" "Database"
click n2 "../../../collection/dev-tools/" "Dev Tools"
click n3 "../../../collection/emoji/" "Emoji"
click n4 "../../../collection/frontend/" "Frontend"
click n5 "../../../collection/game-dev/" "Game Dev"
click n6 "../../../collection/" "Collection"
click n7 "../../../collection/languages/" "Languages"
click n8 "../../../collection/maps/" "Maps"
click n9 "../../../collection/media/" "Media"
click n10 "../../../collection/monitor/" "Monitor"
click n11 "../../../collection/scraps/plans/" "Plans"
click n12 "../../../knowledge/infrastructure/cloud/object-storage/basic-usage/" "对象存储基本用法(Bucket / Object / 常用操作)"
click n13 "../../../knowledge/infrastructure/cloud/object-storage/data-migration/" "对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"
click n14 "../../../knowledge/infrastructure/cloud/object-storage/" "Object Storage"
click n15 "../../../knowledge/infrastructure/cloud/object-storage/mount-bucket/" "挂载 Bucket 为本地文件系统(FUSE Mount)"
click n16 "../../../knowledge/infrastructure/cloud/object-storage/signed-url/" "对象存储签名 URL(Signed URL)原理与实战"
click n17 "../../../knowledge/infrastructure/cloud/object-storage/vendors-comparison/" "对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"
click n18 "../../../prototypes/" "Prototypes"
click n19 "../../" "Research"
click n20 "../better-auth/" "Better Auth 源码阅读指南"
click n21 "../duckdb/basic-usage/" "DuckDB 环境与基本使用"
click n22 "../duckdb/" "DuckDB 实战研究"
click n23 "../duckdb/mock-data/" "DuckDB 模拟数据"
click n24 "../duckdb/postgresql-acceleration/" "PostgreSQL 数据用 DuckDB 加速查询"
click n25 "../english/hollow-knight/characters/" "HK 角色阅读"
click n26 "../english/hollow-knight/dialogues/" "HK 台词阅读"
click n27 "../english/hollow-knight/" "Hollow Knight 英语主题"
click n28 "../english/hollow-knight/items/" "HK 物品阅读"
click n29 "../english/hollow-knight/locations/" "HK 地点阅读"
click n30 "../english/hollow-knight/lore/" "HK 世界观 / Lore 阅读"
click n31 "../english/hollow-knight/resources/" "HK 英语学习素材清单"
click n32 "../english/" "英语学习 Dashboard"
click n33 "../english/scraps/archive/" "English Scraps Archive"
click n34 "../english/scraps/" "English Scraps 使用指南"
click n35 "../jellyfin/" "Jellyfin 源码阅读指南"
click n36 "../lux/" "Lux 资料整理"
click n37 "./" "Nest Commander 学习资料"
click n38 "../nestjs/" "NestJS 源码阅读指南"
click n39 "../protomaps/" "Protomaps 自建底图研究"
click n40 "../protomaps/make-own-map/" "自制 PMTiles 地图(最简单例子)"
click n41 "../protomaps/maplibre/" "MapLibre 集成 Protomaps"
click n42 "../protomaps/pmtiles/" "PMTiles 格式与工具链"
click n43 "../protomaps/shanghai-map/" "上海地区底图项目"
click n44 "../redash/" "Redash 源码阅读指南"
click n45 "../rust/" "Rust 学习计划"
click n46 "../shadcn-ui/" "shadcn/ui 源码阅读指南"
click n47 "../trip/" "TRIP 项目核心原理与代码阅读指南"