本篇讲清楚在这个仓库里"如何正确地装依赖、编译、检查、跑测试、从源码运行 pi"——这些命令在 AGENTS.md 里被明确规定,很多规则是踩过坑之后加上的约束,不是随意选择。
学习目标
- 知道为什么装依赖要用
npm install --ignore-scripts,而不是普通的npm install。 - 理解
npm run build和npm run build:offline的区别,以及各自适用的场景。 - 拆解
npm run check到底检查了哪几件事。 - 弄清楚
./test.sh、直接跑vitest、npm test三者之间的区别,以及为什么AGENTS.md明确禁止直接跑全量 vitest。 - 会用
./pi-test.sh从源码运行 pi 做本地调试,包括用 tmux 交互式验证 TUI。
安装依赖:为什么要 --ignore-scripts
仓库 README 的 Development 一节给出的第一条命令是:
npm install --ignore-scripts # Install all dependencies without running lifecycle scripts
--ignore-scripts 会跳过依赖包的 preinstall/install/postinstall 生命周期脚本。这是供应链安全的第一道闸门:npm 依赖的生命周期脚本是任意代码执行的常见攻击面,一个被投毒的间接依赖可以在 npm install 阶段就执行恶意代码。AGENTS.md 的 "Dependency and Install Security" 一节把这条规则说得更明确:
Hydrate/update locally withnpm install --ignore-scripts; clean/CI-style withnpm ci --ignore-scripts. Don't run lifecycle scripts unless the user asks.
也就是说:本地拉取/更新依赖用 npm install --ignore-scripts,CI 或全新环境用 npm ci --ignore-scripts。哪些依赖确实需要生命周期脚本、为什么可以放行,由后面一篇《发布流程与供应链安全》里的 shrinkwrap 白名单机制单独管理,而不是让每个人在装依赖时自行决定要不要跑脚本。
构建:npm run build 与 npm run build:offline
根目录 package.json 里两个构建脚本几乎一样,只有一步不同:
build: chord → tui → telemetry → ai(build) → agent → session-backends/sqlite-node → protocol → client → server → coding-agent
build:offline: chord → tui → telemetry → ai(build:offline) → agent → session-backends/sqlite-node → protocol → client → server → coding-agent
chord(packages/chord,一个不依赖任何 pi 包、可独立使用的应用组合运行时)排在整个构建链的最前面——这是上一篇《Monorepo结构与包职责地图》里更新过的依赖图的直接体现:agent、protocol、client、server、coding-agent 现在都直接依赖它,必须等它先产出 dist 才能编译。
差别在 packages/ai 这一步。pi-ai 的 package.json 里:
"build": "npm run generate-models && npm run build:offline",
"build:offline": "npm run check:model-data && tsgo -p tsconfig.build.json && shx rm -rf dist/providers/data && shx cp -r src/providers/data dist/providers/data"
npm run build:先执行generate-models(联网从各 Provider 拉取/刷新模型目录数据,即 README 里说的 "Refresh model data"),再做真正的 TypeScript 编译。npm run build:offline:跳过联网刷新,直接用仓库里已有的模型数据快照(src/providers/data)编译。适合没有网络、或者只是想快速验证代码能否编译通过、不关心模型目录是否是最新的场景。
发布源码包里附带的正是某个版本点的模型数据快照,scripts/build-binaries.sh --offline-model-data 这条从 release 源码包构建 standalone 二进制的命令,用的也是这个离线模式(详见下一篇发布流程)。
代码检查:npm run check 里到底跑了什么
npm run check 的定义:
"check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:runtime-deps && npm run check:ts-imports && npm run check:entry-graphs && npm run check:shrinkwrap && npm run check:install-lock:coding-agent && tsgo --noEmit && npm run check:browser-smoke"
拆开看,这一条命令实际串联了八项检查,任何一步失败都会中断(&&):
biome check --write --error-on-warnings .:用 Biome 做代码格式化 + lint,--write会就地修正可自动修复的问题,--error-on-warnings让警告也导致失败退出。这一步同时承担了 lint 和 format 两个角色。check:pinned-deps(scripts/check-pinned-deps.mjs):遍历仓库里所有package.json,检查每一个非工作区(非@earendil-works/*)的直接外部依赖是否使用精确版本号(如1.2.3而非^1.2.3)。check:runtime-deps(scripts/check-runtime-deps.mjs):新增的一项检查,静态分析每个包源码里的 import,确认引用的模块要么是该包自己声明的dependencies/optionalDependencies/peerDependencies之一,要么是 Node 内置模块——防止"代码里 import 了一个包,但package.json忘了声明依赖"这种只有在别的包恰好把它当传递依赖装进来时才会侥幸跑通、一旦依赖关系变化就悄悄炸掉的问题。check:ts-imports(scripts/check-ts-relative-imports.mjs):检查 TypeScript 相对导入的写法是否符合 Node 原生 ESM 兼容要求。check:entry-graphs(scripts/check-entry-graphs.mjs):同样是新增的检查,脚本注释里把它的动机总结成一句话——"入口点就是成本契约"(Entry points are cost contracts)。它会遍历每个包exports字段声明的公开入口,统计每个入口实际会拉入多少模块的求值图,并对照预算校验,防止某个入口不小心export *出一整个 barrel 文件,导致别人只是想用一个几行的纯函数、却要在导入时多付出几十 MB 的模块体积。check:shrinkwrap(scripts/generate-coding-agent-shrinkwrap.mjs --check):校验packages/coding-agent/npm-shrinkwrap.json是否与根package-lock.json保持同步(详见下一篇)。check:install-lock:coding-agent(scripts/generate-coding-agent-install-lock.mjs --check):校验 coding-agent 安装锁文件的一致性。tsgo --noEmit:跑一次不产出文件的 TypeScript 类型检查,这是"type check"这一环。check:browser-smoke(scripts/check-browser-smoke.mjs):一个轻量的浏览器相关冒烟检查。
check:runtime-deps 和 check:entry-graphs 都是随着 chord 这类被广泛依赖的基础包出现之后新增的检查——包越是处于依赖图底层、被依赖的面越广,"依赖声明不准确"和"入口点体积失控"这两类问题造成的连锁影响就越大,值得单独用自动化检查兜底,而不是依赖代码审查肉眼发现。
AGENTS.md 明确要求:改动代码(非文档)之后必须跑一次完整的 npm run check,把全部输出看完、修掉所有 error/warning/info 才能提交,并且强调"这一步不跑测试"——测试是单独的一步。
测试:./test.sh vs 直接跑 vitest vs npm test
这是最容易踩坑的地方,AGENTS.md 用一整段专门强调:
Never run the full vitest suite directly: it includes e2e tests that activate when endpoint/auth env vars are present. For all non-e2e tests, run ./test.sh from the repo root. Otherwise run specific tests from the package root.
也就是说,仓库里混杂着两类测试:
- 普通单元/集成测试:不需要真实模型 API,可以随时随地跑。
- e2e 测试:一旦环境里存在对应的 endpoint/认证环境变量(比如某个 Provider 的 API Key),这些测试就会被"激活",进而产生真实的网络调用和真实的计费开销。如果你本地环境本来就配置了
ANTHROPIC_API_KEY之类的变量去正常使用 pi,直接跑vitest(或者不加隔离的npm test)就有可能意外触发这些 e2e 测试。
test.sh 存在的意义就是隔离掉这个风险。打开脚本可以看到它做的事情:
env -i "${test_env[@]}" npm test
env -i 表示"以空环境启动",然后只显式传入一份白名单环境变量(test_env 数组),包括:
- 把
HOME、TMPDIR、XDG_CONFIG_HOME、XDG_CACHE_HOME、npm 的 user/global config、npm 缓存目录全部重定向到一个新建的临时目录(mktemp -d),确保测试不会读到你本机真实的~/.pi、~/.npmrc、npm 缓存等状态,也不会污染它们; - 显式禁用 Git 的交互式认证(
GIT_TERMINAL_PROMPT=0、GIT_ASKPASS=$(type -P false))和系统级/全局 Git 配置(GIT_CONFIG_NOSYSTEM=1、GIT_CONFIG_GLOBAL=/dev/null); - 设置
PI_NO_LOCAL_LLM=1和AWS_EC2_METADATA_DISABLED=true; - 关键的一点:不传递任何 Provider API Key 类的环境变量——因为它们根本没有被加入
test_env白名单,所以在这个隔离环境里,任何依赖真实凭证才会激活的 e2e 测试都不会被触发; - 退出时校验并清理自己创建、标记过的临时目录(写入
.pi-test-owned标记文件后才允许rm -rf),避免误删无关目录。
清理干净环境后,脚本最终调用的是根目录的 npm test:
"test": "npm run test:scripts && npm run test --workspaces --if-present"
即先跑仓库脚本自身的 Node 原生测试(scripts/*.test.mjs),再对每个声明了 test 脚本的 workspace 包依次执行其测试命令(大多数包用 vitest --run,packages/tui 用 node --test)。
如果你只想跑某一个具体的测试文件,AGENTS.md 给出的做法是从包目录内直接调用 vitest 或 node:test,而不是走 test.sh:
# vitest 包(多数包)
node "$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js" --run test/specific.test.ts
# packages/tui(用 node:test,不是 vitest)
node --test test/specific.test.ts
对于新写或修改的测试文件,规则要求"跑起来,并根据结果迭代实现或测试代码,直到通过";packages/coding-agent/test/suite/ 下的回归测试必须使用 test/suite/harness.ts 配合一个假的(faux)Provider,禁止使用真实 Provider API、密钥或产生实际费用的调用;针对具体 issue 的回归测试文件要放在 packages/coding-agent/test/suite/regressions/,命名格式是 <issue编号>-<简短slug>.test.ts。
从源码运行 pi:./pi-test.sh
./pi-test.sh 的作用是完全绕开"先构建再运行已构建产物"的流程,直接用 tsx 跑 TypeScript 源码:
"$SCRIPT_DIR/node_modules/.bin/tsx" --tsconfig "$SCRIPT_DIR/tsconfig.json" "$SCRIPT_DIR/packages/coding-agent/src/experimental/cli.ts" "$@"
注意入口文件现在是 packages/coding-agent/src/experimental/cli.ts,而不是同目录下仍然存在的 src/cli.ts——仓库里这两个文件目前并存,pi-test.sh 这条本地快速验证路径已经切到了 experimental/ 版本,说明 CLI 入口正在经历迁移/重构,阅读源码时留意别搞混两者。
几个关键细节:
- 脚本用
SCRIPT_DIR定位到仓库根目录,因此可以从任意工作目录调用它(比如cd到你自己想测试的项目目录后再执行/path/to/pi/pi-test.sh),pi 会保留调用者的当前工作目录——packages/coding-agent/docs/development.md明确写了这一点。 - 支持
--no-env参数:如果加上这个参数,脚本会unset掉一长串 Provider 相关的环境变量(ANTHROPIC_API_KEY、OPENAI_API_KEY、GEMINI_API_KEY、AWS_*系列、GH_TOKEN/GITHUB_TOKEN等),用于验证 pi 在"完全没有任何凭证"状态下的行为(比如登录引导流程)。 - 这是修改
coding-agent源码后最快的验证方式:不需要npm run build,改完代码直接重新执行pi-test.sh就能看到效果。
用 tmux 做交互式 TUI 调试
AGENTS.md 的 "Testing pi Interactive Mode with tmux" 一节现在只有一句话,指向一个独立的技能文档 .pi/skills/interactive-testing.md,具体的 tmux 驱动交互模式验证套路都写在那里,方便在无法直接看终端的环境里(比如 Agent 自己在验证自己修改的 TUI 代码)获取渲染结果:
tmux new-session -d -s pi-test -x 80 -y 24
tmux send-keys -t pi-test "./pi-test.sh" Enter
sleep 3 && tmux capture-pane -t pi-test -p # 启动后截取当前终端内容
tmux send-keys -t pi-test "your prompt here" Enter
tmux send-keys -t pi-test Escape # 也可以发送 C-o 等组合键
tmux kill-session -t pi-test
配合这个流程,packages/coding-agent/docs/development.md 里提到的隐藏调试命令 /debug 会把渲染出的 TUI 行内容(含 ANSI 转义码)以及发给模型的最后几条消息写入 ~/.pi/agent/pi-debug.log,是排查渲染问题或消息拼装问题的另一个入口。
项目结构速览
development.md 给出的顶层结构提示(按职责简化列出):
packages/
ai/ # LLM provider 抽象层
agent/ # Agent 循环与消息类型
tui/ # 终端 UI 组件
coding-agent/ # CLI 与交互模式
这份速览目前仍然只列了开发时最常打交道的四个包,还没有把新加入的 chord(应用组合运行时)写进去;结合上一篇《Monorepo结构与包职责地图》里画出的完整依赖图来看,chord 现在是这四个包共同依赖的地基包之一,实际打交道的频率不低于这里列出的任何一个。
动手练习
- 在本地仓库根目录执行
npm install --ignore-scripts,观察终端输出中是否有依赖尝试运行生命周期脚本却被跳过的提示。 - 执行
./test.sh,留意脚本开头打印的Running tests without API keys in isolated home: ...提示,并对照test.sh源码,找出它具体重定向了哪些环境变量。 - 用
./pi-test.sh --no-env启动一次交互模式,观察在没有任何 Provider 凭证的情况下 pi 展示的引导界面,与正常带凭证启动时的差异。
小结
这一套流程背后的核心思路是"把每一类风险都用专门的机制隔离掉":装依赖用 --ignore-scripts 隔离生命周期脚本的代码执行风险;test.sh 用空白环境 + 白名单变量隔离真实凭证触发 e2e 测试和污染本机配置的风险;npm run check 把格式化、lint、依赖版本、运行时依赖声明、导入写法、入口点体积、锁文件一致性、类型检查串成一条不可跳过的流水线(check:runtime-deps、check:entry-graphs 是随着 chord 这样被广泛依赖的基础包出现后新加的两道检查);pi-test.sh 则反过来去掉了"先构建"这个环节,让源码级的快速迭代成为可能(目前指向的是仍在演进中的 experimental/cli.ts 入口)。理解这几条命令各自解决什么问题,比死记命令本身更重要。
|PI(七)构建测试与开发流程&spm=1001.2101.3001.5002&articleId=165476102&d=1&t=3&u=d35a8333807849a099bfddab3aa5ed82)
3012

被折叠的 条评论
为什么被折叠?



