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