33const { spawn } = require ( 'child_process' )
44const fs = require ( 'fs' )
55const https = require ( 'https' )
6+ const os = require ( 'os' )
67const path = require ( 'path' )
78const zlib = require ( 'zlib' )
89
@@ -11,17 +12,18 @@ const tar = require('tar')
1112const packageName = 'codecane'
1213
1314function createConfig ( packageName ) {
14- // Store binary in package directory instead of ~/.config/manicode
15- const packageDir = __dirname
16- const binDir = path . join ( packageDir , 'bin' )
15+ const homeDir = os . homedir ( )
16+ const configDir = path . join ( homeDir , '.config' , 'manicode' )
1717 const binaryName =
1818 process . platform === 'win32' ? `${ packageName } .exe` : packageName
1919
2020 return {
21- packageDir ,
22- binDir ,
21+ homeDir ,
22+ configDir ,
2323 binaryName,
24- binaryPath : path . join ( binDir , binaryName ) ,
24+ binaryPath : path . join ( configDir , binaryName ) ,
25+ metadataPath : path . join ( configDir , 'codecane-metadata.json' ) ,
26+ tempDownloadDir : path . join ( configDir , '.download-temp-staging' ) ,
2527 userAgent : `${ packageName } -cli` ,
2628 requestTimeout : 20000 ,
2729 }
@@ -111,57 +113,75 @@ function streamToString(stream) {
111113}
112114
113115function getCurrentVersion ( ) {
114- if ( ! fs . existsSync ( CONFIG . binaryPath ) ) return null
115-
116116 try {
117- return new Promise ( ( resolve ) => {
118- const child = spawn ( CONFIG . binaryPath , [ '--version' ] , {
119- cwd : CONFIG . packageDir ,
120- stdio : 'pipe' ,
121- } )
117+ if ( ! fs . existsSync ( CONFIG . metadataPath ) ) {
118+ return null
119+ }
120+ const metadata = JSON . parse ( fs . readFileSync ( CONFIG . metadataPath , 'utf8' ) )
121+ // Also verify the binary still exists
122+ if ( ! fs . existsSync ( CONFIG . binaryPath ) ) {
123+ return null
124+ }
125+ return metadata . version || null
126+ } catch ( error ) {
127+ return null
128+ }
129+ }
122130
123- let output = ''
131+ function runSmokeTest ( binaryPath ) {
132+ return new Promise ( ( resolve ) => {
133+ if ( ! fs . existsSync ( binaryPath ) ) {
134+ resolve ( false )
135+ return
136+ }
124137
125- child . stdout . on ( 'data' , ( data ) => {
126- output += data . toString ( )
127- } )
138+ const child = spawn ( binaryPath , [ '--version' ] , {
139+ cwd : os . homedir ( ) ,
140+ stdio : 'pipe' ,
141+ } )
128142
129- child . stderr . on ( 'data' , ( ) => {
130- // Ignore stderr output
131- } )
143+ let output = ''
132144
133- const timeout = setTimeout ( ( ) => {
134- child . kill ( 'SIGTERM' )
135- setTimeout ( ( ) => {
136- if ( ! child . killed ) {
137- child . kill ( 'SIGKILL' )
138- }
139- } , 4000 )
140- resolve ( 'error' )
141- } , 4000 )
142-
143- child . on ( 'exit' , ( code ) => {
144- clearTimeout ( timeout )
145- if ( code === 0 ) {
146- resolve ( output . trim ( ) )
147- } else {
148- resolve ( 'error' )
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' )
149154 }
150- } )
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 + ) * ( - b e t a \. \d + ) ? $ / ) ) {
163+ resolve ( true )
164+ } else {
165+ resolve ( false )
166+ }
167+ } )
151168
152- child . on ( 'error' , ( ) => {
153- clearTimeout ( timeout )
154- resolve ( 'error' )
155- } )
169+ child . on ( 'error' , ( ) => {
170+ clearTimeout ( timeout )
171+ resolve ( false )
156172 } )
157- } catch ( error ) {
158- return 'error'
159- }
173+ } )
160174}
161175
162176function compareVersions ( v1 , v2 ) {
163177 if ( ! v1 || ! v2 ) return 0
164178
179+ // Always update if the current version is not a valid semver
180+ // e.g. 1.0.420-beta.1
181+ if ( ! v1 . match ( / ^ \d + ( \. \d + ) * $ / ) ) {
182+ return - 1
183+ }
184+
165185 const parseVersion = ( version ) => {
166186 const parts = version . split ( '-' )
167187 const mainParts = parts [ 0 ] . split ( '.' ) . map ( Number )
@@ -243,32 +263,21 @@ async function downloadBinary(version) {
243263 process . env . NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
244264 } /api/releases/download/${ version } /${ fileName } `
245265
246- // Create bin directory in package directory
247- fs . mkdirSync ( CONFIG . binDir , { recursive : true } )
266+ // Ensure config directory exists
267+ fs . mkdirSync ( CONFIG . configDir , { recursive : true } )
248268
249- if ( fs . existsSync ( CONFIG . binaryPath ) ) {
250- try {
251- fs . unlinkSync ( CONFIG . binaryPath )
252- } catch ( err ) {
253- const backupPath = CONFIG . binaryPath + `.old.${ Date . now ( ) } `
254-
255- try {
256- fs . renameSync ( CONFIG . binaryPath , backupPath )
257- } catch ( renameErr ) {
258- throw new Error (
259- `Failed to replace existing binary. ` +
260- `unlink error: ${ err . code || err . message } , ` +
261- `rename error: ${ renameErr . code || renameErr . message } ` ,
262- )
263- }
264- }
269+ // Clean up any previous temp download directory
270+ if ( fs . existsSync ( CONFIG . tempDownloadDir ) ) {
271+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
265272 }
273+ fs . mkdirSync ( CONFIG . tempDownloadDir , { recursive : true } )
266274
267275 term . write ( 'Downloading...' )
268276
269277 const res = await httpGet ( downloadUrl )
270278
271279 if ( res . statusCode !== 200 ) {
280+ fs . rmSync ( CONFIG . tempDownloadDir , { recursive : true } )
272281 throw new Error ( `Download failed: HTTP ${ res . statusCode } ` )
273282 }
274283
@@ -294,40 +303,80 @@ async function downloadBinary(version) {
294303 }
295304 } )
296305
306+ // Extract to temp directory
297307 await new Promise ( ( resolve , reject ) => {
298308 res
299309 . pipe ( zlib . createGunzip ( ) )
300- . pipe ( tar . x ( { cwd : CONFIG . binDir } ) )
310+ . pipe ( tar . x ( { cwd : CONFIG . tempDownloadDir } ) )
301311 . on ( 'finish' , resolve )
302312 . on ( 'error' , reject )
303313 } )
304314
305- try {
306- const files = fs . readdirSync ( CONFIG . binDir )
307- const extractedPath = path . join ( CONFIG . binDir , 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+ }
308330
309- if ( fs . existsSync ( extractedPath ) ) {
310- if ( process . platform !== 'win32' ) {
311- fs . chmodSync ( extractedPath , 0o755 )
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+ }
339+
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+ }
312357 }
313- } else {
314- throw new Error (
315- `Binary not found after extraction. Expected: ${ extractedPath } , Available files: ${ files . join ( ', ' ) } ` ,
316- )
317358 }
318- } catch ( error ) {
319- term . clearLine ( )
320- console . error ( `Extraction failed: ${ error . message } ` )
321- 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+ }
322371 }
323372
324373 term . clearLine ( )
325374 console . log ( 'Download complete! Starting Codecane...' )
326375}
327376
328377async function ensureBinaryExists ( ) {
329- const currentVersion = await getCurrentVersion ( )
330- if ( currentVersion !== null && currentVersion !== 'error' ) {
378+ const currentVersion = getCurrentVersion ( )
379+ if ( currentVersion !== null ) {
331380 return
332381 }
333382
@@ -350,14 +399,14 @@ async function ensureBinaryExists() {
350399
351400async function checkForUpdates ( runningProcess , exitListener ) {
352401 try {
353- const currentVersion = await getCurrentVersion ( )
354- if ( ! currentVersion ) return
402+ const currentVersion = getCurrentVersion ( )
355403
356404 const latestVersion = await getLatestVersion ( )
357405 if ( ! latestVersion ) return
358406
359407 if (
360- currentVersion === 'error' ||
408+ // Download new version if current version is unknown or outdated.
409+ currentVersion === null ||
361410 compareVersions ( currentVersion , latestVersion ) < 0
362411 ) {
363412 term . clearLine ( )
0 commit comments