@beads/bd 0.52.0 → 0.55.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beads/bd",
3
- "version": "0.52.0",
3
+ "version": "0.55.4",
4
4
  "description": "Beads issue tracker - lightweight memory system for coding agents with native binary support",
5
5
  "main": "bin/bd.js",
6
6
  "bin": {
@@ -128,6 +128,30 @@ function sleep(ms) {
128
128
  return new Promise(resolve => setTimeout(resolve, ms));
129
129
  }
130
130
 
131
+ // Wait for a file to become accessible (Windows file lock workaround).
132
+ // After fs.createWriteStream closes, Windows may hold the file lock for a
133
+ // short period. This function polls until the file can be opened exclusively.
134
+ async function waitForFileAccess(filePath, timeoutMs = 30000) {
135
+ const intervalMs = 200;
136
+ const startTime = Date.now();
137
+
138
+ while (Date.now() - startTime < timeoutMs) {
139
+ try {
140
+ const fd = fs.openSync(filePath, 'r');
141
+ fs.closeSync(fd);
142
+ return; // File is accessible
143
+ } catch (err) {
144
+ if (err.code === 'EBUSY' || err.code === 'EPERM') {
145
+ await sleep(intervalMs);
146
+ } else {
147
+ return; // Not a lock error — let extraction attempt handle it
148
+ }
149
+ }
150
+ }
151
+ // Timed out, but proceed anyway — extractZip has its own retry logic
152
+ console.warn(`Warning: file ${path.basename(filePath)} may still be locked after ${timeoutMs}ms, attempting extraction anyway...`);
153
+ }
154
+
131
155
  // Extract zip file (for Windows) with retry logic
132
156
  async function extractZip(zipPath, destDir, binaryName) {
133
157
  console.log(`Extracting ${zipPath}...`);
@@ -139,9 +163,10 @@ async function extractZip(zipPath, destDir, binaryName) {
139
163
  try {
140
164
  // Use unzip command or powershell on Windows
141
165
  if (os.platform() === 'win32') {
142
- execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'inherit' });
166
+ // Use stdio: 'pipe' to capture error output for file-lock detection
167
+ execSync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'pipe' });
143
168
  } else {
144
- execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
169
+ execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
145
170
  }
146
171
 
147
172
  // The binary should now be in destDir
@@ -154,20 +179,26 @@ async function extractZip(zipPath, destDir, binaryName) {
154
179
  console.log(`Binary extracted to: ${extractedBinary}`);
155
180
  return; // Success
156
181
  } catch (err) {
157
- const isFileLockError = err.message && (
158
- err.message.includes('being used by another process') ||
159
- err.message.includes('Access is denied') ||
160
- err.message.includes('cannot access the file')
161
- );
182
+ // Combine all available error output for reliable detection.
183
+ // With stdio: 'pipe', the PowerShell error text is in err.stderr,
184
+ // while err.message only contains "Command failed: ...".
185
+ const stderr = err.stderr ? err.stderr.toString() : '';
186
+ const errorText = `${err.message} ${stderr}`;
187
+
188
+ const isFileLockError =
189
+ errorText.includes('being used by another process') ||
190
+ errorText.includes('Access is denied') ||
191
+ errorText.includes('cannot access the file') ||
192
+ errorText.includes('EBUSY');
162
193
 
163
194
  if (isFileLockError && attempt < maxRetries) {
164
195
  const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
165
- console.log(`File may be locked (attempt ${attempt}/${maxRetries}). Retrying in ${delayMs}ms...`);
196
+ console.log(`File appears locked (attempt ${attempt}/${maxRetries}). Retrying in ${delayMs}ms...`);
166
197
  await sleep(delayMs);
167
198
  } else if (attempt === maxRetries) {
168
- throw new Error(`Failed to extract archive after ${maxRetries} attempts: ${err.message}`);
199
+ throw new Error(`Failed to extract archive after ${maxRetries} attempts: ${err.message}\n${stderr}`);
169
200
  } else {
170
- throw new Error(`Failed to extract archive: ${err.message}`);
201
+ throw new Error(`Failed to extract archive: ${err.message}\n${stderr}`);
171
202
  }
172
203
  }
173
204
  }
@@ -201,6 +232,12 @@ async function install() {
201
232
  console.log(`Downloading bd binary...`);
202
233
  await downloadFile(downloadUrl, archivePath);
203
234
 
235
+ // On Windows, wait for the OS to release the file lock before extracting.
236
+ // Windows may hold the file handle for a short time after close().
237
+ if (process.platform === 'win32') {
238
+ await waitForFileAccess(archivePath);
239
+ }
240
+
204
241
  // Extract the archive based on platform
205
242
  if (platformName === 'windows') {
206
243
  await extractZip(archivePath, binDir, binaryName);