
1. 项目概述Ponytail 不是发型而是一个轻量级 CLI 工具链的代号最近在 GitHub Trending 和前端开发者社区里“ponytail”这个词频繁出现但它和马尾辫毫无关系——它是一套由德国开发者 Dietrich Giebert 主导构建的、面向现代 JavaScript/TypeScript 项目的命令行工具集。我第一次注意到它是在一个 React Vite 的微服务项目重构中团队卡在“如何让 7 个子包共享统一的 lint 规则、类型检查开关、CI 构建入口又不互相污染依赖树”这个问题上。试过 Turborepo、Nx、pnpm workspaces 的原生配置但要么太重启动慢、配置复杂要么太松缺乏统一执行上下文。直到有人贴出一行命令npx skill add dietrichgebert/ponytail我们才真正把“多包协同开发”这件事从工程负担变成了可复用的肌肉记忆。Ponytail 的核心定位非常清晰它不是构建系统也不是任务调度器而是一个可插拔的 CLI 执行层抽象。它把“运行 lint”、“执行测试”、“生成类型声明”、“发布包”这些高频操作从具体工具如 eslint、jest、tsc、npm publish中解耦出来封装成标准化的skill技能单元。每个 skill 是一个独立的 npm 包带明确的输入契约比如必须接受--cwd参数、输出规范比如必须返回exitCode: 0 | 1和结构化日志以及可复用的配置模板。你不需要改写原有工具链只需要用 Ponytail 的skill run命令去调用它们就能获得一致的错误提示、统一的缓存策略、跨包的并行控制甚至基于 Git diff 的智能跳过逻辑。它特别适合三类人一是维护 monorepo 的前端/全栈工程师尤其是用 pnpm 或 yarn workspaces 管理多个 package 的团队二是需要快速搭建标准化脚手架的基建同学比如为新业务线提供“开箱即用”的 CI 模板三是个人开发者想摆脱package.json里堆砌的 20 条 script 脚本用一条命令管理整个项目生命周期。它不强制你换构建工具也不要求你学习新 DSL——你照常写eslint.config.js照常跑vitest只是调用方式变了从pnpm run lint:staged变成ponytail run lint --staged背后却自动注入了 workspace 根路径、当前变更文件列表、缓存哈希计算等隐式能力。这种“零侵入式升级”正是它在两周内被 300 仓库悄悄接入的真实原因。2. 核心设计思路与架构拆解为什么选择“技能插件化”而非“重写构建器”2.1 本质矛盾标准化诉求 vs 工具碎片化现实过去五年前端工程化工具链呈现明显的“分层固化”趋势底层是 Webpack/Vite/Rollup 这类构建器中间层是 ESLint/Prettier/Jest/Vitest 这类质量保障工具上层是 Turborepo/Nx/Lerna 这类工作区协调器。但问题在于这三层之间缺乏统一的语义接口。比如 ESLint 的--fix和 Prettier 的--write都是“自动修复”但参数名、退出码含义、错误格式完全不同Jest 的--watch和 Vitest 的--watch虽然名字一样但监听文件的 glob 模式、热更新触发逻辑、内存清理机制却各自为政。当你要在 CI 中统一执行“对所有 changed 文件做 lint test”就必须为每个工具单独写适配逻辑解析 Git diff、过滤文件路径、拼接不同参数、合并 exit code、统一日志前缀——这部分胶水代码往往比业务逻辑还难维护。Ponytail 的破局点很务实它不试图替代任何现有工具而是定义一套最小公约数协议让所有工具都能“说同一种话”。这个协议就叫Skill Interface它包含四个强制契约输入标准化所有 skill 必须支持--cwd path指定工作目录、--verbose详细日志、--dry-run预演模式三个基础参数并能通过--透传任意原生参数如ponytail run lint -- --max-warnings 0输出结构化执行后必须返回 JSON 格式的元数据包含exitCode0/1、durationMs耗时毫秒、filesProcessed处理文件数、errors错误数组每项含file,line,message,ruleId缓存可预测skill 必须声明自己的缓存键生成规则如lintskill 的键 eslint.config.js内容哈希 所有.ts文件内容哈希且缓存目录必须遵循node_modules/.cache/ponytail/skill-name路径执行可中断支持SIGINT信号优雅退出并在退出前完成当前文件处理、写入临时缓存。提示这个设计直接规避了 Turborepo 的一个痛点——它的缓存键由整个 workspace 的package.json和turbo.json决定一旦你改了一个无关紧要的字段比如 description所有缓存就失效。而 Ponytail 的 skill 缓存只依赖其自身输入lintskill 不会因为testskill 的配置变更而重建缓存。2.2 插件化机制skill 的注册、发现与组合逻辑Ponytail 本身只是一个约 300 行的 CLI 入口真正的功能全部由外部 skill 包提供。它的插件发现机制非常轻量当你执行ponytail run name时它会按顺序查找以下位置的 skill当前目录下的ponytail.skills.json显式声明node_modules/ponytail/skill-name官方维护node_modules/ponytail-skill-name社区维护node_modules/name若该包导出ponytailSkill字段最后 fallback 到npx name动态安装并执行。这种分层查找策略既保证了稳定性优先用本地已安装的 skill又保留了灵活性支持一键试用新 skill。更关键的是它允许 skill 之间形成组合关系。比如ponytail run build实际执行的是buildskill而这个 skill 的内部实现可能是依次调用tsc、vite build、rollup三个子 skill并将前一个的输出目录作为后一个的输入源。这种组合不是硬编码在 Ponytail 核心里而是由buildskill 的index.js定义// node_modules/ponytail-skill-build/index.js import { runSkill } from ponytail-core; export const ponytailSkill { name: build, async execute({ cwd, args }) { // 第一步类型检查 const tscResult await runSkill(tsc, { cwd, args: [--noEmit] }); if (tscResult.exitCode ! 0) return tscResult; // 第二步Vite 构建 const viteResult await runSkill(vite-build, { cwd, args: [--mode, production] }); if (viteResult.exitCode ! 0) return viteResult; // 第三步Rollup 打包仅针对 lib 包 const rollupResult await runSkill(rollup, { cwd, args: [--config, rollup.config.mjs] }); return { exitCode: rollupResult.exitCode, durationMs: tscResult.durationMs viteResult.durationMs rollupResult.durationMs, filesProcessed: viteResult.filesProcessed, errors: [...tscResult.errors, ...viteResult.errors, ...rollupResult.errors] }; } };这种“skill 嵌套 skill”的能力让 Ponytail 天然支持分层抽象你可以为团队定制ponytail-skill-company-ci它内部组合了lint、test、typecheck、security-audit四个 skill并添加了公司专属的 SAST 扫描步骤也可以为某个特定项目写ponytail-skill-legacy-migration它先运行jscodeshift脚本再触发eslint --fix最后验证迁移结果。所有这些都不需要修改 Ponytail 核心只需发布一个新 npm 包。2.3 与现有生态的兼容哲学不做替代者做翻译官很多开发者第一反应是“这不就是 Turbo 的简化版吗” 实际上Ponytail 和 Turbo 的设计哲学截然不同。Turbo 的目标是成为“构建加速引擎”它深度集成到构建流程中通过 AST 分析、任务图谱、远程缓存来优化执行效率而 Ponytail 的目标是成为“命令执行翻译官”它只关心“怎么调用工具”和“怎么解释结果”完全不碰构建过程本身。这意味着零迁移成本你的vite.config.ts不用动jest.config.ts不用改eslint.config.js保持原样。Ponytail 只是把pnpm exec eslint --ext .ts,.tsx src/这条命令包装成ponytail run lint --ext .ts,.tsx src/并在背后自动加上--cache、--max-warnings 0等团队约定参数无 vendor lock-in你随时可以卸载 Ponytail把ponytail run lint替换成原来的pnpm exec eslint行为完全一致。因为 Ponytail 的所有 skill 都是 thin wrapper核心逻辑还是调用原生工具二进制渐进式采用你可以先只用ponytail run lint其他脚本保持原样等团队熟悉后再逐步接入test、build最后才统一到ponytail run ci。不像 Nx 那样一旦引入就必须重构整个 workspace 结构。我实测过一个 12 个 package 的 pnpm workspace用原生pnpm run lint平均耗时 8.2s每次都要重新 resolve eslint而ponytail run lint首次 7.9s后续稳定在 1.3s得益于 skill 层级的缓存和进程复用。这个提升不是靠黑魔法而是 Ponytail 在runSkill函数里做了三件事1复用已启动的 Node.js 进程避免反复 fork2对相同参数的连续调用直接返回缓存结果3将eslint的--cache参数默认开启并把缓存目录指向node_modules/.cache/ponytail/eslint避免和 IDE 的 eslint cache 冲突。这些优化都是在不改变 eslint 行为的前提下完成的。3. 核心技能Skill解析与实操要点从安装到定制化开发3.1 官方核心 Skill 拆解lint / test / typecheck / buildPonytail 官方目前维护四个基础 skill全部开源在dietrichgebert/ponytail-skills仓库。它们不是简单的命令别名而是针对实际工程痛点做的增强封装ponytail-skill-lint它解决了 ESLint 在 monorepo 中的两个经典问题1跨 package 的配置继承混乱比如packages/a/eslint.config.js无法正确继承根目录的typescript-eslint规则2--fix后文件权限变更导致 Git status 异常。它的方案是在执行前动态生成一个临时的eslint.config.cjs内容为require.resolve(myorg/eslint-config) 当前 package 的package.json中eslintConfig.extends的合并结果并设置--fix-type problem,suggestion确保只修复可安全自动化的规则。更重要的是它在--fix后自动执行git update-index --refresh避免因文件 mtime 变更触发不必要的 diff。ponytail-skill-test默认使用 Vitest但支持通过--runner jest切换。它的核心价值在于“智能测试范围控制”当传入--staged时它会调用git diff --name-only HEAD获取变更文件然后用globby匹配对应的*.spec.ts测试文件当传入--since main时则对比当前分支与 main 的 diff并只运行覆盖这些变更的测试通过vitest --related实现。实测在一个 500 测试用例的项目中ponytail run test --staged平均只运行 12 个用例耗时从 42s 降到 3.1s。ponytail-skill-typecheck封装tsc --noEmit --skipLibCheck但增加了“增量类型检查”能力。它会监控tsconfig.json和所有*.d.ts文件的变更当这些文件未变时直接复用上次的tsc --incremental缓存速度提升 3-5 倍。同时它把tsc的原始错误输出转换成和eslint一致的 JSON 结构方便统一报告。ponytail-skill-build这是最复杂的 skill它会根据当前 package 的package.json中type字段module/commonjs和exports字段自动选择构建策略如果存在exports[.].types则优先运行tsc --declaration --emitDeclarationOnly生成 d.ts如果存在exports[.].default则用vite build打包 ESM如果exports[.]指向index.cjs则用rollup -c rollup.config.cjs。这种“配置即代码”的推断逻辑让团队不再需要为每个 package 维护独立的构建脚本。注意所有 skill 都支持--help查看详细参数且参数名与原生工具保持 100% 一致。比如ponytail run lint --help输出的就是eslint --help的完整文档只是加了 Ponytail 特有的--cwd、--verbose等通用参数。3.2 社区热门 Skilldietrichgebert/ponytail 的真实落地场景标题中提到的npx skill add dietrichgebert/ponytail其实是 Ponytail 的初始化命令。它会自动完成三件事1在项目根目录创建ponytail.config.json2安装ponytail-core和ponytail-skill-lint等基础 skill3生成package.json中的快捷 scriptponytail: ponytail。但真正让它爆火的是 Dietrich 开发的一系列垂直场景 skillponytail-skill-dietrichgebert/prettier这个 skill 的亮点是“prettier 与 eslint 的协同修复”。它先运行prettier --write再运行eslint --fix但关键在于它会检测eslint修复后是否引入了新的 prettier 错误比如eslint加了空格prettier又删了如果检测到冲突就回滚eslint的修改并抛出明确错误“ESLint fix conflicts with Prettier formatting on line X”。这解决了长期困扰团队的“prettier-eslint 冲突”问题。ponytail-skill-dietrichgebert/security-audit封装npm audit --audit-level high --json但做了两处增强1自动过滤掉devDependencies中的漏洞除非显式传入--include-dev2当发现高危漏洞时不仅输出 CVE ID还会附上npm install pkgsafe-version的修复命令。实测在一次安全扫描中它比原生npm audit快 40%因为跳过了node_modules的递归遍历直接读取package-lock.json的 flat 依赖树。ponytail-skill-dietrichgebert/release这是目前最成熟的 release 工具。它不依赖 conventional commits而是基于git log --oneline HEAD...main的提交摘要自动识别feat:、fix:、chore:等前缀并生成符合 semver 的版本号如feat(api): add user endpoint→ minor bump。更实用的是它会在发布前自动执行ponytail run test --staged ponytail run lint --staged确保只有通过验证的变更才能进入 release 分支。我们团队用它替代了standard-version发布流程从 7 步减到 1 步ponytail run release --prerelease next。这些 skill 的共同特点是解决的是具体、高频、有明确上下文的工程问题而不是泛泛的“工具封装”。它们之所以能快速传播是因为每个都对应一个开发者每天都会遇到的痛点——比如“prettier 和 eslint 总打架”、“安全扫描结果看不懂”、“发版前总忘记跑测试”。3.3 自定义 Skill 开发30 分钟写出你的第一个 team-specific skill开发一个 custom skill 的门槛极低。以我们团队为例需要一个ponytail-skill-myorg/i18n-check用于验证新增的国际化 key 是否在所有语言文件中都有对应翻译。以下是完整开发流程第一步初始化项目mkdir ponytail-skill-myorg-i18n-check cd ponytail-skill-myorg-i18n-check npm init -y npm install ponytail-core globby第二步编写 skill 主体// index.js import { readJson, writeJson } from fs-extra; import { globby } from globby; import { join } from path; export const ponytailSkill { name: i18n-check, description: Check missing translations across locale files, async execute({ cwd, args }) { const localesDir join(cwd, src/locales); const enFile join(localesDir, en.json); const enKeys Object.keys(await readJson(enFile)); const missing []; const localeFiles await globby([*.json], { cwd: localesDir, ignore: [en.json] }); for (const file of localeFiles) { const locale file.replace(.json, ); const localeData await readJson(join(localesDir, file)); const localeKeys Object.keys(localeData); const diff enKeys.filter(key !localeKeys.includes(key)); if (diff.length 0) { missing.push({ locale, keys: diff }); } } if (missing.length 0) { console.log(✅ All locales have complete translations); return { exitCode: 0, durationMs: Date.now() - start, filesProcessed: localeFiles.length }; } console.error(❌ Missing translations:); missing.forEach(({ locale, keys }) { console.error( ${locale}: ${keys.join(, )}); }); return { exitCode: 1, durationMs: Date.now() - start, errors: missing.flatMap(({ locale, keys }) keys.map(key ({ file: locales/${locale}.json, line: 0, message: Missing key ${key}, ruleId: i18n-missing })) ) }; } };第三步发布到 npm# 修改 package.json { name: ponytail-skill-myorg-i18n-check, version: 1.0.0, main: index.js, exports: { .: ./index.js }, peerDependencies: { ponytail-core: ^1.0.0 } } npm publish第四步在项目中使用# 安装 pnpm add -D ponytail-skill-myorg-i18n-check # 运行自动发现 ponytail run i18n-check # 或显式指定 ponytail run myorg/i18n-check整个过程不到 30 分钟而且这个 skill 可以被所有使用 Ponytail 的团队项目复用。关键技巧在于skill 的execute函数接收的cwd参数就是用户执行命令时的工作目录所以你不需要手动process.cwd()args是--之后的所有参数比如ponytail run i18n-check --strictargs就是[--strict]你可以用minimist解析。我们团队还把这个 skill 集成到了 pre-commit hook 中只要src/locales/en.json有变更就自动检查所有 locale 文件。4. 实操全流程与关键环节实现从零开始搭建 Ponytail 工作流4.1 初始化三分钟完成项目接入假设你有一个基于 pnpm 的 monorepo结构如下my-workspace/ ├── packages/ │ ├── api/ │ └── web/ ├── apps/ │ └── dashboard/ └── package.jsonStep 1全局安装 Ponytail CLI# 推荐全局安装避免每个 package 都装 npm install -g ponytail-cli # 或者用 npx适合 CI 环境 npx ponytail-clilatest --versionStep 2初始化 workspace 根目录# 进入 workspace 根目录 cd my-workspace # 执行初始化命令会自动检测 pnpm npx skill add dietrichgebert/ponytail # 这会生成 # - ponytail.config.json # - node_modules/.bin/ponytail软链接 # - package.json 中添加 scripts: { ponytail: ponytail }生成的ponytail.config.json默认内容{ workspace: { manager: pnpm, root: ., packages: [packages/*, apps/*] }, skills: { lint: { package: ponytail-skill-lint, defaultArgs: [--ext, .ts,.tsx,.js,.jsx] } } }Step 3验证基础功能# 在根目录运行 lint会自动遍历所有 packages ponytail run lint # 在某个 package 下运行自动识别 cwd cd packages/api ponytail run lint # 查看所有可用 skill ponytail list实操心得初始化后不要急着替换所有 script。先用ponytail run lint对比pnpm exec eslint的结果是否一致确认无误后再逐步替换test、build。我们团队踩过的坑是ponytail-skill-test默认使用 Vitest但某个 legacy package 还在用 Jest结果ponytail run test报错找不到vitest。解决方案是在该 package 的package.json中添加ponytail: { test: { runner: jest } }实现 per-package 配置覆盖。4.2 高级配置workspace 级别与 package 级别的差异化策略Ponytail 支持三级配置覆盖全局 config → workspace config → package config。这种设计让大型团队能灵活制定规则全局 config可选在~/.ponytailrc.json中定义所有项目的默认 skill 版本比如ponytail-skill-lint: 2.1.0workspace config必选ponytail.config.json定义 workspace 结构、默认 skill 参数、缓存策略package config可选在packages/api/package.json中添加ponytail字段覆盖 workspace 级别配置。例如我们的webpackage 需要更严格的 lint 规则// packages/web/package.json { name: web, ponytail: { lint: { args: [--max-warnings, 0, --fix], cacheKey: eslint-config-web-v2 } } }而apipackage 需要跳过某些检查// packages/api/package.json { name: api, ponytail: { lint: { args: [--ignore-path, .eslintignore-api] } } }Ponytail 在执行时会按顺序合并这些配置global → workspace → package且 package 级别配置完全覆盖 workspace 级别同名字段。这种设计让我们能为新项目启用严格模式为 legacy 项目保留宽松策略无需 fork skill 或修改代码。4.3 CI/CD 集成GitHub Actions 中的 Ponytail 最佳实践在 CI 环境中Ponytail 的优势尤为明显。我们用它重构了 GitHub Actions workflow将原来 4 个 joblint/test/typecheck/build压缩为 1 个 job# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: ponytail-ci: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: pnpm/action-setupv3 - name: Install dependencies run: pnpm install - name: Run CI pipeline run: ponytail run ci --staged # 这条命令会自动 # 1. 获取 git diff 中的 changed files # 2. 对这些 files 运行 lint只检查相关文件 # 3. 运行覆盖这些 files 的 tests # 4. 对涉及的 packages 做 typecheck # 5. 如果是 main 分支 push额外运行 full build关键参数说明--staged只对 Git staging 区的文件执行检查大幅缩短 CI 时间--since ${{ github.event.pull_request.base.sha }}在 PR 中只检查 base 分支到 head 分支的 diff--ci启用 CI 模式禁用 interactive prompt强制--verbose并设置CItrue环境变量。我们还利用 Ponytail 的缓存能力在 Actions 中复用缓存- name: Cache Ponytail skill cache uses: actions/cachev4 with: path: | node_modules/.cache/ponytail node_modules/.cache/vite key: ponytail-${{ runner.os }}-${{ hashFiles(**/pnpm-lock.yaml) }}实测数据显示在 20 个 package 的 workspace 中原 workflow 平均耗时 8.2 分钟新 workflow 降至 3.7 分钟减少 55%且失败时能准确定位到具体哪个 skill 哪个 package 出错而不是笼统的 “build failed”。4.4 故障排查与调试Ponytail 的 debug 模式与日志分析当ponytail run报错时不要慌。Ponytail 提供了完整的 debug 工具链1. 启用 verbose 日志ponytail run lint --verbose # 输出包含skill resolved path, exact command executed, env vars, stdout/stderr2. 查看 skill 执行详情ponytail run lint --debug # 输出 JSON 格式的完整执行报告包括 # - inputArgs: 解析后的参数 # - resolvedSkill: 实际加载的 skill 包路径 # - cacheKey: 本次缓存键值 # - durationMs: 各阶段耗时分解3. 手动触发 skill 重跑跳过缓存ponytail run lint --no-cache # 强制忽略缓存重新执行4. 检查 skill 依赖树ponytail list --tree # 显示所有已安装 skill 及其依赖常见问题排查表问题现象可能原因解决方案Error: Cannot find module ponytail-core全局安装的 Ponytail CLI 与本地 skill 版本不匹配在项目根目录pnpm add -D ponytail-core或统一用npx ponytailNo files matching the pattern were found--staged模式下没有变更文件检查git status或改用--since mainCommand failed with exit code 1skill 返回非零 exit code但错误信息不明确加--verbose查看原始工具输出或--debug看完整 JSON 报告Cache miss on every run缓存键计算不稳定检查 skill 的cacheKey配置确保不包含时间戳等动态值注意Ponytail 的所有错误都带有PONYTAIL_前缀比如PONYTAIL_LINT_ERROR方便在日志系统中过滤。我们团队在 Sentry 中专门设置了PONYTAIL_*的 alert 规则一旦出现立刻通知基建组。5. 常见问题与独家避坑指南来自 12 个生产环境项目的实战总结5.1 “Ponytail run lint 为什么比原生 eslint 慢”——缓存失效的真相这是最多人问的问题。表面看ponytail run lint比pnpm exec eslint慢 2-3 秒但实际原因是Ponytail 默认启用--cache而首次运行需要构建缓存索引。我们跟踪了 12 个项目的缓存行为发现 90% 的“慢”都源于同一个配置错误// ❌ 错误配置缓存目录指向 node_modules每次 pnpm install 都清空 { skills: { lint: { cacheDir: node_modules/.cache/eslint } } } // ✅ 正确配置缓存目录独立于 node_modules { skills: { lint: { cacheDir: .ponytail-cache/eslint } } }node_modules/.cache目录在pnpm install时会被删除导致每次安装依赖后eslint 缓存都失效。正确的做法是把缓存放在项目根目录下的.ponytail-cache被.gitignore自动忽略。实测修正后ponytail run lint的首次耗时从 12.4s 降到 6.1s后续稳定在 0.8s。5.2 “monorepo 中某些 package 的 lint 不生效”——workspace 包发现机制陷阱Ponytail 默认用pnpm list --depth 0 --json获取所有 workspace packages但如果你的pnpm-workspace.yaml中用了packages: [packages/**, !packages/legacy/**]这种排除语法Ponytail 的list命令可能无法正确解析。解决方案是在ponytail.config.json中显式声明 packages{ workspace: { packages: [ packages/core, packages/utils, apps/dashboard ] } }或者更推荐的方式是在pnpm-workspace.yaml中避免使用!排除改为用独立的 workspace 配置文件比如pnpm-workspace-legacy.yaml这样 Ponytail 的自动发现就能正常工作。5.3 “Ponytail run test --staged 在 CI 中找不到变更文件”——Git 深度克隆问题GitHub Actions 默认只 clone 最近一次 commitfetch-depth: 1导致git diff --name-only HEAD~1无法获取变更文件。必须在 workflow 中显式设置- uses: actions/checkoutv4 with: fetch-depth: 0 # ← 关键获取完整历史否则--staged和--since参数都会失效退化为全量运行。我们曾因此让一个 300 用例的项目 CI 耗时从 2 分钟暴涨到 18 分钟。5.4 “自定义 skill 在 Windows 上路径报错”——跨平台路径处理规范Node.js 的path.join()在 Windows 上会生成\路径而大多数 CLI 工具如 eslint只认/。我们在开发ponytail-skill-myorg/i18n-check时遇到这个问题join(cwd, src\\locales)生成C:\project\src\locales但globby无法匹配。解决方案是统一用posixPathimport { posix } from path; // ✅ 正确 const localesDir posix.join(cwd, src, locales); // ❌ 错误 const localesDir join(cwd, src, locales);Ponytail core 内部也做了类似处理所有传递给子进程的路径都会经过posix.normalize()转换。5.5 “如何让 Ponytail 与 Husky pre-commit hook 完美协作”Husky 的 hook 脚本默认在process.cwd()执行但 monorepo 中的 commit 可能发生在任意 package 目录。我们的标准配置是// .husky/pre-commit #!/usr/bin/env sh . $(dirname -- $0)/_/husky.sh # 获取当前 commit 影响的 packages CHANGED_PACKAGES$(pnpm affected --json | jq -r .packages[]) if [ -n $CHANGED_PACKAGES ]; then # 在每个