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); - }); }); }