@@ -266,6 +266,136 @@ export function getAuthHeaders(): Record<string, string> {
266266 return headers
267267}
268268
269+ /**
270+ * Pattern for matching release assets.
271+ * Can be either a prefix/suffix pair or a RegExp.
272+ */
273+ export type AssetPattern = { prefix : string ; suffix : string } | RegExp
274+
275+ /**
276+ * Result of finding a release asset.
277+ */
278+ export interface FindReleaseAssetResult {
279+ /** The release tag name. */
280+ tag : string
281+ /** The matching asset name. */
282+ assetName : string
283+ }
284+
285+ /**
286+ * Find a release asset matching a pattern in the latest release.
287+ * Searches for the first release matching the tool prefix,
288+ * then finds the first asset matching the provided pattern.
289+ *
290+ * @param toolPrefix - Tool name prefix to search for (e.g., 'yoga-layout-')
291+ * @param assetPattern - Pattern to match asset names (prefix/suffix or RegExp)
292+ * @param repoConfig - Repository configuration (owner/repo)
293+ * @param options - Additional options
294+ * @returns Result with tag and asset name, or null if not found
295+ *
296+ * @example
297+ * ```ts
298+ * // Find yoga-sync asset with timestamped name
299+ * const result = await findReleaseAsset(
300+ * 'yoga-layout-',
301+ * { prefix: 'yoga-sync-', suffix: '.mjs' },
302+ * { owner: 'SocketDev', repo: 'socket-btm' }
303+ * )
304+ * // result = { tag: 'yoga-layout-2024-01-15-abc123', assetName: 'yoga-sync-2024-01-15-abc123.mjs' }
305+ * ```
306+ *
307+ * @example
308+ * ```ts
309+ * // Find models tar.gz with regex
310+ * const result = await findReleaseAsset(
311+ * 'models-',
312+ * /^models-\d{4}-\d{2}-\d{2}-.+\.tar\.gz$/,
313+ * { owner: 'SocketDev', repo: 'socket-btm' }
314+ * )
315+ * ```
316+ */
317+ export async function findReleaseAsset (
318+ toolPrefix : string ,
319+ assetPattern : AssetPattern ,
320+ repoConfig : RepoConfig ,
321+ options : { quiet ?: boolean } = { } ,
322+ ) : Promise < FindReleaseAssetResult | null > {
323+ const { owner, repo } = repoConfig
324+ const { quiet = false } = options
325+
326+ return await pRetry (
327+ async ( ) => {
328+ // Fetch recent releases (100 should cover all tool releases).
329+ const response = await httpRequest (
330+ `https://api.github.com/repos/${ owner } /${ repo } /releases?per_page=100` ,
331+ {
332+ headers : getAuthHeaders ( ) ,
333+ } ,
334+ )
335+
336+ if ( ! response . ok ) {
337+ throw new Error ( `Failed to fetch releases: ${ response . status } ` )
338+ }
339+
340+ const releases = JSON . parse ( response . body . toString ( 'utf8' ) )
341+
342+ // Find the first release matching the tool prefix.
343+ for ( const release of releases ) {
344+ const { assets, tag_name : tag } = release
345+ if ( ! tag . startsWith ( toolPrefix ) ) {
346+ continue
347+ }
348+
349+ // Find matching asset in this release.
350+ let matchingAsset : { name : string } | undefined
351+
352+ if ( assetPattern instanceof RegExp ) {
353+ matchingAsset = assets . find ( ( a : { name : string } ) =>
354+ assetPattern . test ( a . name ) ,
355+ )
356+ } else {
357+ const { prefix, suffix } = assetPattern
358+ matchingAsset = assets . find (
359+ ( a : { name : string } ) =>
360+ a . name . startsWith ( prefix ) && a . name . endsWith ( suffix ) ,
361+ )
362+ }
363+
364+ if ( matchingAsset ) {
365+ if ( ! quiet ) {
366+ logger . info ( `Found release: ${ tag } ` )
367+ logger . info ( `Found asset: ${ matchingAsset . name } ` )
368+ }
369+ return {
370+ assetName : matchingAsset . name ,
371+ tag,
372+ }
373+ }
374+ }
375+
376+ // No matching release or asset found.
377+ if ( ! quiet ) {
378+ logger . info ( `No ${ toolPrefix } release with matching asset found` )
379+ }
380+ return null
381+ } ,
382+ {
383+ ...RETRY_CONFIG ,
384+ onRetry : ( attempt , error ) => {
385+ if ( ! quiet ) {
386+ logger . info (
387+ `Retry attempt ${ attempt + 1 } /${ RETRY_CONFIG . retries + 1 } for release asset search...` ,
388+ )
389+ logger . warn (
390+ `Attempt ${ attempt + 1 } /${ RETRY_CONFIG . retries + 1 } failed: ${ error instanceof Error ? error . message : String ( error ) } ` ,
391+ )
392+ }
393+ return undefined
394+ } ,
395+ } ,
396+ )
397+ }
398+
269399/**
270400 * Get latest release tag matching a tool prefix.
271401 *
0 commit comments