Skip to content

学习前置准备

在学习本课程之前,请先将 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 小时)

  1. 环境准备

  2. 安装 Node.js (推荐 LTS 版本)

  3. 克隆项目仓库
  4. 安装 pnpm 依赖: pnpm install

  5. 基础概念学习

  6. 阅读官方文档: https://nest-commander.jaymcdoniel.dev

  7. 理解 @Command() 装饰器
  8. 理解 @Option() 装饰器
  9. 了解 CommandRunner 接口

  10. Hello World 示例

  11. 创建第一个命令

  12. 使用 CommandFactory 启动

5.2 进阶阶段 (预计 3-4 小时)

  1. 依赖注入

  2. 在命令中使用 NestJS 依赖注入

  3. 自定义 Provider 注册

  4. 子命令与嵌套

#### 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 → 执行 TopCommand
  • top mid-1 → 执行 Mid1Command
  • top 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-2top 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(别名)
  1. 选项处理

  2. 必选选项 vs 可选选项

  3. 选项类型转换 (boolean, number, array)

5.3 高级阶段 (预计 4-5 小时)

  1. 交互式命令行

#### 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 {}
  1. 配置管理

#### 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 从项目根目录开始搜索:

  1. 当前目录
  2. 逐级向上查找直到根目录

#### 8.3 插件配置

在配置文件中使用 plugins 字段加载自定义插件:

// .myclirc.json
{
  "plugins": ["./plugins/my-plugin"]
}
// my-plugin.js - 导出默认 NestJS 模块
import { Module } from '@nestjs/common';

@Module({
  providers: [MyCustomProvider],
})
export class MyPluginModule {}

工作原理

  1. CommandFactory.run() 调用 cosmiconfig 搜索配置文件
  2. 读取配置中的 plugins 数组
  3. 使用 import() 动态加载插件模块
  4. 将插件模块添加到 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 会自动:

  1. 搜索 .myapprc.json 配置文件
  2. 加载 auth-pluginlogger-plugin 模块
  3. 将它们注册到 NestJS 应用中

#### 8.6 自定义配置加载位置

如果需要自定义配置加载行为,可以扩展 CommandFactory

import { CommandFactory } from 'nest-commander';

class CustomCommandFactory extends CommandFactory {
  // 自定义逻辑
}

但通常不需要这样做,默认的 cosmiconfig 集成已足够满足大多数场景。

  1. 测试

  2. 使用 nest-commander-testing 进行单元测试

  3. 集成测试编写

5.4 生产实践 (预计 2-3 小时)

  1. 插件系统

    • 理解 Commander 插件机制
    • 自定义插件开发
  2. Schematics

    • 使用 nest-commander-schematics 生成代码
    • 自定义 schematics 开发
  3. 最佳实践

    • 错误处理
    • 日志记录
    • 构建与发布

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 项目核心原理与代码阅读指南"