@@ -81,8 +81,9 @@ async function isCacheValid(
8181/**
8282 * Download a file from a URL with integrity checking and concurrent download protection.
8383 * Uses downloadWithLock to prevent multiple processes from downloading the same binary simultaneously.
84+ * Internal helper function for downloading binary files.
8485 */
85- async function downloadBinary (
86+ async function downloadBinaryFile (
8687 url : string ,
8788 destPath : string ,
8889 checksum ?: string | undefined ,
@@ -270,7 +271,7 @@ export async function dlxBinary(
270271 await fs . mkdir ( cacheEntryDir , { recursive : true } )
271272
272273 // Download the binary.
273- computedChecksum = await downloadBinary ( url , binaryPath , checksum )
274+ computedChecksum = await downloadBinaryFile ( url , binaryPath , checksum )
274275 await writeMetadata ( cacheEntryDir , url , computedChecksum || '' )
275276 }
276277
@@ -313,6 +314,102 @@ export async function dlxBinary(
313314 }
314315}
315316
317+ /**
318+ * Download a binary from a URL with caching (without execution).
319+ * Similar to downloadPackage from dlx-package.
320+ *
321+ * @returns Object containing the path to the cached binary and whether it was downloaded
322+ */
323+ export async function downloadBinary (
324+ options : Omit < DlxBinaryOptions , 'spawnOptions' > ,
325+ ) : Promise < { binaryPath : string ; downloaded : boolean } > {
326+ const {
327+ cacheTtl = /*@__INLINE__ */ require ( '#constants/time' ) . DLX_BINARY_CACHE_TTL ,
328+ checksum,
329+ force = false ,
330+ name,
331+ url,
332+ } = { __proto__ : null , ...options } as DlxBinaryOptions
333+
334+ // Generate cache paths similar to pnpm/npx structure.
335+ const cacheDir = getDlxCachePath ( )
336+ const binaryName = name || `binary-${ process . platform } -${ os . arch ( ) } `
337+ // Create spec from URL and binary name for unique cache identity.
338+ const spec = `${ url } :${ binaryName } `
339+ const cacheKey = generateCacheKey ( spec )
340+ const cacheEntryDir = path . join ( cacheDir , cacheKey )
341+ const binaryPath = normalizePath ( path . join ( cacheEntryDir , binaryName ) )
342+
343+ let downloaded = false
344+
345+ // Check if we need to download.
346+ if (
347+ ! force &&
348+ existsSync ( cacheEntryDir ) &&
349+ ( await isCacheValid ( cacheEntryDir , cacheTtl ) )
350+ ) {
351+ // Binary is cached and valid.
352+ downloaded = false
353+ } else {
354+ // Ensure cache directory exists.
355+ await fs . mkdir ( cacheEntryDir , { recursive : true } )
356+
357+ // Download the binary.
358+ const computedChecksum = await downloadBinaryFile ( url , binaryPath , checksum )
359+ await writeMetadata ( cacheEntryDir , url , computedChecksum || '' )
360+ downloaded = true
361+ }
362+
363+ return {
364+ binaryPath,
365+ downloaded,
366+ }
367+ }
368+
369+ /**
370+ * Execute a cached binary without re-downloading.
371+ * Similar to executePackage from dlx-package.
372+ * Binary must have been previously downloaded via downloadBinary or dlxBinary.
373+ *
374+ * @param binaryPath Path to the cached binary (from downloadBinary result)
375+ * @param args Arguments to pass to the binary
376+ * @param spawnOptions Spawn options for execution
377+ * @param spawnExtra Extra spawn configuration
378+ * @returns The spawn promise for the running process
379+ */
380+ export function executeBinary (
381+ binaryPath : string ,
382+ args : readonly string [ ] | string [ ] ,
383+ spawnOptions ?: SpawnOptions | undefined ,
384+ spawnExtra ?: SpawnExtra | undefined ,
385+ ) : ReturnType < typeof spawn > {
386+ // On Windows, script files (.bat, .cmd, .ps1) require shell: true because
387+ // they are not executable on their own and must be run through cmd.exe.
388+ // Note: .exe files are actual binaries and don't need shell mode.
389+ const needsShell = WIN32 && / \. (?: b a t | c m d | p s 1 ) $ / i. test ( binaryPath )
390+
391+ // Windows cmd.exe PATH resolution behavior:
392+ // When shell: true on Windows with .cmd/.bat/.ps1 files, spawn will automatically
393+ // strip the full path down to just the basename without extension. Windows cmd.exe
394+ // then searches for the binary in directories listed in PATH.
395+ //
396+ // Since our binaries are downloaded to a custom cache directory that's not in PATH,
397+ // we must prepend the cache directory to PATH so cmd.exe can locate the binary.
398+ const cacheEntryDir = path . dirname ( binaryPath )
399+ const finalSpawnOptions = needsShell
400+ ? {
401+ ...spawnOptions ,
402+ env : {
403+ ...spawnOptions ?. env ,
404+ PATH : `${ cacheEntryDir } ${ path . delimiter } ${ process . env [ 'PATH' ] || '' } ` ,
405+ } ,
406+ shell : true ,
407+ }
408+ : spawnOptions
409+
410+ return spawn ( binaryPath , args , finalSpawnOptions , spawnExtra )
411+ }
412+
316413/**
317414 * Get the DLX binary cache directory path.
318415 * Returns normalized path for cross-platform compatibility.
0 commit comments