@@ -245,19 +245,135 @@ const getBinaries = (dir) => {
245245 if ( appDirectories . some ( ( app ) => f . startsWith ( app + "/" ) ) ) {
246246 return false ;
247247 }
248- return execSync ( `file ${ JSON . stringify ( path . resolve ( dir , f ) ) } ` ) . includes (
249- "executable"
250- ) ;
248+
249+ const filePath = path . resolve ( dir , f ) ;
250+ const fileOutput = execSync ( `file ${ JSON . stringify ( filePath ) } ` ) . toString ( ) ;
251+
252+ // Check if it's an executable binary or script
253+ return fileOutput . includes ( "executable" ) ||
254+ fileOutput . includes ( "ELF" ) ||
255+ fileOutput . includes ( "Mach-O" ) ||
256+ fileOutput . includes ( "script" ) ;
251257 } catch {
252258 return false ;
253259 }
254260 } ;
255261
256- binaries . push ( ...allFiles . filter ( isExecutable ) ) ;
262+ // Also check for files that look like binaries based on naming patterns
263+ const looksLikeBinary = ( f ) => {
264+ const filename = path . basename ( f ) . toLowerCase ( ) ;
265+ const dirname = path . dirname ( f ) . toLowerCase ( ) ;
266+
267+ // Skip files in common non-binary directories
268+ if ( dirname . includes ( "doc" ) || dirname . includes ( "docs" ) || dirname . includes ( "man" ) ||
269+ dirname . includes ( "runtime" ) || dirname . includes ( "lib" ) || dirname . includes ( "share" ) ) {
270+ return false ;
271+ }
272+
273+ // Skip obvious non-binary files
274+ if ( filename . includes ( "readme" ) || filename . includes ( "license" ) ||
275+ filename . includes ( "changelog" ) || filename . includes ( "copying" ) ||
276+ filename . includes ( "install" ) || filename . includes ( "makefile" ) ||
277+ filename . includes ( ".md" ) || filename . includes ( ".txt" ) ||
278+ filename . includes ( ".1" ) || filename . includes ( ".json" ) ||
279+ filename . includes ( ".yaml" ) || filename . includes ( ".yml" ) ||
280+ filename . includes ( ".toml" ) || filename . includes ( ".cfg" ) ||
281+ filename . includes ( ".conf" ) || filename . includes ( ".ini" ) ) {
282+ return false ;
283+ }
284+
285+ // Common binary naming patterns
286+ const binaryPatterns = [
287+ / ^ [ a - z 0 - 9 _ - ] + $ / , // Simple name like hx, git, node
288+ / ^ [ a - z 0 - 9 _ - ] + \. ( e x e | b i n | r u n ) $ / , // Extensions
289+ ] ;
290+
291+ return binaryPatterns . some ( pattern => pattern . test ( filename ) ) ;
292+ } ;
293+
294+ const executableFiles = allFiles . filter ( isExecutable ) ;
295+ const potentialBinaries = allFiles . filter ( looksLikeBinary ) ;
296+
297+ // Combine and remove duplicates
298+ binaries . push ( ...executableFiles ) ;
299+ binaries . push ( ...potentialBinaries . filter ( f => ! executableFiles . includes ( f ) ) ) ;
257300
258301 return [ ...new Set ( binaries ) ] ; // Remove duplicates
259302} ;
260303
304+ /**
305+ * Prompt user to choose from multiple binaries found in a package
306+ * @param {string[] } binaries - List of binary files found
307+ * @param {string } packageName - Name of the package being installed
308+ * @param {Object } logger - Logger instance
309+ * @returns {Promise<string[]> } Selected binaries
310+ */
311+ const selectBinaries = async ( binaries , packageName , logger = null ) => {
312+ if ( binaries . length <= 1 ) {
313+ return binaries ;
314+ }
315+
316+ // Filter out obvious non-binaries for cleaner selection
317+ const filteredBinaries = binaries . filter ( f => {
318+ const filename = path . basename ( f ) . toLowerCase ( ) ;
319+ return ! filename . includes ( 'readme' ) &&
320+ ! filename . includes ( 'license' ) &&
321+ ! filename . includes ( 'changelog' ) &&
322+ ! filename . includes ( '.md' ) &&
323+ ! filename . includes ( '.txt' ) &&
324+ ! filename . includes ( '.1' ) && // man pages
325+ ! f . includes ( 'doc/' ) &&
326+ ! f . includes ( 'docs/' ) &&
327+ ! f . includes ( 'man/' ) ;
328+ } ) ;
329+
330+ // If filtering leaves us with just one, use it
331+ if ( filteredBinaries . length === 1 ) {
332+ if ( logger ) {
333+ logger . log ( `Auto-selected binary: ${ filteredBinaries [ 0 ] } ` ) ;
334+ }
335+ return filteredBinaries ;
336+ }
337+
338+ // If filtering leaves none, fall back to original list
339+ const binariesToChoose = filteredBinaries . length > 0 ? filteredBinaries : binaries ;
340+
341+ if ( logger ) {
342+ logger . log ( `Multiple binaries found in ${ packageName } . Please choose which to install:` ) ;
343+ binariesToChoose . forEach ( ( binary , index ) => {
344+ logger . log ( ` ${ index + 1 } . ${ binary } ` ) ;
345+ } ) ;
346+ }
347+
348+ const readline = require ( 'readline' ) ;
349+ const rli = readline . createInterface ( {
350+ input : process . stdin ,
351+ output : process . stdout ,
352+ } ) ;
353+
354+ const choice = await new Promise ( ( resolve ) => {
355+ rli . question ( `Enter binary number to install (1-${ binariesToChoose . length } ), or 'all' for all: ` , ( ans ) => {
356+ rli . close ( ) ;
357+ resolve ( ans . trim ( ) . toLowerCase ( ) ) ;
358+ } ) ;
359+ } ) ;
360+
361+ if ( choice === 'all' ) {
362+ return binariesToChoose ;
363+ }
364+
365+ const numericChoice = parseInt ( choice ) ;
366+ if ( numericChoice >= 1 && numericChoice <= binariesToChoose . length ) {
367+ return [ binariesToChoose [ numericChoice - 1 ] ] ;
368+ }
369+
370+ // Default to first binary if invalid choice
371+ if ( logger ) {
372+ logger . log ( `Invalid choice, installing first binary: ${ binariesToChoose [ 0 ] } ` ) ;
373+ }
374+ return [ binariesToChoose [ 0 ] ] ;
375+ } ;
376+
261377// Use extractName from config for consistent name normalization
262378
263379/**
@@ -328,8 +444,9 @@ const processExtractedPackages = async (
328444 } ;
329445 } else if ( dmgBinaries . length > 0 ) {
330446 logger . log ( "No .app or .pkg found in DMG, trying to install executables" ) ;
447+ const selectedBinaries = await selectBinaries ( dmgBinaries , selectedName , logger ) ;
331448 const destinations = await installBinaries (
332- dmgBinaries ,
449+ selectedBinaries ,
333450 mountDir ,
334451 selectedName ,
335452 checkPathFn ,
@@ -339,7 +456,7 @@ const processExtractedPackages = async (
339456 return {
340457 method : "dmg_binaries" ,
341458 destinations,
342- binaries : dmgBinaries . map ( ( bin ) => path . basename ( bin ) ) ,
459+ binaries : selectedBinaries . map ( ( bin ) => path . basename ( bin ) ) ,
343460 } ;
344461 } else {
345462 throw new Error ( "No installable files found in DMG" ) ;
@@ -517,6 +634,7 @@ module.exports = {
517634 selectBestAsset,
518635 extractArchive,
519636 getBinaries,
637+ selectBinaries,
520638 processExtractedPackages,
521639 installApp,
522640 installPkg,
0 commit comments