首页
/ caveman 的 create-caveman-agent:一个零运行时依赖的原子化 npm 初始化器设计剖析

caveman 的 create-caveman-agent:一个零运行时依赖的原子化 npm 初始化器设计剖析

2026-09-06 11:46:00作者:胡唯隽

本文以 packages/create-caveman-agent/CLAUDE.md 为骨架,逐条落实其中对 @caveman-ai/create-agent(CLI 名 create-caveman-agent)的每一项行为约定——参数解析、临时目录脚手架、默认安装依赖、原子重命名、密钥不落盘、歧义选择失败——并用 src/index.ts 的源码与 tests/initializer.test.mjs 的测试断言给出佐证。读完后你可以掌握该初始化器的完整命令行用法、provider 选择策略、原子化目录生成的实现原理,以及生成项目的文件结构与验收测试方式。

组件定位:为 @caveman-ai/agent 提供零运行时依赖的脚手架

CLAUDE.md 给出的核心定位是:这是 @caveman-ai/agentZero-runtime-dependency npm initializer——解析 provider/install 标志,把脚手架写入临时目录,默认安装依赖,然后原子化地重命名到目标路径。它同时列出了四条必须遵守的不变量:

  1. 生成的项目只保留一个必需的源文件
  2. 永远不打印、不持久化 provider 密钥;
  3. 非交互场景下 provider 选择歧义时必须失败,且不留下部分生成的目标目录;
  4. --no-install 让调用方可以自行管理依赖。

这些约定都可以与源码核对。从 package.json 可以看到:包内 devDependencies 仅有 typescript@types/node,没有任何 dependencies 字段,印证了"零运行时依赖"的说法;bin 入口指向 ./dist/index.jsengines 要求 node >=22.19.0。该目录在 pnpm workspace 中注册为 packages/create-caveman-agent(见 pnpm-workspace.yaml)。

CLAUDE.md 开头还有一段仓库路由说明:初始化器产品工作的 source of truth 位于 JuliusBrussee/caveman-agent-sdk,本目录是"historical consumer copy",仅在 pinned 集成、迁移/移除或显式跨仓库同步时才编辑。可以推断这是该 monorepo 对该子包的维护边界约定:读代码没问题,但改动需谨慎。

命令行接口:完整用法与标志语义

初始化的标准用法(与 README.md 一致):

npm create @caveman-ai/agent@latest my-agent
cd my-agent
npm run doctor
npm run dev

--help / -h 时输出的 usage 行由 src/index.ts 中的 USAGE 常量定义:

usage: npm create @caveman-ai/agent@latest <project> [--provider anthropic|openai|google] [--no-install]

两个标志的解析逻辑在 parseArgs

  • --provider 必须跟随一个值;若值缺失或以下一个 -- 开头,抛出 --provider requires a value
  • --no-installinstall 置为 false(默认为 true,即默认安装依赖);
  • 任何未识别的 --xxx 选项都会抛出 unknown option <value>,且此时尚未写入任何目标文件
  • 位置参数必须恰好一个,否则直接回显 usage 报错。

provider 的取值由 parseProvider 做归一化(trim + 小写),只接受 anthropicopenaigoogle 三个值,其他值报 unsupported provider

Provider 选择:凭据探测、静默选择与非交互失败

CLAUDE.md 约定"Ambiguous noninteractive provider selection fails without partial target",README 进一步描述为"Exactly one detected provider credential selects silently. Zero or multiple credentials prompt once"。对应实现是 chooseProvider,决策顺序如下:

  1. 显式 --provider 优先:直接解析,不做凭据探测;
  2. 凭据探测:依次检查 ANTHROPIC_API_KEYOPENAI_API_KEYGEMINI_API_KEY(或 GOOGLE_API_KEY),命中的 provider 收集进 detected 列表;
  3. 恰好命中一个 → 静默选择,不打扰用户;
  4. 命中 0 个或多个
    • 若 stdin/stdout 都不是 TTY(如 CI、管道),直接抛错——0 个报 no provider credential detected; pass --provider,多个报 multiple provider credentials detected; pass --provider
    • 若是交互终端,则用 readline 提问一次 Provider (anthropic/openai/google):

每种 provider 在生成项目中写入的默认模型由 MODELS 固定:

Provider 默认模型(写入 .caveman/provider.json 探测的环境变量
anthropic anthropic/claude-haiku-4-5 ANTHROPIC_API_KEY
openai openai/gpt-5.4-mini OPENAI_API_KEY
google google/gemini-2.5-flash GEMINI_API_KEYGOOGLE_API_KEY

注意选定的 provider 与模型只是写进 .caveman/provider.json{ provider, model }(见 projectFiles),从不写入任何密钥本身——这正是"Never print or persist provider secrets"约定的落点之一。

原子化脚手架流程:临时目录、wx 独占写、失败即清理

CLAUDE.md 中最关键的一句是"writes scaffold into temporary directory ... then atomically renames into target"。main 函数 的完整流程:

  1. resolve(targetArg) 解析目标路径,safeName(basename(target)) 生成项目名(下文详述);
  2. assertAbsent:若目标路径已存在(stat 不返回 ENOENT)立即报 target already exists,在任何写入之前拦截;
  3. 选择 provider;
  4. 构造临时目录:.<name>.<pid>.<uuid>.tmpsrc/index.ts#L29),与目标同目录、带 PID 和 crypto.randomUUID(),保证并发运行互不冲突;
  5. 在临时目录内创建 src/evals/.caveman/ 三个子目录,然后逐文件写入 projectFiles(name, provider) 返回的文件——每个 writeFile 都使用 flag: "wx", mode: 0o600wx 要求独占创建(文件已存在即失败),0o600 限定属主读写权限;
  6. 若未指定 --no-install,调用 installDependencies(temporary)
  7. rename(temporary, target) 完成原子替换——用户在成功前永远看不到半成品目录
  8. 任何一步抛错都会 rm(temporary, { recursive: true, force: true }) 清理临时目录后再抛出。

这个"先写临时目录、rename 原子落地、失败全量清理"的设计,正是 CLAUDE.md 所说"fails without partial target"的机制基础:即使非交互场景因 provider 歧义失败(该失败发生在步骤 3,更早于任何写入),磁盘上也不会有残留目标。

成功后 stdout 输出的引导信息(src/index.ts#L44-L52)会列出 created <name>provider <p> (<model>)cd <target>,并在跳过安装时补一行 npm install,最后固定提示:eval starts unapproved; inspect it, set approved: true, then npm run build

依赖安装:npm_execpath 复用、Windows 适配与环境变量白名单

installDependencies 对安装方式做了三层适配:

  • process.env.npm_execpath 存在(npm create 启动时通常会有),直接用当前 Node 解释器执行该 npm-cli.js,避免 PATH 中 npm 版本不一致的问题;
  • Windows 且无 npm_execpath 时,回退到 ComSpec(默认 cmd.exe)执行 /d /s /c npm install ...
  • 其他环境直接 npm install

三者的安装参数统一为 install --no-audit --no-fund --ignore-scripts:跳过审计与基金提示、且不执行依赖包的安装脚本,降低脚手架阶段的不确定性。

更值得注意的是 dependencyInstallEnv:安装子进程的环境变量不是透传 process.env,而是白名单式重建——只放行 PATHHOMETEMP 系列、代理变量、NPM_CONFIG_*/npm_config_* 等约 28 个与安装相关的键。这意味着运行 npm create 时环境里的 ANTHROPIC_API_KEYOPENAI_API_KEY 等 provider 凭据(以及其他任何无关密钥)根本不会进入 npm install 子进程。CLAUDE.md 的"Never print or persist provider secrets"在这里得到了最严格的一条实现证据:测试 initializer.test.mjs#L164-L192 注入 OPENAI_API_KEYCAVE_API_KEYAWS_SECRET_ACCESS_KEY 三个假密钥,用伪造的 npm_execpath 捕获子进程环境后断言它们均不存在,而 PATH 仍然可用。

--no-install 则把安装步骤整体跳过,对应 CLAUDE.md 中"supports callers that manage dependencies"的场景;此时成功输出会补一行 npm install 提示,README 中的非交互示例即:

npm create @caveman-ai/agent@latest my-agent -- --provider openai --no-install

生成的项目文件:一个必需源文件与显式的预算拨盘

CLAUDE.md 要求"Keep generated project at one required source file"——即 src/agent.ts 是唯一的必需 agent 定义文件。projectFiles 实际生成 9 个文件,各自角色如下:

文件 作用
package.json 定义 doctor/dev/run/build/check/typecheck 六个脚本,依赖 @caveman-ai/agent ^0.1.0engines.node >=22.19.0
tsconfig.json ES2024 + NodeNext + strict: true + noEmitinclude 覆盖 src/**evals/**caveman.config.ts
src/agent.ts 唯一必需源文件:定义一个 readiness 工具与一个 agent() 导出
src/run.ts 演示 run() 调用与预算配置(预算拨盘)
evals/smoke.eval.ts 起步 eval,初始 approved: false
caveman.config.ts 构建配置:entryevals glob、maxSearchCostUsd: 2
.caveman/provider.json { provider, model },无密钥
.gitignore 忽略 node_modules/.caveman/traces/.env*
README.md 按 provider 生成对应的凭据环境变量名(如 OPENAI_API_KEY)与运行指引

生成的 src/agent.ts 是一个最小但完整的 agent:

import { agent, auto, schema, tool } from "@caveman-ai/agent";

const readiness = tool({
  name: "readiness",
  description: "Read fixture readiness before answering.",
  input: schema.object({}),
  effect: "read",
  execute: () => ({ status: "ready" }),
});

export default agent({
  id: "<name>",
  instructions: "Call readiness once. Then reply with exactly: ready",
  model: auto(),
  tools: [readiness],
});

src/run.ts 则刻意把"预算拨盘"写出来,源码注释解释了每个字段的语义:

const result = await run(definition, "Reply with exactly: ready", {
  rootDir,
  entryPath: "src/agent.ts",
  budget: {
    // 调用前准入上限:每次调用按估算最坏情况预留,
    // 若 provider 最终用量超过预留,receipt 记录 capBreached/overspent 并停止。
    maxUsd: 0.5,
    // 预留余量逼近上限时的行为:"compact"(默认)重写上下文继续,
    // "stop" 直接钳制输出并停止。
    onExhausted: "compact",
  },
});

生成的 README 中还包含一段关于诚实性口径的说明:provider 后来报告超过估算的用量时,receipt 会如实记录 capBreachedoverspent 后运行停止;本地结果标记为 inferred,"Verified savings remain $0 until active real traffic passes existing Caveman rollout and ledger gates"。这套表述与测试断言一一对应——initializer.test.mjs#L79-L85 明确检查生成 README 与 src/run.ts 不得出现"never spend past"这类营销式措辞,而必须包含 capBreached/overspent 说明。

起步 eval(evals/smoke.eval.ts)默认 approved: false,质量检查为 exact_match: "ready"tool_called: ["readiness"]。CLAUDE.md 与 README 一致要求:先审查预期行为、手动把 approved 改为 true,再执行 npm run buildnpm run check

项目名归一化与错误出口

main 末尾的错误处理 将所有失败收敛为 stderr 一行 create-caveman-agent: <message> 加退出码 1;safeName 的归一化规则是:小写、非 [a-z0-9_-] 字符替换为 -、去掉首尾连字符、空名报 project name must contain a letter or number、截断到 96 字符。由于项目名同时是生成的 package.jsonnameagentid,这套限制保证了脚手架产物可直接 npm install

测试证据:每条约定都有断言

CLAUDE.md 指定运行 pnpm --dir public/create-caveman-agent test;文档中的 public/ 路径与当前仓库实际位置不一致——workspace 中注册的是 packages/create-caveman-agent,等价命令应为 pnpm --dir packages/create-caveman-agent test。该命令先 tsc 编译出 dist/,再用 node --test --test-force-exittests/initializer.test.mjs。各测试与 CLAUDE.md 约定的对应关系:

  • 打包生命周期prepack 必须等于 npm run buildbin 首行必须是 #!/usr/bin/env node#L27-L42);
  • --help 无副作用:退出码 0、stdout 以 usage 开头、stderr 为空、无凭据要求;
  • 单一必需源文件 + 确定性 provider:用 --provider openai --no-install 生成后,校验 src/agent.ts 存在且含 agent(.caveman/provider.json 的 model 为 openai/gpt-5.4-mini、各 npm 脚本与 TypeScript 版本精确匹配;随后用 mock 的 @caveman-ai/agent 包实际执行 src/run.ts,断言 run() 收到的 rootDir 等于项目根、entryPathsrc/agent.ts#L53-L134);
  • 密钥不落盘不打印:注入 OPENAI_API_KEY: "secret-never-print",断言 stdout+stderr 中不出现该字符串;
  • 恰好一个凭据静默选择:仅设 ANTHROPIC_API_KEY 时不问任何交互问题,provider.json 得到 anthropic
  • 非交互歧义失败无残留:同时设 ANTHROPIC_API_KEYOPENAI_API_KEY,断言退出码 1、stderr 含 multiple provider credentials,且目标目录 stat 返回 ENOENT
  • 默认安装行为:伪造 npm_execpath,断言安装的 argv 恰为 install --no-audit --no-fund --ignore-scripts,且三个注入的密钥均未进入子进程环境、PATH 仍在;同时断言成功输出不含裸 npm install 行但含 npm run dev 行;
  • 未知选项拒绝--wat 导致退出码 1、unknown option --wat,且目标目录不存在。

小结:如何验收与使用该初始化器

  • 本地开发/验收:pnpm --dir packages/create-caveman-agent test(先编译再跑 node:test),package.json 中的 linttsc --noEmit
  • 生产使用:npm create @caveman-ai/agent@latest <project>,CI 中务必显式传 --provider,由他方管理依赖时加 --no-install
  • 生成项目的第一条路径是 npm run doctor(doctor 不调用 provider;如报缺少签名运行时产物,按其唯一提示执行 caveman setup --install 后重跑);
  • 起步 eval 在人工确认前保持 approved: false,锁定构建后用 npm run check 复核。

从源码结构看,这个初始化器把"零依赖"做成了硬约束:没有第三方运行时库,全部能力来自 node:child_processnode:fs/promisesnode:readline/promises 等内置模块;而"原子性""无密钥泄漏""歧义即失败"三条不变量各有对应的测试断言守护。若需要理解生成项目里 run()/build/eval 的更完整语义,可继续阅读 packages/agent/README.md

登录后查看全文
热门项目推荐
相关项目推荐