freebuff 0.0.121 → 0.0.122

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/http.js +44 -2
  2. package/index.js +120 -64
  3. package/package.json +1 -1
package/http.js CHANGED
@@ -142,13 +142,29 @@ function createReleaseHttpClient({
142
142
  }
143
143
 
144
144
  async function httpGet(url, options = {}) {
145
+ const redirectCount = options.redirectCount || 0
145
146
  const reqOptions = await buildRequestOptions(url, options)
146
147
 
147
148
  return new Promise((resolve, reject) => {
148
149
  const req = httpsModule.get(reqOptions, (res) => {
149
- if (res.statusCode === 301 || res.statusCode === 302) {
150
+ if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
150
151
  res.resume()
151
- httpGet(new URL(res.headers.location, url).href, options)
152
+
153
+ if (!res.headers.location) {
154
+ reject(
155
+ new Error(`Redirect ${res.statusCode} missing Location header.`),
156
+ )
157
+ return
158
+ }
159
+ if (redirectCount >= (options.maxRedirects ?? 10)) {
160
+ reject(new Error('Too many redirects.'))
161
+ return
162
+ }
163
+
164
+ httpGet(new URL(res.headers.location, url).href, {
165
+ ...options,
166
+ redirectCount: redirectCount + 1,
167
+ })
152
168
  .then(resolve)
153
169
  .catch(reject)
154
170
  return
@@ -165,9 +181,35 @@ function createReleaseHttpClient({
165
181
  })
166
182
  }
167
183
 
184
+ async function withRetries(
185
+ operation,
186
+ {
187
+ maxAttempts = 1,
188
+ baseDelayMs = 1000,
189
+ shouldRetry = () => true,
190
+ onRetry = () => {},
191
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
192
+ },
193
+ ) {
194
+ for (let attempt = 1; ; attempt++) {
195
+ try {
196
+ return await operation(attempt)
197
+ } catch (error) {
198
+ if (attempt >= maxAttempts || !shouldRetry(error)) {
199
+ throw error
200
+ }
201
+
202
+ const delayMs = baseDelayMs * 2 ** (attempt - 1)
203
+ await onRetry({ error, attempt, nextAttempt: attempt + 1, delayMs })
204
+ await sleep(delayMs)
205
+ }
206
+ }
207
+ }
208
+
168
209
  return {
169
210
  getProxyUrl,
170
211
  httpGet,
212
+ withRetries,
171
213
  }
172
214
  }
173
215
 
package/index.js CHANGED
@@ -6,6 +6,7 @@ const http = require('http')
6
6
  const https = require('https')
7
7
  const os = require('os')
8
8
  const path = require('path')
9
+ const { pipeline } = require('stream/promises')
9
10
  const zlib = require('zlib')
10
11
 
11
12
  const tar = require('tar')
@@ -99,11 +100,13 @@ function createConfig(packageName) {
99
100
  tempDownloadDir: path.join(configDir, '.freebuff-download-temp'),
100
101
  userAgent: `${packageName}-cli`,
101
102
  requestTimeout: 20000,
103
+ downloadRequestTimeout: 120000,
104
+ downloadMaxAttempts: 3,
102
105
  }
103
106
  }
104
107
 
105
108
  const CONFIG = createConfig(packageName)
106
- const { getProxyUrl, httpGet } = createReleaseHttpClient({
109
+ const { httpGet, withRetries } = createReleaseHttpClient({
107
110
  env: process.env,
108
111
  userAgent: CONFIG.userAgent,
109
112
  requestTimeout: CONFIG.requestTimeout,
@@ -509,79 +512,142 @@ function createProgressBar(percentage, width = 30) {
509
512
  return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
510
513
  }
511
514
 
512
- async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
513
- const fileName = PLATFORM_TARGETS[targetKey]
514
-
515
- if (!fileName) {
516
- const error = new Error(`Unsupported platform: ${process.platform} ${process.arch}`)
517
- trackUpdateFailed(error.message, version, { stage: 'platform_check', target: targetKey })
518
- throw error
519
- }
520
-
521
- const downloadUrl = `${
522
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
523
- }/api/releases/download/${version}/${fileName}`
515
+ function isRetryableDownloadStatus(statusCode) {
516
+ return (
517
+ statusCode === 408 ||
518
+ statusCode === 425 ||
519
+ statusCode === 429 ||
520
+ statusCode >= 500
521
+ )
522
+ }
524
523
 
525
- fs.mkdirSync(CONFIG.configDir, { recursive: true })
524
+ function isRetryableDownloadError(error) {
525
+ if (error && typeof error.retryable === 'boolean') return error.retryable
526
+ return !['EACCES', 'ENOSPC', 'EPERM', 'EROFS'].includes(error?.code)
527
+ }
526
528
 
529
+ function prepareTempDownloadDir() {
527
530
  if (fs.existsSync(CONFIG.tempDownloadDir)) {
528
531
  fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
529
532
  }
530
533
  fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
534
+ }
531
535
 
532
- term.write('Downloading...')
536
+ async function downloadAndExtract(downloadUrl, version, targetKey) {
537
+ let attempts = 0
533
538
 
534
- const res = await httpGet(downloadUrl)
539
+ try {
540
+ return await withRetries(
541
+ async (attempt) => {
542
+ attempts = attempt
543
+ prepareTempDownloadDir()
544
+ term.write('Downloading...')
545
+
546
+ const res = await httpGet(downloadUrl, {
547
+ timeout: CONFIG.downloadRequestTimeout,
548
+ })
535
549
 
536
- if (res.statusCode !== 200) {
537
- fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
538
- const error = new Error(`Download failed: HTTP ${res.statusCode}`)
539
- trackUpdateFailed(error.message, version, { stage: 'http_download', statusCode: res.statusCode, target: targetKey })
540
- throw error
541
- }
550
+ if (res.statusCode !== 200) {
551
+ res.resume()
552
+ const error = new Error(`Download failed: HTTP ${res.statusCode}`)
553
+ error.statusCode = res.statusCode
554
+ error.retryable = isRetryableDownloadStatus(res.statusCode)
555
+ throw error
556
+ }
542
557
 
543
- const totalSize = parseInt(res.headers['content-length'] || '0', 10)
544
- let downloadedSize = 0
545
- let lastProgressTime = Date.now()
546
-
547
- res.on('data', (chunk) => {
548
- downloadedSize += chunk.length
549
- const now = Date.now()
550
- if (now - lastProgressTime >= 100 || downloadedSize === totalSize) {
551
- lastProgressTime = now
552
- if (totalSize > 0) {
553
- const pct = Math.round((downloadedSize / totalSize) * 100)
554
- term.write(
555
- `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(
556
- totalSize,
557
- )}`,
558
+ const totalSize = parseInt(res.headers['content-length'] || '0', 10)
559
+ let downloadedSize = 0
560
+ let lastProgressTime = Date.now()
561
+
562
+ res.on('data', (chunk) => {
563
+ downloadedSize += chunk.length
564
+ const now = Date.now()
565
+ if (now - lastProgressTime >= 100 || downloadedSize === totalSize) {
566
+ lastProgressTime = now
567
+ if (totalSize > 0) {
568
+ const pct = Math.round((downloadedSize / totalSize) * 100)
569
+ term.write(
570
+ `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(
571
+ totalSize,
572
+ )}`,
573
+ )
574
+ } else {
575
+ term.write(`Downloading... ${formatBytes(downloadedSize)}`)
576
+ }
577
+ }
578
+ })
579
+
580
+ await pipeline(
581
+ res,
582
+ zlib.createGunzip(),
583
+ tar.x({ cwd: CONFIG.tempDownloadDir }),
558
584
  )
559
- } else {
560
- term.write(`Downloading... ${formatBytes(downloadedSize)}`)
561
- }
585
+
586
+ const tempBinaryPath = path.join(
587
+ CONFIG.tempDownloadDir,
588
+ CONFIG.binaryName,
589
+ )
590
+ if (!fs.existsSync(tempBinaryPath)) {
591
+ const files = fs.readdirSync(CONFIG.tempDownloadDir)
592
+ const error = new Error(
593
+ `Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
594
+ )
595
+ error.retryable = false
596
+ throw error
597
+ }
598
+
599
+ return tempBinaryPath
600
+ },
601
+ {
602
+ maxAttempts: CONFIG.downloadMaxAttempts,
603
+ shouldRetry: isRetryableDownloadError,
604
+ onRetry: ({ nextAttempt, delayMs }) => {
605
+ term.writeLine(
606
+ `Download interrupted. Retrying in ${delayMs / 1000}s (${nextAttempt}/${CONFIG.downloadMaxAttempts})...`,
607
+ )
608
+ },
609
+ },
610
+ )
611
+ } catch (error) {
612
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
613
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
562
614
  }
563
- })
564
615
 
565
- await new Promise((resolve, reject) => {
566
- res
567
- .pipe(zlib.createGunzip())
568
- .pipe(tar.x({ cwd: CONFIG.tempDownloadDir }))
569
- .on('finish', resolve)
570
- .on('error', reject)
571
- })
616
+ trackUpdateFailed(error.message, version, {
617
+ stage: 'download',
618
+ statusCode: error.statusCode,
619
+ target: targetKey,
620
+ attempts,
621
+ })
622
+ throw error
623
+ }
624
+ }
572
625
 
573
- const tempBinaryPath = path.join(CONFIG.tempDownloadDir, CONFIG.binaryName)
626
+ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
627
+ const fileName = PLATFORM_TARGETS[targetKey]
574
628
 
575
- if (!fs.existsSync(tempBinaryPath)) {
576
- const files = fs.readdirSync(CONFIG.tempDownloadDir)
577
- fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
629
+ if (!fileName) {
578
630
  const error = new Error(
579
- `Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
631
+ `Unsupported platform: ${process.platform} ${process.arch}`,
580
632
  )
581
- trackUpdateFailed(error.message, version, { stage: 'extraction', target: targetKey })
633
+ trackUpdateFailed(error.message, version, {
634
+ stage: 'platform_check',
635
+ target: targetKey,
636
+ })
582
637
  throw error
583
638
  }
584
639
 
640
+ const downloadUrl = `${
641
+ process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
642
+ }/api/releases/download/${version}/${fileName}`
643
+
644
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
645
+ const tempBinaryPath = await downloadAndExtract(
646
+ downloadUrl,
647
+ version,
648
+ targetKey,
649
+ )
650
+
585
651
  if (process.platform !== 'win32') {
586
652
  fs.chmodSync(tempBinaryPath, 0o755)
587
653
  }
@@ -650,11 +716,6 @@ async function ensureBinaryExists() {
650
716
  if (!version) {
651
717
  console.error('❌ Failed to determine latest version')
652
718
  console.error('Please check your internet connection and try again')
653
- if (!getProxyUrl()) {
654
- console.error(
655
- 'If you are behind a proxy, set the HTTPS_PROXY environment variable',
656
- )
657
- }
658
719
  process.exit(1)
659
720
  }
660
721
 
@@ -664,11 +725,6 @@ async function ensureBinaryExists() {
664
725
  term.clearLine()
665
726
  console.error('❌ Failed to download freebuff:', error.message)
666
727
  console.error('Please check your internet connection and try again')
667
- if (!getProxyUrl()) {
668
- console.error(
669
- 'If you are behind a proxy, set the HTTPS_PROXY environment variable',
670
- )
671
- }
672
728
  process.exit(1)
673
729
  }
674
730
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freebuff",
3
- "version": "0.0.121",
3
+ "version": "0.0.122",
4
4
  "description": "The world's strongest free coding agent",
5
5
  "license": "MIT",
6
6
  "bin": {