@@ -23,6 +23,7 @@ function createConfig(packageName) {
2323 binaryName,
2424 binaryPath : path . join ( configDir , binaryName ) ,
2525 metadataPath : path . join ( configDir , 'codebuff-metadata.json' ) ,
26+ tempDownloadDir : path . join ( configDir , '.download-temp' ) ,
2627 userAgent : `${ packageName } -cli` ,
2728 requestTimeout : 20000 ,
2829 }
@@ -127,6 +128,51 @@ function getCurrentVersion() {
127128 }
128129}
129130
131+ function runSmokeTest ( binaryPath ) {
132+ return new Promise ( ( resolve ) => {
133+ if ( ! fs . existsSync ( binaryPath ) ) {
134+ resolve ( false )
135+ return
136+ }
137+
138+ const child = spawn ( binaryPath , [ '--version' ] , {
139+ cwd : os . homedir ( ) ,
140+ stdio : 'pipe' ,
141+ } )
142+
143+ let output = ''
144+
145+ child . stdout . on ( 'data' , ( data ) => {
146+ output += data . toString ( )
147+ } )
148+
149+ const timeout = setTimeout ( ( ) => {
150+ child . kill ( 'SIGTERM' )
151+ setTimeout ( ( ) => {
152+ if ( ! child . killed ) {
153+ child . kill ( 'SIGKILL' )
154+ }
155+ } , 1000 )
156+ resolve ( false )
157+ } , 5000 )
158+
159+ child . on ( 'exit' , ( code ) => {
160+ clearTimeout ( timeout )
161+ // Check that it exits successfully and outputs something that looks like a version
162+ if ( code === 0 && output . trim ( ) . match ( / ^ \d + ( \. \d + ) * $ / ) ) {
163+ resolve ( true )
164+ } else {
165+ resolve ( false )
166+ }
167+ } )
168+
169+ child . on ( 'error' , ( ) => {
170+ clearTimeout ( timeout )
171+ resolve ( false )
172+ } )
173+ } )
174+ }
175+
130176function compareVersions ( v1 , v2 ) {
131177 if ( ! v1 || ! v2 ) return 0
132178
@@ -217,33 +263,21 @@ async function downloadBinary(version) {
217263 process . env . NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
218264 } /api/releases/download/${ version } /${ fileName } `
219265
266+ // Ensure config directory exists
220267 fs . mkdirSync ( CONFIG . configDir , { recursive : true } )
221268
222- if ( fs . existsSync ( CONFIG . binaryPath ) ) {
223- try {
224- fs . unlinkSync ( CONFIG . binaryPath )
225- } catch ( err ) {
226- // Fallback: try renaming the locked/undeletable binary
227- const backupPath = CONFIG . binaryPath + `.old.${ Date . now ( ) } `
228-
229- try {
230- fs . renameSync ( CONFIG . binaryPath , backupPath )
231- } catch ( renameErr ) {
232- // If we can't unlink OR rename, we can't safely proceed
233- throw new Error (
234- `Failed to replace existing binary. ` +
235- `unlink error: ${ err . code || err . message } , ` +
236- `rename error: ${ renameErr . code || renameErr . message } ` ,
237- )
238- }
239- }
269+ // Clean up any previous temp download directory
270+ if ( fs . existsSync ( CONFIG . tempDownloadDir ) ) {
271+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
240272 }
273+ fs . mkdirSync ( CONFIG . tempDownloadDir , { recursive : true } )
241274
242275 term . write ( 'Downloading...' )
243276
244277 const res = await httpGet ( downloadUrl )
245278
246279 if ( res . statusCode !== 200 ) {
280+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
247281 throw new Error ( `Download failed: HTTP ${ res . statusCode } ` )
248282 }
249283
@@ -269,36 +303,71 @@ async function downloadBinary(version) {
269303 }
270304 } )
271305
306+ // Extract to temp directory
272307 await new Promise ( ( resolve , reject ) => {
273308 res
274309 . pipe ( zlib . createGunzip ( ) )
275- . pipe ( tar . x ( { cwd : CONFIG . configDir } ) )
310+ . pipe ( tar . x ( { cwd : CONFIG . tempDownloadDir } ) )
276311 . on ( 'finish' , resolve )
277312 . on ( 'error' , reject )
278313 } )
279314
280- try {
281- const files = fs . readdirSync ( CONFIG . configDir )
282- const extractedPath = path . join ( CONFIG . configDir , CONFIG . binaryName )
315+ const tempBinaryPath = path . join ( CONFIG . tempDownloadDir , CONFIG . binaryName )
316+
317+ // Verify the binary was extracted
318+ if ( ! fs . existsSync ( tempBinaryPath ) ) {
319+ const files = fs . readdirSync ( CONFIG . tempDownloadDir )
320+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
321+ throw new Error (
322+ `Binary not found after extraction. Expected: ${ CONFIG . binaryName } , Available files: ${ files . join ( ', ' ) } ` ,
323+ )
324+ }
325+
326+ // Set executable permissions
327+ if ( process . platform !== 'win32' ) {
328+ fs . chmodSync ( tempBinaryPath , 0o755 )
329+ }
330+
331+ // Run smoke test on the downloaded binary
332+ term . write ( 'Verifying download...' )
333+ const smokeTestPassed = await runSmokeTest ( tempBinaryPath )
334+
335+ if ( ! smokeTestPassed ) {
336+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
337+ throw new Error ( 'Downloaded binary failed smoke test (--version check)' )
338+ }
283339
284- if ( fs . existsSync ( extractedPath ) ) {
285- if ( process . platform !== 'win32' ) {
286- fs . chmodSync ( extractedPath , 0o755 )
340+ // Smoke test passed - move binary to final location
341+ try {
342+ if ( fs . existsSync ( CONFIG . binaryPath ) ) {
343+ try {
344+ fs . unlinkSync ( CONFIG . binaryPath )
345+ } catch ( err ) {
346+ // Fallback: try renaming the locked/undeletable binary (Windows)
347+ const backupPath = CONFIG . binaryPath + `.old.${ Date . now ( ) } `
348+ try {
349+ fs . renameSync ( CONFIG . binaryPath , backupPath )
350+ } catch ( renameErr ) {
351+ throw new Error (
352+ `Failed to replace existing binary. ` +
353+ `unlink error: ${ err . code || err . message } , ` +
354+ `rename error: ${ renameErr . code || renameErr . message } ` ,
355+ )
356+ }
287357 }
288- // Save version metadata for fast version checking
289- fs . writeFileSync (
290- CONFIG . metadataPath ,
291- JSON . stringify ( { version } , null , 2 ) ,
292- )
293- } else {
294- throw new Error (
295- `Binary not found after extraction. Expected: ${ extractedPath } , Available files: ${ files . join ( ', ' ) } ` ,
296- )
297358 }
298- } catch ( error ) {
299- term . clearLine ( )
300- console . error ( `Extraction failed: ${ error . message } ` )
301- process . exit ( 1 )
359+ fs . renameSync ( tempBinaryPath , CONFIG . binaryPath )
360+
361+ // Save version metadata for fast version checking
362+ fs . writeFileSync (
363+ CONFIG . metadataPath ,
364+ JSON . stringify ( { version } , null , 2 ) ,
365+ )
366+ } finally {
367+ // Clean up temp directory even if rename fails
368+ if ( fs . existsSync ( CONFIG . tempDownloadDir ) ) {
369+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
370+ }
302371 }
303372
304373 term . clearLine ( )
0 commit comments