claude-mem 安装器失败透明化:错误分类法、统一决策点与 12×4 跨 IDE 验证矩阵
npx claude-mem install 是 claude-mem 面向 12 种 IDE/Agent CLI 的通用安装入口。安装器一旦在某个环节"悄悄吞掉错误却仍打印 installed successfully",用户拿到的就是一个看起来正常、实际残缺的安装。本文基于仓库中的实施计划 installer-transparency 及其已落地的源码,完整讲解 claude-mem 是如何把安装器内所有"错误静默点"收敛为一张显式错误分类表(taxonomy)、一个统一决策函数 installerError(),并用 12×4 跨 IDE 故障矩阵与 postinstall CI 守卫固化下来,让 ERESOLVE 冲突、缺失的 uv/bun 都以平台级修复指引"大声失败"。读完你可以掌握:如何为 CLI 安装器设计 fail-loud 错误语义、如何在 npm/Bun 的 peer 依赖冲突上做"strict-first 回退"策略,以及如何在 CI 中防止带 postinstall 的依赖重新拖垮安装流程。
背景问题:被吞掉的错误长什么样
计划文档用一张带行号引用的清单(plans/04-installer-transparency.md "Problem Statement" 一节)枚举了 9 类既有的静默失败模式,值得逐条理解,因为它们定义了"失败透明"到底要解决什么:
- npm install 错误被降级为提示:
install.ts中npm install的 catch 分支只console.warn并返回 "Dependencies may need manual install ⚠",整个安装仍以绿色 "installed successfully!" 收尾。真正的ERESOLVE冲突就此变成一行黄字。 - 无条件
--legacy-peer-deps:runNpmInstallInMarketplace()原本总是携带--legacy-peer-deps,把任何真实 peer 冲突无条件掩盖。文档特别指出,下次 tree-sitter 的 peer 范围收紧时,这个 flag 会"安静地装出一棵坏依赖树",问题只会推迟到运行时爆发。 uv失败信息被 spinner 覆盖:setup-runtime.ts中ensureUv()在自动安装后探测版本失败时,抛出的 "uv installed but version probe failed" 会在 clack spinner 刷新中被冲掉,用户看到的只是一条被折行的模糊报错。- 失败 IDE 不可见:per-IDE 安装失败经由
bufferConsole收集到pendingErrors[],但汇总逻辑只读取failedIDEs.length——在 buffer 返回 0 之后抛错的 IDE 完全隐形,"Failed: …" 是屏幕上唯一信号,且很快被绿色结尾滚走。 - 各集成安装器各搞一套:
src/services/integrations/下 7 个 IDE 集成(Codex、Cursor、Gemini、OpenCode、OpenClaw、Windsurf、MCP)50+ 个 catch 块,错误处理形状互不相同,12 个 IDE 就有 12 条不同的失败 UX 路径。 - 构建脚本无守卫:
scripts/build-hooks.js生成的plugin/package.json依赖全部 tree-sitter 语法包,但没有 CI 守卫阻止新增一个带scripts.postinstall的包进入白名单外。
计划文档同时把审计规模量化了:仅 src/npx-cli/commands/install.ts 一个文件就有 14 个 catch 块(行号 387、393、406、455、596、613、631、725、980、1056、1131、1161、1243、1252),setup-runtime.ts 5 个,七个集成安装器合计 37 个,openclaw/install.sh 还有 36 处 || true / 2>/dev/null shell 抑制——审计总量约 105 个静默点,每一个都要在审计 CSV 中给出建议严重级别。
参考事故:v12.6.1 → v12.6.2 的 tree-sitter-swift 悬停
计划将 CHANGELOG.md 中记录的 v12.6.1 → v12.6.2 事故定为本项目的"标准教训":PR #2300 把 21 个 tree-sitter 语法包从 devDependencies 移入 dependencies,其中 tree-sitter-swift 的 postinstall 会拉取嵌套的 tree-sitter-cli,后者下载 Rust 二进制并被 SIGINT,直接挂死 npx claude-mem install。核心教训是:npm 不认 trustedDependencies(那是 Bun 专属机制)——任何新增的、带网络 postinstall 的传递依赖都可能拖垮安装。这一条既是 Phase 7(postinstall CI 守卫)的动机,也是运行时给所有 npm install / bun install 默认加上 --ignore-scripts 的理由。
错误分类法:四级严重度与 12 个内置分类
Phase 2 交付的分类法已落地为 error-taxonomy.ts,它是"每个安装器错误如何处置"的唯一事实来源。设计约束直接写在文件头部注释里:fail loud over silent(未知错误默认 ABORT)、修复文案必须插值解析后的 dataDir 而非硬编码 ~/.claude-mem(多账号安全)、不存在 SILENT 级别。
严重度定义
计划文档定义了四级语义(ABORT / FAIL_LOUD_PER_IDE / WARN_CONTINUE / SILENT_RETRY)。当前实现中 SILENT_RETRY 的"先静默重试一次、再升级为警告"语义在调用方通过重试逻辑表达,taxonomy 层收敛为三级:
export enum ErrorSeverity {
/** exit 1, do not continue. */
ABORT = 'ABORT',
/** exit 1 only if all IDEs fail; otherwise partial summary, continue. */
FAIL_LOUD_PER_IDE = 'FAIL_LOUD_PER_IDE',
/** print warning to end-of-install summary, continue (exit 0). */
WARN_CONTINUE = 'WARN_CONTINUE',
}
export interface ErrorCategory {
id: string;
severity: ErrorSeverity;
match: (cause: unknown, ctx: MatchContext) => boolean;
remediation: (ctx: RemediationContext) => string;
}
其中 RemediationContext 携带 platform 与 dataDir(dataDir 解析自 CLAUDE_MEM_DATA_DIR),保证每条修复指引都能给出当前用户真实的安装路径。
内置 12 个错误分类
ERROR_CATEGORIES 按"最具体优先"排序,classifyError() 返回第一个命中的分类,末尾兜底项保证任何未分类错误都走 ABORT:
| id | 严重度 | 匹配逻辑(源码 error-taxonomy.ts) | 修复指引要点 |
|---|---|---|---|
bun-missing-after-install |
ABORT | 消息含 Bun executable not found / Failed to install Bun |
按平台给出手动安装命令:Windows winget install Oven-sh.Bun,macOS/Linux curl -fsSL https://bun.sh/install | bash |
uv-missing-after-install |
ABORT | 消息含 uv executable not found / uv installed but version probe failed / uv binary not found |
Windows winget install astral-sh.uv;macOS/Linux curl -LsSf https://astral.sh/uv/install.sh | sh |
tree-sitter-eresolve |
ABORT | 消息匹配正则 /\bERESOLVE\b/(大写 token,刻意不用 /eresolve/i) |
提示 --legacy-peer-deps 已失效,携带上方打印的 peer 冲突块提交 issue |
marketplace-dir-not-writable |
ABORT | 消息含 EACCES / EPERM |
检查文件系统权限,或将 CLAUDE_MEM_DATA_DIR 指向可写路径 |
plugin-json-corrupt |
ABORT | component 为 plugin-json 且消息含 Unexpected token / JSON / parse |
rm -rf ~/.claude/plugins/marketplaces/thedotmack 后重装 |
all-ides-failed |
ABORT | component 为 all-ides |
所有选中 IDE 均失败,用 --ide=<single> 隔离 |
single-ide-failed |
FAIL_LOUD_PER_IDE | phase 为 ide-install |
回显已捕获的 stderr,npx claude-mem install --ide=<name> 单独重试 |
path-update-failed |
WARN_CONTINUE | component 为 path-update |
手动把打印的 export 行追加到 shell 配置 |
auto-memory-toggle-failed |
WARN_CONTINUE | component 为 auto-memory |
在 ~/.claude/settings.json env 块中加 "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1" |
version-probe-transient |
WARN_CONTINUE | component 以 -version-probe 结尾 |
版本探测失败但安装大概率正常,功能异常时重跑安装 |
child-process-timeout |
ABORT | 消息匹配 timed out / ETIMEDOUT / SIGTERM / did not finish |
检查网络;慢机器用 CLAUDE_MEM_INSTALL_TIMEOUT_MS 调高预算 |
unknown-install-error |
ABORT | 恒真(兜底) | 指引用户收集 last-install-error.json 并提交 issue |
注意两个与文档原计划(14 个 seed 分类)的差异点,体现了实现时的取舍:计划中的 bun-install-network-fail 与 idempotent-json-merge-race(均为 SILENT_RETRY 语义)没有单列为分类——网络抖动由带重试的 spawn 包装吸收,JSON 原子写竞态由 writeJsonFileAtomic 内部重试处理,taxonomy 只保留"值得让用户看到"的分类;而 unknown-install-error 兜底项正是文档"反模式护栏"的直接产物:不允许存在低严重度的 "unknown error" 分类,未分类错误必须默认 ABORT。
测试 install-error-matrix.test.ts 的 "error taxonomy" 描述块直接锁定这些语义:断言导出面完整、断言不存在 SILENT 级别、断言缺 bun / 缺 uv / ERESOLVE stderr 分别归类为 ABORT、断言未知错误兜底为 ABORT、并验证修复文案插值的是传入的 dataDir 而非硬编码路径。
installerError():每个 catch 的唯一决策点
Phase 3 交付的 error-reporter.ts 把"错误如何处置"从散落的 console.warn 收敛为一个函数。它的文件头注释说明了三条行为契约:
- ABORT → 写
last-install-error.json,抛出InstallAbortError(顶层处理器打印修复块并exit 1); - FAIL_LOUD_PER_IDE → 记录失败 IDE 与警告,继续执行(全部失败才升级为 ABORT);
- WARN_CONTINUE → 警告入 summary 队列,继续执行(exit 0)。
ABORT 路径:结构化诊断 + 类型化异常
export class InstallAbortError extends Error {
readonly category: ErrorCategory;
readonly remediation: string;
// constructor 使用 super(message, { cause }) 保留原始异常链
}
export function installerError(
severity: ErrorSeverity,
ctx: ErrorContext,
summary: InstallSummary,
): void {
const dataDir = resolveDataDir();
const category = classifyError(ctx.cause, { component: ctx.component, phase: ctx.phase });
const remediation =
ctx.remediation ??
category.remediation({ platform: process.platform, dataDir });
switch (severity) {
case ErrorSeverity.ABORT: {
writeLastInstallError(category, ctx, remediation, dataDir);
throw new InstallAbortError(
`${ctx.component} failed during ${ctx.phase}: ${causeMessage(ctx.cause)}`,
{ category, remediation, cause: ctx.cause },
);
}
// ... FAIL_LOUD_PER_IDE / WARN_CONTINUE 分支
}
}
两个细节值得强调。其一,ABORT 会尽力把一份结构化记录写入 ${dataDir}/last-install-error.json(即 ~/.claude-mem/last-install-error.json,或 CLAUDE_MEM_DATA_DIR 指向的位置),字段包括 severity、categoryId、component、phase、cause、remediation、details(原始 stderr 块)、timestamp——这个写入是 best-effort 的,失败本身绝不能掩盖原始错误。其二,installerError 内部绝不调用 process.exit():文档明确指出 process.exitCode = 1 不会中断进行中的 await 链,正确做法是抛出 InstallAbortError,由顶层 runInstallCommand 捕获后先 flushSummary 再退出 1,保证用户能看到完整 outro。
ErrorContext 的字段设计也来自计划文档的骨架:component(如 cursor、marketplace-npm-install、uv-install)、phase(如 setup-runtime、ide-install、marketplace-deps)、cause、可选的 remediation 覆盖、以及 details(原样透传的 stderr 块,例如 ERESOLVE 冲突详情)。InstallSummary 则只有 warnings 与 failedIDEs 两个字段——summary 作为显式参数贯穿调用链而不是模块级全局状态,这是文档"可测试性"护栏的直接体现。
为什么警告不能"实时打印"
文档在反模式一节点破了一个交互细节:clack 的 p.spinner() 在 .stop() 时会重写当前行,spinner 存续期间经 console.warn 输出的错误会丢失;同理 bufferConsole 包装(install.ts 开头)在非交互模式下会吞掉缓冲内的 stderr。因此 WARN_CONTINUE 的实现纪律是"只入队、不打印",由 flushSummary 在 spinner 与 outro 之后统一冲刷:
export function flushSummary(
summary: InstallSummary,
emit: (line: string) => void,
): void {
if (summary.warnings.length === 0) return;
emit('');
emit('Warnings & remediation:');
for (const warning of summary.warnings) {
emit(` • [${warning.component}] ${warning.message}`);
if (warning.remediation && warning.remediation !== 'No action required.') {
emit(` → ${warning.remediation}`);
}
}
}
顶层接线(install.ts 的 runInstallCommand)按计划要求:在命令开头创建 summary,传给 setupIDEs、每个 runTasks 任务、ensureBun/ensureUv、runNpmInstallInMarketplace;在所有任务结束后、打印最终状态行之前调用 flushSummary;整个命令体包在 try/catch 中,InstallAbortError 分支打印 "Installation Aborted: <category.id>" 标题并退出 1。
ERESOLVE 处理:strict-first 与确认制回退
Phase 4 的核心是把 runNpmInstallInMarketplace 从"无条件 --legacy-peer-deps"改写为"先 strict,确认 ERESOLVE 后才回退"。当前实现(install.ts 第 755–805 行)与计划骨架一致:
async function runNpmInstallInMarketplace(summary: InstallSummary): Promise<void> {
const marketplaceDir = marketplaceDirectory();
const packageJsonPath = join(marketplaceDir, 'package.json');
if (!existsSync(packageJsonPath)) return;
const baseFlags = ['install', '--omit=dev', '--ignore-scripts'];
const strictResult = await runNpmStrict(marketplaceDir, baseFlags);
if (strictResult.code === 0) return;
if (strictResult.timedOut) {
installerError(ErrorSeverity.ABORT, {
component: 'marketplace-npm-install',
phase: 'marketplace-deps',
cause: new Error('npm install timed out'),
details: strictResult.stderr.slice(0, 4000),
}, summary);
}
if (!isEresolve(strictResult.stderr)) {
// A strict failure with no ERESOLVE is a real bug — never retry, ABORT.
installerError(ErrorSeverity.ABORT, {
component: 'marketplace-npm-install',
phase: 'marketplace-deps',
cause: new Error(`npm install failed (exit ${strictResult.code})`),
details: strictResult.stderr.slice(0, 4000),
}, summary);
}
// Confirmed ERESOLVE — log loudly, attempt one fallback with --legacy-peer-deps.
log.warn('npm reported an ERESOLVE peer-dependency conflict in marketplace deps; retrying once with --legacy-peer-deps.');
log.warn(extractEresolveBlock(strictResult.stderr));
const legacyResult = await runNpmStrict(marketplaceDir, [...baseFlags, '--legacy-peer-deps']);
if (legacyResult.code === 0) {
summary.warnings.push({
component: 'marketplace-npm-install',
message: 'tree-sitter peer-dep ERESOLVE was resolved with the --legacy-peer-deps fallback. Benign for the marketplace install; re-evaluate when tree-sitter peer ranges change.',
remediation: 'No action required.',
});
return;
}
installerError(ErrorSeverity.ABORT, { /* ... ERESOLVE 二次失败,携带前 4000 字符 stderr */ });
}
这段代码把文档中的四条反模式护栏全部落实了:
- strict 失败且无 ERESOLVE 时绝不重试——没有 ERESOLVE token 的 strict 失败是真实 bug,重试只会掩盖它,直接 ABORT;
- 确认 ERESOLVE 时才回退,且大声宣布——回退前先
log.warn说明"检测到 ERESOLVE,重试一次",并打印extractEresolveBlock(stderr)提取的While resolving:…Conflicting peer dependency:冲突块; - 匹配必须用
/\bERESOLVE\b/精确大写 token——npm v10+ 的 stderr 前缀是npm error code ERESOLVE,用小写宽松正则会把别的输出误判为 peer 冲突; - 回退成功也要在 summary 里留痕——"benign but should be re-evaluated" 的警告会在结尾冲刷,保证这类回退不会无人知晓。
npm 标志的语义辨析(文档 Phase 0 "External facts" 表格)值得单独强调:--legacy-peer-deps 是跳过 peer 依赖解析,--force 是接受冲突依赖树——两者不等价,--force 更激进且"不是我们想要的"。所以文档明令 Phase 4 不得把回退"升级"为 --force。另一个易错点:npm 没有 --no-postinstall CLI 标志,正确的是 --ignore-scripts——这也是为什么 baseFlags 默认带 --ignore-scripts(v12.6.2 教训:任何新增传递依赖的 postinstall 都可能挂死安装)。
配套的 runNpmStrict / isEresolve / extractEresolveBlock 工具函数提取在 npm-install-helper.ts,用 spawnSync 包装并带超时(Phase 7),extractEresolveBlock 在找不到块标记时防御性地返回原始 stderr。Bun 侧的 installPluginDependencies(setup-runtime.ts)采用同一模式:解析 stderr 区分 error: failed to resolve(网络问题,可重试)与包真缺失(ABORT)。
uv / bun 缺失:平台级失败与向量搜索降级
uv(Python 工具链)与 bun(运行时)由 setup-runtime.ts 的 ensureUv()(第 358 行起)与 ensureBun()(第 307 行起)保证存在。计划文档指出原实现的两个缺陷:uv 自动安装脚本成功返回 0 不代表二进制在 PATH 里(可能落在 ~/.local/bin 且当前 shell 尚未包含该目录);以及版本探测失败时被 spinner 覆盖。修复方案在 ensureUv 中落实:
- 安装后直接复核
UV_COMMON_PATHS:getUvPath()返回空时,逐个existsSync检查候选路径("当前 shell 的 PATH 可能还没包含~/.local/bin,必须显式重探"),这直接对应文档引用的外部事实——astral.sh 安装脚本在二进制落到非 PATH 目录时同样退出 0; - 找不到二进制时,向量搜索降级路径生效:
ensureUv接受allowVectorSearchOptOut选项,若用户在设置(SettingsDefaultsManager加载的用户设置,键为CLAUDE_MEM_DISABLE_VECTOR_SEARCH,当前实现在 setup-runtime.ts 第 52 行附近从 settings 记录或 env 块读取)中关闭了向量搜索,则降级为WARN_CONTINUE——"uv 二进制安装后未找到;向量搜索已禁用,继续安装"——否则按 ABORT 处理,且平台特定修复块作为主消息而非被折行的次要行:
if (!uvPath) {
if (options.allowVectorSearchOptOut && userHasOptedOutOfVectorSearch()) {
installerError(ErrorSeverity.WARN_CONTINUE, {
component: 'uv-install',
phase: 'setup-runtime',
cause: new Error('uv binary not found after install; vector search disabled — continuing.'),
}, summary);
return { uvPath: null, version: null };
}
installerError(ErrorSeverity.ABORT, {
component: 'uv-install',
phase: 'setup-runtime',
cause: new Error('uv binary not found after auto-install attempt'),
remediation: UV_REMEDIATION({ platform: process.platform, dataDir }),
}, summary);
}
- 版本探测带一次 1 秒重试:新二进制偶尔需要一点时间响应
--version,首次探测失败后await1 秒再探一次;两次都失败则记WARN_CONTINUE并返回version: 'unknown',而不是谎报版本。
ensureBun 应用同一模式(重试 + platformBunRemediation()),但没有降级通道——文档的理由是 bun 是 hooks 的必备运行时,"uv 可以因向量搜索关闭而降级,bun 不行"。
ensureUv/ensureBun 的三条反模式护栏同样值得照单全收:不要在一次 ensureUv 调用里多次调用 installUv()(自动安装是 one-shot,失败就给手动指引,不许循环重试);不要吞掉 installUv() 抛出的错误(其消息本就含平台指引,应作为 ABORT 修复文案直接传播);不要为缺 uv 加 "press enter to continue" 交互提示——非交互安装会直接挂死。
跨 IDE 验证矩阵:12 个 IDE × 4 种故障场景
Phase 6 把"失败透明"从代码约定变成可回归断言的测试矩阵,落地为 install-error-matrix.test.ts。矩阵的维度是计划文档给出的 12 个 IDE(claude-code、gemini-cli、opencode、openclaw、windsurf、codex-cli、cursor、copilot-cli、antigravity、goose、roo-code、warp,与 ide-detection.ts 的规范清单一致)和 4 个故障场景:
| 场景 | Mock 方式 | 断言要点 |
|---|---|---|
| Happy path | spawnSync mock:bun --version、uv --version、npm install 全返回 0 |
exit 0;stdout 含 installed successfully;failedIDEs.length === 0、warnings.length === 0 |
| tree-sitter ERESOLVE | npm install mock 退出 1 且 stderr 含 npm error code ERESOLVE;--legacy-peer-deps 重试也退出 1 |
exit 1;stderr 含 Installation Aborted: tree-sitter-eresolve 与 peer 冲突块;stdout 不含 installed successfully |
| 缺 uv(自动安装失败) | getUvPath 返回 null,installUv 抛错 |
exit 1;stderr 含 Installation Aborted: uv-missing-after-install 与平台手动安装指令(Linux curl -LsSf https://astral.sh/uv/install.sh | sh,Windows winget install astral-sh.uv) |
| 缺 bun(自动安装失败) | getBunPath 返回 null,installBun 抛错 |
exit 1;stderr 含 Installation Aborted: bun-missing-after-install 与平台手动安装指令 |
从源码结构看,当前测试文件组织为四个 describe 块:taxonomy 导出与分类语义、installerError 决策逻辑(ABORT 抛错并写 last-install-error.json、WARN_CONTINUE 入队不抛、FAIL_LOUD_PER_IDE 记录 IDE、flushSummary 逐条冲刷)、npm ERESOLVE 检测(大写 token 识别、普通失败不误判、冲突块提取、标记缺失时返回原始 stderr)、以及跨 IDE 故障矩阵——后者当前实现覆盖 11 个 IDE × 4 场景,与计划中的 48 格(12×4)的差距来自个别 IDE 在后续版本中的集成路径调整,但每个测试格的断言结构(退出码、汇总标题、修复文案子串)与计划一致。
矩阵的辅助设施与纪律(均来自文档 Phase 6):
setupIsolatedHome():创建临时 HOME 并设置CLAUDE_MEM_DATA_DIR,所有用例禁止触碰真实~/.claude;- mock 打在
spawnSync/existsSync层而不是installerError层——要跑完整管线,而不是测自己的封装; - 不在测试进程内改
process.env.HOME,而是带 env 覆盖启动子进程; - Docker 载体复用 Dockerfile.test-installer(干净 Linux 镜像,不预装 bun/uv),CI 在 PR 触及
src/npx-cli/、src/services/integrations/、scripts/build-hooks.js或tests/install-*时运行矩阵。
postinstall 回归守卫:把 v12.6.2 教训固化进 CI
Phase 7 的第一道防线是 check-postinstall-allowlist.js——一个发布前 CI 脚本,检查 plugin/package.json 的直接依赖中凡带 preinstall/install/postinstall 脚本的包必须在显式白名单内,否则构建失败。脚本头部的注释完整引用了事故背景:
// Postinstall regression guard.
//
// Enforces: every DIRECT dependency declared in plugin/package.json that ships
// an install / preinstall / postinstall script must be explicitly allowlisted.
// A new dep with a network postinstall that is NOT allowlisted fails CI.
//
// Why: see CHANGELOG.md (v12.6.1 -> v12.6.2 incident). PR #2300 moved 21
// tree-sitter grammars into dependencies; tree-sitter-swift's postinstall pulled
// a nested tree-sitter-cli that downloaded a Rust binary and SIGINT'd,
// hanging `npx claude-mem install`. npm does NOT honor trustedDependencies
// (Bun-only), which is why the runtime install paths pass --ignore-scripts.
实现白名单覆盖了全部 tree-sitter 语法包(tree-sitter、tree-sitter-c … tree-sitter-swift 等)、tree-sitter-cli、esbuild、@biomejs/biome、better-sqlite3 等已知带安装脚本的依赖。脚本头部还明确了范围界定:只查 plugin/package.json 直接依赖——仓库自身 dev node_modules 里的测试用语法包不属于用户经 npx 获取的安装面,不在守卫范围内。文档给出的护栏在此一并成立:CI 失败时不得自动把新包加进白名单("失败 CI 是目的本身,每条新增 postinstall 依赖都应经过人工评审");该白名单是 CI 时守卫,不复制 trustedDependencies(Bun 运行时机制),两者各司其职。
第二道防线是运行时 --ignore-scripts 默认值:installPluginDependencies 对 bun install 与 runNpmInstallInMarketplace 对 npm install 都默认带 --ignore-scripts(Bun 认 trustedDependencies、npm 不认,双保险是刻意的,文档明确"即使 Bun 认白名单也不要移除 --ignore-scripts")。第三道是超时包装:所有安装用 execSync/spawnSync 都带显式超时——首次运行 5 分钟、后续 2 分钟,可用 CLAUDE_MEM_INSTALL_TIMEOUT_MS 覆盖;spawnSync 超时返回 signal === 'SIGTERM',转为 child-process-timeout ABORT。文档列出的审计包装清单包括 installBun、installUv、installPluginDependencies 的 bun install、runNpmStrict 及其回退路径、installClaudeCode。
阶段依赖、验证清单与回滚策略
整个改造按严格顺序分阶段执行(plans/04-installer-transparency.md "Phase boundaries / ordering"):
- Phase 0(文档发现)→ Phase 1(审计全部静默点,产出 CSV,每行给出建议严重度);
- Phase 2(taxonomy)阻塞 Phase 3(reporter 使用 enum);
- Phase 3(
installerError)阻塞 Phase 4 / 5(二者都调用它); - Phase 4 + 5 在 Phase 3 之后独立落地;
- Phase 6(矩阵测试)依赖 3/4/5 完成才能断言正确行为;
- Phase 7(postinstall 守卫)在 Phase 3 之后任意时间落地;
- Phase 8(文档:
troubleshooting页覆盖全部分类、CLAUDE.md"Exit Code Strategy" 补安装器退出码说明——安装器不是 hook,允许 ABORT 时退出非 0;CHANGELOG.md自动生成、禁止手改)殿后。
每个阶段一个独立 commit,使部分回滚可行;回滚时用户侧的诊断锚点是 last-install-error.json。文档收尾的"最终验证"清单给出了可复制的检查手段,例如:grep -rE 'console\.warn\(.*install' src/npx-cli/ src/services/integrations/ 应返回 0 命中(所有警告都走 installerError);安装器路径不得再引入空的 try {} catch {};安装脚本中不得出现 --force;bun/npm 安装命令中不得移除 --ignore-scripts。
值得借鉴的设计决策
从这份计划及其落地代码中,有几条对任何多依赖 CLI 安装器都通用的原则:
- 单一决策点 + 分类表:错误处置逻辑只存在于
classifier + severity switch两处,新增失败模式只需加一个分类条目,而不是在 N 个 catch 里各写一套; - fail-loud 是安全默认:未分类错误默认 ABORT,"静默"这个级别根本不存在——安装器场景里"安静地装坏"永远比"大声地装失败"代价更高;
- 交互层会吃掉错误:spinner 重写、stderr 缓冲、非 TTY 模式都会让"实时打印"的警告消失,警告必须入队、在 outro 后统一冲刷;
- 回退必须确认制:
--legacy-peer-deps只在确认到ERESOLVEtoken 后使用一次,并大声宣布;普通失败直接 ABORT,因为重试会掩盖真实 bug; - 事故要变成守卫:一次线上事故(tree-sitter-swift 挂死安装)转化为 CI 白名单 + 运行时
--ignore-scripts+ 超时包装三层防线,教训就再也不会"可复现"。
延伸阅读
- 计划全文(含逐行审计数据与 9 个阶段的完整 checklist):plans/04-installer-transparency.md
- 错误分类法与决策点实现:src/npx-cli/install/error-taxonomy.ts、src/npx-cli/install/error-reporter.ts
- ERESOLVE strict-first 流程:src/npx-cli/commands/install.ts(
runNpmInstallInMarketplace,约 745–805 行) - bun/uv 运行时保障:src/npx-cli/install/setup-runtime.ts、src/npx-cli/install/npm-install-helper.ts
- postinstall 白名单守卫:scripts/check-postinstall-allowlist.js
- 回归测试矩阵:tests/install-error-matrix.test.ts、tests/setup-runtime.test.ts、tests/install-non-tty.test.ts
- 事故记录:CHANGELOG.md(v12.6.1 → v12.6.2 条目)
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00