Skip to content

Commit bbc5faa

Browse files
committed
[codecane] Sync staging release scripts with production
- Add smoke test verification before installing binary - Use ~/.config/manicode/ for binary storage (matching production) - Add metadata file for faster version checks - Add temp directory extraction with cleanup - Add postinstall.js with staging-specific welcome message - Add preuninstall script
1 parent b06a105 commit bbc5faa

File tree

3 files changed

+170
-83
lines changed

3 files changed

+170
-83
lines changed

cli/release-staging/index.js

Lines changed: 131 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const { spawn } = require('child_process')
44
const fs = require('fs')
55
const https = require('https')
6+
const os = require('os')
67
const path = require('path')
78
const zlib = require('zlib')
89

@@ -11,17 +12,18 @@ const tar = require('tar')
1112
const packageName = 'codecane'
1213

1314
function 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

113115
function 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+)*(-beta\.\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

162176
function 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

328377
async 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

351400
async 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()

cli/release-staging/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
"codecane": "index.js"
88
},
99
"scripts": {
10-
"postinstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codecane.exe' : 'codecane'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
10+
"postinstall": "node postinstall.js",
11+
"preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'manicode', process.platform === 'win32' ? 'codecane.exe' : 'codecane'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
1112
},
1213
"files": [
1314
"index.js",
15+
"postinstall.js",
1416
"README.md"
1517
],
1618
"os": [

cli/release-staging/postinstall.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const os = require('os');
5+
const path = require('path');
6+
7+
// Clean up old binary
8+
const binaryPath = path.join(
9+
os.homedir(),
10+
'.config',
11+
'manicode',
12+
process.platform === 'win32' ? 'codecane.exe' : 'codecane'
13+
);
14+
15+
try {
16+
fs.unlinkSync(binaryPath);
17+
} catch (e) {
18+
/* ignore if file doesn't exist */
19+
}
20+
21+
// Print welcome message
22+
console.log('\n');
23+
console.log('🧪 Welcome to Codecane (Staging)!');
24+
console.log('\n');
25+
console.log('⚠️ This is a staging/beta release for testing purposes.');
26+
console.log('\n');
27+
console.log('To get started:');
28+
console.log(' 1. cd to your project directory');
29+
console.log(' 2. Run: codecane');
30+
console.log('\n');
31+
console.log('Example:');
32+
console.log(' $ cd ~/my-project');
33+
console.log(' $ codecane');
34+
console.log('\n');
35+
console.log('For more information, visit: https://codebuff.com/docs');
36+
console.log('\n');

0 commit comments

Comments
 (0)