@@ -208,16 +208,17 @@ export async function downloadGitHubRelease(
208208
209209/**
210210 * Download a specific release asset.
211+ * Supports pattern matching for dynamic asset discovery.
211212 *
212213 * @param tag - Release tag name
213- * @param assetName - Asset name to download
214+ * @param assetPattern - Asset name or pattern (glob string, prefix/suffix object, or RegExp)
214215 * @param outputPath - Path to write the downloaded file
215216 * @param repoConfig - Repository configuration (owner/repo)
216217 * @param options - Additional options
217218 */
218219export async function downloadReleaseAsset (
219220 tag : string ,
220- assetName : string ,
221+ assetPattern : string | AssetPattern ,
221222 outputPath : string ,
222223 repoConfig : RepoConfig ,
223224 options : { quiet ?: boolean } = { } ,
@@ -228,13 +229,15 @@ export async function downloadReleaseAsset(
228229 // Get the browser_download_url for the asset.
229230 const downloadUrl = await getReleaseAssetUrl (
230231 tag ,
231- assetName ,
232+ assetPattern ,
232233 { owner, repo } ,
233234 { quiet } ,
234235 )
235236
236237 if ( ! downloadUrl ) {
237- throw new Error ( `Asset ${ assetName } not found in release ${ tag } ` )
238+ const patternDesc =
239+ typeof assetPattern === 'string' ? assetPattern : 'matching pattern'
240+ throw new Error ( `Asset ${ patternDesc } not found in release ${ tag } ` )
238241 }
239242
240243 const path = getPath ( )
@@ -287,16 +290,6 @@ export function getAuthHeaders(): Record<string, string> {
287290 */
288291export type AssetPattern = string | { prefix : string ; suffix : string } | RegExp
289292
290- /**
291- * Result of finding a release asset.
292- */
293- export interface FindReleaseAssetResult {
294- /** The release tag name. */
295- tag : string
296- /** The matching asset name. */
297- assetName : string
298- }
299-
300293/**
301294 * Create a matcher function for a pattern using picomatch for glob patterns
302295 * or simple prefix/suffix matching for object patterns.
@@ -334,78 +327,25 @@ function createMatcher(
334327}
335328
336329/**
337- * Find a release asset matching a pattern in the latest release.
338- * Searches for the first release matching the tool prefix,
339- * then finds the first asset matching the provided pattern.
330+ * Get latest release tag matching a tool prefix.
331+ * Optionally filter by releases containing a matching asset.
340332 *
341- * @param toolPrefix - Tool name prefix to search for (e.g., 'yoga-layout-')
342- * @param assetPattern - Pattern to match asset names (glob string, prefix/suffix object, or RegExp)
333+ * @param toolPrefix - Tool name prefix to search for (e.g., 'node-smol-')
343334 * @param repoConfig - Repository configuration (owner/repo)
344335 * @param options - Additional options
345- * @returns Result with tag and asset name, or null if not found
346- *
347- * @example
348- * ```ts
349- * // Find yoga-sync asset with glob pattern
350- * const result = await findReleaseAsset(
351- * 'yoga-layout-',
352- * 'yoga-sync-*.mjs',
353- * { owner: 'SocketDev', repo: 'socket-btm' }
354- * )
355- * // result = { tag: 'yoga-layout-2024-01-15-abc123', assetName: 'yoga-sync-2024-01-15-abc123.mjs' }
356- * ```
357- *
358- * @example
359- * ```ts
360- * // Find models tar.gz with glob pattern
361- * const result = await findReleaseAsset(
362- * 'models-',
363- * 'models-*.tar.gz',
364- * { owner: 'SocketDev', repo: 'socket-btm' }
365- * )
366- * ```
367- *
368- * @example
369- * ```ts
370- * // Find asset with glob braces pattern
371- * const result = await findReleaseAsset(
372- * 'yoga-layout-',
373- * 'yoga-{sync,layout}-*.{mjs,js}',
374- * { owner: 'SocketDev', repo: 'socket-btm' }
375- * )
376- * ```
377- *
378- * @example
379- * ```ts
380- * // Find asset with object pattern (backward compatible)
381- * const result = await findReleaseAsset(
382- * 'yoga-layout-',
383- * { prefix: 'yoga-sync-', suffix: '.mjs' },
384- * { owner: 'SocketDev', repo: 'socket-btm' }
385- * )
386- * ```
387- *
388- * @example
389- * ```ts
390- * // Find models tar.gz with regex
391- * const result = await findReleaseAsset(
392- * 'models-',
393- * /^models-\d{4}-\d{2}-\d{2}-.+\.tar\.gz$/,
394- * { owner: 'SocketDev', repo: 'socket-btm' }
395- * )
396- * ```
336+ * @param options.assetPattern - Optional pattern to filter releases by matching asset
337+ * @returns Latest release tag or null if not found
397338 */
398- export async function findReleaseAsset (
339+ export async function getLatestRelease (
399340 toolPrefix : string ,
400- assetPattern : AssetPattern ,
401341 repoConfig : RepoConfig ,
402- options : { quiet ?: boolean } = { } ,
403- ) : Promise < FindReleaseAssetResult | null > {
342+ options : { assetPattern ?: AssetPattern ; quiet ?: boolean } = { } ,
343+ ) : Promise < string | null > {
344+ const { assetPattern, quiet = false } = options
404345 const { owner, repo } = repoConfig
405- const { quiet = false } = options
406346
407- // Create matcher function for the pattern.
408- const isMatch = createMatcher ( assetPattern )
347+ // Create matcher function if pattern provided .
348+ const isMatch = assetPattern ? createMatcher ( assetPattern ) : undefined
409349
410350 return await pRetry (
411351 async ( ) => {
@@ -430,87 +370,20 @@ export async function findReleaseAsset(
430370 continue
431371 }
432372
433- // Find matching asset in this release using the matcher function.
434- const matchingAsset = assets . find ( ( a : { name : string } ) =>
435- isMatch ( a . name ) ,
436- )
437-
438- if ( matchingAsset ) {
439- if ( ! quiet ) {
440- logger . info ( `Found release: ${ tag } ` )
441- logger . info ( `Found asset: ${ matchingAsset . name } ` )
442- }
443- return {
444- assetName : matchingAsset . name ,
445- tag,
373+ // If asset pattern provided, check if release has matching asset.
374+ if ( isMatch ) {
375+ const hasMatchingAsset = assets . some ( ( a : { name : string } ) =>
376+ isMatch ( a . name ) ,
377+ )
378+ if ( ! hasMatchingAsset ) {
379+ continue
446380 }
447381 }
448- }
449382
450- // No matching release or asset found.
451- if ( ! quiet ) {
452- logger . info ( `No ${ toolPrefix } release with matching asset found` )
453- }
454- return null
455- } ,
456- {
457- ...RETRY_CONFIG ,
458- onRetry : ( attempt , error ) => {
459383 if ( ! quiet ) {
460- logger . info (
461- `Retry attempt ${ attempt + 1 } /${ RETRY_CONFIG . retries + 1 } for release asset search...` ,
462- )
463- logger . warn (
464- `Attempt ${ attempt + 1 } /${ RETRY_CONFIG . retries + 1 } failed: ${ error instanceof Error ? error . message : String ( error ) } ` ,
465- )
466- }
467- return undefined
468- } ,
469- } ,
470- )
471- }
472-
473- /**
474- * Get latest release tag matching a tool prefix.
475- *
476- * @param toolPrefix - Tool name prefix to search for (e.g., 'node-smol-')
477- * @param repoConfig - Repository configuration (owner/repo)
478- * @param options - Additional options
479- * @returns Latest release tag or null if not found
480- */
481- export async function getLatestRelease (
482- toolPrefix : string ,
483- repoConfig : RepoConfig ,
484- options : { quiet ?: boolean } = { } ,
485- ) : Promise < string | null > {
486- const { owner, repo } = repoConfig
487- const { quiet = false } = options
488-
489- return await pRetry (
490- async ( ) => {
491- // Fetch recent releases (100 should cover all tool releases).
492- const response = await httpRequest (
493- `https://api.github.com/repos/${ owner } /${ repo } /releases?per_page=100` ,
494- {
495- headers : getAuthHeaders ( ) ,
496- } ,
497- )
498-
499- if ( ! response . ok ) {
500- throw new Error ( `Failed to fetch releases: ${ response . status } ` )
501- }
502-
503- const releases = JSON . parse ( response . body . toString ( 'utf8' ) )
504-
505- // Find the first release matching the tool prefix.
506- for ( const release of releases ) {
507- const { tag_name : tag } = release
508- if ( tag . startsWith ( toolPrefix ) ) {
509- if ( ! quiet ) {
510- logger . info ( `Found release: ${ tag } ` )
511- }
512- return tag
384+ logger . info ( `Found release: ${ tag } ` )
513385 }
386+ return tag
514387 }
515388
516389 // No matching release found.
@@ -538,22 +411,31 @@ export async function getLatestRelease(
538411
539412/**
540413 * Get download URL for a specific release asset.
414+ * Supports pattern matching for dynamic asset discovery.
541415 *
542416 * @param tag - Release tag name
543- * @param assetName - Asset name to download
417+ * @param assetPattern - Asset name or pattern (glob string, prefix/suffix object, or RegExp)
544418 * @param repoConfig - Repository configuration (owner/repo)
545419 * @param options - Additional options
546420 * @returns Browser download URL for the asset
547421 */
548422export async function getReleaseAssetUrl (
549423 tag : string ,
550- assetName : string ,
424+ assetPattern : string | AssetPattern ,
551425 repoConfig : RepoConfig ,
552426 options : { quiet ?: boolean } = { } ,
553427) : Promise < string | null > {
554428 const { owner, repo } = repoConfig
555429 const { quiet = false } = options
556430
431+ // Create matcher function for the pattern.
432+ const isMatch =
433+ typeof assetPattern === 'string' &&
434+ ! assetPattern . includes ( '*' ) &&
435+ ! assetPattern . includes ( '{' )
436+ ? ( input : string ) => input === assetPattern
437+ : createMatcher ( assetPattern as AssetPattern )
438+
557439 return await pRetry (
558440 async ( ) => {
559441 const response = await httpRequest (
@@ -570,16 +452,18 @@ export async function getReleaseAssetUrl(
570452 const release = JSON . parse ( response . body . toString ( 'utf8' ) )
571453
572454 // Find the matching asset.
573- const asset = release . assets . find (
574- ( a : { name : string } ) => a . name === assetName ,
455+ const asset = release . assets . find ( ( a : { name : string } ) =>
456+ isMatch ( a . name ) ,
575457 )
576458
577459 if ( ! asset ) {
578- throw new Error ( `Asset ${ assetName } not found in release ${ tag } ` )
460+ const patternDesc =
461+ typeof assetPattern === 'string' ? assetPattern : 'matching pattern'
462+ throw new Error ( `Asset ${ patternDesc } not found in release ${ tag } ` )
579463 }
580464
581465 if ( ! quiet ) {
582- logger . info ( `Found asset: ${ assetName } ` )
466+ logger . info ( `Found asset: ${ asset . name } ` )
583467 }
584468
585469 return asset . browser_download_url
0 commit comments