From c64ad028b77cbf233a87e432e92aa772abd76612 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 25 Dec 2025 19:47:01 -0800 Subject: [PATCH] fix(npm): Windows postinstall file locking issue (GH#670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move write stream creation after redirect handling to avoid orphan streams that keep file handles open - Add 100ms delay after file.close() on Windows to ensure handle release - Fixes "The process cannot access the file" error during extraction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 5 +++++ npm-package/scripts/postinstall.js | 34 +++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb88cad..22426071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Skips interactions.jsonl and molecules.jsonl in sync checks - These files are runtime state, not sync targets +- **Windows npm postinstall file locking** (GH#670) - Windows install fix + - Fixed file handle not being released before extraction on Windows + - Moved write stream creation after redirect handling to avoid orphan streams + - Added delay after file close to ensure Windows releases file handle + - **FatalErrorRespectJSON** (bd-28sq) - Consistent error output - All commands respect `--json` flag for error output - Errors return proper JSON structure when flag is set diff --git a/npm-package/scripts/postinstall.js b/npm-package/scripts/postinstall.js index 7e24059e..1403834e 100755 --- a/npm-package/scripts/postinstall.js +++ b/npm-package/scripts/postinstall.js @@ -50,14 +50,18 @@ function getPlatformInfo() { return { platformName, archName, binaryName }; } +// Small delay helper for Windows file handle release +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + // Download file from URL function downloadFile(url, dest) { return new Promise((resolve, reject) => { console.log(`Downloading from: ${url}`); - const file = fs.createWriteStream(dest); const request = https.get(url, (response) => { - // Handle redirects + // Handle redirects - must happen BEFORE creating write stream if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; console.log(`Following redirect to: ${redirectUrl}`); @@ -70,27 +74,37 @@ function downloadFile(url, dest) { return; } + // Only create write stream after we know we have the final URL + const file = fs.createWriteStream(dest); + response.pipe(file); file.on('finish', () => { // Wait for file.close() to complete before resolving // This is critical on Windows where the file may still be locked - file.close((err) => { - if (err) reject(err); - else resolve(); + file.close(async (err) => { + if (err) { + reject(err); + return; + } + // On Windows, add a small delay to ensure file handle is fully released + if (os.platform() === 'win32') { + await delay(100); + } + resolve(); }); }); + + file.on('error', (err) => { + fs.unlink(dest, () => {}); + reject(err); + }); }); request.on('error', (err) => { fs.unlink(dest, () => {}); reject(err); }); - - file.on('error', (err) => { - fs.unlink(dest, () => {}); - reject(err); - }); }); }