From a00526fe8d8f2bceea81f1e9b4467d1ce3c8bfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davor=20Raci=C4=87?= Date: Wed, 25 Feb 2026 12:13:16 +0100 Subject: [PATCH] fix(opencode): improve removeEmptyParents error handling and loop clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Distinguish recoverable errors (ENOTEMPTY, ENOENT) from fatal errors in removeEmptyParents() catch block — skip level and continue upward on TOCTOU races or concurrent removal, break only on fatal errors (EACCES) - Add comment clarifying loop invariant for missing-path continue branch Co-Authored-By: Claude Sonnet 4.6 --- tools/cli/installers/lib/ide/_config-driven.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 6e8c7c8a0..d1552700f 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -557,14 +557,21 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break; try { if (!(await fs.pathExists(fullPath))) { + // Dir already gone — advance current; last is reset at top of next iteration current = path.dirname(current); continue; } const remaining = await fs.readdir(fullPath); if (remaining.length > 0) break; await fs.rmdir(fullPath); - } catch { - break; + } catch (error) { + // ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward + // ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward + if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') { + current = path.dirname(current); + continue; + } + break; // fatal error (e.g. EACCES) — stop upward walk } current = path.dirname(current); }