1
+ #!/usr/bin/env tsx
2
+
3
+ import * as https from 'node:https' ;
4
+ import * as fs from 'node:fs' ;
5
+ import * as path from 'node:path' ;
6
+ import { execSync } from 'node:child_process' ;
7
+ import { pipeline } from 'node:stream/promises' ;
8
+ import { createWriteStream , mkdirSync , chmodSync } from 'node:fs' ;
9
+
10
+ // Node.js version to download
11
+ const NODE_VERSION = '22.17.0' ;
12
+
13
+ // Platform/arch types
14
+ type Platform = 'darwin' | 'win32' | 'linux' ;
15
+ type Architecture = 'arm64' | 'x64' ;
16
+
17
+ interface PlatformConfig {
18
+ platform : Platform ;
19
+ arch : Architecture ;
20
+ url : string ;
21
+ binary : string ;
22
+ }
23
+
24
+ // Platform configurations
25
+ const PLATFORMS : PlatformConfig [ ] = [
26
+ {
27
+ platform : 'darwin' ,
28
+ arch : 'arm64' ,
29
+ url : `https://nodejs.org/dist/v${ NODE_VERSION } /node-v${ NODE_VERSION } -darwin-arm64.tar.gz` ,
30
+ binary : 'bin/node'
31
+ } ,
32
+ {
33
+ platform : 'darwin' ,
34
+ arch : 'x64' ,
35
+ url : `https://nodejs.org/dist/v${ NODE_VERSION } /node-v${ NODE_VERSION } -darwin-x64.tar.gz` ,
36
+ binary : 'bin/node'
37
+ } ,
38
+ {
39
+ platform : 'win32' ,
40
+ arch : 'x64' ,
41
+ url : `https://nodejs.org/dist/v${ NODE_VERSION } /node-v${ NODE_VERSION } -win-x64.zip` ,
42
+ binary : 'node.exe'
43
+ } ,
44
+ {
45
+ platform : 'linux' ,
46
+ arch : 'x64' ,
47
+ url : `https://nodejs.org/dist/v${ NODE_VERSION } /node-v${ NODE_VERSION } -linux-x64.tar.gz` ,
48
+ binary : 'bin/node'
49
+ }
50
+ ] ;
51
+
52
+ const RESOURCES_DIR = path . join ( __dirname , '..' , 'resources' , 'node-binaries' ) ;
53
+
54
+ // Parse command line arguments
55
+ const args = process . argv . slice ( 2 ) ;
56
+ const downloadAll = args . includes ( '--all' ) ;
57
+
58
+ async function downloadFile ( url : string , dest : string ) : Promise < void > {
59
+ return new Promise ( ( resolve , reject ) => {
60
+ const file = createWriteStream ( dest ) ;
61
+
62
+ https . get ( url , ( response ) => {
63
+ if ( response . statusCode === 302 || response . statusCode === 301 ) {
64
+ // Handle redirect
65
+ const redirectUrl = response . headers . location ;
66
+ if ( ! redirectUrl ) {
67
+ reject ( new Error ( 'Redirect without location header' ) ) ;
68
+ return ;
69
+ }
70
+ https . get ( redirectUrl , async ( redirectResponse ) => {
71
+ if ( redirectResponse . statusCode !== 200 ) {
72
+ reject ( new Error ( `Failed to download: ${ redirectResponse . statusCode } ` ) ) ;
73
+ return ;
74
+ }
75
+
76
+ // Show download progress
77
+ const totalSize = parseInt ( redirectResponse . headers [ 'content-length' ] || '0' , 10 ) ;
78
+ let downloadedSize = 0 ;
79
+
80
+ redirectResponse . on ( 'data' , ( chunk ) => {
81
+ downloadedSize += chunk . length ;
82
+ if ( totalSize > 0 ) {
83
+ const percent = Math . round ( ( downloadedSize / totalSize ) * 100 ) ;
84
+ process . stdout . write ( `\r Downloading: ${ percent } %` ) ;
85
+ }
86
+ } ) ;
87
+
88
+ await pipeline ( redirectResponse , file ) ;
89
+ process . stdout . write ( '\n' ) ;
90
+ resolve ( ) ;
91
+ } ) . on ( 'error' , reject ) ;
92
+ } else if ( response . statusCode === 200 ) {
93
+ // Direct download
94
+ const totalSize = parseInt ( response . headers [ 'content-length' ] || '0' , 10 ) ;
95
+ let downloadedSize = 0 ;
96
+
97
+ response . on ( 'data' , ( chunk ) => {
98
+ downloadedSize += chunk . length ;
99
+ if ( totalSize > 0 ) {
100
+ const percent = Math . round ( ( downloadedSize / totalSize ) * 100 ) ;
101
+ process . stdout . write ( `\r Downloading: ${ percent } %` ) ;
102
+ }
103
+ } ) ;
104
+
105
+ pipeline ( response , file ) . then ( ( ) => {
106
+ process . stdout . write ( '\n' ) ;
107
+ resolve ( ) ;
108
+ } ) . catch ( reject ) ;
109
+ } else {
110
+ reject ( new Error ( `Failed to download: ${ response . statusCode } ` ) ) ;
111
+ }
112
+ } ) . on ( 'error' , reject ) ;
113
+ } ) ;
114
+ }
115
+
116
+ async function extractArchive ( archivePath : string , platform : Platform ) : Promise < string > {
117
+ const tempDir = path . join ( path . dirname ( archivePath ) , 'temp' ) ;
118
+ mkdirSync ( tempDir , { recursive : true } ) ;
119
+
120
+ console . log ( ' Extracting archive...' ) ;
121
+
122
+ if ( platform === 'win32' ) {
123
+ // Use unzip command (available on macOS) to extract zip files
124
+ execSync ( `unzip -q "${ archivePath } " -d "${ tempDir } "` , { stdio : 'inherit' } ) ;
125
+ } else {
126
+ // Use tar for Unix-like systems
127
+ execSync ( `tar -xzf "${ archivePath } " -C "${ tempDir } "` , { stdio : 'inherit' } ) ;
128
+ }
129
+
130
+ return tempDir ;
131
+ }
132
+
133
+ async function downloadNodeBinary ( config : PlatformConfig ) : Promise < void > {
134
+ const { platform, arch, url, binary } = config ;
135
+ const platformDir = path . join ( RESOURCES_DIR , `${ platform } -${ arch } ` ) ;
136
+ const binaryPath = path . join ( platformDir , platform === 'win32' ? 'node.exe' : 'node' ) ;
137
+
138
+ // Skip if already exists
139
+ if ( fs . existsSync ( binaryPath ) ) {
140
+ console . log ( `✓ ${ platform } -${ arch } binary already exists` ) ;
141
+ return ;
142
+ }
143
+
144
+ console . log ( `\nDownloading Node.js for ${ platform } -${ arch } ...` ) ;
145
+
146
+ // Create directory
147
+ mkdirSync ( platformDir , { recursive : true } ) ;
148
+
149
+ // Download archive
150
+ const archiveExt = platform === 'win32' ? '.zip' : '.tar.gz' ;
151
+ const archivePath = path . join ( platformDir , `node-v${ NODE_VERSION } ${ archiveExt } ` ) ;
152
+
153
+ try {
154
+ await downloadFile ( url , archivePath ) ;
155
+ console . log ( ' Download complete' ) ;
156
+
157
+ // Extract archive
158
+ const tempDir = await extractArchive ( archivePath , platform ) ;
159
+
160
+ // Find the node binary in extracted files
161
+ // Windows uses different directory naming convention (win instead of win32)
162
+ const extractedDirName = platform === 'win32'
163
+ ? `node-v${ NODE_VERSION } -win-${ arch } `
164
+ : `node-v${ NODE_VERSION } -${ platform } -${ arch } ` ;
165
+ const extractedBinaryPath = path . join ( tempDir , extractedDirName , binary ) ;
166
+
167
+ // Verify binary exists
168
+ if ( ! fs . existsSync ( extractedBinaryPath ) ) {
169
+ throw new Error ( `Binary not found at expected path: ${ extractedBinaryPath } ` ) ;
170
+ }
171
+
172
+ // Copy binary to final location
173
+ console . log ( ' Installing binary...' ) ;
174
+ fs . copyFileSync ( extractedBinaryPath , binaryPath ) ;
175
+
176
+ // Make executable on Unix-like systems
177
+ if ( platform !== 'win32' ) {
178
+ chmodSync ( binaryPath , '755' ) ;
179
+ }
180
+
181
+ // Clean up
182
+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
183
+ fs . unlinkSync ( archivePath ) ;
184
+
185
+ console . log ( `✓ Successfully installed ${ platform } -${ arch } binary` ) ;
186
+ } catch ( error ) {
187
+ console . error ( `✗ Failed to download ${ platform } -${ arch } :` , error instanceof Error ? error . message : error ) ;
188
+ // Clean up on failure
189
+ if ( fs . existsSync ( archivePath ) ) {
190
+ fs . unlinkSync ( archivePath ) ;
191
+ }
192
+ throw error ;
193
+ }
194
+ }
195
+
196
+ function getCurrentPlatform ( ) : PlatformConfig | undefined {
197
+ const currentPlatform = process . platform as string ;
198
+ const currentArch = process . arch as string ;
199
+
200
+ return PLATFORMS . find ( p =>
201
+ p . platform === currentPlatform &&
202
+ p . arch === currentArch
203
+ ) ;
204
+ }
205
+
206
+ async function main ( ) {
207
+ console . log ( `Node.js Binary Downloader v${ NODE_VERSION } ` ) ;
208
+ console . log ( '=====================================\n' ) ;
209
+
210
+ // Create base directory
211
+ mkdirSync ( RESOURCES_DIR , { recursive : true } ) ;
212
+
213
+ if ( downloadAll ) {
214
+ console . log ( 'Mode: Download all platforms\n' ) ;
215
+
216
+ // Download binaries for all platforms
217
+ let success = 0 ;
218
+ let failed = 0 ;
219
+
220
+ for ( const platform of PLATFORMS ) {
221
+ try {
222
+ await downloadNodeBinary ( platform ) ;
223
+ success ++ ;
224
+ } catch ( error ) {
225
+ failed ++ ;
226
+ }
227
+ }
228
+
229
+ console . log ( `\nSummary: ${ success } succeeded, ${ failed } failed` ) ;
230
+ if ( failed > 0 ) {
231
+ process . exit ( 1 ) ;
232
+ }
233
+ } else {
234
+ console . log ( 'Mode: Download current platform only\n' ) ;
235
+
236
+ // Download only for current platform
237
+ const currentPlatform = getCurrentPlatform ( ) ;
238
+
239
+ if ( ! currentPlatform ) {
240
+ console . error ( `✗ Unsupported platform: ${ process . platform } -${ process . arch } ` ) ;
241
+ console . error ( ' Supported platforms:' ) ;
242
+ PLATFORMS . forEach ( p => {
243
+ console . error ( ` - ${ p . platform } -${ p . arch } ` ) ;
244
+ } ) ;
245
+ process . exit ( 1 ) ;
246
+ }
247
+
248
+ await downloadNodeBinary ( currentPlatform ) ;
249
+ }
250
+
251
+ console . log ( '\nDone! Node.js binaries available at:' , RESOURCES_DIR ) ;
252
+ }
253
+
254
+ // Run if called directly
255
+ if ( require . main === module ) {
256
+ main ( ) . catch ( ( error ) => {
257
+ console . error ( '\nFatal error:' , error ) ;
258
+ process . exit ( 1 ) ;
259
+ } ) ;
260
+ }
261
+
262
+ // Export for potential programmatic use
263
+ export { downloadNodeBinary , PLATFORMS , NODE_VERSION , getCurrentPlatform } ;
0 commit comments