codecane 1.0.420-beta.260 → 1.0.420-beta.261

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +131 -82
  2. package/package.json +4 -2
  3. package/postinstall.js +36 -0
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { spawn } = require('child_process')
4
4
  const fs = require('fs')
5
5
  const https = require('https')
6
+ const os = require('os')
6
7
  const path = require('path')
7
8
  const zlib = require('zlib')
8
9
 
@@ -11,17 +12,18 @@ const tar = require('tar')
11
12
  const packageName = 'codecane'
12
13
 
13
14
  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')
17
17
  const binaryName =
18
18
  process.platform === 'win32' ? `${packageName}.exe` : packageName
19
19
 
20
20
  return {
21
- packageDir,
22
- binDir,
21
+ homeDir,
22
+ configDir,
23
23
  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'),
25
27
  userAgent: `${packageName}-cli`,
26
28
  requestTimeout: 20000,
27
29
  }
@@ -111,57 +113,75 @@ function streamToString(stream) {
111
113
  }
112
114
 
113
115
  function getCurrentVersion() {
114
- if (!fs.existsSync(CONFIG.binaryPath)) return null
115
-
116
116
  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
+ }
122
130
 
123
- let output = ''
131
+ function runSmokeTest(binaryPath) {
132
+ return new Promise((resolve) => {
133
+ if (!fs.existsSync(binaryPath)) {
134
+ resolve(false)
135
+ return
136
+ }
124
137
 
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
+ })
128
142
 
129
- child.stderr.on('data', () => {
130
- // Ignore stderr output
131
- })
143
+ let output = ''
132
144
 
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')
149
154
  }
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
+ })
151
168
 
152
- child.on('error', () => {
153
- clearTimeout(timeout)
154
- resolve('error')
155
- })
169
+ child.on('error', () => {
170
+ clearTimeout(timeout)
171
+ resolve(false)
156
172
  })
157
- } catch (error) {
158
- return 'error'
159
- }
173
+ })
160
174
  }
161
175
 
162
176
  function compareVersions(v1, v2) {
163
177
  if (!v1 || !v2) return 0
164
178
 
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
+
165
185
  const parseVersion = (version) => {
166
186
  const parts = version.split('-')
167
187
  const mainParts = parts[0].split('.').map(Number)
@@ -243,32 +263,21 @@ async function downloadBinary(version) {
243
263
  process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
244
264
  }/api/releases/download/${version}/${fileName}`
245
265
 
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 })
248
268
 
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 })
265
272
  }
273
+ fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
266
274
 
267
275
  term.write('Downloading...')
268
276
 
269
277
  const res = await httpGet(downloadUrl)
270
278
 
271
279
  if (res.statusCode !== 200) {
280
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
272
281
  throw new Error(`Download failed: HTTP ${res.statusCode}`)
273
282
  }
274
283
 
@@ -294,31 +303,71 @@ async function downloadBinary(version) {
294
303
  }
295
304
  })
296
305
 
306
+ // Extract to temp directory
297
307
  await new Promise((resolve, reject) => {
298
308
  res
299
309
  .pipe(zlib.createGunzip())
300
- .pipe(tar.x({ cwd: CONFIG.binDir }))
310
+ .pipe(tar.x({ cwd: CONFIG.tempDownloadDir }))
301
311
  .on('finish', resolve)
302
312
  .on('error', reject)
303
313
  })
304
314
 
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
+ }
308
330
 
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
+ }
312
357
  }
313
- } else {
314
- throw new Error(
315
- `Binary not found after extraction. Expected: ${extractedPath}, Available files: ${files.join(', ')}`,
316
- )
317
358
  }
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
+ }
322
371
  }
323
372
 
324
373
  term.clearLine()
@@ -326,8 +375,8 @@ async function downloadBinary(version) {
326
375
  }
327
376
 
328
377
  async function ensureBinaryExists() {
329
- const currentVersion = await getCurrentVersion()
330
- if (currentVersion !== null && currentVersion !== 'error') {
378
+ const currentVersion = getCurrentVersion()
379
+ if (currentVersion !== null) {
331
380
  return
332
381
  }
333
382
 
@@ -350,14 +399,14 @@ async function ensureBinaryExists() {
350
399
 
351
400
  async function checkForUpdates(runningProcess, exitListener) {
352
401
  try {
353
- const currentVersion = await getCurrentVersion()
354
- if (!currentVersion) return
402
+ const currentVersion = getCurrentVersion()
355
403
 
356
404
  const latestVersion = await getLatestVersion()
357
405
  if (!latestVersion) return
358
406
 
359
407
  if (
360
- currentVersion === 'error' ||
408
+ // Download new version if current version is unknown or outdated.
409
+ currentVersion === null ||
361
410
  compareVersions(currentVersion, latestVersion) < 0
362
411
  ) {
363
412
  term.clearLine()
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "codecane",
3
- "version": "1.0.420-beta.260",
3
+ "version": "1.0.420-beta.261",
4
4
  "description": "AI coding agent CLI (staging)",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "codecane": "index.js"
8
8
  },
9
9
  "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 */ }\""
11
12
  },
12
13
  "files": [
13
14
  "index.js",
15
+ "postinstall.js",
14
16
  "README.md"
15
17
  ],
16
18
  "os": [
package/postinstall.js ADDED
@@ -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');