freebuff 0.0.122 → 0.0.124

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.
package/index.js CHANGED
@@ -1,992 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn, execFileSync } = require('child_process')
4
3
  const fs = require('fs')
5
- const http = require('http')
6
- const https = require('https')
7
- const os = require('os')
8
4
  const path = require('path')
9
- const { pipeline } = require('stream/promises')
10
- const zlib = require('zlib')
11
5
 
12
- const tar = require('tar')
13
- const { createReleaseHttpClient } = require('./http')
14
-
15
- const packageName = 'freebuff'
16
-
17
- /**
18
- * Terminal escape sequences to reset terminal state after the child process exits.
19
- * When the binary is SIGKILL'd, it can't clean up its own terminal state.
20
- * The wrapper (this process) survives and must reset these modes.
21
- */
22
- const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
23
- const SAFE_TERMINAL_RESET_SEQUENCES =
24
- '\x1b[?1000l' + // Disable X10 mouse mode
25
- '\x1b[?1002l' + // Disable button event mouse mode
26
- '\x1b[?1003l' + // Disable any-event mouse mode (all motion)
27
- '\x1b[?1006l' + // Disable SGR extended mouse mode
28
- '\x1b[?1004l' + // Disable focus reporting
29
- '\x1b[?2004l' + // Disable bracketed paste mode
30
- '\x1b[<u' + // Pop kitty keyboard protocol flags
31
- '\x1b[>4;0m' + // Reset modifyOtherKeys
32
- '\x1b[?25h' // Show cursor
33
-
34
- const FULL_TERMINAL_RESET_SEQUENCES =
35
- EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
36
-
37
- function resetTerminal(options = {}) {
38
- const { exitAlternateScreen = false } = options
39
-
40
- try {
41
- if (process.stdin.isTTY && process.stdin.setRawMode) {
42
- process.stdin.setRawMode(false)
43
- }
44
- } catch {
45
- // stdin may be closed
46
- }
47
- try {
48
- if (process.stdout.isTTY) {
49
- // Exiting the alternate screen is only safe after an interactive child.
50
- // Plain CLI paths like --help never enter it, and ?1049l can erase output.
51
- process.stdout.write(
52
- exitAlternateScreen
53
- ? FULL_TERMINAL_RESET_SEQUENCES
54
- : SAFE_TERMINAL_RESET_SEQUENCES,
55
- )
56
- }
57
- } catch {
58
- // stdout may be closed
59
- }
60
- }
61
-
62
- function getUnsignedExitCode(code) {
63
- return code != null && code < 0 ? (code >>> 0) : code
64
- }
65
-
66
- function isWindowsNativeCrashCode(code) {
67
- const unsignedCode = getUnsignedExitCode(code)
68
- return (
69
- process.platform === 'win32' &&
70
- (unsignedCode === 0xC000001D ||
71
- unsignedCode === 0xC0000005 ||
72
- unsignedCode === 0xC0000409)
73
- )
74
- }
75
-
76
- function shouldExitAlternateScreen(code, signal) {
77
- return Boolean(signal) || isWindowsNativeCrashCode(code)
78
- }
79
-
80
- function isIllegalInstructionExit(code, signal) {
81
- const unsignedCode = getUnsignedExitCode(code)
82
- return (
83
- signal === 'SIGILL' ||
84
- (process.platform === 'win32' && unsignedCode === 0xC000001D)
85
- )
86
- }
87
-
88
- function createConfig(packageName) {
89
- const homeDir = os.homedir()
90
- const configDir = path.join(homeDir, '.config', 'manicode')
91
- const binaryName =
92
- process.platform === 'win32' ? `${packageName}.exe` : packageName
93
-
94
- return {
95
- homeDir,
96
- configDir,
97
- binaryName,
98
- binaryPath: path.join(configDir, binaryName),
99
- metadataPath: path.join(configDir, 'freebuff-metadata.json'),
100
- tempDownloadDir: path.join(configDir, '.freebuff-download-temp'),
101
- userAgent: `${packageName}-cli`,
102
- requestTimeout: 20000,
103
- downloadRequestTimeout: 120000,
104
- downloadMaxAttempts: 3,
105
- }
106
- }
107
-
108
- const CONFIG = createConfig(packageName)
109
- const { httpGet, withRetries } = createReleaseHttpClient({
110
- env: process.env,
111
- userAgent: CONFIG.userAgent,
112
- requestTimeout: CONFIG.requestTimeout,
6
+ const packagedLauncherPath = path.join(__dirname, 'launcher.js')
7
+ const sourceLauncherPath = path.join(
8
+ __dirname,
9
+ '..',
10
+ '..',
11
+ '..',
12
+ 'cli',
13
+ 'release-core',
14
+ 'launcher.js',
15
+ )
16
+ // Published packages must not let an unrelated sibling path shadow their
17
+ // bundled launcher. Source checkouts only fall back when that copy is absent.
18
+ const { createLauncher } = require(
19
+ fs.existsSync(packagedLauncherPath)
20
+ ? packagedLauncherPath
21
+ : sourceLauncherPath,
22
+ )
23
+
24
+ const launcher = createLauncher({
25
+ packageName: 'freebuff',
26
+ displayName: 'Freebuff',
27
+ telemetryEvent: 'cli.update_freebuff_failed',
113
28
  })
114
29
 
115
- function getPostHogConfig() {
116
- const apiKey =
117
- process.env.CODEBUFF_POSTHOG_API_KEY ||
118
- process.env.NEXT_PUBLIC_POSTHOG_API_KEY
119
- const host =
120
- process.env.CODEBUFF_POSTHOG_HOST ||
121
- process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
122
-
123
- if (!apiKey || !host) {
124
- return null
125
- }
126
-
127
- return { apiKey, host }
128
- }
129
-
130
- /**
131
- * Track update failure event to PostHog.
132
- * Fire-and-forget - errors are silently ignored.
133
- */
134
- function trackUpdateFailed(errorMessage, version, context = {}) {
135
- try {
136
- const posthogConfig = getPostHogConfig()
137
- if (!posthogConfig) {
138
- return
139
- }
140
-
141
- const payload = JSON.stringify({
142
- api_key: posthogConfig.apiKey,
143
- event: 'cli.update_freebuff_failed',
144
- properties: {
145
- distinct_id: `anonymous-${CONFIG.homeDir}`,
146
- error: errorMessage,
147
- version: version || 'unknown',
148
- platform: process.platform,
149
- arch: process.arch,
150
- ...context,
151
- },
152
- timestamp: new Date().toISOString(),
153
- })
154
-
155
- const parsedUrl = new URL(`${posthogConfig.host}/capture/`)
156
- const isHttps = parsedUrl.protocol === 'https:'
157
- const options = {
158
- hostname: parsedUrl.hostname,
159
- port: parsedUrl.port || (isHttps ? 443 : 80),
160
- path: parsedUrl.pathname + parsedUrl.search,
161
- method: 'POST',
162
- headers: {
163
- 'Content-Type': 'application/json',
164
- 'Content-Length': Buffer.byteLength(payload),
165
- },
166
- }
167
-
168
- const transport = isHttps ? https : http
169
- const req = transport.request(options)
170
- req.on('error', () => {})
171
- req.write(payload)
172
- req.end()
173
- } catch (e) {
174
- // Silently ignore any tracking errors
175
- }
176
- }
177
-
178
- const PLATFORM_TARGETS = {
179
- 'linux-x64': `${packageName}-linux-x64.tar.gz`,
180
- 'linux-x64-baseline': `${packageName}-linux-x64-baseline.tar.gz`,
181
- 'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
182
- 'darwin-x64': `${packageName}-darwin-x64.tar.gz`,
183
- 'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`,
184
- 'win32-x64': `${packageName}-win32-x64.tar.gz`,
185
- 'win32-x64-baseline': `${packageName}-win32-x64-baseline.tar.gz`,
186
- }
187
-
188
- const BASELINE_FALLBACK_TARGETS = {
189
- 'linux-x64': 'linux-x64-baseline',
190
- 'win32-x64': 'win32-x64-baseline',
191
- }
192
-
193
- const term = {
194
- clearLine: () => {
195
- if (process.stderr.isTTY) {
196
- process.stderr.write('\r\x1b[K')
197
- }
198
- },
199
- write: (text) => {
200
- term.clearLine()
201
- process.stderr.write(text)
202
- },
203
- writeLine: (text) => {
204
- term.clearLine()
205
- process.stderr.write(text + '\n')
206
- },
207
- }
208
-
209
- function getPlatformKey() {
210
- return `${process.platform}-${process.arch}`
211
- }
212
-
213
- function getTargetOverride() {
214
- const envNames = [
215
- `${packageName.toUpperCase()}_BINARY_TARGET`,
216
- 'CODEBUFF_BINARY_TARGET',
217
- 'CLI_BINARY_TARGET',
218
- ]
219
-
220
- for (const envName of envNames) {
221
- const target = process.env[envName]
222
- if (target && PLATFORM_TARGETS[target]) {
223
- return target
224
- }
225
- }
226
-
227
- return null
228
- }
229
-
230
- function linuxCpuHasAvx2() {
231
- try {
232
- return /\bavx2\b/i.test(fs.readFileSync('/proc/cpuinfo', 'utf8'))
233
- } catch {
234
- return true
235
- }
236
- }
237
-
238
- // Returns true (AVX2 present), false (absent), or null (couldn't determine).
239
- // Ask the OS directly via IsProcessorFeaturePresent (kernel32), which is
240
- // backed by CPUID — far more reliable than matching CPU model names, and it
241
- // works on the stock Windows PowerShell that ships with every supported
242
- // Windows version. Feature 40 = PF_AVX2_INSTRUCTIONS_AVAILABLE.
243
- function probeWindowsAvx2() {
244
- const script =
245
- "$f = Add-Type -MemberDefinition '[DllImport(\"kernel32.dll\")] " +
246
- "public static extern bool IsProcessorFeaturePresent(uint feature);' " +
247
- "-Name Cpu -Namespace Win32 -PassThru; $f::IsProcessorFeaturePresent(40)"
248
- try {
249
- const out = execFileSync(
250
- 'powershell.exe',
251
- ['-NoProfile', '-NonInteractive', '-Command', script],
252
- { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] },
253
- ).trim()
254
- if (out === 'True') return true
255
- if (out === 'False') return false
256
- return null
257
- } catch {
258
- // No PowerShell, locked-down policy, timeout, etc. — inconclusive.
259
- return null
260
- }
261
- }
262
-
263
- let _hasAvx2Cache
264
-
265
- function machineHasAvx2() {
266
- if (_hasAvx2Cache === undefined) {
267
- _hasAvx2Cache = detectMachineHasAvx2()
268
- }
269
- return _hasAvx2Cache
270
- }
271
-
272
- function detectMachineHasAvx2() {
273
- if (process.arch !== 'x64') {
274
- return true
275
- }
276
-
277
- // Linux detection is a cheap file read, so we don't bother persisting it.
278
- if (process.platform === 'linux') {
279
- return linuxCpuHasAvx2()
280
- }
281
-
282
- if (process.platform !== 'win32') {
283
- return true
284
- }
285
-
286
- // Windows detection shells out to PowerShell. getDefaultTargetKey runs on
287
- // every launch (via the version check), so cache the result on disk to keep
288
- // startup fast after the first probe.
289
- const cached = readCachedAvx2()
290
- if (cached !== null) {
291
- return cached
292
- }
293
- const detected = probeWindowsAvx2()
294
- if (detected === null) {
295
- // Inconclusive probe: assume AVX2 for this launch and rely on the SIGILL
296
- // fallback, but don't persist it — a transient failure must not lock in a
297
- // wrong answer for the lifetime of the install. We'll re-probe next launch.
298
- return true
299
- }
300
- writeCachedAvx2(detected)
301
- return detected
302
- }
303
-
304
- function getCpuFeatureCachePath() {
305
- return path.join(CONFIG.configDir, 'cpu-features.json')
306
- }
307
-
308
- function readCachedAvx2() {
309
- try {
310
- const cache = JSON.parse(fs.readFileSync(getCpuFeatureCachePath(), 'utf8'))
311
- return typeof cache.avx2 === 'boolean' ? cache.avx2 : null
312
- } catch {
313
- return null
314
- }
315
- }
316
-
317
- function writeCachedAvx2(value) {
318
- try {
319
- fs.mkdirSync(CONFIG.configDir, { recursive: true })
320
- fs.writeFileSync(getCpuFeatureCachePath(), JSON.stringify({ avx2: value }))
321
- } catch {
322
- // Best effort; we'll just re-probe next launch.
323
- }
324
- }
325
-
326
- function getDefaultTargetKey() {
327
- const override = getTargetOverride()
328
- if (override) {
329
- return override
330
- }
331
-
332
- const platformKey = getPlatformKey()
333
- // Select the binary up front from explicit CPU feature detection rather than
334
- // optimistically launching the AVX2 build and waiting for it to crash with
335
- // an illegal instruction. The crash isn't always a clean immediate failure —
336
- // it can surface later from a deeper code path — so older CPUs (e.g. an
337
- // Intel Xeon with AVX but no AVX2) are safer on baseline from the start.
338
- //
339
- // This assumes every baseline target is gated on AVX2 specifically, which
340
- // holds today (only linux-x64 and win32-x64 have baseline builds, both
341
- // AVX2-gated). If a baseline build is ever added for a different reason, give
342
- // BASELINE_FALLBACK_TARGETS a per-target capability and check that instead.
343
- if (BASELINE_FALLBACK_TARGETS[platformKey] && !machineHasAvx2()) {
344
- return BASELINE_FALLBACK_TARGETS[platformKey]
345
- }
346
-
347
- return platformKey
348
- }
349
-
350
- function getBaselineFallbackTargetKey() {
351
- // Runtime safety net: if proactive detection was unavailable or wrong and the
352
- // optimized binary still dies with SIGILL, fall back to baseline.
353
- return BASELINE_FALLBACK_TARGETS[getPlatformKey()] || null
354
- }
355
-
356
- function isTargetAllowedForThisMachine(target) {
357
- const override = getTargetOverride()
358
- if (override) {
359
- return target === override
360
- }
361
- // Check the baseline fallback first: it's always safe on its platform and
362
- // avoids running CPU detection when a baseline binary is already installed.
363
- return (
364
- target === getBaselineFallbackTargetKey() ||
365
- target === getDefaultTargetKey()
366
- )
367
- }
368
-
369
- function getDownloadTargetKey() {
370
- const override = getTargetOverride()
371
- if (override) {
372
- return override
373
- }
374
-
375
- const metadata = getCurrentMetadata()
376
- if (metadata?.target && isTargetAllowedForThisMachine(metadata.target)) {
377
- return metadata.target
378
- }
379
-
380
- return getDefaultTargetKey()
381
- }
382
-
383
- async function getLatestVersion() {
384
- try {
385
- const res = await httpGet(
386
- `https://registry.npmjs.org/${packageName}/latest`,
387
- )
388
-
389
- if (res.statusCode !== 200) return null
390
-
391
- const body = await streamToString(res)
392
- const packageData = JSON.parse(body)
393
-
394
- return packageData.version || null
395
- } catch (error) {
396
- return null
397
- }
398
- }
399
-
400
- function streamToString(stream) {
401
- return new Promise((resolve, reject) => {
402
- let data = ''
403
- stream.on('data', (chunk) => (data += chunk))
404
- stream.on('end', () => resolve(data))
405
- stream.on('error', reject)
406
- })
407
- }
408
-
409
- function getCurrentVersion() {
410
- try {
411
- const metadata = getCurrentMetadata()
412
- if (!metadata) {
413
- return null
414
- }
415
- if (!fs.existsSync(CONFIG.binaryPath)) {
416
- return null
417
- }
418
- const metadataTarget = metadata.target || getPlatformKey()
419
- if (!isTargetAllowedForThisMachine(metadataTarget)) {
420
- return null
421
- }
422
- return metadata.version || null
423
- } catch (error) {
424
- return null
425
- }
426
- }
427
-
428
- function getCurrentMetadata() {
429
- try {
430
- if (!fs.existsSync(CONFIG.metadataPath)) {
431
- return null
432
- }
433
- return JSON.parse(fs.readFileSync(CONFIG.metadataPath, 'utf8'))
434
- } catch {
435
- return null
436
- }
437
- }
438
-
439
- function compareVersions(v1, v2) {
440
- if (!v1 || !v2) return 0
441
-
442
- if (!v1.match(/^\d+(\.\d+)*$/)) {
443
- return -1
444
- }
445
-
446
- const parseVersion = (version) => {
447
- const parts = version.split('-')
448
- const mainParts = parts[0].split('.').map(Number)
449
- const prereleaseParts = parts[1] ? parts[1].split('.') : []
450
- return { main: mainParts, prerelease: prereleaseParts }
451
- }
452
-
453
- const p1 = parseVersion(v1)
454
- const p2 = parseVersion(v2)
455
-
456
- for (let i = 0; i < Math.max(p1.main.length, p2.main.length); i++) {
457
- const n1 = p1.main[i] || 0
458
- const n2 = p2.main[i] || 0
459
-
460
- if (n1 < n2) return -1
461
- if (n1 > n2) return 1
462
- }
463
-
464
- if (p1.prerelease.length === 0 && p2.prerelease.length === 0) {
465
- return 0
466
- } else if (p1.prerelease.length === 0) {
467
- return 1
468
- } else if (p2.prerelease.length === 0) {
469
- return -1
470
- } else {
471
- for (
472
- let i = 0;
473
- i < Math.max(p1.prerelease.length, p2.prerelease.length);
474
- i++
475
- ) {
476
- const pr1 = p1.prerelease[i] || ''
477
- const pr2 = p2.prerelease[i] || ''
478
-
479
- const isNum1 = !isNaN(parseInt(pr1))
480
- const isNum2 = !isNaN(parseInt(pr2))
30
+ module.exports = launcher
481
31
 
482
- if (isNum1 && isNum2) {
483
- const num1 = parseInt(pr1)
484
- const num2 = parseInt(pr2)
485
- if (num1 < num2) return -1
486
- if (num1 > num2) return 1
487
- } else if (isNum1 && !isNum2) {
488
- return 1
489
- } else if (!isNum1 && isNum2) {
490
- return -1
491
- } else if (pr1 < pr2) {
492
- return -1
493
- } else if (pr1 > pr2) {
494
- return 1
495
- }
496
- }
497
- return 0
498
- }
499
- }
500
-
501
- function formatBytes(bytes) {
502
- if (bytes === 0) return '0 B'
503
- const k = 1024
504
- const sizes = ['B', 'KB', 'MB', 'GB']
505
- const i = Math.floor(Math.log(bytes) / Math.log(k))
506
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
507
- }
508
-
509
- function createProgressBar(percentage, width = 30) {
510
- const filled = Math.round((width * percentage) / 100)
511
- const empty = width - filled
512
- return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
513
- }
514
-
515
- function isRetryableDownloadStatus(statusCode) {
516
- return (
517
- statusCode === 408 ||
518
- statusCode === 425 ||
519
- statusCode === 429 ||
520
- statusCode >= 500
521
- )
522
- }
523
-
524
- function isRetryableDownloadError(error) {
525
- if (error && typeof error.retryable === 'boolean') return error.retryable
526
- return !['EACCES', 'ENOSPC', 'EPERM', 'EROFS'].includes(error?.code)
527
- }
528
-
529
- function prepareTempDownloadDir() {
530
- if (fs.existsSync(CONFIG.tempDownloadDir)) {
531
- fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
532
- }
533
- fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
534
- }
535
-
536
- async function downloadAndExtract(downloadUrl, version, targetKey) {
537
- let attempts = 0
538
-
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
- })
549
-
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
- }
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 }),
584
- )
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 })
614
- }
615
-
616
- trackUpdateFailed(error.message, version, {
617
- stage: 'download',
618
- statusCode: error.statusCode,
619
- target: targetKey,
620
- attempts,
621
- })
622
- throw error
623
- }
624
- }
625
-
626
- async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
627
- const fileName = PLATFORM_TARGETS[targetKey]
628
-
629
- if (!fileName) {
630
- const error = new Error(
631
- `Unsupported platform: ${process.platform} ${process.arch}`,
632
- )
633
- trackUpdateFailed(error.message, version, {
634
- stage: 'platform_check',
635
- target: targetKey,
636
- })
637
- throw error
638
- }
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
-
651
- if (process.platform !== 'win32') {
652
- fs.chmodSync(tempBinaryPath, 0o755)
653
- }
654
-
655
- try {
656
- if (fs.existsSync(CONFIG.binaryPath)) {
657
- try {
658
- fs.unlinkSync(CONFIG.binaryPath)
659
- } catch (err) {
660
- const backupPath = CONFIG.binaryPath + `.old.${Date.now()}`
661
- try {
662
- fs.renameSync(CONFIG.binaryPath, backupPath)
663
- } catch (renameErr) {
664
- throw new Error(
665
- `Failed to replace existing binary. ` +
666
- `unlink error: ${err.code || err.message}, ` +
667
- `rename error: ${renameErr.code || renameErr.message}`,
668
- )
669
- }
670
- }
671
- }
672
- fs.renameSync(tempBinaryPath, CONFIG.binaryPath)
673
-
674
- // Move tree-sitter.wasm next to the binary if the tarball included
675
- // it. The CLI binary loads this at startup; embedding it inside the
676
- // binary itself was unreliable on Windows (bun --compile asset
677
- // bundling silently dropped or unbound it across several attempts),
678
- // so we ship it as a sibling file instead. Older artifacts that
679
- // pre-date this change won't have the wasm and will still install —
680
- // they'll just hit the same crash they had before, which is fine.
681
- const tempWasmPath = path.join(CONFIG.tempDownloadDir, 'tree-sitter.wasm')
682
- if (fs.existsSync(tempWasmPath)) {
683
- const targetWasmPath = path.join(
684
- path.dirname(CONFIG.binaryPath),
685
- 'tree-sitter.wasm',
686
- )
687
- try {
688
- if (fs.existsSync(targetWasmPath)) fs.unlinkSync(targetWasmPath)
689
- } catch {
690
- // best effort; rename below will surface the real error if it matters
691
- }
692
- fs.renameSync(tempWasmPath, targetWasmPath)
693
- }
694
-
695
- fs.writeFileSync(
696
- CONFIG.metadataPath,
697
- JSON.stringify({ version, target: targetKey }, null, 2),
698
- )
699
- } finally {
700
- if (fs.existsSync(CONFIG.tempDownloadDir)) {
701
- fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
702
- }
703
- }
704
-
705
- term.clearLine()
706
- console.log('Download complete! Starting Freebuff...')
707
- }
708
-
709
- async function ensureBinaryExists() {
710
- const currentVersion = getCurrentVersion()
711
- if (currentVersion !== null) {
712
- return
713
- }
714
-
715
- const version = await getLatestVersion()
716
- if (!version) {
717
- console.error('❌ Failed to determine latest version')
718
- console.error('Please check your internet connection and try again')
719
- process.exit(1)
720
- }
721
-
722
- try {
723
- await downloadBinary(version)
724
- } catch (error) {
725
- term.clearLine()
726
- console.error('❌ Failed to download freebuff:', error.message)
727
- console.error('Please check your internet connection and try again')
32
+ if (require.main === module) {
33
+ launcher.main().catch((error) => {
34
+ console.error('❌ Unexpected error:', error.message)
728
35
  process.exit(1)
729
- }
730
- }
731
-
732
- async function checkForUpdates(runningProcess, exitListener) {
733
- try {
734
- const currentVersion = getCurrentVersion()
735
-
736
- const latestVersion = await getLatestVersion()
737
- if (!latestVersion) return
738
-
739
- if (
740
- currentVersion === null ||
741
- compareVersions(currentVersion, latestVersion) < 0
742
- ) {
743
- term.clearLine()
744
-
745
- runningProcess.removeListener('exit', exitListener)
746
-
747
- await new Promise((resolve) => {
748
- let exited = false
749
- runningProcess.once('exit', () => {
750
- exited = true
751
- resolve()
752
- })
753
- runningProcess.kill('SIGTERM')
754
- setTimeout(() => {
755
- if (!exited) {
756
- runningProcess.kill('SIGKILL')
757
- // Safety: resolve after giving SIGKILL time to take effect
758
- setTimeout(() => resolve(), 1000)
759
- }
760
- }, 5000)
761
- })
762
-
763
- resetTerminal({ exitAlternateScreen: true })
764
- console.log(`Update available: ${currentVersion} → ${latestVersion}`)
765
-
766
- await downloadBinary(latestVersion)
767
-
768
- const newChild = spawnInstalledBinary({ detached: false })
769
- attachExitHandler(newChild)
770
-
771
- return new Promise(() => {})
772
- }
773
- } catch (error) {
774
- // Ignore update failures
775
- }
776
- }
777
-
778
- function printCrashDiagnostics(code, signal) {
779
- // Windows NTSTATUS codes (unsigned DWORD)
780
- const unsignedCode = getUnsignedExitCode(code)
781
- const isIllegalInstruction = isIllegalInstructionExit(code, signal)
782
- const isAccessViolation =
783
- signal === 'SIGSEGV' ||
784
- (process.platform === 'win32' && unsignedCode === 0xC0000005)
785
- const isBusError = signal === 'SIGBUS'
786
- const isAbort =
787
- signal === 'SIGABRT' ||
788
- (process.platform === 'win32' && unsignedCode === 0xC0000409)
789
-
790
- if (!isIllegalInstruction && !isAccessViolation && !isBusError && !isAbort) return
791
-
792
- const exitInfo = signal ? `signal ${signal}` : `code ${code}`
793
- console.error('')
794
- console.error(`❌ ${packageName} exited immediately (${exitInfo})`)
795
- console.error('')
796
-
797
- if (isIllegalInstruction) {
798
- console.error('Your CPU may not support the required instruction set (AVX2).')
799
- console.error('This typically affects CPUs from before 2013.')
800
- console.error('')
801
- printBaselineOverrideHint()
802
- } else if (isAccessViolation) {
803
- console.error('The binary crashed with an access violation.')
804
- console.error('')
805
- } else if (isBusError) {
806
- console.error('The binary crashed with a bus error.')
807
- console.error('This may indicate a platform compatibility issue.')
808
- console.error('')
809
- } else if (isAbort) {
810
- console.error('The binary crashed with an abort signal.')
811
- console.error('')
812
- }
813
-
814
- printSystemInfo()
815
- console.error('')
816
- console.error('Please report this issue at:')
817
- console.error(' https://github.com/CodebuffAI/codebuff/issues')
818
- console.error('')
819
- }
820
-
821
- function printBaselineOverrideHint() {
822
- const fallbackTarget = getBaselineFallbackTargetKey()
823
- if (!fallbackTarget) return
824
- console.error('To force the baseline (non-AVX2) build, set:')
825
- console.error(` ${packageName.toUpperCase()}_BINARY_TARGET=${fallbackTarget}`)
826
- console.error('')
827
- }
828
-
829
- function printSystemInfo() {
830
- const metadata = getCurrentMetadata()
831
- console.error('System info:')
832
- console.error(` Platform: ${process.platform} ${process.arch}`)
833
- console.error(` Node: ${process.version}`)
834
- if (process.arch === 'x64') {
835
- console.error(` AVX2: ${machineHasAvx2() ? 'yes' : 'no'}`)
836
- }
837
- console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
838
- console.error(` Binary: ${CONFIG.binaryPath}`)
839
- }
840
-
841
- function getInstalledBinaryStatus() {
842
- try {
843
- const stats = fs.statSync(CONFIG.binaryPath)
844
- return stats.isFile() ? `yes (${formatBytes(stats.size)})` : 'no'
845
- } catch {
846
- return 'no'
847
- }
848
- }
849
-
850
- function printSpawnFailure(err) {
851
- resetTerminal()
852
- const code = err && err.code ? ` (${err.code})` : ''
853
-
854
- console.error(`Failed to start ${packageName}: ${err.message}${code}`)
855
- console.error('')
856
- printSystemInfo()
857
- console.error(` Exists: ${getInstalledBinaryStatus()}`)
858
-
859
- if (process.platform === 'win32') {
860
- console.error('')
861
- console.error(
862
- 'On Windows, this can happen when Windows Security or antivirus blocks',
863
- )
864
- console.error(
865
- 'or quarantines the downloaded executable, or when the binary requires',
866
- )
867
- console.error('CPU instructions that are not available on this machine.')
868
- console.error('')
869
- printBaselineOverrideHint()
870
- }
871
-
872
- console.error('')
873
- console.error('Try deleting the downloaded files and running again:')
874
- console.error(` ${CONFIG.configDir}`)
875
- console.error('')
876
- }
877
-
878
- function exitOnSpawnFailure(err) {
879
- printSpawnFailure(err)
880
- process.exit(1)
881
- }
882
-
883
- function spawnInstalledBinary(options = {}) {
884
- if (!fs.existsSync(CONFIG.binaryPath)) {
885
- try {
886
- if (fs.existsSync(CONFIG.metadataPath)) fs.unlinkSync(CONFIG.metadataPath)
887
- } catch {
888
- // best effort
889
- }
890
- const error = new Error(
891
- `downloaded binary is missing at ${CONFIG.binaryPath}`,
892
- )
893
- error.code = 'BINARY_MISSING'
894
- exitOnSpawnFailure(error)
895
- }
896
-
897
- // spawn() only emits 'error' asynchronously for a few errno values
898
- // (EACCES, EAGAIN, EMFILE, ENFILE, ENOENT); everything else — notably
899
- // UNKNOWN on Windows when antivirus or Smart App Control blocks the
900
- // exe or the download is corrupt — is thrown synchronously.
901
- let child
902
- try {
903
- child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
904
- stdio: 'inherit',
905
- ...options,
906
- })
907
- } catch (err) {
908
- exitOnSpawnFailure(err)
909
- }
910
-
911
- child.on('error', exitOnSpawnFailure)
912
-
913
- return child
914
- }
915
-
916
- async function tryFallbackToBaseline(code, signal) {
917
- if (!isIllegalInstructionExit(code, signal)) {
918
- return false
919
- }
920
-
921
- const fallbackTarget = getBaselineFallbackTargetKey()
922
- if (!fallbackTarget) {
923
- return false
924
- }
925
-
926
- const metadata = getCurrentMetadata()
927
- const currentTarget = metadata?.target || getDefaultTargetKey()
928
- if (currentTarget === fallbackTarget) {
929
- return false
930
- }
931
-
932
- const version = metadata?.version || (await getLatestVersion())
933
- if (!version) {
934
- return false
935
- }
936
-
937
- resetTerminal({
938
- exitAlternateScreen: shouldExitAlternateScreen(code, signal),
939
36
  })
940
- console.error('')
941
- console.error(
942
- `${packageName} is switching to the older-CPU binary for this machine.`,
943
- )
944
-
945
- try {
946
- await downloadBinary(version, fallbackTarget)
947
- } catch (error) {
948
- term.clearLine()
949
- console.error(`Failed to download ${fallbackTarget}: ${error.message}`)
950
- return false
951
- }
952
-
953
- const child = spawnInstalledBinary({ detached: false })
954
- attachExitHandler(child, false)
955
- return true
956
- }
957
-
958
- function attachExitHandler(child, allowBaselineFallback = true) {
959
- const exitListener = async (code, signal) => {
960
- if (
961
- allowBaselineFallback &&
962
- (await tryFallbackToBaseline(code, signal))
963
- ) {
964
- return
965
- }
966
-
967
- resetTerminal({
968
- exitAlternateScreen: shouldExitAlternateScreen(code, signal),
969
- })
970
- printCrashDiagnostics(code, signal)
971
- process.exit(signal ? 1 : (code || 0))
972
- }
973
-
974
- child.on('exit', exitListener)
975
- return exitListener
976
37
  }
977
-
978
- async function main() {
979
- await ensureBinaryExists()
980
-
981
- const child = spawnInstalledBinary()
982
- const exitListener = attachExitHandler(child)
983
-
984
- setTimeout(() => {
985
- checkForUpdates(child, exitListener)
986
- }, 100)
987
- }
988
-
989
- main().catch((error) => {
990
- console.error('❌ Unexpected error:', error.message)
991
- process.exit(1)
992
- })