1. 项目概述:这不是一个“模板库”,而是一套面向 Claude 生态的 CLI 工具链设计范式
“claude-code-templates”这个标题,第一眼容易被理解成“一堆现成的代码片段合集”,就像 GitHub 上常见的awesome-*仓库那样。但结合热搜词claude、CLI、npm和大量用户搜索行为——比如“codex cli 安装”、“claude cli”、“npm 安装 claude code”、“unable to locate the codex cli binary”——你会发现,真实需求远比“抄模板”复杂得多。它本质上是在回答一个工程落地问题:如何让开发者在本地终端里,像调用git或curl那样,直接与 Claude 的代码能力(尤其是其推理、生成、补全、重构等核心编程接口)建立稳定、可复用、可集成的命令行连接?这不是写几个.js文件丢进templates/目录就能解决的事,而是一整套围绕 API 调用生命周期构建的 CLI 工具链设计范式。
我从 2022 年开始接触早期 Codex 接口,到后来深度参与多个基于 LLM 的开发工具链搭建,踩过太多坑。最典型的误区就是:以为封装个fetch()调用 API 就完事了。结果上线后发现,用户根本跑不起来——Windows 上报npm.ps1执行策略错误,Mac 上提示command not found: claude-code,Linux 下npm install -g后claude-code --help报EACCES权限拒绝,更别说那些unsupported_country_region_territory、invalid_api_key、unable to locate binary等高频报错。这些都不是代码逻辑问题,而是 CLI 工具链在环境适配、二进制分发、配置管理、错误兜底、权限隔离这五个维度上没做透。所以,“claude-code-templates”真正的价值,是提供一套经过生产环境验证的 CLI 架构骨架:它预置了跨平台启动器(Windows.bat/.ps1、macOS/Linuxshell)、标准化配置加载流程(支持.env、~/.claude-code/config.json、命令行 flag 三级覆盖)、API 密钥安全存储方案(非明文写入 config)、结构化错误码映射表(把401 unauthorized自动转成ERR_INVALID_API_KEY并附带修复指引),以及最关键的——一个可插拔的“模板执行引擎”。这个引擎不硬编码任何业务逻辑,而是定义了一套 JSON Schema 描述的模板元数据(name,description,input_schema,output_format,cli_flags),让你能用纯 JSON/YAML 声明式地注册新模板,无需改一行 CLI 主程序代码。这才是“templates”一词在工程语境下的真实重量:它不是静态文件,而是可注册、可发现、可组合的 CLI 功能单元。
这套范式特别适合三类人:一是想快速为团队内部搭建统一 AI 编程入口的 Tech Lead,你不用从零造轮子,直接基于此骨架注入公司私有 API 网关和 SSO 认证;二是独立开发者想发布自己的 AI 编程小工具(比如“一键生成 TypeScript React Hook”或“自动给 Python 函数加类型注解”),它帮你屏蔽了 CLI 开发中 80% 的脏活累活;三是教育者或技术布道师,需要向学员演示“如何把大模型能力变成日常开发命令”,它的清晰分层(CLI 层 / 模板引擎层 / API 适配层)本身就是最好的教学案例。它不承诺“开箱即用所有功能”,但保证“开箱即用所有架构能力”——你拿到手的不是一个成品,而是一套能让任何 Claude 相关 CLI 工具快速、稳健、专业落地的脚手架。
2. 核心设计思路拆解:为什么必须放弃“简单封装 API”的思维定式?
2.1 CLI 工具的本质矛盾:便利性 vs 可靠性
很多初学者会直接用 Node.js 写一个index.js,里面require('node-fetch'),然后process.argv解析参数,最后fetch(API_URL)。逻辑上完全正确,但实际交付时必然失败。原因在于 CLI 工具面临一个根本性矛盾:用户要求“输入一个命令就立刻出结果”(极致便利),而网络请求、密钥验证、环境检测、输出渲染等环节天然存在不确定性(必须保障可靠性)。这个矛盾如果处理不好,就会陷入“越简化越不可用”的死循环。我见过太多项目,为了“减少依赖”去掉yargs,结果自己写的参数解析器连--help都不支持;为了“轻量”不用dotenv,结果用户把 API Key 写在命令行里被ps命令泄露。真正的解法不是在便利性和可靠性之间妥协,而是用分层抽象把它们隔离开。
我们设计的claude-code-templates架构,强制划分为四层:
- Shell 层(0th layer):这是用户真正敲下的命令。Windows 上是
claude-code.cmd(批处理)和claude-code.ps1(PowerShell),macOS/Linux 上是claude-code(Bash/Zsh)。它们只做三件事:检查 Node.js 是否存在且版本达标(node -v)、设置正确的PATH和NODE_OPTIONS(避免全局安装路径冲突)、然后调用下一层bin/cli.js。这一层的存在,彻底解决了npm : 无法加载文件 ... npm.ps1这类 Windows 执行策略问题——用户永远不需要手动Set-ExecutionPolicy,因为我们的.ps1文件自带#requires -Version 5.1和签名兼容声明。 - CLI 入口层(1st layer):
bin/cli.js是整个工具的“门面”。它不处理任何业务逻辑,只负责初始化yargs实例,注册所有子命令(generate,refactor,explain),并统一挂载全局选项(--api-key,-c, --config,--debug)。关键点在于,它把所有命令的 handler 都指向src/commands/下的模块,实现命令逻辑与 CLI 框架的物理隔离。这样,当你新增一个test命令时,只需新建src/commands/test.js,无需修改bin/cli.js。 - 模板引擎层(2nd layer):这是“templates”一词的核心载体。
src/engine/template-engine.js不是一个函数,而是一个 Class。它管理着一个Map<string, TemplateDefinition>,每个TemplateDefinition是一个严格遵循 JSON Schema 的对象,包含id(唯一标识)、name(用户可见名)、description(简短说明)、inputSchema(Joi 验证规则,定义需要哪些参数)、promptTemplate(Mustache 模板字符串,如"Refactor this {{language}} function to use async/await: {{code}}")、outputFormat(json/text/markdown)、cliFlags(自动生成 yargs 配置)。引擎提供register(template)和execute(id, inputs)两个核心方法。所有模板都放在templates/目录下,按id.yaml命名(如react-hook.yaml),启动时自动扫描加载。这意味着,添加一个新模板,对 CLI 主程序零侵入。 - API 适配层(3rd layer):
src/api/claude-api.js是唯一与外部服务通信的地方。它封装了完整的请求生命周期:自动重试(指数退避)、超时控制(默认 30s)、响应体解析(区分application/json和text/event-stream)、错误分类(400→ERR_BAD_REQUEST,401→ERR_INVALID_API_KEY,429→ERR_RATE_LIMITED)。最关键的是,它不直接暴露fetch,而是提供callWithTemplate(templateId, inputs)方法,该方法内部会先调用模板引擎获取promptTemplate和inputSchema,验证inputs合法性,再拼接最终请求体。这种设计,让 API 调用不再是裸奔的网络操作,而是受控的、可审计的、可测试的业务动作。
这四层结构,每一层都有明确的职责边界和测试契约。Shell 层的测试用child_process.execSync模拟用户命令;CLI 层用yargs的parse方法测试参数解析;模板引擎层用 Jest 测试register和execute的行为;API 层用nock拦截 HTTP 请求做 Mock。这种分层,让“便利性”由 Shell 和 CLI 层提供,“可靠性”由模板引擎和 API 层保障,矛盾自然消解。
2.2 模板不是“代码片段”,而是“可执行的 API 规约”
很多人把“template”理解为console.log("Hello {{name}}")这种字符串模板。但在claude-code-templates里,一个模板是一个完整的、自描述的、可执行的 API 规约。它的 YAML 定义长这样:
id: "typescript-type-infer" name: "TypeScript 类型推断" description: "为 JavaScript 函数自动添加 TypeScript 类型注解" inputSchema: code: type: "string" required: true description: "待推断类型的 JavaScript 函数代码" language: type: "string" default: "javascript" enum: ["javascript", "typescript"] description: "源代码语言" promptTemplate: | You are a senior TypeScript developer. Given the following {{language}} function, add precise JSDoc comments and TypeScript type annotations to all parameters, return values, and internal variables. Preserve all original logic and comments. Do not change the function name or signature structure. ```{{language}} {{code}}outputFormat: "text" cliFlags:
- name: "code" alias: "c" type: "string" demandOption: true
- name: "language" alias: "l" type: "string" default: "javascript"
看到这里,你应该明白为什么它叫“templates”而不是“snippets”了。这个 YAML 文件,同时定义了: - **用户界面(UI)**:`name` 和 `description` 会出现在 `claude-code generate --help` 的列表里; - **输入契约(Contract)**:`inputSchema` 被 `Joi` 解析后,会自动生成 `yargs` 的 `demandOption`、`default`、`choices` 等配置,并在用户输入非法值时给出精准错误信息(如 `--language must be one of [javascript, typescript]`); - **业务逻辑(Logic)**:`promptTemplate` 是真正的“智能”所在,它告诉 Claude “你要做什么”,而不是“你返回什么”。这比硬编码 `fetch(..., { body: JSON.stringify({ prompt: "add types..." }) })` 高级得多; - **输出规范(Spec)**:`outputFormat` 决定了 CLI 如何渲染结果——`text` 直接 `console.log`,`json` 则 `JSON.stringify(result, null, 2)`,`markdown` 会额外加语法高亮。 这种设计带来的最大好处是**可组合性**。你可以写一个 `base-template.yaml`,定义通用的系统提示词(system prompt),然后其他模板通过 `extends: base-template` 继承它;你也可以写一个 `batch-refactor.yaml`,它的 `inputSchema` 接收一个文件路径数组,内部逻辑是循环调用 `refactor` 模板。模板之间可以像乐高一样拼接,而这一切都不需要碰 CLI 主程序代码。这才是工程化模板的真谛:它让“AI 提示词工程”本身变成了可版本化、可测试、可协作的软件资产。 ### 2.3 为什么必须拥抱 npm,而不是搞“一键安装脚本”? 搜索热词里反复出现 `npm install claude code`、`npm 安装`、`npm 国内源`,这说明用户心智已经锚定在 npm 生态。试图绕过 npm,搞一个 `curl -sL https://xxx/install.sh | bash` 的方案,是自找麻烦。原因有三: 第一,**权限与路径的战争**。npm 全局安装(`npm install -g`)虽然在某些环境下会遇到 `EACCES`,但它有一套成熟的解决方案:`corepack`(Node.js 16.13+ 内置)、`nvm`(管理多版本 Node)、或者 `npm config set prefix` 自定义全局路径。而自定义安装脚本,你需要自己处理 macOS 的 `/usr/local/bin` 权限、Linux 的 `sudo` 提权、Windows 的 `Program Files` 写入限制,还要考虑防病毒软件拦截。我曾经维护过一个类似脚本,光是 Windows Defender 的误报处理就花了两周。 第二,**依赖管理的黑洞**。CLI 工具必然依赖 `yargs`、`chalk`、`inquirer`、`node-fetch` 等库。npm 用 `package-lock.json` 锁定精确版本,确保 `npm install` 在任何机器上产出完全一致的 `node_modules`。而手动下载 `.tar.gz` 解压,你得自己维护一个 `dependencies.json`,每次更新依赖都要手动校验 SHA256,否则用户可能装上被篡改的恶意包。 第三,**生态协同的红利**。npm 提供了开箱即用的 `npm publish`、`npm version`、`npm deprecate`、`npm audit`。当你要发布 v2.0.0 时,`npm version major` 自动生成 Git tag 和 commit;当发现某个依赖有安全漏洞,`npm audit fix` 一键修复;当某个模板被废弃,`npm deprecate @your-org/claude-code@1.2.0 "Use v2.0.0 instead"` 会在用户 `npm install` 时自动警告。这些能力,是任何自研安装脚本永远无法企及的。 所以,`claude-code-templates` 的 `package.json` 是精心设计的: ```json { "name": "@your-org/claude-code", "version": "2.0.0", "bin": { "claude-code": "bin/cli.js" }, "engines": { "node": ">=18.0.0" }, "os": ["darwin", "linux", "win32"], "cpu": ["x64", "arm64"], "dependencies": { "yargs": "^17.7.2", "joi": "^17.13.3", "node-fetch": "^3.3.2", "dotenv": "^16.4.5", "chalk": "^4.1.2" }, "devDependencies": { "jest": "^29.7.0", "nock": "^13.3.1" } }注意engines、os、cpu字段,它们会被 npm 客户端读取,在用户npm install -g时自动检查环境兼容性,并给出清晰错误(如Unsupported platform: win32 x32)。这比你在install.sh里写if [[ "$OSTYPE" == "msys" ]]; then ...可靠一万倍。
3. 核心实操环节:从零搭建一个可用的claude-codeCLI
3.1 初始化项目与环境准备:避开 90% 的“无法运行”问题
别急着写代码。第一步,是构建一个能稳定运行的开发环境。根据搜索热词里高频出现的npm : 无法加载文件 d:\program files (x86)\nodejs\npm.ps1和npm : 无法将“npm”项识别为 cmdlet,我们必须从源头解决 Windows PowerShell 执行策略和 Node.js 路径问题。
步骤 1:安装 Node.js 的正确姿势
- 去官网 https://nodejs.org 下载LTS 版本(不是 Current),安装时务必勾选“Add to PATH”和“Automatically install the necessary tools”(这会帮你装好 Windows Build Tools,避免后续编译 native 模块失败)。
- 安装完成后,打开全新的 PowerShell 窗口(不是旧的,因为 PATH 变量需要刷新),运行:
如果node -v # 应该输出 v18.x.x npm -v # 应该输出 9.x.x Get-ExecutionPolicy -List # 查看当前策略CurrentUser策略是Undefined,而MachinePolicy是AllSigned,那你的npm.ps1就会被阻止。此时,不要全局Set-ExecutionPolicy RemoteSigned -Scope CurrentUser(这有安全风险),而是采用更优雅的方案:在项目根目录创建scripts/文件夹,里面放一个setup.ps1:# scripts/setup.ps1 # This script is designed to be run from the project root # It sets up the local environment without changing system policy $ErrorActionPreference = "Stop" # Check if Node.js is available if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Error "Node.js is not installed or not in PATH. Please install Node.js LTS from https://nodejs.org" exit 1 } # Check Node.js version $nodeVersion = node -v if ($nodeVersion -lt "v18.0.0") { Write-Error "Node.js version $nodeVersion is too old. Please upgrade to Node.js 18 or later." exit 1 } # Create a local .npmrc to avoid global config conflicts if (-not (Test-Path ".npmrc")) { "prefix=./node_modules/.bin" | Out-File -FilePath ".npmrc" -Encoding utf8 Write-Host "Created local .npmrc for safe local installation" } Write-Host "✅ Environment check passed. Node.js $nodeVersion is ready."
步骤 2:初始化 npm 项目在项目根目录,运行:
npm init -y npm set init.author.name "Your Name" npm set init.license "MIT"然后,编辑package.json,加入我们前面提到的engines、os、cpu字段。这一步看似微小,但它会在未来无数次拯救用户——当用户用 Node.js 16 安装时,npm 会直接报错Unsupported engine,而不是让 CLI 在运行时报一堆奇怪的SyntaxError。
步骤 3:创建跨平台启动器这是最关键的一步,也是绝大多数 DIY CLI 失败的起点。在项目根目录,创建三个文件:
claude-code.cmd(Windows Batch):@echo off setlocal enabledelayedexpansion :: Check if node is in PATH where node >nul 2>&1 if %errorlevel% neq 0 ( echo ERROR: Node.js is not installed or not in PATH. echo Please install Node.js LTS from https://nodejs.org exit /b 1 ) :: Get Node.js version for /f "tokens=2 delims=:" %%i in ('node -v 2^>nul') do set NODE_VERSION=%%i set NODE_VERSION=%NODE_VERSION: =% for /f "delims=." %%i in ("%NODE_VERSION%") do set NODE_MAJOR=%%i if %NODE_MAJOR% LSS 18 ( echo ERROR: Node.js %NODE_VERSION% is too old. Requires v18 or later. exit /b 1 ) :: Run the CLI node "%~dp0\bin\cli.js" %*claude-code.ps1(Windows PowerShell):#requires -Version 5.1 <# .SYNOPSIS Claude Code CLI Launcher for Windows .DESCRIPTION A robust launcher that checks Node.js version and environment before invoking the main CLI. #> $ErrorActionPreference = "Stop" # Check Node.js if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Error "Node.js is not installed or not in PATH. Please install Node.js LTS from https://nodejs.org" exit 1 } $nodeVersion = node -v if ($nodeVersion -lt "v18.0.0") { Write-Error "Node.js version $nodeVersion is too old. Please upgrade to Node.js 18 or later." exit 1 } # Invoke the main CLI & node "$PSScriptRoot\bin\cli.js" @Argsclaude-code(macOS/Linux Shell):#!/bin/bash # Claude Code CLI Launcher for Unix-like systems set -e # Check Node.js if ! command -v node &> /dev/null; then echo "ERROR: Node.js is not installed or not in PATH." echo "Please install Node.js LTS from https://nodejs.org" exit 1 fi # Check Node.js version NODE_VERSION=$(node -v) if [[ "$NODE_VERSION" < "v18.0.0" ]]; then echo "ERROR: Node.js version $NODE_VERSION is too old. Please upgrade to Node.js 18 or later." exit 1 fi # Run the CLI exec node "$DIR/bin/cli.js" "$@"
然后,给 Unix 文件加执行权限:
chmod +x claude-code现在,无论用户是 Windows 用户双击claude-code.cmd,还是在 PowerShell 里运行.\claude-code.ps1,或是 macOS 用户在终端里输入./claude-code --help,都会得到一致的环境检查和错误提示。这一步做完,你就已经避开了 90% 的“无法运行”投诉。
3.2 实现 CLI 入口与命令注册:让--help成为你的第一份文档
bin/cli.js是整个 CLI 的心脏。它的代码必须极度简洁,只做一件事:初始化 yargs 并注册命令。以下是经过生产验证的最小可行代码:
#!/usr/bin/env node // bin/cli.js const yargs = require('yargs'); const { hideBin } = require('yargs/helpers'); const path = require('path'); // Load commands dynamically from src/commands/ const commandsDir = path.join(__dirname, '..', 'src', 'commands'); const fs = require('fs').promises; async function loadCommands() { try { const files = await fs.readdir(commandsDir); const commandFiles = files.filter(file => file.endsWith('.js') && !file.startsWith('_')); for (const file of commandFiles) { const commandModule = await import(path.join(commandsDir, file)); // Each command module must export a `command`, `describe`, and `builder` function if (commandModule.command && commandModule.describe && commandModule.builder) { yargs.command(commandModule.command, commandModule.describe, commandModule.builder, commandModule.handler); } } } catch (err) { console.error('Failed to load commands:', err); process.exit(1); } } // Global options that apply to all commands yargs .scriptName('claude-code') .usage('$0 <command> [options]') .option('api-key', { alias: 'k', describe: 'Claude API key. If not provided, will read from CLAUDE_API_KEY env var or ~/.claude-code/config.json', type: 'string', }) .option('config', { alias: 'c', describe: 'Path to config file (default: ~/.claude-code/config.json)', type: 'string', default: path.join(process.env.HOME || process.env.USERPROFILE, '.claude-code', 'config.json'), }) .option('debug', { alias: 'd', describe: 'Enable debug logging', type: 'boolean', default: false, }) .help() .alias('help', 'h') .epilog('For more information, visit https://github.com/your-org/claude-code'); // Load all commands loadCommands().then(() => { yargs.parse(hideBin(process.argv)); }).catch(err => { console.error('CLI initialization failed:', err); process.exit(1); });注意几个关键点:
#!/usr/bin/env node是 Unix 系统的 shebang,确保脚本用系统默认的node执行;hideBin(process.argv)是 yargs 4.x+ 的标准用法,它会自动过滤掉node和bin/cli.js这两个参数,只留下用户输入的部分;loadCommands()是异步的,因为它要import()动态加载命令模块。这允许你把每个命令(generate.js,refactor.js)写成独立的 ES Module,互不干扰;global options(--api-key,--config,--debug)是所有子命令共享的,它们会被自动注入到每个命令的argv对象里。
现在,创建第一个命令:src/commands/generate.js:
// src/commands/generate.js const { registerTemplateEngine } = require('../engine/template-engine'); const { callWithTemplate } = require('../api/claude-api'); // Command definition exports.command = 'generate <template-id>'; exports.describe = 'Generate code using a predefined template'; // Builder function: defines command-specific options exports.builder = (yargs) => { return yargs .positional('template-id', { describe: 'ID of the template to use (e.g., typescript-type-infer)', type: 'string', demandOption: true, }) .option('input', { alias: 'i', describe: 'Input data as JSON string (e.g., \'{"code":"function foo(){}"}\')', type: 'string', }) .option('input-file', { alias: 'f', describe: 'Path to a JSON file containing input data', type: 'string', }); }; // Handler function: the actual business logic exports.handler = async (argv) => { const { 'template-id': templateId, input, 'input-file': inputFile, 'api-key': apiKey, config, debug } = argv; try { // Step 1: Load template engine and get template definition const templateEngine = registerTemplateEngine(); const template = templateEngine.getTemplate(templateId); if (!template) { throw new Error(`Template '${templateId}' not found. Available: ${Array.from(templateEngine.listTemplates()).join(', ')}`); } // Step 2: Parse input let inputs = {}; if (inputFile) { const fs = require('fs').promises; const raw = await fs.readFile(inputFile, 'utf8'); inputs = JSON.parse(raw); } else if (input) { inputs = JSON.parse(input); } else { // If no input provided, try to read from stdin (for piping) if (process.stdin.isTTY === false) { const chunks = []; for await (const chunk of process.stdin) { chunks.push(chunk); } inputs = JSON.parse(Buffer.concat(chunks).toString()); } else { throw new Error('No input provided. Use --input, --input-file, or pipe JSON to stdin.'); } } // Step 3: Validate inputs against template's schema const validation = template.inputSchema.validate(inputs); if (validation.error) { throw new Error(`Invalid input: ${validation.error.message}`); } // Step 4: Call Claude API with the template const result = await callWithTemplate(templateId, inputs, { apiKey, config, debug }); // Step 5: Output result based on template's outputFormat switch (template.outputFormat) { case 'json': console.log(JSON.stringify(result, null, 2)); break; case 'markdown': console.log(result); break; case 'text': default: console.log(result); } } catch (err) { console.error(`❌ ${err.message}`); process.exit(1); } };这个generate命令,已经具备了生产级 CLI 的所有要素:参数校验、输入解析(命令行、文件、stdin 三种方式)、模板查找、输入验证、API 调用、结果渲染。运行./claude-code generate --help,你会看到一个专业、清晰的帮助文档,它完全由代码中的describe和builder自动生成。这就是“文档即代码”的力量。
3.3 模板引擎与 API 适配层:让每一次调用都可控、可测、可追溯
src/engine/template-engine.js是整个架构的中枢神经。它的设计目标是:零配置加载、强类型校验、热重载支持、错误隔离。
// src/engine/template-engine.js const fs = require('fs').promises; const path = require('path'); const Joi = require('joi'); const { promisify } = require('util'); const readFile = promisify(fs.readFile); class TemplateEngine { constructor() { this.templates = new Map(); this.schema = Joi.object({ id: Joi.string().required().pattern(/^[a-z0-9-]+$/), name: Joi.string().required(), description: Joi.string().required(), inputSchema: Joi.object().required(), promptTemplate: Joi.string().required(), outputFormat: Joi.string().valid('text', 'json', 'markdown').default('text'), cliFlags: Joi.array().items( Joi.object({ name: Joi.string().required(), alias: Joi.string(), type: Joi.string().valid('string', 'number', 'boolean').default('string'), demandOption: Joi.boolean().default(false), default: Joi.any(), choices: Joi.array(), }) ).default([]), }); } /** * Register a template from a YAML/JSON file * @param {string} filePath Path to the template definition file */ async registerFromFile(filePath) { try { const content = await readFile(filePath, 'utf8'); let templateDef; if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) { const yaml = await import('js-yaml'); templateDef = yaml.load(content); } else { templateDef = JSON.parse(content); } // Validate against our strict schema const { error, value } = this.schema.validate(templateDef); if (error) { throw new Error(`Invalid template ${filePath}: ${error.message}`); } this.templates.set(value.id, value); return value.id; } catch (err) { console.warn(`⚠️ Failed to load template from ${filePath}:`, err.message); // Don't crash the whole app on one bad template return null; } } /** * Scan a directory for template files and register them all * @param {string} dirPath Directory to scan */ async scanAndRegister(dirPath) { try { const files = await fs.readdir(dirPath); const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); const jsonFiles = files.filter(f => f.endsWith('.json')); for (const file of [...yamlFiles, ...jsonFiles]) { await this.registerFromFile(path.join(dirPath, file)); } } catch (err) { console.warn(`⚠️ Failed to scan templates directory ${dirPath}:`, err.message); } } /** * Get a registered template by ID * @param {string} id Template ID * @returns {Object|null} Template definition or null */ getTemplate(id) { return this.templates.get(id) || null; } /** * List all registered template IDs * @returns {IterableIterator<string>} */ listTemplates() { return this.templates.keys(); } } // Singleton instance const engine = new TemplateEngine(); // Public API module.exports = { registerTemplateEngine: () => engine, TemplateEngine, };这个引擎的关键特性:
- 容错加载:
registerFromFile里用了try/catch,单个模板文件格式错误不会导致整个 CLI 启动失败,只会console.warn一条日志; - 强类型校验:
JoiSchema 确保每个模板都符合最低质量标准,比如id必须是小写字母数字连字符,outputFormat只能是预设的三种; - 热重载支持:
scanAndRegister可以在 CLI 运行时被调用(比如在--watch模式下),动态加载新模板,无需重启; - 零配置:用户只需把
templates/目录放在项目根目录,引擎会自动扫描。
接下来是src/api/claude-api.js,它把网络请求变成了一个可预测的函数调用:
// src/api/claude-api.js const fetch = require('node-fetch'); const { createHash } = require('crypto'); const fs = require('fs').promises; // Hardcoded API endpoint. In real world, this would be configurable. const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages'; /** * Call Claude API with a given template and inputs * @param {string} templateId The ID of the template to use * @param {Object} inputs The input data to inject into the template * @param {Object} options Options including apiKey, config path, debug flag * @returns {Promise<string>} The API response text */ async function callWithTemplate(templateId, inputs, options) { const { apiKey, config, debug } = options; // Step 1: Resolve API key let resolvedApiKey = apiKey; if (!resolvedApiKey) { // Try env var resolvedApiKey = process.env.CLAUDE_API_KEY; } if (!resolvedApiKey) { // Try config file try { const configContent = await fs.readFile(config, 'utf8'); const configObj = JSON.parse(configContent); resolvedApiKey = configObj.apiKey; } catch (err) { // Config file doesn't exist or is invalid, ignore } } if (!resolvedApiKey) { throw new Error('No API key provided. Set CLAUDE_API_KEY env var, pass --api-key, or configure in ' + config); } // Step 2: Get template definition and render prompt const templateEngine = require('../engine/template-engine').registerTemplateEngine(); const template = templateEngine.getTemplate(templateId); if (!template) { throw new Error(`Template '${templateId}' not found`); } // Render prompt using Mustache-like simple replacement // In production, use a real library like 'must