1+ /**
2+ * @fileoverview NPM postinstall script for Socket CLI binary distribution.
3+ * Downloads the appropriate platform-specific binary from GitHub releases.
4+ * This runs when users install the `socket` npm package.
5+ */
6+
7+ import { createWriteStream , existsSync } from 'node:fs'
8+ import { chmod , rename , unlink } from 'node:fs/promises'
9+ import https from 'node:https'
10+ import os from 'node:os'
11+ import path from 'node:path'
12+ import { fileURLToPath } from 'node:url'
13+
14+ const __filename = fileURLToPath ( import . meta. url )
15+ const __dirname = path . dirname ( __filename )
16+
17+ // Binary naming constants
18+ const BINARY_NAME = 'socket'
19+ const GITHUB_ORG = 'SocketDev'
20+ const GITHUB_REPO = 'socket-cli'
21+
22+ // Map Node.js platform/arch to our binary naming convention
23+ const PLATFORM_MAP : Record < string , string > = {
24+ darwin : 'darwin' , // macOS
25+ linux : 'linux' ,
26+ win32 : 'win32' // Windows
27+ }
28+
29+ const ARCH_MAP : Record < string , string > = {
30+ arm64 : 'arm64' ,
31+ x64 : 'x64'
32+ }
33+
34+ /**
35+ * Get the binary filename for the current platform.
36+ */
37+ function getBinaryName ( ) : string {
38+ const platform = PLATFORM_MAP [ os . platform ( ) ]
39+ const arch = ARCH_MAP [ os . arch ( ) ]
40+
41+ if ( ! platform || ! arch ) {
42+ throw new Error (
43+ `Unsupported platform: ${ os . platform ( ) } ${ os . arch ( ) } . ` +
44+ `Please install @socketsecurity/cli directly instead.`
45+ )
46+ }
47+
48+ const extension = os . platform ( ) === 'win32' ? '.exe' : ''
49+ return `socket-${ platform } -${ arch } ${ extension } `
50+ }
51+
52+ /**
53+ * Get package version from package.json.
54+ */
55+ async function getPackageVersion ( ) : Promise < string > {
56+ // In production, this will be in npm-package/package.json
57+ const packagePath = path . join ( __dirname , '..' , '..' , 'npm-package' , 'package.json' )
58+ if ( existsSync ( packagePath ) ) {
59+ const { default : pkg } = await import ( packagePath , { with : { type : 'json' } } )
60+ return pkg . version
61+ }
62+
63+ // Fallback for development
64+ const devPackagePath = path . join ( __dirname , '..' , '..' , '..' , 'package.json' )
65+ const { default : pkg } = await import ( devPackagePath , { with : { type : 'json' } } )
66+ return pkg . version
67+ }
68+
69+ /**
70+ * Download a file from a URL with redirect handling.
71+ */
72+ async function downloadFile ( url : string , destPath : string ) : Promise < void > {
73+ return new Promise ( ( resolve , reject ) => {
74+ const file = createWriteStream ( destPath )
75+
76+ const request = https . get (
77+ url ,
78+ {
79+ headers : {
80+ 'User-Agent' : 'socket-cli-installer'
81+ }
82+ } ,
83+ response => {
84+ // Handle redirects
85+ if ( response . statusCode === 301 || response . statusCode === 302 ) {
86+ const redirectUrl = response . headers . location
87+ if ( ! redirectUrl ) {
88+ file . close ( )
89+ unlink ( destPath ) . catch ( ( ) => { } )
90+ reject ( new Error ( 'Redirect without location header' ) )
91+ return
92+ }
93+
94+ file . close ( )
95+ downloadFile ( redirectUrl , destPath ) . then ( resolve , reject )
96+ return
97+ }
98+
99+ // Check for successful response
100+ if ( response . statusCode !== 200 ) {
101+ file . close ( )
102+ unlink ( destPath ) . catch ( ( ) => { } )
103+ reject ( new Error (
104+ `Failed to download binary: HTTP ${ response . statusCode } `
105+ ) )
106+ return
107+ }
108+
109+ // Pipe response to file
110+ response . pipe ( file )
111+
112+ file . on ( 'finish' , ( ) => {
113+ file . close ( ( ) => resolve ( ) )
114+ } )
115+ }
116+ )
117+
118+ request . on ( 'error' , err => {
119+ file . close ( )
120+ unlink ( destPath ) . catch ( ( ) => { } )
121+ reject ( err )
122+ } )
123+ } )
124+ }
125+
126+ /**
127+ * Get the download URL for the binary.
128+ */
129+ async function getBinaryUrl ( ) : Promise < string > {
130+ const version = await getPackageVersion ( )
131+ const binaryName = getBinaryName ( )
132+
133+ // GitHub releases URL pattern
134+ return `https://github.com/${ GITHUB_ORG } /${ GITHUB_REPO } /releases/download/v${ version } /${ binaryName } `
135+ }
136+
137+ /**
138+ * Install the platform-specific binary.
139+ */
140+ async function install ( ) : Promise < void > {
141+ try {
142+ const binaryName = getBinaryName ( )
143+ const targetName = BINARY_NAME + ( os . platform ( ) === 'win32' ? '.exe' : '' )
144+ const binaryPath = path . join ( __dirname , '..' , '..' , 'npm-package' , targetName )
145+
146+ // For development, use local path
147+ const devBinaryPath = path . join ( __dirname , targetName )
148+ const finalPath = existsSync ( path . dirname ( binaryPath ) ) ? binaryPath : devBinaryPath
149+
150+ // Check if binary already exists
151+ if ( existsSync ( finalPath ) ) {
152+ console . log ( 'Socket CLI binary already installed.' )
153+ return
154+ }
155+
156+ console . log ( `Downloading Socket CLI for ${ os . platform ( ) } -${ os . arch ( ) } ...` )
157+
158+ const url = await getBinaryUrl ( )
159+ const tempPath = `${ finalPath } .download`
160+
161+ // Download the binary
162+ await downloadFile ( url , tempPath )
163+
164+ // Make executable on Unix-like systems
165+ if ( os . platform ( ) !== 'win32' ) {
166+ await chmod ( tempPath , 0o755 )
167+ }
168+
169+ // Atomic rename to final location
170+ await rename ( tempPath , finalPath )
171+
172+ console . log ( '✓ Socket CLI installed successfully!' )
173+ console . log ( ` Binary: ${ finalPath } ` )
174+ console . log ( ` Run 'socket --help' to get started.` )
175+ } catch ( error ) {
176+ const message = error instanceof Error ? error . message : String ( error )
177+
178+ console . error ( `Failed to install Socket CLI binary: ${ message } ` )
179+ console . error ( '' )
180+ console . error ( 'You can try:' )
181+ console . error ( ' 1. Installing from source: npm install -g @socketsecurity/cli' )
182+ console . error ( ' 2. Downloading manually from: https://github.com/SocketDev/socket-cli/releases' )
183+ console . error ( '' )
184+ console . error ( 'For help, visit: https://github.com/SocketDev/socket-cli/issues' )
185+
186+ // Don't fail the npm install - allow fallback to source
187+ process . exitCode = 0
188+ }
189+ }
190+
191+ // Run if this is the main module
192+ if ( import . meta. url === `file://${ process . argv [ 1 ] } ` ) {
193+ install ( ) . catch ( error => {
194+ console . error ( 'Unexpected error:' , error )
195+ process . exitCode = 0 // Still don't fail npm install
196+ } )
197+ }
198+
199+ export { install , getBinaryName , getBinaryUrl }
0 commit comments