Skip to content

Commit 1c46fea

Browse files
committed
feat: add wildcard support to asset parameter in release utilities
Add support for simple glob patterns using a single wildcard (*) in asset parameters. Users can now use intuitive patterns like 'yoga-sync-*.mjs' instead of { prefix: 'yoga-sync-', suffix: '.mjs' }. Changes: - Updated AssetPattern type to accept string patterns - Added parseWildcardPattern() helper to convert wildcards to prefix/suffix - Updated findReleaseAsset() to handle string patterns - Updated socket-btm.ts to properly detect and handle wildcard vs exact strings - Enhanced JSDoc with wildcard examples Backward compatible: exact strings, object patterns, and RegExp still work. Examples: - 'yoga-sync-*.mjs' → matches any file starting with 'yoga-sync-' and ending with '.mjs' - 'models-*.tar.gz' → matches any file starting with 'models-' and ending with '.tar.gz' - '*-models.tar.gz' → matches any file ending with '-models.tar.gz' - 'exact-name.txt' → exact match (no wildcard)
1 parent 9364a45 commit 1c46fea

2 files changed

Lines changed: 111 additions & 12 deletions

File tree

src/releases/github.ts

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,18 @@ export function getAuthHeaders(): Record<string, string> {
268268

269269
/**
270270
* Pattern for matching release assets.
271-
* Can be either a prefix/suffix pair or a RegExp.
271+
* Can be either:
272+
* - A string with wildcard (*) for simple glob patterns (e.g., 'yoga-sync-*.mjs')
273+
* - A prefix/suffix pair for explicit matching
274+
* - A RegExp for complex patterns
275+
*
276+
* String patterns support a single wildcard (*) which matches any characters:
277+
* - 'yoga-sync-*.mjs' → prefix: 'yoga-sync-', suffix: '.mjs'
278+
* - 'models-*.tar.gz' → prefix: 'models-', suffix: '.tar.gz'
279+
* - '*-models.tar.gz' → prefix: '', suffix: '-models.tar.gz'
280+
* - 'yoga-*' → prefix: 'yoga-', suffix: ''
272281
*/
273-
export type AssetPattern = { prefix: string; suffix: string } | RegExp
282+
export type AssetPattern = string | { prefix: string; suffix: string } | RegExp
274283

275284
/**
276285
* Result of finding a release asset.
@@ -282,30 +291,102 @@ export interface FindReleaseAssetResult {
282291
assetName: string
283292
}
284293

294+
/**
295+
* Parse a wildcard pattern string into prefix/suffix components.
296+
* Supports a single wildcard (*) character.
297+
*
298+
* @param pattern - Pattern string with optional wildcard (e.g., 'yoga-sync-*.mjs')
299+
* @returns Prefix/suffix pair for matching
300+
* @throws Error if pattern contains multiple wildcards
301+
*
302+
* @example
303+
* ```ts
304+
* parseWildcardPattern('yoga-sync-*.mjs')
305+
* // Returns: { prefix: 'yoga-sync-', suffix: '.mjs' }
306+
*
307+
* parseWildcardPattern('models-*.tar.gz')
308+
* // Returns: { prefix: 'models-', suffix: '.tar.gz' }
309+
*
310+
* parseWildcardPattern('*-models.tar.gz')
311+
* // Returns: { prefix: '', suffix: '-models.tar.gz' }
312+
*
313+
* parseWildcardPattern('yoga-*')
314+
* // Returns: { prefix: 'yoga-', suffix: '' }
315+
*
316+
* parseWildcardPattern('exact-name.txt')
317+
* // Returns: { prefix: 'exact-name.txt', suffix: '' } (exact match)
318+
* ```
319+
*/
320+
function parseWildcardPattern(pattern: string): {
321+
prefix: string
322+
suffix: string
323+
} {
324+
const wildcardIndex = pattern.indexOf('*')
325+
326+
// No wildcard - treat as exact match (prefix only).
327+
if (wildcardIndex === -1) {
328+
return { prefix: pattern, suffix: '' }
329+
}
330+
331+
// Check for multiple wildcards.
332+
const lastWildcardIndex = pattern.lastIndexOf('*')
333+
if (wildcardIndex !== lastWildcardIndex) {
334+
throw new Error(
335+
`Pattern contains multiple wildcards: ${pattern}. Only single wildcard (*) is supported.`,
336+
)
337+
}
338+
339+
// Split at wildcard position.
340+
const prefix = pattern.slice(0, wildcardIndex)
341+
const suffix = pattern.slice(wildcardIndex + 1)
342+
343+
return { prefix, suffix }
344+
}
345+
285346
/**
286347
* Find a release asset matching a pattern in the latest release.
287348
* Searches for the first release matching the tool prefix,
288349
* then finds the first asset matching the provided pattern.
289350
*
290351
* @param toolPrefix - Tool name prefix to search for (e.g., 'yoga-layout-')
291-
* @param assetPattern - Pattern to match asset names (prefix/suffix or RegExp)
352+
* @param assetPattern - Pattern to match asset names (string with wildcard, prefix/suffix object, or RegExp)
292353
* @param repoConfig - Repository configuration (owner/repo)
293354
* @param options - Additional options
294355
* @returns Result with tag and asset name, or null if not found
295356
*
296357
* @example
297358
* ```ts
298-
* // Find yoga-sync asset with timestamped name
359+
* // Find yoga-sync asset with wildcard pattern
299360
* const result = await findReleaseAsset(
300361
* 'yoga-layout-',
301-
* { prefix: 'yoga-sync-', suffix: '.mjs' },
362+
* 'yoga-sync-*.mjs',
302363
* { owner: 'SocketDev', repo: 'socket-btm' }
303364
* )
304365
* // result = { tag: 'yoga-layout-2024-01-15-abc123', assetName: 'yoga-sync-2024-01-15-abc123.mjs' }
305366
* ```
306367
*
307368
* @example
308369
* ```ts
370+
* // Find models tar.gz with wildcard pattern
371+
* const result = await findReleaseAsset(
372+
* 'models-',
373+
* 'models-*.tar.gz',
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
309390
* // Find models tar.gz with regex
310391
* const result = await findReleaseAsset(
311392
* 'models-',
@@ -323,6 +404,14 @@ export async function findReleaseAsset(
323404
const { owner, repo } = repoConfig
324405
const { quiet = false } = options
325406

407+
// Normalize string patterns to prefix/suffix objects.
408+
let normalizedPattern: { prefix: string; suffix: string } | RegExp
409+
if (typeof assetPattern === 'string') {
410+
normalizedPattern = parseWildcardPattern(assetPattern)
411+
} else {
412+
normalizedPattern = assetPattern
413+
}
414+
326415
return await pRetry(
327416
async () => {
328417
// Fetch recent releases (100 should cover all tool releases).
@@ -349,12 +438,12 @@ export async function findReleaseAsset(
349438
// Find matching asset in this release.
350439
let matchingAsset: { name: string } | undefined
351440

352-
if (assetPattern instanceof RegExp) {
441+
if (normalizedPattern instanceof RegExp) {
353442
matchingAsset = assets.find((a: { name: string }) =>
354-
assetPattern.test(a.name),
443+
normalizedPattern.test(a.name),
355444
)
356445
} else {
357-
const { prefix, suffix } = assetPattern
446+
const { prefix, suffix } = normalizedPattern
358447
matchingAsset = assets.find(
359448
(a: { name: string }) =>
360449
a.name.startsWith(prefix) && a.name.endsWith(suffix),

src/releases/socket-btm.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ export interface SocketBtmAssetConfig {
5959
downloadDir?: string
6060
/** Tool/package name for directory structure and release matching. */
6161
tool: string
62-
/** Asset name or pattern on GitHub. Can be an exact name (string) or a pattern (prefix/suffix or RegExp). */
62+
/**
63+
* Asset name or pattern on GitHub.
64+
* Can be:
65+
* - A string with wildcard (*) for simple glob patterns (e.g., 'yoga-sync-*.mjs')
66+
* - An exact asset name (string without wildcard)
67+
* - A pattern object with prefix/suffix
68+
* - A RegExp for complex patterns
69+
*/
6370
asset: string | AssetPattern
6471
/** Output filename. @default resolved asset name */
6572
output?: string
@@ -155,11 +162,14 @@ export async function downloadSocketBtmRelease(
155162
let resolvedAsset: string
156163
let resolvedTag = tag
157164

158-
if (typeof asset === 'string') {
165+
// Check if asset is a string without wildcard (exact match).
166+
const isExactMatch = typeof asset === 'string' && !asset.includes('*')
167+
168+
if (isExactMatch) {
159169
// Exact asset name provided.
160-
resolvedAsset = asset
170+
resolvedAsset = asset as string
161171
} else {
162-
// Pattern provided - need to find matching asset.
172+
// Pattern provided (wildcard string, object, or RegExp) - need to find matching asset.
163173
if (tag) {
164174
throw new Error(
165175
'Cannot use asset pattern with explicit tag. Either provide exact asset name or omit tag.',

0 commit comments

Comments
 (0)