@@ -20,7 +20,13 @@ import type {
2020 DlxPackageOptions ,
2121 DlxPackageResult ,
2222} from '@socketsecurity/lib/dlx/package'
23- import { npmPurl } from '@socketsecurity/lib/dlx/package'
23+ import {
24+ executePackage ,
25+ findBinaryPath ,
26+ makePackageBinsExecutable ,
27+ npmPurl ,
28+ resolveBinaryPath ,
29+ } from '@socketsecurity/lib/dlx/package'
2430import { runWithTempDir } from '../utils/temp-file-helper'
2531
2632describe ( 'dlx-package' , ( ) => {
@@ -896,4 +902,279 @@ describe('dlx-package', () => {
896902 }
897903 } )
898904 } )
905+
906+ describe ( 'findBinaryPath' , ( ) => {
907+ it ( 'returns the bin path when bin is a string' , ( ) => {
908+ runWithTempDir ( async tmpDir => {
909+ const pkgDir = path . join ( tmpDir , 'pkg' )
910+ const installedDir = path . join ( pkgDir , 'node_modules' , 'my-tool' )
911+ mkdirSync ( installedDir , { recursive : true } )
912+ writeFileSync (
913+ path . join ( installedDir , 'package.json' ) ,
914+ JSON . stringify ( { name : 'my-tool' , version : '1.0.0' , bin : 'cli.js' } ) ,
915+ )
916+ const result = findBinaryPath ( pkgDir , 'my-tool' )
917+ expect ( result ) . toContain ( 'cli.js' )
918+ expect ( result ) . toContain ( 'my-tool' )
919+ } )
920+ } )
921+
922+ it ( 'uses the single binary when bin is an object with one entry' , ( ) => {
923+ runWithTempDir ( async tmpDir => {
924+ const pkgDir = path . join ( tmpDir , 'pkg' )
925+ const installedDir = path . join ( pkgDir , 'node_modules' , 'pkg-a' )
926+ mkdirSync ( installedDir , { recursive : true } )
927+ writeFileSync (
928+ path . join ( installedDir , 'package.json' ) ,
929+ JSON . stringify ( {
930+ name : 'pkg-a' ,
931+ version : '1.0.0' ,
932+ bin : { 'only-one' : 'bin/main.js' } ,
933+ } ) ,
934+ )
935+ const result = findBinaryPath ( pkgDir , 'pkg-a' )
936+ expect ( result ) . toContain ( 'bin/main.js' )
937+ } )
938+ } )
939+
940+ it ( 'throws when no binary is declared' , ( ) => {
941+ runWithTempDir ( async tmpDir => {
942+ const pkgDir = path . join ( tmpDir , 'pkg' )
943+ const installedDir = path . join ( pkgDir , 'node_modules' , 'no-bins' )
944+ mkdirSync ( installedDir , { recursive : true } )
945+ writeFileSync (
946+ path . join ( installedDir , 'package.json' ) ,
947+ JSON . stringify ( { name : 'no-bins' , version : '1.0.0' } ) ,
948+ )
949+ expect ( ( ) => findBinaryPath ( pkgDir , 'no-bins' ) ) . toThrow (
950+ / N o b i n a r y f o u n d / ,
951+ )
952+ } )
953+ } )
954+
955+ it ( 'uses the binaryName option when bin is an object with multiple entries' , ( ) => {
956+ runWithTempDir ( async tmpDir => {
957+ const pkgDir = path . join ( tmpDir , 'pkg' )
958+ const installedDir = path . join ( pkgDir , 'node_modules' , 'multi' )
959+ mkdirSync ( installedDir , { recursive : true } )
960+ writeFileSync (
961+ path . join ( installedDir , 'package.json' ) ,
962+ JSON . stringify ( {
963+ name : 'multi' ,
964+ version : '1.0.0' ,
965+ bin : { 'tool-a' : 'a.js' , 'tool-b' : 'b.js' } ,
966+ } ) ,
967+ )
968+ const result = findBinaryPath ( pkgDir , 'multi' , 'tool-b' )
969+ expect ( result ) . toContain ( 'b.js' )
970+ } )
971+ } )
972+
973+ it ( 'falls back to last package-name segment for scoped packages' , ( ) => {
974+ runWithTempDir ( async tmpDir => {
975+ const pkgDir = path . join ( tmpDir , 'pkg' )
976+ const installedDir = path . join (
977+ pkgDir ,
978+ 'node_modules' ,
979+ '@socketsecurity' ,
980+ 'cli' ,
981+ )
982+ mkdirSync ( installedDir , { recursive : true } )
983+ writeFileSync (
984+ path . join ( installedDir , 'package.json' ) ,
985+ JSON . stringify ( {
986+ name : '@socketsecurity/cli' ,
987+ version : '1.0.0' ,
988+ bin : { socket : 'socket.js' , cli : 'cli.js' } ,
989+ } ) ,
990+ )
991+ const result = findBinaryPath ( pkgDir , '@socketsecurity/cli' )
992+ // Either npm's resolver picks one, or fallback finds 'cli'.
993+ expect ( result ) . toMatch ( / s o c k e t \. j s $ | c l i \. j s $ / )
994+ } )
995+ } )
996+ } )
997+
998+ describe ( 'makePackageBinsExecutable' , ( ) => {
999+ it ( 'is a no-op on Windows (returns without throwing)' , ( ) => {
1000+ runWithTempDir ( async tmpDir => {
1001+ // We can only assert non-throw for the current platform.
1002+ const pkgDir = path . join ( tmpDir , 'pkg' )
1003+ const installedDir = path . join ( pkgDir , 'node_modules' , 'pkg-x' )
1004+ mkdirSync ( installedDir , { recursive : true } )
1005+ writeFileSync (
1006+ path . join ( installedDir , 'package.json' ) ,
1007+ JSON . stringify ( {
1008+ name : 'pkg-x' ,
1009+ version : '1.0.0' ,
1010+ bin : 'bin.js' ,
1011+ } ) ,
1012+ )
1013+ // Function should complete without throwing regardless of platform.
1014+ expect ( ( ) => makePackageBinsExecutable ( pkgDir , 'pkg-x' ) ) . not . toThrow ( )
1015+ } )
1016+ } )
1017+
1018+ it ( 'chmods all binaries from object bin spec on Unix' , ( ) => {
1019+ if ( process . platform === 'win32' ) {
1020+ return
1021+ }
1022+ runWithTempDir ( async tmpDir => {
1023+ const pkgDir = path . join ( tmpDir , 'pkg' )
1024+ const installedDir = path . join ( pkgDir , 'node_modules' , 'multi-bin' )
1025+ mkdirSync ( installedDir , { recursive : true } )
1026+ writeFileSync (
1027+ path . join ( installedDir , 'package.json' ) ,
1028+ JSON . stringify ( {
1029+ name : 'multi-bin' ,
1030+ version : '1.0.0' ,
1031+ bin : { a : 'a.js' , b : 'b.js' } ,
1032+ } ) ,
1033+ )
1034+ // Create the binary files (without exec bits).
1035+ writeFileSync ( path . join ( installedDir , 'a.js' ) , '#!/usr/bin/env node\n' )
1036+ writeFileSync ( path . join ( installedDir , 'b.js' ) , '#!/usr/bin/env node\n' )
1037+ const fs = require ( 'node:fs' )
1038+ fs . chmodSync ( path . join ( installedDir , 'a.js' ) , 0o644 )
1039+ fs . chmodSync ( path . join ( installedDir , 'b.js' ) , 0o644 )
1040+
1041+ makePackageBinsExecutable ( pkgDir , 'multi-bin' )
1042+
1043+ const aMode = fs . statSync ( path . join ( installedDir , 'a.js' ) ) . mode & 0o777
1044+ const bMode = fs . statSync ( path . join ( installedDir , 'b.js' ) ) . mode & 0o777
1045+ expect ( aMode ) . toBe ( 0o755 )
1046+ expect ( bMode ) . toBe ( 0o755 )
1047+ } )
1048+ } )
1049+
1050+ it ( 'handles missing package.json gracefully' , ( ) => {
1051+ runWithTempDir ( async tmpDir => {
1052+ // No package.json, just an empty installed dir.
1053+ const pkgDir = path . join ( tmpDir , 'pkg' )
1054+ const installedDir = path . join ( pkgDir , 'node_modules' , 'missing' )
1055+ mkdirSync ( installedDir , { recursive : true } )
1056+ // Should not throw — function swallows errors.
1057+ expect ( ( ) => makePackageBinsExecutable ( pkgDir , 'missing' ) ) . not . toThrow ( )
1058+ } )
1059+ } )
1060+
1061+ it ( 'handles package.json without bin field' , ( ) => {
1062+ runWithTempDir ( async tmpDir => {
1063+ const pkgDir = path . join ( tmpDir , 'pkg' )
1064+ const installedDir = path . join ( pkgDir , 'node_modules' , 'no-bin' )
1065+ mkdirSync ( installedDir , { recursive : true } )
1066+ writeFileSync (
1067+ path . join ( installedDir , 'package.json' ) ,
1068+ JSON . stringify ( { name : 'no-bin' , version : '1.0.0' } ) ,
1069+ )
1070+ expect ( ( ) => makePackageBinsExecutable ( pkgDir , 'no-bin' ) ) . not . toThrow ( )
1071+ } )
1072+ } )
1073+
1074+ it ( 'handles single string bin field' , ( ) => {
1075+ if ( process . platform === 'win32' ) {
1076+ return
1077+ }
1078+ runWithTempDir ( async tmpDir => {
1079+ const pkgDir = path . join ( tmpDir , 'pkg' )
1080+ const installedDir = path . join ( pkgDir , 'node_modules' , 'single' )
1081+ mkdirSync ( installedDir , { recursive : true } )
1082+ writeFileSync (
1083+ path . join ( installedDir , 'package.json' ) ,
1084+ JSON . stringify ( {
1085+ name : 'single' ,
1086+ version : '1.0.0' ,
1087+ bin : 'cli.js' ,
1088+ } ) ,
1089+ )
1090+ writeFileSync (
1091+ path . join ( installedDir , 'cli.js' ) ,
1092+ '#!/usr/bin/env node\n' ,
1093+ )
1094+ const fs = require ( 'node:fs' )
1095+ fs . chmodSync ( path . join ( installedDir , 'cli.js' ) , 0o644 )
1096+ makePackageBinsExecutable ( pkgDir , 'single' )
1097+ const mode = fs . statSync ( path . join ( installedDir , 'cli.js' ) ) . mode & 0o777
1098+ expect ( mode ) . toBe ( 0o755 )
1099+ } )
1100+ } )
1101+
1102+ it ( 'skips chmod for non-existent binary files' , ( ) => {
1103+ if ( process . platform === 'win32' ) {
1104+ return
1105+ }
1106+ runWithTempDir ( async tmpDir => {
1107+ const pkgDir = path . join ( tmpDir , 'pkg' )
1108+ const installedDir = path . join ( pkgDir , 'node_modules' , 'ghost-bin' )
1109+ mkdirSync ( installedDir , { recursive : true } )
1110+ writeFileSync (
1111+ path . join ( installedDir , 'package.json' ) ,
1112+ JSON . stringify ( {
1113+ name : 'ghost-bin' ,
1114+ version : '1.0.0' ,
1115+ bin : 'does-not-exist.js' ,
1116+ } ) ,
1117+ )
1118+ // Should not throw even though the binary file doesn't exist.
1119+ expect ( ( ) =>
1120+ makePackageBinsExecutable ( pkgDir , 'ghost-bin' ) ,
1121+ ) . not . toThrow ( )
1122+ } )
1123+ } )
1124+ } )
1125+
1126+ describe ( 'resolveBinaryPath' , ( ) => {
1127+ it ( 'returns the path unchanged on Unix' , ( ) => {
1128+ if ( process . platform === 'win32' ) {
1129+ return
1130+ }
1131+ runWithTempDir ( async tmpDir => {
1132+ const file = path . join ( tmpDir , 'binary' )
1133+ writeFileSync ( file , '' )
1134+ expect ( resolveBinaryPath ( file ) ) . toBe ( file )
1135+ } )
1136+ } )
1137+
1138+ it ( 'returns base path when no wrapper exists on Windows' , ( ) => {
1139+ // Cannot fully exercise Windows path lookups on non-Windows; just
1140+ // verify the function doesn't throw given an arbitrary path.
1141+ const result = resolveBinaryPath ( '/nonexistent/path/binary' )
1142+ expect ( typeof result ) . toBe ( 'string' )
1143+ } )
1144+ } )
1145+
1146+ describe ( 'executePackage' , ( ) => {
1147+ it ( 'returns a spawn promise from a binary path' , async ( ) => {
1148+ // Use a real binary that should exist on every system.
1149+ const { promise } = ( ( ) => {
1150+ const promise = executePackage ( process . execPath , [
1151+ '-e' ,
1152+ 'process.exit(0)' ,
1153+ ] )
1154+ return { promise }
1155+ } ) ( )
1156+ const result = await promise
1157+ expect ( result . code ) . toBe ( 0 )
1158+ } )
1159+
1160+ it ( 'forwards args to the spawned process' , async ( ) => {
1161+ // Echo via node -p
1162+ const promise = executePackage ( process . execPath , [
1163+ '-p' ,
1164+ '"hello-from-execute"' ,
1165+ ] )
1166+ const result = await promise
1167+ expect ( String ( result . stdout ) ) . toContain ( 'hello-from-execute' )
1168+ } )
1169+
1170+ it ( 'passes spawn options through' , async ( ) => {
1171+ const promise = executePackage (
1172+ process . execPath ,
1173+ [ '-e' , 'process.stderr.write("err"); process.exit(0)' ] ,
1174+ { stdio : [ 'ignore' , 'ignore' , 'pipe' ] } ,
1175+ )
1176+ const result = await promise
1177+ expect ( String ( result . stderr ) ) . toContain ( 'err' )
1178+ } )
1179+ } )
8991180} )
0 commit comments