@@ -50,6 +50,185 @@ export interface RepoConfig {
5050 repo : string
5151}
5252
53+ /**
54+ * Configuration for downloading a GitHub release.
55+ */
56+ export interface DownloadGitHubReleaseConfig {
57+ /** GitHub repository owner/organization. */
58+ owner : string
59+ /** GitHub repository name. */
60+ repo : string
61+ /** Working directory (defaults to process.cwd()). */
62+ cwd ?: string
63+ /** Download destination directory. @default 'build/downloaded' */
64+ downloadDir ?: string
65+ /** Tool name for directory structure. */
66+ toolName : string
67+ /** Platform-arch identifier (e.g., 'linux-x64-musl'). */
68+ platformArch : string
69+ /** Binary filename (e.g., 'node', 'binject'). */
70+ binaryName : string
71+ /** Asset name on GitHub. */
72+ assetName : string
73+ /** Tool prefix for finding latest release. */
74+ toolPrefix ?: string
75+ /** Specific release tag to download. */
76+ tag ?: string
77+ /** Suppress log messages. @default false */
78+ quiet ?: boolean
79+ /** Remove macOS quarantine attribute after download. @default true */
80+ removeMacOSQuarantine ?: boolean
81+ }
82+
83+ /**
84+ * Download a binary from any GitHub repository with version caching.
85+ *
86+ * @param config - Download configuration
87+ * @returns Path to the downloaded binary
88+ */
89+ export async function downloadGitHubRelease (
90+ config : DownloadGitHubReleaseConfig ,
91+ ) : Promise < string > {
92+ const {
93+ assetName,
94+ binaryName,
95+ cwd = process . cwd ( ) ,
96+ downloadDir = 'build/downloaded' ,
97+ owner,
98+ platformArch,
99+ quiet = false ,
100+ removeMacOSQuarantine = true ,
101+ repo,
102+ tag : explicitTag ,
103+ toolName,
104+ toolPrefix,
105+ } = config
106+
107+ // Get release tag (either explicit or latest).
108+ let tag : string
109+ if ( explicitTag ) {
110+ tag = explicitTag
111+ } else if ( toolPrefix ) {
112+ const latestTag = await getLatestRelease (
113+ toolPrefix ,
114+ { owner, repo } ,
115+ { quiet } ,
116+ )
117+ if ( ! latestTag ) {
118+ throw new Error ( `No ${ toolPrefix } release found in ${ owner } /${ repo } ` )
119+ }
120+ tag = latestTag
121+ } else {
122+ throw new Error ( 'Either toolPrefix or tag must be provided' )
123+ }
124+
125+ // Resolve download directory (can be absolute or relative to cwd).
126+ const resolvedDownloadDir = path . isAbsolute ( downloadDir )
127+ ? downloadDir
128+ : path . join ( cwd , downloadDir )
129+
130+ // Build download paths following socket-cli pattern.
131+ const binaryDir = path . join ( resolvedDownloadDir , toolName , platformArch )
132+ const binaryPath = path . join ( binaryDir , binaryName )
133+ const versionPath = path . join ( binaryDir , '.version' )
134+
135+ // Check if already downloaded.
136+ if ( existsSync ( versionPath ) && existsSync ( binaryPath ) ) {
137+ const cachedVersion = ( await readFile ( versionPath , 'utf8' ) ) . trim ( )
138+ if ( cachedVersion === tag ) {
139+ if ( ! quiet ) {
140+ logger . info ( `Using cached ${ toolName } (${ platformArch } ): ${ binaryPath } ` )
141+ }
142+ return binaryPath
143+ }
144+ }
145+
146+ // Download the asset.
147+ if ( ! quiet ) {
148+ logger . info ( `Downloading ${ toolName } for ${ platformArch } ...` )
149+ }
150+ await downloadReleaseAsset (
151+ tag ,
152+ assetName ,
153+ binaryPath ,
154+ { owner, repo } ,
155+ { quiet } ,
156+ )
157+
158+ // Make executable on Unix-like systems.
159+ const isWindows = binaryName . endsWith ( '.exe' )
160+ if ( ! isWindows ) {
161+ chmodSync ( binaryPath , 0o755 )
162+
163+ // Remove macOS quarantine attribute if present (only on macOS host for macOS target).
164+ if (
165+ removeMacOSQuarantine &&
166+ process . platform === 'darwin' &&
167+ platformArch . startsWith ( 'darwin' )
168+ ) {
169+ try {
170+ await spawn ( 'xattr' , [ '-d' , 'com.apple.quarantine' , binaryPath ] , {
171+ stdio : 'ignore' ,
172+ } )
173+ } catch {
174+ // Ignore errors - attribute might not exist or xattr might not be available.
175+ }
176+ }
177+ }
178+
179+ // Write version file.
180+ await writeFile ( versionPath , tag , 'utf8' )
181+
182+ if ( ! quiet ) {
183+ logger . info ( `Downloaded ${ toolName } to ${ binaryPath } ` )
184+ }
185+
186+ return binaryPath
187+ }
188+
189+ /**
190+ * Download a specific release asset.
191+ *
192+ * @param tag - Release tag name
193+ * @param assetName - Asset name to download
194+ * @param outputPath - Path to write the downloaded file
195+ * @param repoConfig - Repository configuration (owner/repo)
196+ * @param options - Additional options
197+ */
198+ export async function downloadReleaseAsset (
199+ tag : string ,
200+ assetName : string ,
201+ outputPath : string ,
202+ repoConfig : RepoConfig ,
203+ options : { quiet ?: boolean } = { } ,
204+ ) : Promise < void > {
205+ const { owner, repo } = repoConfig
206+ const { quiet = false } = options
207+
208+ // Get the browser_download_url for the asset.
209+ const downloadUrl = await getReleaseAssetUrl (
210+ tag ,
211+ assetName ,
212+ { owner, repo } ,
213+ { quiet } ,
214+ )
215+
216+ if ( ! downloadUrl ) {
217+ throw new Error ( `Asset ${ assetName } not found in release ${ tag } ` )
218+ }
219+
220+ // Create output directory.
221+ await safeMkdir ( path . dirname ( outputPath ) )
222+
223+ // Download using httpDownload which supports redirects and retries.
224+ await httpDownload ( downloadUrl , outputPath , {
225+ logger : quiet ? undefined : logger ,
226+ progressInterval : 10 ,
227+ retries : 2 ,
228+ retryDelay : 5000 ,
229+ } )
230+ }
231+
53232/**
54233 * Get GitHub authentication headers if token is available.
55234 * Checks GH_TOKEN or GITHUB_TOKEN environment variables.
@@ -198,182 +377,3 @@ export async function getReleaseAssetUrl(
198377 } ,
199378 )
200379}
201-
202- /**
203- * Download a specific release asset.
204- *
205- * @param tag - Release tag name
206- * @param assetName - Asset name to download
207- * @param outputPath - Path to write the downloaded file
208- * @param repoConfig - Repository configuration (owner/repo)
209- * @param options - Additional options
210- */
211- export async function downloadReleaseAsset (
212- tag : string ,
213- assetName : string ,
214- outputPath : string ,
215- repoConfig : RepoConfig ,
216- options : { quiet ?: boolean } = { } ,
217- ) : Promise < void > {
218- const { owner, repo } = repoConfig
219- const { quiet = false } = options
220-
221- // Get the browser_download_url for the asset.
222- const downloadUrl = await getReleaseAssetUrl (
223- tag ,
224- assetName ,
225- { owner, repo } ,
226- { quiet } ,
227- )
228-
229- if ( ! downloadUrl ) {
230- throw new Error ( `Asset ${ assetName } not found in release ${ tag } ` )
231- }
232-
233- // Create output directory.
234- await safeMkdir ( path . dirname ( outputPath ) )
235-
236- // Download using httpDownload which supports redirects and retries.
237- await httpDownload ( downloadUrl , outputPath , {
238- logger : quiet ? undefined : logger ,
239- progressInterval : 10 ,
240- retries : 2 ,
241- retryDelay : 5000 ,
242- } )
243- }
244-
245- /**
246- * Configuration for downloading a GitHub release.
247- */
248- export interface DownloadGitHubReleaseConfig {
249- /** GitHub repository owner/organization. */
250- owner : string
251- /** GitHub repository name. */
252- repo : string
253- /** Working directory (defaults to process.cwd()). */
254- cwd ?: string
255- /** Download destination directory. @default 'build/downloaded' */
256- downloadDir ?: string
257- /** Tool name for directory structure. */
258- toolName : string
259- /** Platform-arch identifier (e.g., 'linux-x64-musl'). */
260- platformArch : string
261- /** Binary filename (e.g., 'node', 'binject'). */
262- binaryName : string
263- /** Asset name on GitHub. */
264- assetName : string
265- /** Tool prefix for finding latest release. */
266- toolPrefix ?: string
267- /** Specific release tag to download. */
268- tag ?: string
269- /** Suppress log messages. @default false */
270- quiet ?: boolean
271- /** Remove macOS quarantine attribute after download. @default true */
272- removeMacOSQuarantine ?: boolean
273- }
274-
275- /**
276- * Download a binary from any GitHub repository with version caching.
277- *
278- * @param config - Download configuration
279- * @returns Path to the downloaded binary
280- */
281- export async function downloadGitHubRelease (
282- config : DownloadGitHubReleaseConfig ,
283- ) : Promise < string > {
284- const {
285- assetName,
286- binaryName,
287- cwd = process . cwd ( ) ,
288- downloadDir = 'build/downloaded' ,
289- owner,
290- platformArch,
291- quiet = false ,
292- removeMacOSQuarantine = true ,
293- repo,
294- tag : explicitTag ,
295- toolName,
296- toolPrefix,
297- } = config
298-
299- // Get release tag (either explicit or latest).
300- let tag : string
301- if ( explicitTag ) {
302- tag = explicitTag
303- } else if ( toolPrefix ) {
304- const latestTag = await getLatestRelease (
305- toolPrefix ,
306- { owner, repo } ,
307- { quiet } ,
308- )
309- if ( ! latestTag ) {
310- throw new Error ( `No ${ toolPrefix } release found in ${ owner } /${ repo } ` )
311- }
312- tag = latestTag
313- } else {
314- throw new Error ( 'Either toolPrefix or tag must be provided' )
315- }
316-
317- // Resolve download directory (can be absolute or relative to cwd).
318- const resolvedDownloadDir = path . isAbsolute ( downloadDir )
319- ? downloadDir
320- : path . join ( cwd , downloadDir )
321-
322- // Build download paths following socket-cli pattern.
323- const binaryDir = path . join ( resolvedDownloadDir , toolName , platformArch )
324- const binaryPath = path . join ( binaryDir , binaryName )
325- const versionPath = path . join ( binaryDir , '.version' )
326-
327- // Check if already downloaded.
328- if ( existsSync ( versionPath ) && existsSync ( binaryPath ) ) {
329- const cachedVersion = ( await readFile ( versionPath , 'utf8' ) ) . trim ( )
330- if ( cachedVersion === tag ) {
331- if ( ! quiet ) {
332- logger . info ( `Using cached ${ toolName } (${ platformArch } ): ${ binaryPath } ` )
333- }
334- return binaryPath
335- }
336- }
337-
338- // Download the asset.
339- if ( ! quiet ) {
340- logger . info ( `Downloading ${ toolName } for ${ platformArch } ...` )
341- }
342- await downloadReleaseAsset (
343- tag ,
344- assetName ,
345- binaryPath ,
346- { owner, repo } ,
347- { quiet } ,
348- )
349-
350- // Make executable on Unix-like systems.
351- const isWindows = binaryName . endsWith ( '.exe' )
352- if ( ! isWindows ) {
353- chmodSync ( binaryPath , 0o755 )
354-
355- // Remove macOS quarantine attribute if present (only on macOS host for macOS target).
356- if (
357- removeMacOSQuarantine &&
358- process . platform === 'darwin' &&
359- platformArch . startsWith ( 'darwin' )
360- ) {
361- try {
362- await spawn ( 'xattr' , [ '-d' , 'com.apple.quarantine' , binaryPath ] , {
363- stdio : 'ignore' ,
364- } )
365- } catch {
366- // Ignore errors - attribute might not exist or xattr might not be available.
367- }
368- }
369- }
370-
371- // Write version file.
372- await writeFile ( versionPath , tag , 'utf8' )
373-
374- if ( ! quiet ) {
375- logger . info ( `Downloaded ${ toolName } to ${ binaryPath } ` )
376- }
377-
378- return binaryPath
379- }
0 commit comments