@@ -10,84 +10,6 @@ import { getDefaultLogger } from '../logger.js'
1010import { pRetry } from '../promises.js'
1111import { spawn } from '../spawn.js'
1212
13- const logger = getDefaultLogger ( )
14-
15- /**
16- * Retry configuration for GitHub API requests.
17- * Uses exponential backoff to handle transient failures and rate limiting.
18- */
19- const RETRY_CONFIG = Object . freeze ( {
20- __proto__ : null ,
21- // Exponential backoff: delay doubles with each retry (5s, 10s, 20s).
22- backoffFactor : 2 ,
23- // Initial delay before first retry.
24- baseDelayMs : 5000 ,
25- // Maximum number of retry attempts (excluding initial request).
26- retries : 2 ,
27- } )
28-
29- let _fs : typeof import ( 'node:fs' ) | undefined
30- let _path : typeof import ( 'node:path' ) | undefined
31-
32- /**
33- * Create a matcher function for a pattern using picomatch for glob patterns
34- * or simple prefix/suffix matching for object patterns.
35- *
36- * @param pattern - Pattern to match (string glob, prefix/suffix object, or RegExp)
37- * @returns Function that tests if a string matches the pattern
38- * @private
39- */
40- function createMatcher (
41- pattern : string | { prefix : string ; suffix : string } | RegExp ,
42- ) : ( input : string ) => boolean {
43- if ( typeof pattern === 'string' ) {
44- // Use picomatch for glob pattern matching.
45- const isMatch = picomatch ( pattern )
46- return ( input : string ) => isMatch ( input )
47- }
48-
49- if ( pattern instanceof RegExp ) {
50- return ( input : string ) => pattern . test ( input )
51- }
52-
53- // Prefix/suffix object pattern (backward compatible).
54- const { prefix, suffix } = pattern
55- return ( input : string ) => input . startsWith ( prefix ) && input . endsWith ( suffix )
56- }
57-
58- /**
59- * Lazily load the fs module to avoid Webpack errors.
60- * Uses non-'node:' prefixed require to prevent Webpack bundling issues.
61- *
62- * @private
63- */
64- /*@__NO_SIDE_EFFECTS__ */
65- function getFs ( ) {
66- if ( _fs === undefined ) {
67- // Use non-'node:' prefixed require to avoid Webpack errors.
68-
69- _fs = /*@__PURE__ */ require ( 'fs' )
70- }
71- return _fs as typeof import ( 'node:fs' )
72- }
73-
74- /**
75- * Lazily load the path module to avoid Webpack errors.
76- * Uses non-'node:' prefixed require to prevent Webpack bundling issues.
77- *
78- * @returns The Node.js path module
79- * @private
80- */
81- /*@__NO_SIDE_EFFECTS__ */
82- function getPath ( ) {
83- if ( _path === undefined ) {
84- // Use non-'node:' prefixed require to avoid Webpack errors.
85-
86- _path = /*@__PURE__ */ require ( 'path' )
87- }
88- return _path as typeof import ( 'node:path' )
89- }
90-
9113/**
9214 * Pattern for matching release assets.
9315 * Can be either:
@@ -151,6 +73,20 @@ export interface RepoConfig {
15173 repo : string
15274}
15375
76+ /**
77+ * Retry configuration for GitHub API requests.
78+ * Uses exponential backoff to handle transient failures and rate limiting.
79+ */
80+ const RETRY_CONFIG = Object . freeze ( {
81+ __proto__ : null ,
82+ // Exponential backoff: delay doubles with each retry (5s, 10s, 20s).
83+ backoffFactor : 2 ,
84+ // Initial delay before first retry.
85+ baseDelayMs : 5000 ,
86+ // Maximum number of retry attempts (excluding initial request).
87+ retries : 2 ,
88+ } )
89+
15490/**
15591 * Socket-btm GitHub repository configuration.
15692 */
@@ -159,6 +95,179 @@ export const SOCKET_BTM_REPO = {
15995 repo : 'socket-btm' ,
16096} as const
16197
98+ const logger = getDefaultLogger ( )
99+
100+ let _fs : typeof import ( 'node:fs' ) | undefined
101+ let _path : typeof import ( 'node:path' ) | undefined
102+
103+ /**
104+ * Lazily load the fs module to avoid Webpack errors.
105+ * Uses non-'node:' prefixed require to prevent Webpack bundling issues.
106+ *
107+ * @private
108+ */
109+ /*@__NO_SIDE_EFFECTS__ */
110+ function getFs ( ) {
111+ if ( _fs === undefined ) {
112+ // Use non-'node:' prefixed require to avoid Webpack errors.
113+
114+ _fs = /*@__PURE__ */ require ( 'fs' )
115+ }
116+ return _fs as typeof import ( 'node:fs' )
117+ }
118+
119+ /**
120+ * Lazily load the path module to avoid Webpack errors.
121+ * Uses non-'node:' prefixed require to prevent Webpack bundling issues.
122+ *
123+ * @returns The Node.js path module
124+ * @private
125+ */
126+ /*@__NO_SIDE_EFFECTS__ */
127+ function getPath ( ) {
128+ if ( _path === undefined ) {
129+ // Use non-'node:' prefixed require to avoid Webpack errors.
130+
131+ _path = /*@__PURE__ */ require ( 'path' )
132+ }
133+ return _path as typeof import ( 'node:path' )
134+ }
135+
136+ /**
137+ * Create a matcher function for a pattern using picomatch for glob patterns
138+ * or simple prefix/suffix matching for object patterns.
139+ *
140+ * @param pattern - Pattern to match (string glob, prefix/suffix object, or RegExp)
141+ * @returns Function that tests if a string matches the pattern
142+ */
143+ export function createMatcher (
144+ pattern : string | { prefix : string ; suffix : string } | RegExp ,
145+ ) : ( input : string ) => boolean {
146+ if ( typeof pattern === 'string' ) {
147+ // Use picomatch for glob pattern matching.
148+ const isMatch = picomatch ( pattern )
149+ return ( input : string ) => isMatch ( input )
150+ }
151+
152+ if ( pattern instanceof RegExp ) {
153+ return ( input : string ) => pattern . test ( input )
154+ }
155+
156+ // Prefix/suffix object pattern (backward compatible).
157+ const { prefix, suffix } = pattern
158+ return ( input : string ) => input . startsWith ( prefix ) && input . endsWith ( suffix )
159+ }
160+
161+ /**
162+ * Download a binary from any GitHub repository with version caching.
163+ *
164+ * @param config - Download configuration
165+ * @returns Path to the downloaded binary
166+ */
167+ export async function downloadGitHubRelease (
168+ config : DownloadGitHubReleaseConfig ,
169+ ) : Promise < string > {
170+ const {
171+ assetName,
172+ binaryName,
173+ cwd = process . cwd ( ) ,
174+ downloadDir = 'build/downloaded' ,
175+ owner,
176+ platformArch,
177+ quiet = false ,
178+ removeMacOSQuarantine = true ,
179+ repo,
180+ tag : explicitTag ,
181+ toolName,
182+ toolPrefix,
183+ } = config
184+
185+ // Get release tag (either explicit or latest).
186+ let tag : string
187+ if ( explicitTag ) {
188+ tag = explicitTag
189+ } else if ( toolPrefix ) {
190+ const latestTag = await getLatestRelease (
191+ toolPrefix ,
192+ { owner, repo } ,
193+ { quiet } ,
194+ )
195+ if ( ! latestTag ) {
196+ throw new Error ( `No ${ toolPrefix } release found in ${ owner } /${ repo } ` )
197+ }
198+ tag = latestTag
199+ } else {
200+ throw new Error ( 'Either toolPrefix or tag must be provided' )
201+ }
202+
203+ const path = getPath ( )
204+ // Resolve download directory (can be absolute or relative to cwd).
205+ const resolvedDownloadDir = path . isAbsolute ( downloadDir )
206+ ? downloadDir
207+ : path . join ( cwd , downloadDir )
208+
209+ // Build download paths following socket-cli pattern.
210+ const binaryDir = path . join ( resolvedDownloadDir , toolName , platformArch )
211+ const binaryPath = path . join ( binaryDir , binaryName )
212+ const versionPath = path . join ( binaryDir , '.version' )
213+
214+ // Check if already downloaded.
215+ const fs = getFs ( )
216+ if ( fs . existsSync ( versionPath ) && fs . existsSync ( binaryPath ) ) {
217+ const cachedVersion = (
218+ await fs . promises . readFile ( versionPath , 'utf8' )
219+ ) . trim ( )
220+ if ( cachedVersion === tag ) {
221+ if ( ! quiet ) {
222+ logger . info ( `Using cached ${ toolName } (${ platformArch } ): ${ binaryPath } ` )
223+ }
224+ return binaryPath
225+ }
226+ }
227+
228+ // Download the asset.
229+ if ( ! quiet ) {
230+ logger . info ( `Downloading ${ toolName } for ${ platformArch } ...` )
231+ }
232+ await downloadReleaseAsset (
233+ tag ,
234+ assetName ,
235+ binaryPath ,
236+ { owner, repo } ,
237+ { quiet } ,
238+ )
239+
240+ // Make executable on Unix-like systems.
241+ const isWindows = binaryName . endsWith ( '.exe' )
242+ if ( ! isWindows ) {
243+ fs . chmodSync ( binaryPath , 0o755 )
244+
245+ // Remove macOS quarantine attribute if present (only on macOS host for macOS target).
246+ if (
247+ removeMacOSQuarantine &&
248+ process . platform === 'darwin' &&
249+ platformArch . startsWith ( 'darwin' )
250+ ) {
251+ try {
252+ await spawn ( 'xattr' , [ '-d' , 'com.apple.quarantine' , binaryPath ] , {
253+ stdio : 'ignore' ,
254+ } )
255+ } catch {
256+ // Ignore errors - attribute might not exist or xattr might not be available.
257+ }
258+ }
259+ }
260+
261+ // Write version file.
262+ await fs . promises . writeFile ( versionPath , tag , 'utf8' )
263+
264+ if ( ! quiet ) {
265+ logger . info ( `Downloaded ${ toolName } to ${ binaryPath } ` )
266+ }
267+
268+ return binaryPath
269+ }
270+
162271/**
163272 * Download a specific release asset.
164273 * Supports pattern matching for dynamic asset discovery.
@@ -404,113 +513,3 @@ export async function getReleaseAssetUrl(
404513 } ,
405514 )
406515}
407-
408- /**
409- * Download a binary from any GitHub repository with version caching.
410- *
411- * @param config - Download configuration
412- * @returns Path to the downloaded binary
413- */
414- export async function downloadGitHubRelease (
415- config : DownloadGitHubReleaseConfig ,
416- ) : Promise < string > {
417- const {
418- assetName,
419- binaryName,
420- cwd = process . cwd ( ) ,
421- downloadDir = 'build/downloaded' ,
422- owner,
423- platformArch,
424- quiet = false ,
425- removeMacOSQuarantine = true ,
426- repo,
427- tag : explicitTag ,
428- toolName,
429- toolPrefix,
430- } = config
431-
432- // Get release tag (either explicit or latest).
433- let tag : string
434- if ( explicitTag ) {
435- tag = explicitTag
436- } else if ( toolPrefix ) {
437- const latestTag = await getLatestRelease (
438- toolPrefix ,
439- { owner, repo } ,
440- { quiet } ,
441- )
442- if ( ! latestTag ) {
443- throw new Error ( `No ${ toolPrefix } release found in ${ owner } /${ repo } ` )
444- }
445- tag = latestTag
446- } else {
447- throw new Error ( 'Either toolPrefix or tag must be provided' )
448- }
449-
450- const path = getPath ( )
451- // Resolve download directory (can be absolute or relative to cwd).
452- const resolvedDownloadDir = path . isAbsolute ( downloadDir )
453- ? downloadDir
454- : path . join ( cwd , downloadDir )
455-
456- // Build download paths following socket-cli pattern.
457- const binaryDir = path . join ( resolvedDownloadDir , toolName , platformArch )
458- const binaryPath = path . join ( binaryDir , binaryName )
459- const versionPath = path . join ( binaryDir , '.version' )
460-
461- // Check if already downloaded.
462- const fs = getFs ( )
463- if ( fs . existsSync ( versionPath ) && fs . existsSync ( binaryPath ) ) {
464- const cachedVersion = (
465- await fs . promises . readFile ( versionPath , 'utf8' )
466- ) . trim ( )
467- if ( cachedVersion === tag ) {
468- if ( ! quiet ) {
469- logger . info ( `Using cached ${ toolName } (${ platformArch } ): ${ binaryPath } ` )
470- }
471- return binaryPath
472- }
473- }
474-
475- // Download the asset.
476- if ( ! quiet ) {
477- logger . info ( `Downloading ${ toolName } for ${ platformArch } ...` )
478- }
479- await downloadReleaseAsset (
480- tag ,
481- assetName ,
482- binaryPath ,
483- { owner, repo } ,
484- { quiet } ,
485- )
486-
487- // Make executable on Unix-like systems.
488- const isWindows = binaryName . endsWith ( '.exe' )
489- if ( ! isWindows ) {
490- fs . chmodSync ( binaryPath , 0o755 )
491-
492- // Remove macOS quarantine attribute if present (only on macOS host for macOS target).
493- if (
494- removeMacOSQuarantine &&
495- process . platform === 'darwin' &&
496- platformArch . startsWith ( 'darwin' )
497- ) {
498- try {
499- await spawn ( 'xattr' , [ '-d' , 'com.apple.quarantine' , binaryPath ] , {
500- stdio : 'ignore' ,
501- } )
502- } catch {
503- // Ignore errors - attribute might not exist or xattr might not be available.
504- }
505- }
506- }
507-
508- // Write version file.
509- await fs . promises . writeFile ( versionPath , tag , 'utf8' )
510-
511- if ( ! quiet ) {
512- logger . info ( `Downloaded ${ toolName } to ${ binaryPath } ` )
513- }
514-
515- return binaryPath
516- }
0 commit comments