@socketsecurity/lib 2.3.0 → 2.5.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.5.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.5.0) - 2025-10-28
9
+
10
+ ### Added
11
+
12
+ - **Process locking utilities**: Added `ProcessLockManager` class providing cross-platform inter-process synchronization using file-system based locks
13
+ - Atomic lock acquisition via `mkdir()` for thread-safe operations
14
+ - Stale lock detection with automatic cleanup (default 10 seconds, aligned with npm's npx strategy)
15
+ - Exponential backoff with jitter for retry attempts
16
+ - Process exit handlers for guaranteed cleanup even on abnormal termination
17
+ - Three main APIs: `acquire()`, `release()`, and `withLock()` (recommended)
18
+ - Comprehensive test suite with `describe.sequential` for proper isolation
19
+ - Export: `@socketsecurity/lib/process-lock`
20
+
21
+ ### Changed
22
+
23
+ - **Script refactoring**: Renamed `spinner.succeed()` to `spinner.success()` for consistency
24
+ - **Script cleanup**: Removed redundant spinner cleanup in interactive-runner
25
+
26
+ ## [2.4.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.4.0) - 2025-10-28
27
+
28
+ ### Changed
29
+
30
+ - **Download locking aligned with npm**: Reduced default `staleTimeout` in `downloadWithLock()` from 300 seconds to 10 seconds to align with npm's npx locking strategy
31
+ - Prevents stale locks from blocking downloads for extended periods
32
+ - Matches npm's battle-tested timeout range (5-10 seconds)
33
+ - Binary downloads now protected against concurrent corruption
34
+ - **Binary download protection**: `dlxBinary.downloadBinary()` now uses `downloadWithLock()` to prevent corruption when multiple processes download the same binary concurrently
35
+ - Eliminates race conditions during parallel binary downloads
36
+ - Maintains checksum verification and executable permissions
37
+
8
38
  ## [2.3.0](https://github.com/SocketDev/socket-lib/releases/tag/v2.3.0) - 2025-10-28
9
39
 
10
40
  ### Added
@@ -1,3 +1,3 @@
1
1
  /* Socket Lib - Built with esbuild */
2
- var I=Object.create;var b=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var H=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var L=(t,n)=>{for(var e in n)b(t,e,{get:n[e],enumerable:!0})},O=(t,n,e,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of F(n))!z.call(t,r)&&r!==e&&b(t,r,{get:()=>n[r],enumerable:!(a=N(n,r))||a.enumerable});return t};var $=(t,n,e)=>(e=t!=null?I(H(t)):{},O(n||!t||!t.__esModule?b(e,"default",{value:t,enumerable:!0}):e,t)),q=t=>O(b({},"__esModule",{value:!0}),t);var G={};L(G,{cleanDlxCache:()=>V,dlxBinary:()=>W,getDlxCachePath:()=>P,listDlxCache:()=>X});module.exports=q(G);var j=require("node:crypto"),s=require("node:fs"),x=$(require("node:os")),p=$(require("node:path")),A=require("#constants/platform"),u=require("./fs"),C=require("./http-request"),S=require("./objects"),T=require("./path"),R=require("./paths"),_=require("./spawn");function Y(t,n){return(0,j.createHash)("sha256").update(`${t}:${n}`).digest("hex")}function y(t){return p.default.join(t,".dlx-metadata.json")}async function J(t,n){try{const e=y(t);if(!(0,s.existsSync)(e))return!1;const a=await(0,u.readJson)(e,{throws:!1});if(!(0,S.isObjectObject)(a))return!1;const r=Date.now(),i=a.timestamp;return typeof i!="number"||i<=0?!1:r-i<n}catch{return!1}}async function K(t,n,e){const a=await(0,C.httpRequest)(t);if(!a.ok)throw new Error(`Failed to download binary: ${a.status} ${a.statusText}`);const r=`${n}.download`,i=(0,j.createHash)("sha256");try{await s.promises.mkdir(p.default.dirname(n),{recursive:!0});const o=a.body;i.update(o);const c=i.digest("hex");if(e&&c!==e)throw new Error(`Checksum mismatch: expected ${e}, got ${c}`);return await s.promises.writeFile(r,o),A.WIN32||await s.promises.chmod(r,493),await s.promises.rename(r,n),c}catch(o){try{await(0,u.safeDelete)(r)}catch{}throw o}}async function M(t,n,e){const a=y(t),r={arch:x.default.arch(),checksum:e,platform:x.default.platform(),timestamp:Date.now(),url:n,version:"1.0.0"};await s.promises.writeFile(a,JSON.stringify(r,null,2))}async function V(t=require("#constants/time").DLX_BINARY_CACHE_TTL){const n=P();if(!(0,s.existsSync)(n))return 0;let e=0;const a=Date.now(),r=await s.promises.readdir(n);for(const i of r){const o=p.default.join(n,i),c=y(o);try{if(!await(0,u.isDir)(o))continue;const f=await(0,u.readJson)(c,{throws:!1});if(!f||typeof f!="object"||Array.isArray(f))continue;const l=f.timestamp;(typeof l=="number"&&l>0?a-l:Number.POSITIVE_INFINITY)>t&&(await(0,u.safeDelete)(o,{force:!0,recursive:!0}),e+=1)}catch{try{(await s.promises.readdir(o)).length||(await(0,u.safeDelete)(o),e+=1)}catch{}}}return e}async function W(t,n,e){const{cacheTtl:a=require("#constants/time").DLX_BINARY_CACHE_TTL,checksum:r,force:i=!1,name:o,spawnOptions:c,url:f}={__proto__:null,...n},l=P(),d=o||`binary-${process.platform}-${x.default.arch()}`,D=Y(f,d),m=p.default.join(l,D),g=(0,T.normalizePath)(p.default.join(m,d));let h=!1,k=r;if(!i&&(0,s.existsSync)(m)&&await J(m,a))try{const E=y(m),w=await(0,u.readJson)(E,{throws:!1});w&&typeof w=="object"&&!Array.isArray(w)&&typeof w.checksum=="string"?k=w.checksum:h=!0}catch{h=!0}else h=!0;h&&(await s.promises.mkdir(m,{recursive:!0}),k=await K(f,g,r),await M(m,f,k||""));const B=A.WIN32&&/\.(?:bat|cmd|ps1)$/i.test(g)?{...c,env:{...c?.env,PATH:`${m}${p.default.delimiter}${process.env.PATH||""}`},shell:!0}:c,v=(0,_.spawn)(g,t,B,e);return{binaryPath:g,downloaded:h,spawnPromise:v}}function P(){return(0,R.getSocketDlxDir)()}async function X(){const t=P();if(!(0,s.existsSync)(t))return[];const n=[],e=Date.now(),a=await s.promises.readdir(t);for(const r of a){const i=p.default.join(t,r);try{if(!await(0,u.isDir)(i))continue;const o=y(i),c=await(0,u.readJson)(o,{throws:!1});if(!c||typeof c!="object"||Array.isArray(c))continue;const l=(await s.promises.readdir(i)).find(d=>!d.startsWith("."));if(l){const d=p.default.join(i,l),D=await s.promises.stat(d),m=c;n.push({age:e-(m.timestamp||0),arch:m.arch||"unknown",checksum:m.checksum||"",name:l,platform:m.platform||"unknown",size:D.size,url:m.url||""})}}catch{}}return n}0&&(module.exports={cleanDlxCache,dlxBinary,getDlxCachePath,listDlxCache});
2
+ var v=Object.create;var b=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var L=Object.getPrototypeOf,z=Object.prototype.hasOwnProperty;var F=(t,n)=>{for(var e in n)b(t,e,{get:n[e],enumerable:!0})},O=(t,n,e,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of H(n))!z.call(t,r)&&r!==e&&b(t,r,{get:()=>n[r],enumerable:!(i=E(n,r))||i.enumerable});return t};var T=(t,n,e)=>(e=t!=null?v(L(t)):{},O(n||!t||!t.__esModule?b(e,"default",{value:t,enumerable:!0}):e,t)),W=t=>O(b({},"__esModule",{value:!0}),t);var G={};F(G,{cleanDlxCache:()=>M,dlxBinary:()=>V,getDlxCachePath:()=>D,listDlxCache:()=>X});module.exports=W(G);var j=require("node:crypto"),s=require("node:fs"),x=T(require("node:os")),p=T(require("node:path")),A=require("#constants/platform"),_=require("./download-lock"),c=require("./fs"),C=require("./objects"),S=require("./path"),B=require("./paths"),R=require("./spawn");function Y(t,n){return(0,j.createHash)("sha256").update(`${t}:${n}`).digest("hex")}function g(t){return p.default.join(t,".dlx-metadata.json")}async function q(t,n){try{const e=g(t);if(!(0,s.existsSync)(e))return!1;const i=await(0,c.readJson)(e,{throws:!1});if(!(0,C.isObjectObject)(i))return!1;const r=Date.now(),a=i.timestamp;return typeof a!="number"||a<=0?!1:r-a<n}catch{return!1}}async function J(t,n,e){await(0,_.downloadWithLock)(t,n,{staleTimeout:1e4,lockTimeout:12e4});const i=await s.promises.readFile(n),r=(0,j.createHash)("sha256");r.update(i);const a=r.digest("hex");if(e&&a!==e)throw await(0,c.safeDelete)(n),new Error(`Checksum mismatch: expected ${e}, got ${a}`);return A.WIN32||await s.promises.chmod(n,493),a}async function K(t,n,e){const i=g(t),r={arch:x.default.arch(),checksum:e,platform:x.default.platform(),timestamp:Date.now(),url:n,version:"1.0.0"};await s.promises.writeFile(i,JSON.stringify(r,null,2))}async function M(t=require("#constants/time").DLX_BINARY_CACHE_TTL){const n=D();if(!(0,s.existsSync)(n))return 0;let e=0;const i=Date.now(),r=await s.promises.readdir(n);for(const a of r){const m=p.default.join(n,a),u=g(m);try{if(!await(0,c.isDir)(m))continue;const f=await(0,c.readJson)(u,{throws:!1});if(!f||typeof f!="object"||Array.isArray(f))continue;const l=f.timestamp;(typeof l=="number"&&l>0?i-l:Number.POSITIVE_INFINITY)>t&&(await(0,c.safeDelete)(m,{force:!0,recursive:!0}),e+=1)}catch{try{(await s.promises.readdir(m)).length||(await(0,c.safeDelete)(m),e+=1)}catch{}}}return e}async function V(t,n,e){const{cacheTtl:i=require("#constants/time").DLX_BINARY_CACHE_TTL,checksum:r,force:a=!1,name:m,spawnOptions:u,url:f}={__proto__:null,...n},l=D(),d=m||`binary-${process.platform}-${x.default.arch()}`,P=Y(f,d),o=p.default.join(l,P),y=(0,S.normalizePath)(p.default.join(o,d));let h=!1,k=r;if(!a&&(0,s.existsSync)(o)&&await q(o,i))try{const N=g(o),w=await(0,c.readJson)(N,{throws:!1});w&&typeof w=="object"&&!Array.isArray(w)&&typeof w.checksum=="string"?k=w.checksum:h=!0}catch{h=!0}else h=!0;h&&(await s.promises.mkdir(o,{recursive:!0}),k=await J(f,y,r),await K(o,f,k||""));const $=A.WIN32&&/\.(?:bat|cmd|ps1)$/i.test(y)?{...u,env:{...u?.env,PATH:`${o}${p.default.delimiter}${process.env.PATH||""}`},shell:!0}:u,I=(0,R.spawn)(y,t,$,e);return{binaryPath:y,downloaded:h,spawnPromise:I}}function D(){return(0,B.getSocketDlxDir)()}async function X(){const t=D();if(!(0,s.existsSync)(t))return[];const n=[],e=Date.now(),i=await s.promises.readdir(t);for(const r of i){const a=p.default.join(t,r);try{if(!await(0,c.isDir)(a))continue;const m=g(a),u=await(0,c.readJson)(m,{throws:!1});if(!u||typeof u!="object"||Array.isArray(u))continue;const l=(await s.promises.readdir(a)).find(d=>!d.startsWith("."));if(l){const d=p.default.join(a,l),P=await s.promises.stat(d),o=u;n.push({age:e-(o.timestamp||0),arch:o.arch||"unknown",checksum:o.checksum||"",name:l,platform:o.platform||"unknown",size:P.size,url:o.url||""})}}catch{}}return n}0&&(module.exports={cleanDlxCache,dlxBinary,getDlxCachePath,listDlxCache});
3
3
  //# sourceMappingURL=dlx-binary.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/dlx-binary.ts"],
4
- "sourcesContent": ["/** @fileoverview DLX binary execution utilities for Socket ecosystem. */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, promises as fs } from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { WIN32 } from '#constants/platform'\n\nimport { isDir, readJson, safeDelete } from './fs'\nimport { httpRequest } from './http-request'\nimport { isObjectObject } from './objects'\nimport { normalizePath } from './path'\nimport { getSocketDlxDir } from './paths'\nimport type { SpawnExtra, SpawnOptions } from './spawn'\nimport { spawn } from './spawn'\n\nexport interface DlxBinaryOptions {\n /** URL to download the binary from. */\n url: string\n /** Optional name for the cached binary (defaults to URL hash). */\n name?: string | undefined\n /** Expected checksum (sha256) for verification. */\n checksum?: string | undefined\n /** Cache TTL in milliseconds (default: 7 days). */\n cacheTtl?: number | undefined\n /** Force re-download even if cached. */\n force?: boolean | undefined\n /** Additional spawn options. */\n spawnOptions?: SpawnOptions | undefined\n}\n\nexport interface DlxBinaryResult {\n /** Path to the cached binary. */\n binaryPath: string\n /** Whether the binary was newly downloaded. */\n downloaded: boolean\n /** The spawn promise for the running process. */\n spawnPromise: ReturnType<typeof spawn>\n}\n\n/**\n * Generate a cache directory name from URL and binary name.\n * Uses SHA256 hash to create content-addressed storage.\n * Includes binary name to prevent collisions when multiple binaries\n * are downloaded from the same URL with different names.\n */\nfunction generateCacheKey(url: string, name: string): string {\n return createHash('sha256').update(`${url}:${name}`).digest('hex')\n}\n\n/**\n * Get metadata file path for a cached binary.\n */\nfunction getMetadataPath(cacheEntryPath: string): string {\n return path.join(cacheEntryPath, '.dlx-metadata.json')\n}\n\n/**\n * Check if a cached binary is still valid.\n */\nasync function isCacheValid(\n cacheEntryPath: string,\n cacheTtl: number,\n): Promise<boolean> {\n try {\n const metaPath = getMetadataPath(cacheEntryPath)\n if (!existsSync(metaPath)) {\n return false\n }\n\n const metadata = await readJson(metaPath, { throws: false })\n if (!isObjectObject(metadata)) {\n return false\n }\n const now = Date.now()\n const timestamp = (metadata as Record<string, unknown>)['timestamp']\n // If timestamp is missing or invalid, cache is invalid\n if (typeof timestamp !== 'number' || timestamp <= 0) {\n return false\n }\n const age = now - timestamp\n\n return age < cacheTtl\n } catch {\n return false\n }\n}\n\n/**\n * Download a file from a URL with integrity checking.\n */\nasync function downloadBinary(\n url: string,\n destPath: string,\n checksum?: string | undefined,\n): Promise<string> {\n const response = await httpRequest(url)\n if (!response.ok) {\n throw new Error(\n `Failed to download binary: ${response.status} ${response.statusText}`,\n )\n }\n\n // Create a temporary file first.\n const tempPath = `${destPath}.download`\n const hasher = createHash('sha256')\n\n try {\n // Ensure directory exists.\n await fs.mkdir(path.dirname(destPath), { recursive: true })\n\n // Get the response as a buffer and compute hash.\n const buffer = response.body\n\n // Compute hash.\n hasher.update(buffer)\n const actualChecksum = hasher.digest('hex')\n\n // Verify checksum if provided.\n if (checksum && actualChecksum !== checksum) {\n throw new Error(\n `Checksum mismatch: expected ${checksum}, got ${actualChecksum}`,\n )\n }\n\n // Write to temp file.\n await fs.writeFile(tempPath, buffer)\n\n // Make executable on POSIX systems.\n if (!WIN32) {\n await fs.chmod(tempPath, 0o755)\n }\n\n // Move temp file to final location.\n await fs.rename(tempPath, destPath)\n\n return actualChecksum\n } catch (e) {\n // Clean up temp file on error.\n try {\n await safeDelete(tempPath)\n } catch {\n // Ignore cleanup errors.\n }\n throw e\n }\n}\n\n/**\n * Write metadata for a cached binary.\n */\nasync function writeMetadata(\n cacheEntryPath: string,\n url: string,\n checksum: string,\n): Promise<void> {\n const metaPath = getMetadataPath(cacheEntryPath)\n const metadata = {\n arch: os.arch(),\n checksum,\n platform: os.platform(),\n timestamp: Date.now(),\n url,\n version: '1.0.0',\n }\n await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2))\n}\n\n/**\n * Clean expired entries from the DLX cache.\n */\nexport async function cleanDlxCache(\n maxAge: number = /*@__INLINE__*/ require('#constants/time').DLX_BINARY_CACHE_TTL,\n): Promise<number> {\n const cacheDir = getDlxCachePath()\n\n if (!existsSync(cacheDir)) {\n return 0\n }\n\n let cleaned = 0\n const now = Date.now()\n const entries = await fs.readdir(cacheDir)\n\n for (const entry of entries) {\n const entryPath = path.join(cacheDir, entry)\n const metaPath = getMetadataPath(entryPath)\n\n try {\n // eslint-disable-next-line no-await-in-loop\n if (!(await isDir(entryPath))) {\n continue\n }\n\n // eslint-disable-next-line no-await-in-loop\n const metadata = await readJson(metaPath, { throws: false })\n if (\n !metadata ||\n typeof metadata !== 'object' ||\n Array.isArray(metadata)\n ) {\n continue\n }\n const timestamp = (metadata as Record<string, unknown>)['timestamp']\n // If timestamp is missing or invalid, treat as expired (age = infinity)\n const age =\n typeof timestamp === 'number' && timestamp > 0\n ? now - timestamp\n : Number.POSITIVE_INFINITY\n\n if (age > maxAge) {\n // Remove entire cache entry directory.\n // eslint-disable-next-line no-await-in-loop\n await safeDelete(entryPath, { force: true, recursive: true })\n cleaned += 1\n }\n } catch {\n // If we can't read metadata, check if directory is empty or corrupted.\n try {\n // eslint-disable-next-line no-await-in-loop\n const contents = await fs.readdir(entryPath)\n if (!contents.length) {\n // Remove empty directory.\n // eslint-disable-next-line no-await-in-loop\n await safeDelete(entryPath)\n cleaned += 1\n }\n } catch {}\n }\n }\n\n return cleaned\n}\n\n/**\n * Download and execute a binary from a URL with caching.\n */\nexport async function dlxBinary(\n args: readonly string[] | string[],\n options?: DlxBinaryOptions | undefined,\n spawnExtra?: SpawnExtra | undefined,\n): Promise<DlxBinaryResult> {\n const {\n cacheTtl = /*@__INLINE__*/ require('#constants/time').DLX_BINARY_CACHE_TTL,\n checksum,\n force = false,\n name,\n spawnOptions,\n url,\n } = { __proto__: null, ...options } as DlxBinaryOptions\n\n // Generate cache paths similar to pnpm/npx structure.\n const cacheDir = getDlxCachePath()\n const binaryName = name || `binary-${process.platform}-${os.arch()}`\n const cacheKey = generateCacheKey(url, binaryName)\n const cacheEntryDir = path.join(cacheDir, cacheKey)\n const binaryPath = normalizePath(path.join(cacheEntryDir, binaryName))\n\n let downloaded = false\n let computedChecksum = checksum\n\n // Check if we need to download.\n if (\n !force &&\n existsSync(cacheEntryDir) &&\n (await isCacheValid(cacheEntryDir, cacheTtl))\n ) {\n // Binary is cached and valid, read the checksum from metadata.\n try {\n const metaPath = getMetadataPath(cacheEntryDir)\n const metadata = await readJson(metaPath, { throws: false })\n if (\n metadata &&\n typeof metadata === 'object' &&\n !Array.isArray(metadata) &&\n typeof (metadata as Record<string, unknown>)['checksum'] === 'string'\n ) {\n computedChecksum = (metadata as Record<string, unknown>)[\n 'checksum'\n ] as string\n } else {\n // If metadata is invalid, re-download.\n downloaded = true\n }\n } catch {\n // If we can't read metadata, re-download.\n downloaded = true\n }\n } else {\n downloaded = true\n }\n\n if (downloaded) {\n // Ensure cache directory exists.\n await fs.mkdir(cacheEntryDir, { recursive: true })\n\n // Download the binary.\n computedChecksum = await downloadBinary(url, binaryPath, checksum)\n await writeMetadata(cacheEntryDir, url, computedChecksum || '')\n }\n\n // Execute the binary.\n // On Windows, script files (.bat, .cmd, .ps1) require shell: true because\n // they are not executable on their own and must be run through cmd.exe.\n // Note: .exe files are actual binaries and don't need shell mode.\n const needsShell = WIN32 && /\\.(?:bat|cmd|ps1)$/i.test(binaryPath)\n // Windows cmd.exe PATH resolution behavior:\n // When shell: true on Windows with .cmd/.bat/.ps1 files, spawn will automatically\n // strip the full path down to just the basename without extension (e.g.,\n // C:\\cache\\test.cmd becomes just \"test\"). Windows cmd.exe then searches for \"test\"\n // in directories listed in PATH, trying each extension from PATHEXT environment\n // variable (.COM, .EXE, .BAT, .CMD, etc.) until it finds a match.\n //\n // Since our binaries are downloaded to a custom cache directory that's not in PATH\n // (unlike system package managers like npm/pnpm/yarn which are already in PATH),\n // we must prepend the cache directory to PATH so cmd.exe can locate the binary.\n //\n // This approach is consistent with how other tools handle Windows command execution:\n // - npm's promise-spawn: uses which.sync() to find commands in PATH\n // - cross-spawn: spawns cmd.exe with escaped arguments\n // - Node.js spawn with shell: true: delegates to cmd.exe which uses PATH\n const finalSpawnOptions = needsShell\n ? {\n ...spawnOptions,\n env: {\n ...spawnOptions?.env,\n PATH: `${cacheEntryDir}${path.delimiter}${process.env['PATH'] || ''}`,\n },\n shell: true,\n }\n : spawnOptions\n const spawnPromise = spawn(binaryPath, args, finalSpawnOptions, spawnExtra)\n\n return {\n binaryPath,\n downloaded,\n spawnPromise,\n }\n}\n\n/**\n * Get the DLX binary cache directory path.\n * Returns normalized path for cross-platform compatibility.\n * Uses same directory as dlx-package for unified DLX storage.\n */\nexport function getDlxCachePath(): string {\n return getSocketDlxDir()\n}\n\n/**\n * Get information about cached binaries.\n */\nexport async function listDlxCache(): Promise<\n Array<{\n age: number\n arch: string\n checksum: string\n name: string\n platform: string\n size: number\n url: string\n }>\n> {\n const cacheDir = getDlxCachePath()\n\n if (!existsSync(cacheDir)) {\n return []\n }\n\n const results = []\n const now = Date.now()\n const entries = await fs.readdir(cacheDir)\n\n for (const entry of entries) {\n const entryPath = path.join(cacheDir, entry)\n try {\n // eslint-disable-next-line no-await-in-loop\n if (!(await isDir(entryPath))) {\n continue\n }\n\n const metaPath = getMetadataPath(entryPath)\n // eslint-disable-next-line no-await-in-loop\n const metadata = await readJson(metaPath, { throws: false })\n if (\n !metadata ||\n typeof metadata !== 'object' ||\n Array.isArray(metadata)\n ) {\n continue\n }\n\n // Find the binary file in the directory.\n // eslint-disable-next-line no-await-in-loop\n const files = await fs.readdir(entryPath)\n const binaryFile = files.find(f => !f.startsWith('.'))\n\n if (binaryFile) {\n const binaryPath = path.join(entryPath, binaryFile)\n // eslint-disable-next-line no-await-in-loop\n const binaryStats = await fs.stat(binaryPath)\n\n const metaObj = metadata as Record<string, unknown>\n results.push({\n age: now - ((metaObj['timestamp'] as number) || 0),\n arch: (metaObj['arch'] as string) || 'unknown',\n checksum: (metaObj['checksum'] as string) || '',\n name: binaryFile,\n platform: (metaObj['platform'] as string) || 'unknown',\n size: binaryStats.size,\n url: (metaObj['url'] as string) || '',\n })\n }\n } catch {}\n }\n\n return results\n}\n"],
5
- "mappings": "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",
6
- "names": ["dlx_binary_exports", "__export", "cleanDlxCache", "dlxBinary", "getDlxCachePath", "listDlxCache", "__toCommonJS", "import_node_crypto", "import_node_fs", "import_node_os", "import_node_path", "import_platform", "import_fs", "import_http_request", "import_objects", "import_path", "import_paths", "import_spawn", "generateCacheKey", "url", "name", "getMetadataPath", "cacheEntryPath", "path", "isCacheValid", "cacheTtl", "metaPath", "metadata", "now", "timestamp", "downloadBinary", "destPath", "checksum", "response", "tempPath", "hasher", "fs", "buffer", "actualChecksum", "e", "writeMetadata", "os", "maxAge", "cacheDir", "cleaned", "entries", "entry", "entryPath", "args", "options", "spawnExtra", "force", "spawnOptions", "binaryName", "cacheKey", "cacheEntryDir", "binaryPath", "downloaded", "computedChecksum", "finalSpawnOptions", "spawnPromise", "results", "binaryFile", "f", "binaryStats", "metaObj"]
4
+ "sourcesContent": ["/** @fileoverview DLX binary execution utilities for Socket ecosystem. */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, promises as fs } from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { WIN32 } from '#constants/platform'\n\nimport { downloadWithLock } from './download-lock'\nimport { isDir, readJson, safeDelete } from './fs'\nimport { isObjectObject } from './objects'\nimport { normalizePath } from './path'\nimport { getSocketDlxDir } from './paths'\nimport type { SpawnExtra, SpawnOptions } from './spawn'\nimport { spawn } from './spawn'\n\nexport interface DlxBinaryOptions {\n /** URL to download the binary from. */\n url: string\n /** Optional name for the cached binary (defaults to URL hash). */\n name?: string | undefined\n /** Expected checksum (sha256) for verification. */\n checksum?: string | undefined\n /** Cache TTL in milliseconds (default: 7 days). */\n cacheTtl?: number | undefined\n /** Force re-download even if cached. */\n force?: boolean | undefined\n /** Additional spawn options. */\n spawnOptions?: SpawnOptions | undefined\n}\n\nexport interface DlxBinaryResult {\n /** Path to the cached binary. */\n binaryPath: string\n /** Whether the binary was newly downloaded. */\n downloaded: boolean\n /** The spawn promise for the running process. */\n spawnPromise: ReturnType<typeof spawn>\n}\n\n/**\n * Generate a cache directory name from URL and binary name.\n * Uses SHA256 hash to create content-addressed storage.\n * Includes binary name to prevent collisions when multiple binaries\n * are downloaded from the same URL with different names.\n */\nfunction generateCacheKey(url: string, name: string): string {\n return createHash('sha256').update(`${url}:${name}`).digest('hex')\n}\n\n/**\n * Get metadata file path for a cached binary.\n */\nfunction getMetadataPath(cacheEntryPath: string): string {\n return path.join(cacheEntryPath, '.dlx-metadata.json')\n}\n\n/**\n * Check if a cached binary is still valid.\n */\nasync function isCacheValid(\n cacheEntryPath: string,\n cacheTtl: number,\n): Promise<boolean> {\n try {\n const metaPath = getMetadataPath(cacheEntryPath)\n if (!existsSync(metaPath)) {\n return false\n }\n\n const metadata = await readJson(metaPath, { throws: false })\n if (!isObjectObject(metadata)) {\n return false\n }\n const now = Date.now()\n const timestamp = (metadata as Record<string, unknown>)['timestamp']\n // If timestamp is missing or invalid, cache is invalid\n if (typeof timestamp !== 'number' || timestamp <= 0) {\n return false\n }\n const age = now - timestamp\n\n return age < cacheTtl\n } catch {\n return false\n }\n}\n\n/**\n * Download a file from a URL with integrity checking and concurrent download protection.\n * Uses downloadWithLock to prevent multiple processes from downloading the same binary simultaneously.\n */\nasync function downloadBinary(\n url: string,\n destPath: string,\n checksum?: string | undefined,\n): Promise<string> {\n // Use downloadWithLock to handle concurrent download protection.\n // This prevents corruption when multiple processes try to download the same binary.\n await downloadWithLock(url, destPath, {\n // Align with npm's npx locking strategy.\n staleTimeout: 10_000,\n // Allow up to 2 minutes for large binary downloads.\n lockTimeout: 120_000,\n })\n\n // Compute checksum of downloaded file.\n const fileBuffer = await fs.readFile(destPath)\n const hasher = createHash('sha256')\n hasher.update(fileBuffer)\n const actualChecksum = hasher.digest('hex')\n\n // Verify checksum if provided.\n if (checksum && actualChecksum !== checksum) {\n // Clean up invalid file.\n await safeDelete(destPath)\n throw new Error(\n `Checksum mismatch: expected ${checksum}, got ${actualChecksum}`,\n )\n }\n\n // Make executable on POSIX systems.\n if (!WIN32) {\n await fs.chmod(destPath, 0o755)\n }\n\n return actualChecksum\n}\n\n/**\n * Write metadata for a cached binary.\n */\nasync function writeMetadata(\n cacheEntryPath: string,\n url: string,\n checksum: string,\n): Promise<void> {\n const metaPath = getMetadataPath(cacheEntryPath)\n const metadata = {\n arch: os.arch(),\n checksum,\n platform: os.platform(),\n timestamp: Date.now(),\n url,\n version: '1.0.0',\n }\n await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2))\n}\n\n/**\n * Clean expired entries from the DLX cache.\n */\nexport async function cleanDlxCache(\n maxAge: number = /*@__INLINE__*/ require('#constants/time').DLX_BINARY_CACHE_TTL,\n): Promise<number> {\n const cacheDir = getDlxCachePath()\n\n if (!existsSync(cacheDir)) {\n return 0\n }\n\n let cleaned = 0\n const now = Date.now()\n const entries = await fs.readdir(cacheDir)\n\n for (const entry of entries) {\n const entryPath = path.join(cacheDir, entry)\n const metaPath = getMetadataPath(entryPath)\n\n try {\n // eslint-disable-next-line no-await-in-loop\n if (!(await isDir(entryPath))) {\n continue\n }\n\n // eslint-disable-next-line no-await-in-loop\n const metadata = await readJson(metaPath, { throws: false })\n if (\n !metadata ||\n typeof metadata !== 'object' ||\n Array.isArray(metadata)\n ) {\n continue\n }\n const timestamp = (metadata as Record<string, unknown>)['timestamp']\n // If timestamp is missing or invalid, treat as expired (age = infinity)\n const age =\n typeof timestamp === 'number' && timestamp > 0\n ? now - timestamp\n : Number.POSITIVE_INFINITY\n\n if (age > maxAge) {\n // Remove entire cache entry directory.\n // eslint-disable-next-line no-await-in-loop\n await safeDelete(entryPath, { force: true, recursive: true })\n cleaned += 1\n }\n } catch {\n // If we can't read metadata, check if directory is empty or corrupted.\n try {\n // eslint-disable-next-line no-await-in-loop\n const contents = await fs.readdir(entryPath)\n if (!contents.length) {\n // Remove empty directory.\n // eslint-disable-next-line no-await-in-loop\n await safeDelete(entryPath)\n cleaned += 1\n }\n } catch {}\n }\n }\n\n return cleaned\n}\n\n/**\n * Download and execute a binary from a URL with caching.\n */\nexport async function dlxBinary(\n args: readonly string[] | string[],\n options?: DlxBinaryOptions | undefined,\n spawnExtra?: SpawnExtra | undefined,\n): Promise<DlxBinaryResult> {\n const {\n cacheTtl = /*@__INLINE__*/ require('#constants/time').DLX_BINARY_CACHE_TTL,\n checksum,\n force = false,\n name,\n spawnOptions,\n url,\n } = { __proto__: null, ...options } as DlxBinaryOptions\n\n // Generate cache paths similar to pnpm/npx structure.\n const cacheDir = getDlxCachePath()\n const binaryName = name || `binary-${process.platform}-${os.arch()}`\n const cacheKey = generateCacheKey(url, binaryName)\n const cacheEntryDir = path.join(cacheDir, cacheKey)\n const binaryPath = normalizePath(path.join(cacheEntryDir, binaryName))\n\n let downloaded = false\n let computedChecksum = checksum\n\n // Check if we need to download.\n if (\n !force &&\n existsSync(cacheEntryDir) &&\n (await isCacheValid(cacheEntryDir, cacheTtl))\n ) {\n // Binary is cached and valid, read the checksum from metadata.\n try {\n const metaPath = getMetadataPath(cacheEntryDir)\n const metadata = await readJson(metaPath, { throws: false })\n if (\n metadata &&\n typeof metadata === 'object' &&\n !Array.isArray(metadata) &&\n typeof (metadata as Record<string, unknown>)['checksum'] === 'string'\n ) {\n computedChecksum = (metadata as Record<string, unknown>)[\n 'checksum'\n ] as string\n } else {\n // If metadata is invalid, re-download.\n downloaded = true\n }\n } catch {\n // If we can't read metadata, re-download.\n downloaded = true\n }\n } else {\n downloaded = true\n }\n\n if (downloaded) {\n // Ensure cache directory exists.\n await fs.mkdir(cacheEntryDir, { recursive: true })\n\n // Download the binary.\n computedChecksum = await downloadBinary(url, binaryPath, checksum)\n await writeMetadata(cacheEntryDir, url, computedChecksum || '')\n }\n\n // Execute the binary.\n // On Windows, script files (.bat, .cmd, .ps1) require shell: true because\n // they are not executable on their own and must be run through cmd.exe.\n // Note: .exe files are actual binaries and don't need shell mode.\n const needsShell = WIN32 && /\\.(?:bat|cmd|ps1)$/i.test(binaryPath)\n // Windows cmd.exe PATH resolution behavior:\n // When shell: true on Windows with .cmd/.bat/.ps1 files, spawn will automatically\n // strip the full path down to just the basename without extension (e.g.,\n // C:\\cache\\test.cmd becomes just \"test\"). Windows cmd.exe then searches for \"test\"\n // in directories listed in PATH, trying each extension from PATHEXT environment\n // variable (.COM, .EXE, .BAT, .CMD, etc.) until it finds a match.\n //\n // Since our binaries are downloaded to a custom cache directory that's not in PATH\n // (unlike system package managers like npm/pnpm/yarn which are already in PATH),\n // we must prepend the cache directory to PATH so cmd.exe can locate the binary.\n //\n // This approach is consistent with how other tools handle Windows command execution:\n // - npm's promise-spawn: uses which.sync() to find commands in PATH\n // - cross-spawn: spawns cmd.exe with escaped arguments\n // - Node.js spawn with shell: true: delegates to cmd.exe which uses PATH\n const finalSpawnOptions = needsShell\n ? {\n ...spawnOptions,\n env: {\n ...spawnOptions?.env,\n PATH: `${cacheEntryDir}${path.delimiter}${process.env['PATH'] || ''}`,\n },\n shell: true,\n }\n : spawnOptions\n const spawnPromise = spawn(binaryPath, args, finalSpawnOptions, spawnExtra)\n\n return {\n binaryPath,\n downloaded,\n spawnPromise,\n }\n}\n\n/**\n * Get the DLX binary cache directory path.\n * Returns normalized path for cross-platform compatibility.\n * Uses same directory as dlx-package for unified DLX storage.\n */\nexport function getDlxCachePath(): string {\n return getSocketDlxDir()\n}\n\n/**\n * Get information about cached binaries.\n */\nexport async function listDlxCache(): Promise<\n Array<{\n age: number\n arch: string\n checksum: string\n name: string\n platform: string\n size: number\n url: string\n }>\n> {\n const cacheDir = getDlxCachePath()\n\n if (!existsSync(cacheDir)) {\n return []\n }\n\n const results = []\n const now = Date.now()\n const entries = await fs.readdir(cacheDir)\n\n for (const entry of entries) {\n const entryPath = path.join(cacheDir, entry)\n try {\n // eslint-disable-next-line no-await-in-loop\n if (!(await isDir(entryPath))) {\n continue\n }\n\n const metaPath = getMetadataPath(entryPath)\n // eslint-disable-next-line no-await-in-loop\n const metadata = await readJson(metaPath, { throws: false })\n if (\n !metadata ||\n typeof metadata !== 'object' ||\n Array.isArray(metadata)\n ) {\n continue\n }\n\n // Find the binary file in the directory.\n // eslint-disable-next-line no-await-in-loop\n const files = await fs.readdir(entryPath)\n const binaryFile = files.find(f => !f.startsWith('.'))\n\n if (binaryFile) {\n const binaryPath = path.join(entryPath, binaryFile)\n // eslint-disable-next-line no-await-in-loop\n const binaryStats = await fs.stat(binaryPath)\n\n const metaObj = metadata as Record<string, unknown>\n results.push({\n age: now - ((metaObj['timestamp'] as number) || 0),\n arch: (metaObj['arch'] as string) || 'unknown',\n checksum: (metaObj['checksum'] as string) || '',\n name: binaryFile,\n platform: (metaObj['platform'] as string) || 'unknown',\n size: binaryStats.size,\n url: (metaObj['url'] as string) || '',\n })\n }\n } catch {}\n }\n\n return results\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["dlx_binary_exports", "__export", "cleanDlxCache", "dlxBinary", "getDlxCachePath", "listDlxCache", "__toCommonJS", "import_node_crypto", "import_node_fs", "import_node_os", "import_node_path", "import_platform", "import_download_lock", "import_fs", "import_objects", "import_path", "import_paths", "import_spawn", "generateCacheKey", "url", "name", "getMetadataPath", "cacheEntryPath", "path", "isCacheValid", "cacheTtl", "metaPath", "metadata", "now", "timestamp", "downloadBinary", "destPath", "checksum", "fileBuffer", "fs", "hasher", "actualChecksum", "writeMetadata", "os", "maxAge", "cacheDir", "cleaned", "entries", "entry", "entryPath", "args", "options", "spawnExtra", "force", "spawnOptions", "binaryName", "cacheKey", "cacheEntryDir", "binaryPath", "downloaded", "computedChecksum", "finalSpawnOptions", "spawnPromise", "results", "binaryFile", "f", "binaryStats", "metaObj"]
7
7
  }
@@ -22,7 +22,8 @@ export interface DownloadWithLockOptions extends HttpDownloadOptions {
22
22
  pollInterval?: number | undefined;
23
23
  /**
24
24
  * Maximum age of a lock before it's considered stale in milliseconds.
25
- * @default 300000 (5 minutes)
25
+ * Aligned with npm's npx locking strategy (5-10 seconds).
26
+ * @default 10000 (10 seconds)
26
27
  */
27
28
  staleTimeout?: number | undefined;
28
29
  }
@@ -1,3 +1,3 @@
1
1
  /* Socket Lib - Built with esbuild */
2
- var d=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var y=(o,t)=>{for(var n in t)d(o,n,{get:t[n],enumerable:!0})},b=(o,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of L(t))!h.call(o,r)&&r!==n&&d(o,r,{get:()=>t[r],enumerable:!(i=T(t,r))||i.enumerable});return o};var I=o=>b(d({},"__esModule",{value:!0}),o);var E={};y(E,{downloadWithLock:()=>z});module.exports=I(E);var m=require("node:fs"),e=require("node:fs/promises"),l=require("node:path"),D=require("./http-request");function O(o,t){const n=t||`${(0,l.dirname)(o)}/.locks`,i=`${o.replace(/[^\w.-]/g,"_")}.lock`;return(0,l.join)(n,i)}function x(o,t){if(Date.now()-o.startTime>t)return!0;try{return process.kill(o.pid,0),!1}catch{return!0}}async function _(o,t,n){const{lockTimeout:i,locksDir:r,pollInterval:u,staleTimeout:f}=n,a=O(o,r),w=(0,l.dirname)(a);await(0,e.mkdir)(w,{recursive:!0});const c=Date.now();for(;;)try{if((0,m.existsSync)(a)){const p=await(0,e.readFile)(a,"utf8"),k=JSON.parse(p);if(x(k,f))await(0,e.rm)(a,{force:!0});else{if(Date.now()-c>i)throw new Error(`Lock acquisition timed out after ${i}ms (held by PID ${k.pid})`);await new Promise(g=>setTimeout(g,u));continue}}const s={pid:process.pid,startTime:Date.now(),url:t};return await(0,e.writeFile)(a,JSON.stringify(s,null,2),{flag:"wx"}),a}catch(s){if(s.code==="EEXIST"){if(Date.now()-c>i)throw new Error(`Lock acquisition timed out after ${i}ms`);await new Promise(p=>setTimeout(p,u));continue}throw s}}async function v(o){try{await(0,e.rm)(o,{force:!0})}catch{}}async function z(o,t,n){const{lockTimeout:i=6e4,locksDir:r,pollInterval:u=1e3,staleTimeout:f=3e5,...a}={__proto__:null,...n};if((0,m.existsSync)(t)){const c=await(0,e.stat)(t).catch(()=>null);if(c&&c.size>0)return{path:t,size:c.size}}const w=await _(t,o,{lockTimeout:i,locksDir:r,pollInterval:u,staleTimeout:f});try{if((0,m.existsSync)(t)){const s=await(0,e.stat)(t).catch(()=>null);if(s&&s.size>0)return{path:t,size:s.size}}return await(0,D.httpDownload)(o,t,a)}finally{await v(w)}}0&&(module.exports={downloadWithLock});
2
+ var d=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var y=(o,t)=>{for(var n in t)d(o,n,{get:t[n],enumerable:!0})},b=(o,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of L(t))!h.call(o,r)&&r!==n&&d(o,r,{get:()=>t[r],enumerable:!(i=T(t,r))||i.enumerable});return o};var I=o=>b(d({},"__esModule",{value:!0}),o);var E={};y(E,{downloadWithLock:()=>z});module.exports=I(E);var m=require("node:fs"),e=require("node:fs/promises"),l=require("node:path"),D=require("./http-request");function O(o,t){const n=t||`${(0,l.dirname)(o)}/.locks`,i=`${o.replace(/[^\w.-]/g,"_")}.lock`;return(0,l.join)(n,i)}function x(o,t){if(Date.now()-o.startTime>t)return!0;try{return process.kill(o.pid,0),!1}catch{return!0}}async function _(o,t,n){const{lockTimeout:i,locksDir:r,pollInterval:u,staleTimeout:f}=n,a=O(o,r),w=(0,l.dirname)(a);await(0,e.mkdir)(w,{recursive:!0});const c=Date.now();for(;;)try{if((0,m.existsSync)(a)){const p=await(0,e.readFile)(a,"utf8"),k=JSON.parse(p);if(x(k,f))await(0,e.rm)(a,{force:!0});else{if(Date.now()-c>i)throw new Error(`Lock acquisition timed out after ${i}ms (held by PID ${k.pid})`);await new Promise(g=>setTimeout(g,u));continue}}const s={pid:process.pid,startTime:Date.now(),url:t};return await(0,e.writeFile)(a,JSON.stringify(s,null,2),{flag:"wx"}),a}catch(s){if(s.code==="EEXIST"){if(Date.now()-c>i)throw new Error(`Lock acquisition timed out after ${i}ms`);await new Promise(p=>setTimeout(p,u));continue}throw s}}async function v(o){try{await(0,e.rm)(o,{force:!0})}catch{}}async function z(o,t,n){const{lockTimeout:i=6e4,locksDir:r,pollInterval:u=1e3,staleTimeout:f=1e4,...a}={__proto__:null,...n};if((0,m.existsSync)(t)){const c=await(0,e.stat)(t).catch(()=>null);if(c&&c.size>0)return{path:t,size:c.size}}const w=await _(t,o,{lockTimeout:i,locksDir:r,pollInterval:u,staleTimeout:f});try{if((0,m.existsSync)(t)){const s=await(0,e.stat)(t).catch(()=>null);if(s&&s.size>0)return{path:t,size:s.size}}return await(0,D.httpDownload)(o,t,a)}finally{await v(w)}}0&&(module.exports={downloadWithLock});
3
3
  //# sourceMappingURL=download-lock.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/download-lock.ts"],
4
- "sourcesContent": ["/** @fileoverview Download locking utilities to prevent concurrent downloads of the same resource. Uses file-based locking for cross-process synchronization. */\n\nimport { existsSync } from 'node:fs'\nimport { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { HttpDownloadOptions, HttpDownloadResult } from './http-request'\nimport { httpDownload } from './http-request'\n\nexport interface DownloadLockInfo {\n pid: number\n startTime: number\n url: string\n}\n\nexport interface DownloadWithLockOptions extends HttpDownloadOptions {\n /**\n * Maximum time to wait for lock acquisition in milliseconds.\n * @default 60000 (1 minute)\n */\n lockTimeout?: number | undefined\n /**\n * Directory where lock files are stored.\n * @default '<destPath>.locks'\n */\n locksDir?: string | undefined\n /**\n * Interval for checking stale locks in milliseconds.\n * @default 1000 (1 second)\n */\n pollInterval?: number | undefined\n /**\n * Maximum age of a lock before it's considered stale in milliseconds.\n * @default 300000 (5 minutes)\n */\n staleTimeout?: number | undefined\n}\n\n/**\n * Get the path to the lock file for a destination path.\n */\nfunction getLockFilePath(destPath: string, locksDir?: string): string {\n const dir = locksDir || `${dirname(destPath)}/.locks`\n const filename = `${destPath.replace(/[^\\w.-]/g, '_')}.lock`\n return join(dir, filename)\n}\n\n/**\n * Check if a lock is stale (process no longer exists or too old).\n */\nfunction isLockStale(\n lockInfo: DownloadLockInfo,\n staleTimeout: number,\n): boolean {\n // Check if lock is too old\n const age = Date.now() - lockInfo.startTime\n if (age > staleTimeout) {\n return true\n }\n\n // Check if process still exists (Node.js specific)\n try {\n // process.kill(pid, 0) doesn't actually kill the process\n // It just checks if the process exists\n process.kill(lockInfo.pid, 0)\n return false\n } catch {\n // Process doesn't exist\n return true\n }\n}\n\n/**\n * Acquire a lock for downloading to a destination path.\n * @throws {Error} When lock cannot be acquired within timeout.\n */\nasync function acquireLock(\n destPath: string,\n url: string,\n options: {\n lockTimeout: number\n locksDir?: string | undefined\n pollInterval: number\n staleTimeout: number\n },\n): Promise<string> {\n const { lockTimeout, locksDir, pollInterval, staleTimeout } = options\n const lockPath = getLockFilePath(destPath, locksDir)\n const lockDir = dirname(lockPath)\n\n // Ensure lock directory exists\n await mkdir(lockDir, { recursive: true })\n\n const startTime = Date.now()\n\n while (true) {\n try {\n // Try to read existing lock\n if (existsSync(lockPath)) {\n // eslint-disable-next-line no-await-in-loop\n const lockContent = await readFile(lockPath, 'utf8')\n const lockInfo: DownloadLockInfo = JSON.parse(lockContent)\n\n // Check if lock is stale\n if (isLockStale(lockInfo, staleTimeout)) {\n // Remove stale lock\n // eslint-disable-next-line no-await-in-loop\n await rm(lockPath, { force: true })\n } else {\n // Lock is valid, check timeout\n if (Date.now() - startTime > lockTimeout) {\n throw new Error(\n `Lock acquisition timed out after ${lockTimeout}ms (held by PID ${lockInfo.pid})`,\n )\n }\n\n // Wait and retry\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, pollInterval))\n continue\n }\n }\n\n // Try to create lock file\n const lockInfo: DownloadLockInfo = {\n pid: process.pid,\n startTime: Date.now(),\n url,\n }\n\n // eslint-disable-next-line no-await-in-loop\n await writeFile(lockPath, JSON.stringify(lockInfo, null, 2), {\n // Use 'wx' flag to fail if file exists (atomic operation)\n flag: 'wx',\n })\n\n // Successfully acquired lock\n return lockPath\n } catch (e) {\n // If file already exists, another process created it first\n if ((e as NodeJS.ErrnoException).code === 'EEXIST') {\n if (Date.now() - startTime > lockTimeout) {\n throw new Error(`Lock acquisition timed out after ${lockTimeout}ms`)\n }\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, pollInterval))\n continue\n }\n\n // Other error\n throw e\n }\n }\n}\n\n/**\n * Release a lock by removing the lock file.\n */\nasync function releaseLock(lockPath: string): Promise<void> {\n try {\n await rm(lockPath, { force: true })\n } catch {\n // Ignore errors when releasing lock\n }\n}\n\n/**\n * Download a file with locking to prevent concurrent downloads of the same resource.\n * If another process is already downloading to the same destination, this will wait\n * for the download to complete (up to lockTimeout) before proceeding.\n *\n * @throws {Error} When download fails or lock cannot be acquired.\n *\n * @example\n * ```typescript\n * const result = await downloadWithLock(\n * 'https://example.com/file.tar.gz',\n * '/tmp/downloads/file.tar.gz',\n * {\n * retries: 3,\n * lockTimeout: 60000, // Wait up to 1 minute for other downloads\n * }\n * )\n * ```\n */\nexport async function downloadWithLock(\n url: string,\n destPath: string,\n options?: DownloadWithLockOptions | undefined,\n): Promise<HttpDownloadResult> {\n const {\n lockTimeout = 60_000,\n locksDir,\n pollInterval = 1000,\n staleTimeout = 300_000,\n ...downloadOptions\n } = { __proto__: null, ...options } as DownloadWithLockOptions\n\n // If file already exists and has content, return immediately\n if (existsSync(destPath)) {\n const statResult = await stat(destPath).catch(() => null)\n if (statResult && statResult.size > 0) {\n return {\n path: destPath,\n size: statResult.size,\n }\n }\n }\n\n // Acquire lock\n const lockPath = await acquireLock(destPath, url, {\n lockTimeout,\n locksDir,\n pollInterval,\n staleTimeout,\n })\n\n try {\n // Check again if file was created while we were waiting for lock\n if (existsSync(destPath)) {\n const statResult = await stat(destPath).catch(() => null)\n if (statResult && statResult.size > 0) {\n return {\n path: destPath,\n size: statResult.size,\n }\n }\n }\n\n // Perform download\n const result = await httpDownload(url, destPath, downloadOptions)\n\n return result\n } finally {\n // Always release lock\n await releaseLock(lockPath)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/** @fileoverview Download locking utilities to prevent concurrent downloads of the same resource. Uses file-based locking for cross-process synchronization. */\n\nimport { existsSync } from 'node:fs'\nimport { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { HttpDownloadOptions, HttpDownloadResult } from './http-request'\nimport { httpDownload } from './http-request'\n\nexport interface DownloadLockInfo {\n pid: number\n startTime: number\n url: string\n}\n\nexport interface DownloadWithLockOptions extends HttpDownloadOptions {\n /**\n * Maximum time to wait for lock acquisition in milliseconds.\n * @default 60000 (1 minute)\n */\n lockTimeout?: number | undefined\n /**\n * Directory where lock files are stored.\n * @default '<destPath>.locks'\n */\n locksDir?: string | undefined\n /**\n * Interval for checking stale locks in milliseconds.\n * @default 1000 (1 second)\n */\n pollInterval?: number | undefined\n /**\n * Maximum age of a lock before it's considered stale in milliseconds.\n * Aligned with npm's npx locking strategy (5-10 seconds).\n * @default 10000 (10 seconds)\n */\n staleTimeout?: number | undefined\n}\n\n/**\n * Get the path to the lock file for a destination path.\n */\nfunction getLockFilePath(destPath: string, locksDir?: string): string {\n const dir = locksDir || `${dirname(destPath)}/.locks`\n const filename = `${destPath.replace(/[^\\w.-]/g, '_')}.lock`\n return join(dir, filename)\n}\n\n/**\n * Check if a lock is stale (process no longer exists or too old).\n */\nfunction isLockStale(\n lockInfo: DownloadLockInfo,\n staleTimeout: number,\n): boolean {\n // Check if lock is too old\n const age = Date.now() - lockInfo.startTime\n if (age > staleTimeout) {\n return true\n }\n\n // Check if process still exists (Node.js specific)\n try {\n // process.kill(pid, 0) doesn't actually kill the process\n // It just checks if the process exists\n process.kill(lockInfo.pid, 0)\n return false\n } catch {\n // Process doesn't exist\n return true\n }\n}\n\n/**\n * Acquire a lock for downloading to a destination path.\n * @throws {Error} When lock cannot be acquired within timeout.\n */\nasync function acquireLock(\n destPath: string,\n url: string,\n options: {\n lockTimeout: number\n locksDir?: string | undefined\n pollInterval: number\n staleTimeout: number\n },\n): Promise<string> {\n const { lockTimeout, locksDir, pollInterval, staleTimeout } = options\n const lockPath = getLockFilePath(destPath, locksDir)\n const lockDir = dirname(lockPath)\n\n // Ensure lock directory exists\n await mkdir(lockDir, { recursive: true })\n\n const startTime = Date.now()\n\n while (true) {\n try {\n // Try to read existing lock\n if (existsSync(lockPath)) {\n // eslint-disable-next-line no-await-in-loop\n const lockContent = await readFile(lockPath, 'utf8')\n const lockInfo: DownloadLockInfo = JSON.parse(lockContent)\n\n // Check if lock is stale\n if (isLockStale(lockInfo, staleTimeout)) {\n // Remove stale lock\n // eslint-disable-next-line no-await-in-loop\n await rm(lockPath, { force: true })\n } else {\n // Lock is valid, check timeout\n if (Date.now() - startTime > lockTimeout) {\n throw new Error(\n `Lock acquisition timed out after ${lockTimeout}ms (held by PID ${lockInfo.pid})`,\n )\n }\n\n // Wait and retry\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, pollInterval))\n continue\n }\n }\n\n // Try to create lock file\n const lockInfo: DownloadLockInfo = {\n pid: process.pid,\n startTime: Date.now(),\n url,\n }\n\n // eslint-disable-next-line no-await-in-loop\n await writeFile(lockPath, JSON.stringify(lockInfo, null, 2), {\n // Use 'wx' flag to fail if file exists (atomic operation)\n flag: 'wx',\n })\n\n // Successfully acquired lock\n return lockPath\n } catch (e) {\n // If file already exists, another process created it first\n if ((e as NodeJS.ErrnoException).code === 'EEXIST') {\n if (Date.now() - startTime > lockTimeout) {\n throw new Error(`Lock acquisition timed out after ${lockTimeout}ms`)\n }\n // eslint-disable-next-line no-await-in-loop\n await new Promise(resolve => setTimeout(resolve, pollInterval))\n continue\n }\n\n // Other error\n throw e\n }\n }\n}\n\n/**\n * Release a lock by removing the lock file.\n */\nasync function releaseLock(lockPath: string): Promise<void> {\n try {\n await rm(lockPath, { force: true })\n } catch {\n // Ignore errors when releasing lock\n }\n}\n\n/**\n * Download a file with locking to prevent concurrent downloads of the same resource.\n * If another process is already downloading to the same destination, this will wait\n * for the download to complete (up to lockTimeout) before proceeding.\n *\n * @throws {Error} When download fails or lock cannot be acquired.\n *\n * @example\n * ```typescript\n * const result = await downloadWithLock(\n * 'https://example.com/file.tar.gz',\n * '/tmp/downloads/file.tar.gz',\n * {\n * retries: 3,\n * lockTimeout: 60000, // Wait up to 1 minute for other downloads\n * }\n * )\n * ```\n */\nexport async function downloadWithLock(\n url: string,\n destPath: string,\n options?: DownloadWithLockOptions | undefined,\n): Promise<HttpDownloadResult> {\n const {\n lockTimeout = 60_000,\n locksDir,\n pollInterval = 1000,\n // Aligned with npm's npx locking (5-10s range).\n staleTimeout = 10_000,\n ...downloadOptions\n } = { __proto__: null, ...options } as DownloadWithLockOptions\n\n // If file already exists and has content, return immediately\n if (existsSync(destPath)) {\n const statResult = await stat(destPath).catch(() => null)\n if (statResult && statResult.size > 0) {\n return {\n path: destPath,\n size: statResult.size,\n }\n }\n }\n\n // Acquire lock\n const lockPath = await acquireLock(destPath, url, {\n lockTimeout,\n locksDir,\n pollInterval,\n staleTimeout,\n })\n\n try {\n // Check again if file was created while we were waiting for lock\n if (existsSync(destPath)) {\n const statResult = await stat(destPath).catch(() => null)\n if (statResult && statResult.size > 0) {\n return {\n path: destPath,\n size: statResult.size,\n }\n }\n }\n\n // Perform download\n const result = await httpDownload(url, destPath, downloadOptions)\n\n return result\n } finally {\n // Always release lock\n await releaseLock(lockPath)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["download_lock_exports", "__export", "downloadWithLock", "__toCommonJS", "import_node_fs", "import_promises", "import_node_path", "import_http_request", "getLockFilePath", "destPath", "locksDir", "dir", "filename", "isLockStale", "lockInfo", "staleTimeout", "acquireLock", "url", "options", "lockTimeout", "pollInterval", "lockPath", "lockDir", "startTime", "lockContent", "resolve", "e", "releaseLock", "downloadOptions", "statResult"]
7
7
  }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Lock acquisition options.
3
+ */
4
+ export interface ProcessLockOptions {
5
+ /**
6
+ * Maximum number of retry attempts.
7
+ * @default 3
8
+ */
9
+ retries?: number | undefined;
10
+ /**
11
+ * Base delay between retries in milliseconds.
12
+ * @default 100
13
+ */
14
+ baseDelayMs?: number | undefined;
15
+ /**
16
+ * Maximum delay between retries in milliseconds.
17
+ * @default 1000
18
+ */
19
+ maxDelayMs?: number | undefined;
20
+ /**
21
+ * Stale lock timeout in milliseconds.
22
+ * Locks older than this are considered abandoned and can be reclaimed.
23
+ * Aligned with npm's npx locking strategy (5-10 seconds).
24
+ * @default 10000 (10 seconds)
25
+ */
26
+ staleMs?: number | undefined;
27
+ }
28
+ /**
29
+ * Process lock manager with stale detection and exit cleanup.
30
+ * Provides cross-platform inter-process synchronization using file-system
31
+ * based locks.
32
+ */
33
+ declare class ProcessLockManager {
34
+ private activeLocks;
35
+ private exitHandlerRegistered;
36
+ /**
37
+ * Ensure process exit handler is registered for cleanup.
38
+ * Registers a handler that cleans up all active locks when the process exits.
39
+ */
40
+ private ensureExitHandler;
41
+ /**
42
+ * Check if a lock is stale based on mtime.
43
+ * A lock is considered stale if it's older than the specified timeout,
44
+ * indicating the holding process likely died abnormally.
45
+ *
46
+ * @param lockPath - Path to the lock directory
47
+ * @param staleMs - Stale timeout in milliseconds
48
+ * @returns True if lock exists and is stale
49
+ */
50
+ private isStale;
51
+ /**
52
+ * Acquire a lock using mkdir for atomic operation.
53
+ * Handles stale locks and includes exit cleanup.
54
+ *
55
+ * This method attempts to create a lock directory atomically. If the lock
56
+ * already exists, it checks if it's stale and removes it before retrying.
57
+ * Uses exponential backoff with jitter for retry attempts.
58
+ *
59
+ * @param lockPath - Path to the lock directory
60
+ * @param options - Lock acquisition options
61
+ * @returns Release function to unlock
62
+ * @throws Error if lock cannot be acquired after all retries
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const release = await processLock.acquire('/tmp/my-lock')
67
+ * try {
68
+ * // Critical section
69
+ * } finally {
70
+ * release()
71
+ * }
72
+ * ```
73
+ */
74
+ acquire(lockPath: string, options?: ProcessLockOptions): Promise<() => void>;
75
+ /**
76
+ * Release a lock and remove from tracking.
77
+ * Removes the lock directory and stops tracking it for exit cleanup.
78
+ *
79
+ * @param lockPath - Path to the lock directory
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * processLock.release('/tmp/my-lock')
84
+ * ```
85
+ */
86
+ release(lockPath: string): void;
87
+ /**
88
+ * Execute a function with exclusive lock protection.
89
+ * Automatically handles lock acquisition, execution, and cleanup.
90
+ *
91
+ * This is the recommended way to use process locks, as it guarantees
92
+ * cleanup even if the callback throws an error.
93
+ *
94
+ * @param lockPath - Path to the lock directory
95
+ * @param fn - Function to execute while holding the lock
96
+ * @param options - Lock acquisition options
97
+ * @returns Result of the callback function
98
+ * @throws Error from callback or lock acquisition failure
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const result = await processLock.withLock('/tmp/my-lock', async () => {
103
+ * // Critical section
104
+ * return someValue
105
+ * })
106
+ * ```
107
+ */
108
+ withLock<T>(lockPath: string, fn: () => Promise<T>, options?: ProcessLockOptions): Promise<T>;
109
+ }
110
+ // Export singleton instance.
111
+ export declare const processLock: ProcessLockManager;
112
+ export {};
@@ -0,0 +1,3 @@
1
+ /* Socket Lib - Built with esbuild */
2
+ var c=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var v=(i,e)=>{for(var r in e)c(i,r,{get:e[r],enumerable:!0})},w=(i,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of g(e))!p.call(i,t)&&t!==r&&c(i,t,{get:()=>e[t],enumerable:!(n=y(e,t))||n.enumerable});return i};var x=i=>w(c({},"__esModule",{value:!0}),i);var E={};v(E,{processLock:()=>S});module.exports=x(E);var s=require("node:fs"),a=require("./fs"),u=require("./logger"),f=require("./promises"),m=require("./signal-exit");class L{activeLocks=new Set;exitHandlerRegistered=!1;ensureExitHandler(){this.exitHandlerRegistered||((0,m.onExit)(()=>{for(const e of this.activeLocks)try{(0,s.existsSync)(e)&&(0,a.safeDeleteSync)(e,{recursive:!0})}catch{}}),this.exitHandlerRegistered=!0)}isStale(e,r){try{if(!(0,s.existsSync)(e))return!1;const n=(0,s.statSync)(e);return Date.now()-n.mtime.getTime()>r}catch{return!1}}async acquire(e,r={}){const{baseDelayMs:n=100,maxDelayMs:t=1e3,retries:l=3,staleMs:d=1e4}=r;return this.ensureExitHandler(),await(0,f.pRetry)(async()=>{try{if((0,s.existsSync)(e)&&this.isStale(e,d)){u.logger.log(`Removing stale lock: ${e}`);try{(0,a.safeDeleteSync)(e,{recursive:!0})}catch{}}return(0,s.mkdirSync)(e,{recursive:!1}),this.activeLocks.add(e),()=>this.release(e)}catch(o){throw o instanceof Error&&o.code==="EEXIST"?this.isStale(e,d)?new Error(`Stale lock detected: ${e}`):new Error(`Lock already exists: ${e}`):o}},{retries:l,baseDelayMs:n,maxDelayMs:t,jitter:!0})}release(e){try{(0,s.existsSync)(e)&&(0,a.safeDeleteSync)(e,{recursive:!0}),this.activeLocks.delete(e)}catch(r){u.logger.warn(`Failed to release lock ${e}: ${r instanceof Error?r.message:String(r)}`)}}async withLock(e,r,n){const t=await this.acquire(e,n);try{return await r()}finally{t()}}}const S=new L;0&&(module.exports={processLock});
3
+ //# sourceMappingURL=process-lock.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/process-lock.ts"],
4
+ "sourcesContent": ["/**\n * @fileoverview Process locking utilities with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system based locks.\n */\n\nimport { existsSync, mkdirSync, statSync } from 'node:fs'\n\nimport { safeDeleteSync } from './fs'\nimport { logger } from './logger'\nimport { pRetry } from './promises'\nimport { onExit } from './signal-exit'\n\n/**\n * Lock acquisition options.\n */\nexport interface ProcessLockOptions {\n /**\n * Maximum number of retry attempts.\n * @default 3\n */\n retries?: number | undefined\n\n /**\n * Base delay between retries in milliseconds.\n * @default 100\n */\n baseDelayMs?: number | undefined\n\n /**\n * Maximum delay between retries in milliseconds.\n * @default 1000\n */\n maxDelayMs?: number | undefined\n\n /**\n * Stale lock timeout in milliseconds.\n * Locks older than this are considered abandoned and can be reclaimed.\n * Aligned with npm's npx locking strategy (5-10 seconds).\n * @default 10000 (10 seconds)\n */\n staleMs?: number | undefined\n}\n\n/**\n * Process lock manager with stale detection and exit cleanup.\n * Provides cross-platform inter-process synchronization using file-system\n * based locks.\n */\nclass ProcessLockManager {\n private activeLocks = new Set<string>()\n private exitHandlerRegistered = false\n\n /**\n * Ensure process exit handler is registered for cleanup.\n * Registers a handler that cleans up all active locks when the process exits.\n */\n private ensureExitHandler() {\n if (this.exitHandlerRegistered) {\n return\n }\n\n onExit(() => {\n for (const lockPath of this.activeLocks) {\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n } catch {\n // Ignore cleanup errors during exit\n }\n }\n })\n\n this.exitHandlerRegistered = true\n }\n\n /**\n * Check if a lock is stale based on mtime.\n * A lock is considered stale if it's older than the specified timeout,\n * indicating the holding process likely died abnormally.\n *\n * @param lockPath - Path to the lock directory\n * @param staleMs - Stale timeout in milliseconds\n * @returns True if lock exists and is stale\n */\n private isStale(lockPath: string, staleMs: number): boolean {\n try {\n if (!existsSync(lockPath)) {\n return false\n }\n\n const stats = statSync(lockPath)\n const age = Date.now() - stats.mtime.getTime()\n return age > staleMs\n } catch {\n return false\n }\n }\n\n /**\n * Acquire a lock using mkdir for atomic operation.\n * Handles stale locks and includes exit cleanup.\n *\n * This method attempts to create a lock directory atomically. If the lock\n * already exists, it checks if it's stale and removes it before retrying.\n * Uses exponential backoff with jitter for retry attempts.\n *\n * @param lockPath - Path to the lock directory\n * @param options - Lock acquisition options\n * @returns Release function to unlock\n * @throws Error if lock cannot be acquired after all retries\n *\n * @example\n * ```typescript\n * const release = await processLock.acquire('/tmp/my-lock')\n * try {\n * // Critical section\n * } finally {\n * release()\n * }\n * ```\n */\n async acquire(\n lockPath: string,\n options: ProcessLockOptions = {},\n ): Promise<() => void> {\n const {\n baseDelayMs = 100,\n maxDelayMs = 1000,\n retries = 3,\n staleMs = 10_000,\n } = options\n\n // Ensure exit handler is registered before any lock acquisition\n this.ensureExitHandler()\n\n return await pRetry(\n async () => {\n try {\n // Check for stale lock and remove if necessary\n if (existsSync(lockPath) && this.isStale(lockPath, staleMs)) {\n logger.log(`Removing stale lock: ${lockPath}`)\n try {\n safeDeleteSync(lockPath, { recursive: true })\n } catch {\n // Ignore errors removing stale lock - will retry\n }\n }\n\n // Atomic lock acquisition via mkdir\n mkdirSync(lockPath, { recursive: false })\n\n // Track lock for cleanup\n this.activeLocks.add(lockPath)\n\n // Return release function\n return () => this.release(lockPath)\n } catch (error) {\n // Handle lock contention\n if (error instanceof Error && (error as any).code === 'EEXIST') {\n if (this.isStale(lockPath, staleMs)) {\n throw new Error(`Stale lock detected: ${lockPath}`)\n }\n throw new Error(`Lock already exists: ${lockPath}`)\n }\n throw error\n }\n },\n {\n retries,\n baseDelayMs,\n maxDelayMs,\n jitter: true,\n },\n )\n }\n\n /**\n * Release a lock and remove from tracking.\n * Removes the lock directory and stops tracking it for exit cleanup.\n *\n * @param lockPath - Path to the lock directory\n *\n * @example\n * ```typescript\n * processLock.release('/tmp/my-lock')\n * ```\n */\n release(lockPath: string): void {\n try {\n if (existsSync(lockPath)) {\n safeDeleteSync(lockPath, { recursive: true })\n }\n this.activeLocks.delete(lockPath)\n } catch (error) {\n logger.warn(\n `Failed to release lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n }\n\n /**\n * Execute a function with exclusive lock protection.\n * Automatically handles lock acquisition, execution, and cleanup.\n *\n * This is the recommended way to use process locks, as it guarantees\n * cleanup even if the callback throws an error.\n *\n * @param lockPath - Path to the lock directory\n * @param fn - Function to execute while holding the lock\n * @param options - Lock acquisition options\n * @returns Result of the callback function\n * @throws Error from callback or lock acquisition failure\n *\n * @example\n * ```typescript\n * const result = await processLock.withLock('/tmp/my-lock', async () => {\n * // Critical section\n * return someValue\n * })\n * ```\n */\n async withLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options?: ProcessLockOptions,\n ): Promise<T> {\n const release = await this.acquire(lockPath, options)\n try {\n return await fn()\n } finally {\n release()\n }\n }\n}\n\n// Export singleton instance.\nexport const processLock = new ProcessLockManager()\n"],
5
+ "mappings": ";4ZAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,iBAAAE,IAAA,eAAAC,EAAAH,GAKA,IAAAI,EAAgD,mBAEhDC,EAA+B,gBAC/BC,EAAuB,oBACvBC,EAAuB,sBACvBC,EAAuB,yBAsCvB,MAAMC,CAAmB,CACf,YAAc,IAAI,IAClB,sBAAwB,GAMxB,mBAAoB,CACtB,KAAK,2BAIT,UAAO,IAAM,CACX,UAAWC,KAAY,KAAK,YAC1B,GAAI,IACE,cAAWA,CAAQ,MACrB,kBAAeA,EAAU,CAAE,UAAW,EAAK,CAAC,CAEhD,MAAQ,CAER,CAEJ,CAAC,EAED,KAAK,sBAAwB,GAC/B,CAWQ,QAAQA,EAAkBC,EAA0B,CAC1D,GAAI,CACF,GAAI,IAAC,cAAWD,CAAQ,EACtB,MAAO,GAGT,MAAME,KAAQ,YAASF,CAAQ,EAE/B,OADY,KAAK,IAAI,EAAIE,EAAM,MAAM,QAAQ,EAChCD,CACf,MAAQ,CACN,MAAO,EACT,CACF,CAyBA,MAAM,QACJD,EACAG,EAA8B,CAAC,EACV,CACrB,KAAM,CACJ,YAAAC,EAAc,IACd,WAAAC,EAAa,IACb,QAAAC,EAAU,EACV,QAAAL,EAAU,GACZ,EAAIE,EAGJ,YAAK,kBAAkB,EAEhB,QAAM,UACX,SAAY,CACV,GAAI,CAEF,MAAI,cAAWH,CAAQ,GAAK,KAAK,QAAQA,EAAUC,CAAO,EAAG,CAC3D,SAAO,IAAI,wBAAwBD,CAAQ,EAAE,EAC7C,GAAI,IACF,kBAAeA,EAAU,CAAE,UAAW,EAAK,CAAC,CAC9C,MAAQ,CAER,CACF,CAGA,sBAAUA,EAAU,CAAE,UAAW,EAAM,CAAC,EAGxC,KAAK,YAAY,IAAIA,CAAQ,EAGtB,IAAM,KAAK,QAAQA,CAAQ,CACpC,OAASO,EAAO,CAEd,MAAIA,aAAiB,OAAUA,EAAc,OAAS,SAChD,KAAK,QAAQP,EAAUC,CAAO,EAC1B,IAAI,MAAM,wBAAwBD,CAAQ,EAAE,EAE9C,IAAI,MAAM,wBAAwBA,CAAQ,EAAE,EAE9CO,CACR,CACF,EACA,CACE,QAAAD,EACA,YAAAF,EACA,WAAAC,EACA,OAAQ,EACV,CACF,CACF,CAaA,QAAQL,EAAwB,CAC9B,GAAI,IACE,cAAWA,CAAQ,MACrB,kBAAeA,EAAU,CAAE,UAAW,EAAK,CAAC,EAE9C,KAAK,YAAY,OAAOA,CAAQ,CAClC,OAASO,EAAO,CACd,SAAO,KACL,0BAA0BP,CAAQ,KAAKO,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAC/F,CACF,CACF,CAuBA,MAAM,SACJP,EACAQ,EACAL,EACY,CACZ,MAAMM,EAAU,MAAM,KAAK,QAAQT,EAAUG,CAAO,EACpD,GAAI,CACF,OAAO,MAAMK,EAAG,CAClB,QAAE,CACAC,EAAQ,CACV,CACF,CACF,CAGO,MAAMjB,EAAc,IAAIO",
6
+ "names": ["process_lock_exports", "__export", "processLock", "__toCommonJS", "import_node_fs", "import_fs", "import_logger", "import_promises", "import_signal_exit", "ProcessLockManager", "lockPath", "staleMs", "stats", "options", "baseDelayMs", "maxDelayMs", "retries", "error", "fn", "release"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/lib",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "license": "MIT",
5
5
  "description": "Core utilities and infrastructure for Socket.dev security tools",
6
6
  "keywords": [
@@ -384,6 +384,10 @@
384
384
  "types": "./plugins/babel-plugin-inline-require-calls.d.ts",
385
385
  "default": "./plugins/babel-plugin-inline-require-calls.js"
386
386
  },
387
+ "./process-lock": {
388
+ "types": "./dist/process-lock.d.ts",
389
+ "default": "./dist/process-lock.js"
390
+ },
387
391
  "./promise-queue": {
388
392
  "types": "./dist/promise-queue.d.ts",
389
393
  "default": "./dist/promise-queue.js"