codebuff 1.0.684 → 1.0.686

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/launcher.js ADDED
@@ -0,0 +1,1469 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process')
4
+ const fs = require('fs')
5
+ const http = require('http')
6
+ const https = require('https')
7
+ const os = require('os')
8
+ const path = require('path')
9
+ const { pipeline } = require('stream/promises')
10
+ const zlib = require('zlib')
11
+
12
+ const tar = require('tar')
13
+ const { createReleaseHttpClient } = require('./http')
14
+
15
+ function createLauncher(productConfig) {
16
+ const {
17
+ packageName,
18
+ displayName,
19
+ wrapperVersion = null,
20
+ includeTreeSitterWasm = true,
21
+ startupBanner = [],
22
+ telemetryEvent = 'cli.update_codebuff_failed',
23
+ telemetryProperties = {},
24
+ tempDownloadDirName = `.${packageName}-download-temp`,
25
+ // Tests only. os.homedir() ignores $HOME under `bun test`, so pointing HOME
26
+ // at a temp dir is not enough to keep a test off the real ~/.config.
27
+ configDir: configDirOverride = null,
28
+ } = productConfig
29
+
30
+ /**
31
+ * Terminal escape sequences to reset terminal state after the child process exits.
32
+ * When the binary is SIGKILL'd, it can't clean up its own terminal state.
33
+ * The wrapper (this process) survives and must reset these modes.
34
+ */
35
+ const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
36
+ const SAFE_TERMINAL_RESET_SEQUENCES =
37
+ '\x1b[?1000l' + // Disable X10 mouse mode
38
+ '\x1b[?1002l' + // Disable button event mouse mode
39
+ '\x1b[?1003l' + // Disable any-event mouse mode (all motion)
40
+ '\x1b[?1006l' + // Disable SGR extended mouse mode
41
+ '\x1b[?1004l' + // Disable focus reporting
42
+ '\x1b[?2004l' + // Disable bracketed paste mode
43
+ '\x1b[<u' + // Pop kitty keyboard protocol flags
44
+ '\x1b[>4;0m' + // Reset modifyOtherKeys
45
+ '\x1b[?25h' // Show cursor
46
+
47
+ const FULL_TERMINAL_RESET_SEQUENCES =
48
+ EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
49
+
50
+ function resetTerminal(options = {}) {
51
+ const { exitAlternateScreen = false } = options
52
+
53
+ try {
54
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
55
+ process.stdin.setRawMode(false)
56
+ }
57
+ } catch {
58
+ // stdin may be closed
59
+ }
60
+ try {
61
+ if (process.stdout.isTTY) {
62
+ // Exiting the alternate screen is only safe after an interactive child.
63
+ // Plain CLI paths like --help never enter it, and ?1049l can erase output.
64
+ process.stdout.write(
65
+ exitAlternateScreen
66
+ ? FULL_TERMINAL_RESET_SEQUENCES
67
+ : SAFE_TERMINAL_RESET_SEQUENCES,
68
+ )
69
+ }
70
+ } catch {
71
+ // stdout may be closed
72
+ }
73
+ }
74
+
75
+ /**
76
+ * How long a binary has to survive before a crash stops looking like a
77
+ * startup failure. Used to bound the STATUS_STACK_BUFFER_OVERRUN heuristic
78
+ * below, which is only trustworthy for deaths during startup.
79
+ */
80
+ const STARTUP_CRASH_WINDOW_MS = 10000
81
+
82
+ /** Bytes of the binary's stderr kept for the crash report. */
83
+ const STDERR_TAIL_BYTES = 8192
84
+
85
+ function getUnsignedExitCode(code) {
86
+ return code != null && code < 0 ? code >>> 0 : code
87
+ }
88
+
89
+ function isWindowsNativeCrashCode(code) {
90
+ const unsignedCode = getUnsignedExitCode(code)
91
+ return (
92
+ process.platform === 'win32' &&
93
+ (unsignedCode === 0xc000001d ||
94
+ unsignedCode === 0xc0000005 ||
95
+ unsignedCode === 0xc0000409)
96
+ )
97
+ }
98
+
99
+ function shouldExitAlternateScreen(code, signal) {
100
+ return Boolean(signal) || isWindowsNativeCrashCode(code)
101
+ }
102
+
103
+ function isIllegalInstructionExit(code, signal) {
104
+ const unsignedCode = getUnsignedExitCode(code)
105
+ return (
106
+ signal === 'SIGILL' ||
107
+ (process.platform === 'win32' && unsignedCode === 0xc000001d)
108
+ )
109
+ }
110
+
111
+ /**
112
+ * A startup death that is probably this machine failing to run the optimized
113
+ * build, reported under the *other* Windows spelling.
114
+ *
115
+ * Bun is written in Zig, and a Zig panic on Windows reports itself with
116
+ * __fastfail(FAST_FAIL_FATAL_APP_EXIT) — NTSTATUS 0xC0000409
117
+ * (STATUS_STACK_BUFFER_OVERRUN, exit code 3221226505), not
118
+ * STATUS_ILLEGAL_INSTRUCTION. So a CPU without AVX2 does not reliably die on
119
+ * the illegal instruction itself: Bun detects the missing feature, then
120
+ * panics while starting up anyway (oven-sh/bun#28399), and the crash arrives
121
+ * as 0xC0000409. Only the SIGILL spelling was wired to the baseline
122
+ * fallback, which is why these machines crash-looped forever.
123
+ *
124
+ * On its own 0xC0000409 only means "the binary aborted", so this is bounded
125
+ * to deaths during startup. A panic ten minutes into a session is an
126
+ * ordinary bug, and reading it as a missing instruction set would send a
127
+ * perfectly capable machine to the slower build.
128
+ */
129
+ function isStartupCpuFeatureCrash(code, signal, msAlive) {
130
+ return (
131
+ process.platform === 'win32' &&
132
+ getUnsignedExitCode(code) === 0xc0000409 &&
133
+ !signal &&
134
+ typeof msAlive === 'number' &&
135
+ msAlive < STARTUP_CRASH_WINDOW_MS
136
+ )
137
+ }
138
+
139
+ function createConfig(packageName) {
140
+ const homeDir = os.homedir()
141
+ const configDir =
142
+ configDirOverride || path.join(homeDir, '.config', 'manicode')
143
+ const binaryName =
144
+ process.platform === 'win32' ? `${packageName}.exe` : packageName
145
+
146
+ return {
147
+ homeDir,
148
+ configDir,
149
+ binaryName,
150
+ binaryPath: path.join(configDir, binaryName),
151
+ metadataPath: path.join(configDir, `${packageName}-metadata.json`),
152
+ tempDownloadDir: path.join(configDir, tempDownloadDirName),
153
+ userAgent: `${packageName}-cli`,
154
+ requestTimeout: 20000,
155
+ downloadRequestTimeout: 120000,
156
+ downloadMaxAttempts: 3,
157
+ }
158
+ }
159
+
160
+ const CONFIG = createConfig(packageName)
161
+ const { downloadFile, httpGet, withRetries } = createReleaseHttpClient({
162
+ env: process.env,
163
+ userAgent: CONFIG.userAgent,
164
+ requestTimeout: CONFIG.requestTimeout,
165
+ })
166
+
167
+ function getPostHogConfig() {
168
+ const apiKey =
169
+ process.env.CODEBUFF_POSTHOG_API_KEY ||
170
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY
171
+ const host =
172
+ process.env.CODEBUFF_POSTHOG_HOST ||
173
+ process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
174
+
175
+ if (!apiKey || !host) {
176
+ return null
177
+ }
178
+
179
+ return { apiKey, host }
180
+ }
181
+
182
+ /**
183
+ * Track update failure event to PostHog.
184
+ * Fire-and-forget - errors are silently ignored.
185
+ */
186
+ function trackUpdateFailed(errorMessage, version, context = {}) {
187
+ try {
188
+ const posthogConfig = getPostHogConfig()
189
+ if (!posthogConfig) {
190
+ return
191
+ }
192
+
193
+ const payload = JSON.stringify({
194
+ api_key: posthogConfig.apiKey,
195
+ event: telemetryEvent,
196
+ properties: {
197
+ distinct_id: `anonymous-${CONFIG.homeDir}`,
198
+ error: errorMessage,
199
+ version: version || 'unknown',
200
+ platform: process.platform,
201
+ arch: process.arch,
202
+ ...telemetryProperties,
203
+ ...context,
204
+ },
205
+ timestamp: new Date().toISOString(),
206
+ })
207
+
208
+ const parsedUrl = new URL(`${posthogConfig.host}/capture/`)
209
+ const isHttps = parsedUrl.protocol === 'https:'
210
+ const options = {
211
+ hostname: parsedUrl.hostname,
212
+ port: parsedUrl.port || (isHttps ? 443 : 80),
213
+ path: parsedUrl.pathname + parsedUrl.search,
214
+ method: 'POST',
215
+ headers: {
216
+ 'Content-Type': 'application/json',
217
+ 'Content-Length': Buffer.byteLength(payload),
218
+ },
219
+ }
220
+
221
+ const transport = isHttps ? https : http
222
+ const req = transport.request(options)
223
+ req.on('error', () => {}) // Silently ignore errors
224
+ req.write(payload)
225
+ req.end()
226
+ } catch (e) {
227
+ // Silently ignore any tracking errors
228
+ }
229
+ }
230
+
231
+ const PLATFORM_TARGETS = {
232
+ 'linux-x64': `${packageName}-linux-x64.tar.gz`,
233
+ 'linux-x64-baseline': `${packageName}-linux-x64-baseline.tar.gz`,
234
+ 'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
235
+ 'darwin-x64': `${packageName}-darwin-x64.tar.gz`,
236
+ 'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`,
237
+ 'win32-x64': `${packageName}-win32-x64.tar.gz`,
238
+ 'win32-x64-baseline': `${packageName}-win32-x64-baseline.tar.gz`,
239
+ }
240
+
241
+ const BASELINE_FALLBACK_TARGETS = {
242
+ 'linux-x64': 'linux-x64-baseline',
243
+ 'win32-x64': 'win32-x64-baseline',
244
+ }
245
+
246
+ const term = {
247
+ clearLine: () => {
248
+ if (process.stderr.isTTY) {
249
+ process.stderr.write('\r\x1b[K')
250
+ }
251
+ },
252
+ write: (text) => {
253
+ term.clearLine()
254
+ process.stderr.write(text)
255
+ },
256
+ writeLine: (text) => {
257
+ term.clearLine()
258
+ process.stderr.write(text + '\n')
259
+ },
260
+ }
261
+
262
+ function getPlatformKey() {
263
+ return `${process.platform}-${process.arch}`
264
+ }
265
+
266
+ function getTargetOverride() {
267
+ const envNames = [
268
+ `${packageName.toUpperCase()}_BINARY_TARGET`,
269
+ 'CODEBUFF_BINARY_TARGET',
270
+ 'CLI_BINARY_TARGET',
271
+ ]
272
+
273
+ for (const envName of envNames) {
274
+ const target = process.env[envName]
275
+ if (target && PLATFORM_TARGETS[target]) {
276
+ return target
277
+ }
278
+ }
279
+
280
+ return null
281
+ }
282
+
283
+ function linuxCpuHasAvx2() {
284
+ try {
285
+ return /\bavx2\b/i.test(fs.readFileSync('/proc/cpuinfo', 'utf8'))
286
+ } catch {
287
+ return true
288
+ }
289
+ }
290
+
291
+ let _hasAvx2Cache
292
+
293
+ function machineHasAvx2() {
294
+ if (_hasAvx2Cache === undefined) {
295
+ _hasAvx2Cache = detectMachineHasAvx2()
296
+ }
297
+ return _hasAvx2Cache
298
+ }
299
+
300
+ function detectMachineHasAvx2() {
301
+ if (process.arch !== 'x64') {
302
+ return true
303
+ }
304
+
305
+ // A recorded illegal-instruction crash outranks everything below it: the
306
+ // binary actually failed on this machine, which beats any inference we
307
+ // could make about the CPU.
308
+ const recorded = readCachedAvx2()
309
+ if (recorded !== null) {
310
+ return recorded
311
+ }
312
+
313
+ // Linux can just ask, and a file read is cheap enough not to cache.
314
+ if (process.platform === 'linux') {
315
+ return linuxCpuHasAvx2()
316
+ }
317
+
318
+ // Everything else assumes AVX2 — true of every x64 CPU since ~2013.
319
+ //
320
+ // Windows is the case that matters, since it's the only other platform with
321
+ // a baseline build. It has no /proc/cpuinfo equivalent we can read without
322
+ // spawning something, and the probe that used to fill the gap asked
323
+ // PowerShell to compile a C# stub and P/Invoke
324
+ // kernel32!IsProcessorFeaturePresent — accurate, but a textbook malware
325
+ // shape that Windows Defender flagged as a "Suspicious PowerShell command
326
+ // line" on real user machines. The illegal-instruction handler corrects us
327
+ // instead: tryFallbackToBaseline() calls recordMachineLacksAvx2(), so a
328
+ // machine without AVX2 pays exactly one failed launch and is answered by
329
+ // the cache read above from then on.
330
+ return true
331
+ }
332
+
333
+ // Called from the illegal-instruction fallback. Persisting here is what keeps
334
+ // the optimistic assumption above from costing more than a single crash — and
335
+ // it is deliberately separate from the metadata target, so a lost or rewritten
336
+ // metadata file can't resurrect the AVX2 build on a CPU that can't run it.
337
+ function recordMachineLacksAvx2() {
338
+ _hasAvx2Cache = false
339
+ writeCachedAvx2(false)
340
+ }
341
+
342
+ function getCpuFeatureCachePath() {
343
+ return path.join(CONFIG.configDir, 'cpu-features.json')
344
+ }
345
+
346
+ function readCachedAvx2() {
347
+ try {
348
+ const cache = JSON.parse(
349
+ fs.readFileSync(getCpuFeatureCachePath(), 'utf8'),
350
+ )
351
+ return typeof cache.avx2 === 'boolean' ? cache.avx2 : null
352
+ } catch {
353
+ return null
354
+ }
355
+ }
356
+
357
+ function writeCachedAvx2(value) {
358
+ try {
359
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
360
+ fs.writeFileSync(
361
+ getCpuFeatureCachePath(),
362
+ JSON.stringify({ avx2: value }),
363
+ )
364
+ } catch {
365
+ // Best effort; we'll just re-probe next launch.
366
+ }
367
+ }
368
+
369
+ function getDefaultTargetKey() {
370
+ const override = getTargetOverride()
371
+ if (override) {
372
+ return override
373
+ }
374
+
375
+ const platformKey = getPlatformKey()
376
+ // Linux still detects up front (reading /proc/cpuinfo is free). Windows
377
+ // cannot without spawning a process, and the PowerShell probe that used to
378
+ // do it tripped Defender, so Windows is optimistic-then-corrected: see
379
+ // detectMachineHasAvx2() and tryFallbackToBaseline(). Once a machine has
380
+ // failed once, readCachedAvx2() answers here and baseline is chosen up front
381
+ // exactly as it used to be.
382
+ //
383
+ // This assumes every baseline target is gated on AVX2 specifically, which
384
+ // holds today (only linux-x64 and win32-x64 have baseline builds, both
385
+ // AVX2-gated). If a baseline build is ever added for a different reason, give
386
+ // BASELINE_FALLBACK_TARGETS a per-target capability and check that instead.
387
+ if (BASELINE_FALLBACK_TARGETS[platformKey] && !machineHasAvx2()) {
388
+ return BASELINE_FALLBACK_TARGETS[platformKey]
389
+ }
390
+
391
+ return platformKey
392
+ }
393
+
394
+ function getBaselineFallbackTargetKey() {
395
+ // Runtime safety net: if proactive detection was unavailable or wrong and the
396
+ // optimized binary still dies with SIGILL, fall back to baseline.
397
+ return BASELINE_FALLBACK_TARGETS[getPlatformKey()] || null
398
+ }
399
+
400
+ function isTargetAllowedForThisMachine(target) {
401
+ const override = getTargetOverride()
402
+ if (override) {
403
+ return target === override
404
+ }
405
+ // Check the baseline fallback first: it's always safe on its platform and
406
+ // avoids running CPU detection when a baseline binary is already installed.
407
+ return (
408
+ target === getBaselineFallbackTargetKey() ||
409
+ target === getDefaultTargetKey()
410
+ )
411
+ }
412
+
413
+ function getDownloadTargetKey() {
414
+ const override = getTargetOverride()
415
+ if (override) {
416
+ return override
417
+ }
418
+
419
+ const metadata = getCurrentMetadata()
420
+ if (metadata?.target && isTargetAllowedForThisMachine(metadata.target)) {
421
+ return metadata.target
422
+ }
423
+
424
+ return getDefaultTargetKey()
425
+ }
426
+
427
+ async function getLatestVersion() {
428
+ try {
429
+ const res = await httpGet(
430
+ `https://registry.npmjs.org/${packageName}/latest`,
431
+ )
432
+
433
+ if (res.statusCode !== 200) return null
434
+
435
+ const body = await streamToString(res)
436
+ const packageData = JSON.parse(body)
437
+
438
+ return packageData.version || null
439
+ } catch (error) {
440
+ return null
441
+ }
442
+ }
443
+
444
+ function streamToString(stream) {
445
+ return new Promise((resolve, reject) => {
446
+ let data = ''
447
+ stream.on('data', (chunk) => (data += chunk))
448
+ stream.on('end', () => resolve(data))
449
+ stream.on('error', reject)
450
+ })
451
+ }
452
+
453
+ function getCurrentVersion() {
454
+ try {
455
+ const metadata = getCurrentMetadata()
456
+ if (!metadata) {
457
+ return null
458
+ }
459
+ // Also verify the binary still exists
460
+ if (!fs.existsSync(CONFIG.binaryPath)) {
461
+ return null
462
+ }
463
+ const metadataTarget = metadata.target || getPlatformKey()
464
+ if (!isTargetAllowedForThisMachine(metadataTarget)) {
465
+ return null
466
+ }
467
+ return getMetadataVersion(metadata)
468
+ } catch (error) {
469
+ return null
470
+ }
471
+ }
472
+
473
+ function getMetadataVersion(metadata) {
474
+ return typeof metadata?.version === 'string' && metadata.version
475
+ ? metadata.version
476
+ : null
477
+ }
478
+
479
+ function getCurrentMetadata() {
480
+ try {
481
+ if (!fs.existsSync(CONFIG.metadataPath)) {
482
+ return null
483
+ }
484
+ return JSON.parse(fs.readFileSync(CONFIG.metadataPath, 'utf8'))
485
+ } catch {
486
+ return null
487
+ }
488
+ }
489
+
490
+ function parseVersion(version) {
491
+ if (typeof version !== 'string') return null
492
+
493
+ const match = version.match(
494
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/,
495
+ )
496
+ if (!match) return null
497
+
498
+ const prerelease = match[4]?.split('.') ?? []
499
+ if (prerelease.some((part) => /^0\d+$/.test(part))) return null
500
+
501
+ return {
502
+ main: match.slice(1, 4).map(BigInt),
503
+ prerelease,
504
+ }
505
+ }
506
+
507
+ function compareVersions(v1, v2) {
508
+ const p1 = parseVersion(v1)
509
+ const p2 = parseVersion(v2)
510
+
511
+ // Published package versions are valid semver. Treat malformed cached
512
+ // metadata as stale so the wrapper repairs it instead of trusting it.
513
+ if (!p1) return -1
514
+ if (!p2) return 1
515
+
516
+ for (let i = 0; i < p1.main.length; i++) {
517
+ if (p1.main[i] < p2.main[i]) return -1
518
+ if (p1.main[i] > p2.main[i]) return 1
519
+ }
520
+
521
+ if (p1.prerelease.length === 0) {
522
+ return p2.prerelease.length === 0 ? 0 : 1
523
+ }
524
+ if (p2.prerelease.length === 0) return -1
525
+
526
+ for (
527
+ let i = 0;
528
+ i < Math.max(p1.prerelease.length, p2.prerelease.length);
529
+ i++
530
+ ) {
531
+ const identifier1 = p1.prerelease[i]
532
+ const identifier2 = p2.prerelease[i]
533
+ if (identifier1 === undefined) return -1
534
+ if (identifier2 === undefined) return 1
535
+ if (identifier1 === identifier2) continue
536
+
537
+ const numeric1 = /^\d+$/.test(identifier1)
538
+ const numeric2 = /^\d+$/.test(identifier2)
539
+ if (numeric1 && numeric2) {
540
+ return BigInt(identifier1) < BigInt(identifier2) ? -1 : 1
541
+ }
542
+ if (numeric1 !== numeric2) return numeric1 ? -1 : 1
543
+ return identifier1 < identifier2 ? -1 : 1
544
+ }
545
+
546
+ return 0
547
+ }
548
+
549
+ function formatBytes(bytes) {
550
+ if (bytes === 0) return '0 B'
551
+ const k = 1024
552
+ const sizes = ['B', 'KB', 'MB', 'GB']
553
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
554
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
555
+ }
556
+
557
+ function createProgressBar(percentage, width = 30) {
558
+ const filled = Math.round((width * percentage) / 100)
559
+ const empty = width - filled
560
+ return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
561
+ }
562
+
563
+ function isRetryableDownloadError(error) {
564
+ if (error && typeof error.retryable === 'boolean') return error.retryable
565
+ return !['EACCES', 'ENOSPC', 'EPERM', 'EROFS'].includes(error?.code)
566
+ }
567
+
568
+ function getPartialArchivePath(version, targetKey) {
569
+ return path.join(
570
+ CONFIG.configDir,
571
+ `.${packageName}-${version}-${targetKey}.tar.gz.part`,
572
+ )
573
+ }
574
+
575
+ function getFileSize(filePath) {
576
+ try {
577
+ return fs.statSync(filePath).size
578
+ } catch (error) {
579
+ if (error.code === 'ENOENT') return 0
580
+ throw error
581
+ }
582
+ }
583
+
584
+ function removeFileIfPresent(filePath) {
585
+ try {
586
+ fs.unlinkSync(filePath)
587
+ } catch (error) {
588
+ if (error.code !== 'ENOENT') throw error
589
+ }
590
+ }
591
+
592
+ function formatDownloadSource(downloadUrl) {
593
+ try {
594
+ const parsedUrl = new URL(downloadUrl)
595
+ return `${parsedUrl.origin}${parsedUrl.pathname}`
596
+ } catch {
597
+ return downloadUrl
598
+ }
599
+ }
600
+
601
+ function printDownloadFailure(error) {
602
+ const code = error.code ? ` (${error.code})` : ''
603
+ console.error(
604
+ `❌ Failed to download ${packageName}: ${error.message}${code}`,
605
+ )
606
+
607
+ if (error.requestUrl) {
608
+ console.error(
609
+ `Download source: ${formatDownloadSource(error.requestUrl)}`,
610
+ )
611
+ }
612
+ if (error.downloadedBytes > 0) {
613
+ const total = error.totalBytes
614
+ ? ` of ${formatBytes(error.totalBytes)}`
615
+ : ''
616
+ console.error(
617
+ `Saved ${formatBytes(error.downloadedBytes)}${total}; the next run will resume this download.`,
618
+ )
619
+ } else if (error.requestUrl) {
620
+ console.error(
621
+ 'Please retry. The release host may be temporarily unavailable.',
622
+ )
623
+ } else {
624
+ console.error(
625
+ 'The downloaded update could not be installed; the existing binary was preserved when possible.',
626
+ )
627
+ }
628
+ }
629
+
630
+ function prepareTempDownloadDir() {
631
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
632
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
633
+ }
634
+ fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
635
+ }
636
+
637
+ async function downloadAndExtract(
638
+ downloadUrl,
639
+ version,
640
+ targetKey,
641
+ { quiet = false } = {},
642
+ ) {
643
+ let attempts = 0
644
+ const partialArchivePath = getPartialArchivePath(version, targetKey)
645
+ let totalBytes = null
646
+
647
+ try {
648
+ return await withRetries(
649
+ async (attempt) => {
650
+ attempts = attempt
651
+ prepareTempDownloadDir()
652
+ if (!quiet) term.write('Downloading...')
653
+
654
+ let lastProgressTime = Date.now()
655
+ const result = await downloadFile(downloadUrl, partialArchivePath, {
656
+ timeout: CONFIG.downloadRequestTimeout,
657
+ onProgress: ({ downloadedBytes, totalBytes: progressTotal }) => {
658
+ totalBytes = progressTotal
659
+ if (quiet) return
660
+ const now = Date.now()
661
+ if (
662
+ now - lastProgressTime < 100 &&
663
+ downloadedBytes !== progressTotal
664
+ ) {
665
+ return
666
+ }
667
+ lastProgressTime = now
668
+ if (progressTotal) {
669
+ const pct = Math.round((downloadedBytes / progressTotal) * 100)
670
+ term.write(
671
+ `Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(progressTotal)}`,
672
+ )
673
+ } else {
674
+ term.write(`Downloading... ${formatBytes(downloadedBytes)}`)
675
+ }
676
+ },
677
+ })
678
+ totalBytes = result.totalBytes
679
+
680
+ try {
681
+ await pipeline(
682
+ fs.createReadStream(partialArchivePath),
683
+ zlib.createGunzip(),
684
+ tar.x({ cwd: CONFIG.tempDownloadDir }),
685
+ )
686
+ } catch (error) {
687
+ // A complete archive that cannot be extracted is corrupt. Do not
688
+ // resume it on the next attempt.
689
+ removeFileIfPresent(partialArchivePath)
690
+ throw error
691
+ }
692
+
693
+ const tempBinaryPath = path.join(
694
+ CONFIG.tempDownloadDir,
695
+ CONFIG.binaryName,
696
+ )
697
+ if (!fs.existsSync(tempBinaryPath)) {
698
+ const files = fs.readdirSync(CONFIG.tempDownloadDir)
699
+ removeFileIfPresent(partialArchivePath)
700
+ const error = new Error(
701
+ `Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
702
+ )
703
+ error.retryable = false
704
+ throw error
705
+ }
706
+
707
+ removeFileIfPresent(partialArchivePath)
708
+ return tempBinaryPath
709
+ },
710
+ {
711
+ maxAttempts: CONFIG.downloadMaxAttempts,
712
+ shouldRetry: isRetryableDownloadError,
713
+ onRetry: ({ nextAttempt, delayMs }) => {
714
+ if (quiet) return
715
+ term.writeLine(
716
+ `Download interrupted. Retrying in ${delayMs / 1000}s (${nextAttempt}/${CONFIG.downloadMaxAttempts})...`,
717
+ )
718
+ },
719
+ },
720
+ )
721
+ } catch (error) {
722
+ try {
723
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
724
+ } catch {
725
+ // Best effort after a failed download.
726
+ }
727
+
728
+ trackUpdateFailed(error.message, version, {
729
+ stage: 'download',
730
+ errorCode: error.code,
731
+ statusCode: error.statusCode,
732
+ target: targetKey,
733
+ attempts,
734
+ bytesDownloaded: getFileSize(partialArchivePath),
735
+ totalBytes: error.totalBytes || totalBytes,
736
+ })
737
+ error.downloadedBytes = getFileSize(partialArchivePath)
738
+ error.totalBytes ||= totalBytes
739
+ error.requestUrl ||= downloadUrl
740
+ throw error
741
+ }
742
+ }
743
+
744
+ async function stageBinary(
745
+ version,
746
+ targetKey = getDownloadTargetKey(),
747
+ options = {},
748
+ ) {
749
+ const fileName = PLATFORM_TARGETS[targetKey]
750
+
751
+ if (!fileName) {
752
+ const error = new Error(
753
+ `Unsupported platform: ${process.platform} ${process.arch}`,
754
+ )
755
+ trackUpdateFailed(error.message, version, {
756
+ stage: 'platform_check',
757
+ target: targetKey,
758
+ })
759
+ throw error
760
+ }
761
+
762
+ const downloadUrl = `${
763
+ process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
764
+ }/api/releases/download/${version}/${fileName}`
765
+
766
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
767
+ const tempBinaryPath = await downloadAndExtract(
768
+ downloadUrl,
769
+ version,
770
+ targetKey,
771
+ options,
772
+ )
773
+
774
+ try {
775
+ if (process.platform !== 'win32') {
776
+ fs.chmodSync(tempBinaryPath, 0o755)
777
+ }
778
+ } catch (error) {
779
+ try {
780
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
781
+ } catch {
782
+ // Preserve the original chmod error.
783
+ }
784
+ throw error
785
+ }
786
+
787
+ return { tempBinaryPath, version, targetKey }
788
+ }
789
+
790
+ function replaceFileWithRollback(sourcePath, targetPath, replacements) {
791
+ const backupPath = fs.existsSync(targetPath)
792
+ ? `${targetPath}.old.${Date.now()}.${replacements.length}`
793
+ : null
794
+
795
+ if (backupPath) {
796
+ fs.renameSync(targetPath, backupPath)
797
+ }
798
+
799
+ try {
800
+ fs.renameSync(sourcePath, targetPath)
801
+ } catch (error) {
802
+ if (backupPath && fs.existsSync(backupPath)) {
803
+ fs.renameSync(backupPath, targetPath)
804
+ }
805
+ throw error
806
+ }
807
+
808
+ replacements.push({ backupPath, targetPath })
809
+ }
810
+
811
+ function rollbackReplacements(replacements) {
812
+ for (const { backupPath, targetPath } of replacements.reverse()) {
813
+ removeFileIfPresent(targetPath)
814
+ if (backupPath && fs.existsSync(backupPath)) {
815
+ fs.renameSync(backupPath, targetPath)
816
+ }
817
+ }
818
+ }
819
+
820
+ function commitReplacements(replacements) {
821
+ for (const { backupPath } of replacements) {
822
+ if (!backupPath) continue
823
+ try {
824
+ removeFileIfPresent(backupPath)
825
+ } catch {
826
+ // The replacement is already committed. A stale backup is safer than
827
+ // rolling back a working install because cleanup failed.
828
+ }
829
+ }
830
+ }
831
+
832
+ function installStagedBinary({ tempBinaryPath, version, targetKey }) {
833
+ const replacements = []
834
+ const metadataTempPath = `${CONFIG.metadataPath}.new.${process.pid}`
835
+
836
+ try {
837
+ fs.writeFileSync(
838
+ metadataTempPath,
839
+ JSON.stringify({ version, target: targetKey }, null, 2),
840
+ )
841
+ replaceFileWithRollback(tempBinaryPath, CONFIG.binaryPath, replacements)
842
+
843
+ // Move tree-sitter.wasm next to the binary if the tarball included
844
+ // it. The CLI binary loads this at startup; embedding it inside the
845
+ // binary itself was unreliable on Windows (bun --compile asset
846
+ // bundling silently dropped or unbound it across several attempts),
847
+ // so we ship it as a sibling file instead. Older artifacts that
848
+ // pre-date this change won't have the wasm and will still install —
849
+ // they'll just hit the same crash they had before, which is fine.
850
+ if (includeTreeSitterWasm) {
851
+ const tempWasmPath = path.join(
852
+ CONFIG.tempDownloadDir,
853
+ 'tree-sitter.wasm',
854
+ )
855
+ if (fs.existsSync(tempWasmPath)) {
856
+ const targetWasmPath = path.join(
857
+ path.dirname(CONFIG.binaryPath),
858
+ 'tree-sitter.wasm',
859
+ )
860
+ replaceFileWithRollback(tempWasmPath, targetWasmPath, replacements)
861
+ }
862
+ }
863
+
864
+ replaceFileWithRollback(
865
+ metadataTempPath,
866
+ CONFIG.metadataPath,
867
+ replacements,
868
+ )
869
+ commitReplacements(replacements)
870
+ } catch (error) {
871
+ rollbackReplacements(replacements)
872
+ throw error
873
+ } finally {
874
+ removeFileIfPresent(metadataTempPath)
875
+ if (fs.existsSync(CONFIG.tempDownloadDir)) {
876
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
877
+ }
878
+ }
879
+
880
+ term.clearLine()
881
+ console.log(`Download complete! Starting ${displayName}...`)
882
+ }
883
+
884
+ async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
885
+ const stagedBinary = await stageBinary(version, targetKey)
886
+ installStagedBinary(stagedBinary)
887
+ }
888
+
889
+ function getRequiredWrapperVersion(currentVersion) {
890
+ if (
891
+ !wrapperVersion ||
892
+ (currentVersion !== null &&
893
+ compareVersions(currentVersion, wrapperVersion) >= 0)
894
+ ) {
895
+ return null
896
+ }
897
+ return wrapperVersion
898
+ }
899
+
900
+ async function ensureBinaryReady() {
901
+ const currentVersion = getCurrentVersion()
902
+ const requiredWrapperVersion = getRequiredWrapperVersion(currentVersion)
903
+
904
+ if (currentVersion !== null && requiredWrapperVersion === null) {
905
+ return
906
+ }
907
+
908
+ // npm installs update this JavaScript wrapper but intentionally preserve the
909
+ // downloaded binary. If that binary exits before the background update
910
+ // check starts, it can otherwise remain stuck forever. The wrapper and its
911
+ // release binary share a version, so repair that stale cache synchronously
912
+ // without adding a registry lookup to healthy launches.
913
+ const version = requiredWrapperVersion ?? (await getLatestVersion())
914
+ if (!version) {
915
+ console.error('❌ Failed to determine latest version')
916
+ console.error('Please check your internet connection and try again')
917
+ process.exit(1)
918
+ }
919
+
920
+ try {
921
+ await downloadBinary(version)
922
+ } catch (error) {
923
+ term.clearLine()
924
+ printDownloadFailure(error)
925
+ if (currentVersion !== null) {
926
+ console.error(
927
+ `Continuing with cached ${packageName} ${currentVersion}.`,
928
+ )
929
+ return
930
+ }
931
+ process.exit(1)
932
+ }
933
+ }
934
+
935
+ function stopRunningProcess(runningProcess) {
936
+ return new Promise((resolve, reject) => {
937
+ let forceKillTimer
938
+ let forceKillTimeout
939
+
940
+ const cleanup = () => {
941
+ clearTimeout(forceKillTimer)
942
+ clearTimeout(forceKillTimeout)
943
+ runningProcess.removeListener('exit', handleExit)
944
+ }
945
+ const handleExit = () => {
946
+ cleanup()
947
+ resolve()
948
+ }
949
+ const fail = (error) => {
950
+ cleanup()
951
+ reject(error)
952
+ }
953
+
954
+ runningProcess.once('exit', handleExit)
955
+ forceKillTimer = setTimeout(() => {
956
+ forceKillTimeout = setTimeout(() => {
957
+ fail(new Error(`${packageName} did not exit after SIGKILL`))
958
+ }, 1000)
959
+ try {
960
+ runningProcess.kill('SIGKILL')
961
+ } catch (error) {
962
+ fail(error)
963
+ }
964
+ }, 5000)
965
+ try {
966
+ runningProcess.kill('SIGTERM')
967
+ } catch (error) {
968
+ fail(error)
969
+ }
970
+ })
971
+ }
972
+
973
+ async function checkForUpdates(runningProcess, exitListener) {
974
+ // main() schedules this 100ms after launch, so the binary it was handed can
975
+ // already be dead — a startup crash that handed off to the baseline
976
+ // fallback, most of all. Updating around a corpse would race that
977
+ // relaunch's download for the shared temp directory (prepareTempDownloadDir
978
+ // rmSyncs it) and then spend six seconds SIGKILLing a process that has
979
+ // already exited.
980
+ if (runningProcess.exitCode !== null || runningProcess.signalCode !== null) {
981
+ return
982
+ }
983
+
984
+ let stoppedForUpdate = false
985
+
986
+ try {
987
+ const currentVersion = getCurrentVersion()
988
+
989
+ const latestVersion = await getLatestVersion()
990
+ if (!latestVersion) return
991
+
992
+ if (
993
+ // Download new version if current version is unknown or outdated.
994
+ currentVersion === null ||
995
+ compareVersions(currentVersion, latestVersion) < 0
996
+ ) {
997
+ const stagedBinary = await stageBinary(
998
+ latestVersion,
999
+ getDownloadTargetKey(),
1000
+ { quiet: true },
1001
+ )
1002
+
1003
+ term.clearLine()
1004
+
1005
+ runningProcess.removeListener('exit', exitListener)
1006
+ try {
1007
+ await stopRunningProcess(runningProcess)
1008
+ } catch (error) {
1009
+ runningProcess.on('exit', exitListener)
1010
+ throw error
1011
+ }
1012
+ stoppedForUpdate = true
1013
+
1014
+ resetTerminal({ exitAlternateScreen: true })
1015
+ console.log(`Update available: ${currentVersion} → ${latestVersion}`)
1016
+
1017
+ installStagedBinary(stagedBinary)
1018
+
1019
+ const newChild = spawnInstalledBinary({ detached: false })
1020
+ attachExitHandler(newChild)
1021
+
1022
+ return new Promise(() => {})
1023
+ }
1024
+ } catch (error) {
1025
+ if (stoppedForUpdate && fs.existsSync(CONFIG.binaryPath)) {
1026
+ console.error(
1027
+ `Update failed; restarting ${packageName} ${getCurrentVersion()}.`,
1028
+ )
1029
+ const child = spawnInstalledBinary({ detached: false })
1030
+ attachExitHandler(child)
1031
+ return new Promise(() => {})
1032
+ }
1033
+ try {
1034
+ fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
1035
+ } catch {
1036
+ // Best effort after a failed background update.
1037
+ }
1038
+ // A staging failure leaves the current process and binary untouched.
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Make captured output safe to print back to the terminal.
1044
+ *
1045
+ * The tail is replayed *after* resetTerminal() has put the terminal back in
1046
+ * order, so it must not be able to undo that: a stray \x1b[?1049h or
1047
+ * \x1b[?1003h in what the binary printed would re-enter the alternate screen
1048
+ * or re-enable mouse reporting, hiding the very report it is part of. Strip
1049
+ * the escapes and keep the words.
1050
+ */
1051
+ function sanitizeForReplay(text) {
1052
+ if (!text) return ''
1053
+ return (
1054
+ text
1055
+ .replace(/\r\n?/g, '\n')
1056
+ // CSI sequences — the ones that actually change terminal state.
1057
+ .replace(/\x1b\[[0-9;:?<>=]*[ -/]*[@-~]/g, '')
1058
+ // Everything else: strip the control bytes and keep the text. An OSC
1059
+ // or a lone escape loses its introducer and degrades to inert
1060
+ // characters, which is all a crash report needs it to be.
1061
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
1062
+ .replace(/\s+$/, '')
1063
+ )
1064
+ }
1065
+
1066
+ function printCrashDiagnostics(code, signal, context = {}) {
1067
+ const { msAlive = null, stderrTail = '' } = context
1068
+ // Windows NTSTATUS codes (unsigned DWORD)
1069
+ const unsignedCode = getUnsignedExitCode(code)
1070
+ const isIllegalInstruction = isIllegalInstructionExit(code, signal)
1071
+ const isAccessViolation =
1072
+ signal === 'SIGSEGV' ||
1073
+ (process.platform === 'win32' && unsignedCode === 0xc0000005)
1074
+ const isBusError = signal === 'SIGBUS'
1075
+ const isAbort =
1076
+ signal === 'SIGABRT' ||
1077
+ (process.platform === 'win32' && unsignedCode === 0xc0000409)
1078
+
1079
+ if (!isIllegalInstruction && !isAccessViolation && !isBusError && !isAbort)
1080
+ return
1081
+
1082
+ const exitInfo = signal ? `signal ${signal}` : `code ${code}`
1083
+ console.error('')
1084
+ console.error(`❌ ${packageName} exited immediately (${exitInfo})`)
1085
+ console.error('')
1086
+
1087
+ if (isIllegalInstruction) {
1088
+ console.error(
1089
+ 'Your CPU may not support the required instruction set (AVX2).',
1090
+ )
1091
+ console.error('This typically affects CPUs from before 2013.')
1092
+ console.error('')
1093
+ printBaselineOverrideHint()
1094
+ } else if (isAccessViolation) {
1095
+ console.error('The binary crashed with an access violation.')
1096
+ console.error('')
1097
+ } else if (isBusError) {
1098
+ console.error('The binary crashed with a bus error.')
1099
+ console.error('This may indicate a platform compatibility issue.')
1100
+ console.error('')
1101
+ } else if (isAbort) {
1102
+ const startupCpuCrash = isStartupCpuFeatureCrash(code, signal, msAlive)
1103
+ console.error(
1104
+ startupCpuCrash
1105
+ ? 'The binary aborted while starting up.'
1106
+ : 'The binary crashed with an abort signal.',
1107
+ )
1108
+ console.error('')
1109
+ // Reached only once the baseline fallback has declined or failed, so name
1110
+ // the case the user can still act on instead of leaving them with an
1111
+ // abort code and a bug-tracker link.
1112
+ if (startupCpuCrash) {
1113
+ if (getCurrentMetadata()?.target === getBaselineFallbackTargetKey()) {
1114
+ console.error(
1115
+ 'This is already the older-CPU (baseline) build, so a missing AVX2',
1116
+ )
1117
+ console.error(
1118
+ 'instruction set is not the whole story — please include the output',
1119
+ )
1120
+ console.error('below in your report.')
1121
+ console.error('')
1122
+ } else {
1123
+ console.error(
1124
+ 'On x64 Windows this is usually a CPU without AVX2 support, which',
1125
+ )
1126
+ console.error('the standard build requires.')
1127
+ console.error('')
1128
+ printBaselineOverrideHint()
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ printSystemInfo()
1134
+ const tailText = sanitizeForReplay(stderrTail)
1135
+ if (tailText) {
1136
+ console.error('')
1137
+ console.error(`Last output from ${packageName}:`)
1138
+ for (const line of tailText.split('\n')) {
1139
+ console.error(` ${line}`)
1140
+ }
1141
+ }
1142
+ console.error('')
1143
+ console.error('Please report this issue at:')
1144
+ console.error(' https://github.com/CodebuffAI/codebuff/issues')
1145
+ console.error('')
1146
+ }
1147
+
1148
+ function printBaselineOverrideHint() {
1149
+ const fallbackTarget = getBaselineFallbackTargetKey()
1150
+ if (!fallbackTarget) return
1151
+ console.error('To force the baseline (non-AVX2) build, set:')
1152
+ console.error(
1153
+ ` ${packageName.toUpperCase()}_BINARY_TARGET=${fallbackTarget}`,
1154
+ )
1155
+ console.error('')
1156
+ }
1157
+
1158
+ /**
1159
+ * What we actually know about AVX2, not what we assume.
1160
+ *
1161
+ * The old line printed a flat "yes" from machineHasAvx2(), which on Windows
1162
+ * is an optimistic default and not a measurement (see detectMachineHasAvx2).
1163
+ * Every crash report that reached us therefore claimed AVX2 was present on
1164
+ * machines we had never asked, which is exactly the evidence that would have
1165
+ * pointed at the CPU.
1166
+ */
1167
+ function describeAvx2Support() {
1168
+ if (readCachedAvx2() === false) return 'no (recorded crash)'
1169
+ if (process.platform === 'linux') return machineHasAvx2() ? 'yes' : 'no'
1170
+ return 'not checked (assumed present)'
1171
+ }
1172
+
1173
+ function printSystemInfo() {
1174
+ const metadata = getCurrentMetadata()
1175
+ console.error('System info:')
1176
+ console.error(` Platform: ${process.platform} ${process.arch}`)
1177
+ console.error(` Node: ${process.version}`)
1178
+ if (process.arch === 'x64') {
1179
+ console.error(` AVX2: ${describeAvx2Support()}`)
1180
+ }
1181
+ console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
1182
+ console.error(` Binary: ${CONFIG.binaryPath}`)
1183
+ }
1184
+
1185
+ function getInstalledBinaryStatus() {
1186
+ try {
1187
+ const stats = fs.statSync(CONFIG.binaryPath)
1188
+ return stats.isFile() ? `yes (${formatBytes(stats.size)})` : 'no'
1189
+ } catch {
1190
+ return 'no'
1191
+ }
1192
+ }
1193
+
1194
+ function printSpawnFailure(err) {
1195
+ resetTerminal()
1196
+ const code = err && err.code ? ` (${err.code})` : ''
1197
+
1198
+ console.error(`Failed to start ${packageName}: ${err.message}${code}`)
1199
+ console.error('')
1200
+ printSystemInfo()
1201
+ console.error(` Exists: ${getInstalledBinaryStatus()}`)
1202
+
1203
+ if (process.platform === 'win32') {
1204
+ console.error('')
1205
+ console.error(
1206
+ 'On Windows, this can happen when Windows Security or antivirus blocks',
1207
+ )
1208
+ console.error(
1209
+ 'or quarantines the downloaded executable, or when the binary requires',
1210
+ )
1211
+ console.error('CPU instructions that are not available on this machine.')
1212
+ console.error('')
1213
+ printBaselineOverrideHint()
1214
+ }
1215
+
1216
+ console.error('')
1217
+ console.error('Try deleting the downloaded files and running again:')
1218
+ console.error(` ${CONFIG.configDir}`)
1219
+ console.error('')
1220
+ }
1221
+
1222
+ function exitOnSpawnFailure(err) {
1223
+ printSpawnFailure(err)
1224
+ process.exit(1)
1225
+ }
1226
+
1227
+ function spawnInstalledBinary(options = {}) {
1228
+ if (!fs.existsSync(CONFIG.binaryPath)) {
1229
+ try {
1230
+ if (fs.existsSync(CONFIG.metadataPath))
1231
+ fs.unlinkSync(CONFIG.metadataPath)
1232
+ } catch {
1233
+ // best effort
1234
+ }
1235
+ const error = new Error(
1236
+ `downloaded binary is missing at ${CONFIG.binaryPath}`,
1237
+ )
1238
+ error.code = 'BINARY_MISSING'
1239
+ exitOnSpawnFailure(error)
1240
+ }
1241
+
1242
+ // spawn() only emits 'error' asynchronously for a few errno values
1243
+ // (EACCES, EAGAIN, EMFILE, ENFILE, ENOENT); everything else — notably
1244
+ // UNKNOWN on Windows when antivirus or Smart App Control blocks the
1245
+ // exe or the download is corrupt — is thrown synchronously.
1246
+ let child
1247
+ try {
1248
+ const { env: optionEnv, ...spawnOptions } = options
1249
+ child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
1250
+ // stdin/stdout stay inherited — the TUI owns the terminal and must see
1251
+ // a real tty. stderr is teed on Windows only; see watchLaunch.
1252
+ stdio:
1253
+ process.platform === 'win32'
1254
+ ? ['inherit', 'inherit', 'pipe']
1255
+ : ['inherit', 'inherit', 'inherit'],
1256
+ ...spawnOptions,
1257
+ env: {
1258
+ ...process.env,
1259
+ ...optionEnv,
1260
+ CODEBUFF_LAUNCHER_PID: String(process.pid),
1261
+ },
1262
+ })
1263
+ } catch (err) {
1264
+ exitOnSpawnFailure(err)
1265
+ }
1266
+
1267
+ child.on('error', exitOnSpawnFailure)
1268
+ child.launch = watchLaunch(child)
1269
+
1270
+ return child
1271
+ }
1272
+
1273
+ /**
1274
+ * Watch a launch so its death can be explained: how long the binary lived,
1275
+ * and what it printed on the way out.
1276
+ *
1277
+ * The stderr tee is what makes a crash report self-contained. When the binary
1278
+ * dies natively we reset the terminal, and that reset leaves the alternate
1279
+ * screen with \x1b[?1049l — discarding its contents, including any panic Bun
1280
+ * printed there. (The CLI's own terminal watchdog sends the same sequence, so
1281
+ * dropping it here wouldn't help.) Every report of codebuff#792 is a launcher
1282
+ * message with no panic text above it; keeping a copy means the next one
1283
+ * arrives with Bun's own words in it.
1284
+ *
1285
+ * Only Windows pipes stderr — that's where the reports come from, and
1286
+ * everywhere else stderr stays a real tty.
1287
+ */
1288
+ function watchLaunch(child) {
1289
+ const startedAt = Date.now()
1290
+ const chunks = []
1291
+ let bufferedBytes = 0
1292
+
1293
+ child.stderr?.on('data', (chunk) => {
1294
+ try {
1295
+ // Straight through: as far as the binary and the user are concerned,
1296
+ // this is still its terminal.
1297
+ process.stderr.write(chunk)
1298
+ } catch {
1299
+ // stderr may be closed
1300
+ }
1301
+ chunks.push(chunk)
1302
+ bufferedBytes += chunk.length
1303
+ while (bufferedBytes > STDERR_TAIL_BYTES && chunks.length > 1) {
1304
+ bufferedBytes -= chunks.shift().length
1305
+ }
1306
+ })
1307
+
1308
+ return {
1309
+ msAlive: () => Date.now() - startedAt,
1310
+ stderrTail: () =>
1311
+ Buffer.concat(chunks).toString('utf8').slice(-STDERR_TAIL_BYTES),
1312
+ // 'exit' can beat the last bytes through the pipe; 'close' is the event
1313
+ // that means the stdio is drained too. The timeout is a bound on a stuck
1314
+ // pipe and is deliberately NOT unref'd — an unref'd timer lets the
1315
+ // process exit before it fires, which would swallow the crash report
1316
+ // entirely. Already-drained streams resolve without waiting for it.
1317
+ drained: () =>
1318
+ child.stderr && !child.stderr.readableEnded
1319
+ ? new Promise((resolve) => {
1320
+ child.once('close', resolve)
1321
+ child.stderr.once('end', resolve)
1322
+ setTimeout(resolve, 250)
1323
+ })
1324
+ : Promise.resolve(),
1325
+ }
1326
+ }
1327
+
1328
+ async function tryFallbackToBaseline(code, signal, msAlive) {
1329
+ // Two spellings of one failure, at two levels of certainty. SIGILL /
1330
+ // STATUS_ILLEGAL_INSTRUCTION is proof this CPU cannot run this build; a
1331
+ // Windows startup abort is a strong suspicion (see isStartupCpuFeatureCrash).
1332
+ const confirmed = isIllegalInstructionExit(code, signal)
1333
+ if (!confirmed && !isStartupCpuFeatureCrash(code, signal, msAlive)) {
1334
+ return false
1335
+ }
1336
+
1337
+ const fallbackTarget = getBaselineFallbackTargetKey()
1338
+ if (!fallbackTarget) {
1339
+ return false
1340
+ }
1341
+
1342
+ // An explicit target is the user's decision; don't download over it.
1343
+ if (getTargetOverride()) {
1344
+ return false
1345
+ }
1346
+
1347
+ const metadata = getCurrentMetadata()
1348
+ const currentTarget = metadata?.target || getDefaultTargetKey()
1349
+ if (currentTarget === fallbackTarget) {
1350
+ return false
1351
+ }
1352
+
1353
+ // Only a confirmed illegal instruction gets written down. Persisting it
1354
+ // before the download is what caps the cost at one crash: even if the
1355
+ // download or the relaunch fails, we never optimistically pick the AVX2
1356
+ // build again. A suspected crash records nothing — installing the baseline
1357
+ // already keeps this machine on it, so a guess that turns out to be wrong
1358
+ // costs the slower build instead of leaving cpu-features.json asserting a
1359
+ // CPU limitation we never observed.
1360
+ if (confirmed) {
1361
+ recordMachineLacksAvx2()
1362
+ }
1363
+
1364
+ const version = metadata?.version || (await getLatestVersion())
1365
+ if (!version) {
1366
+ return false
1367
+ }
1368
+
1369
+ resetTerminal({
1370
+ exitAlternateScreen: shouldExitAlternateScreen(code, signal),
1371
+ })
1372
+ console.error('')
1373
+ console.error(
1374
+ confirmed
1375
+ ? `${packageName} is switching to the older-CPU binary for this machine.`
1376
+ : `${packageName} crashed on startup; trying the older-CPU binary.`,
1377
+ )
1378
+
1379
+ try {
1380
+ await downloadBinary(version, fallbackTarget)
1381
+ } catch (error) {
1382
+ term.clearLine()
1383
+ console.error(`Failed to download ${fallbackTarget}: ${error.message}`)
1384
+ return false
1385
+ }
1386
+
1387
+ const child = spawnInstalledBinary({ detached: false })
1388
+ attachExitHandler(child, false)
1389
+ return true
1390
+ }
1391
+
1392
+ function attachExitHandler(child, allowBaselineFallback = true) {
1393
+ const exitListener = async (code, signal) => {
1394
+ // A child we never watched (only reachable from a test) reports no age
1395
+ // rather than a suspiciously young one: absent evidence must not be read
1396
+ // as a startup crash and trigger a download.
1397
+ const msAlive = child.launch ? child.launch.msAlive() : Infinity
1398
+
1399
+ let stderrTail = ''
1400
+ if (child.launch && (isWindowsNativeCrashCode(code) || signal)) {
1401
+ await child.launch.drained()
1402
+ stderrTail = child.launch.stderrTail()
1403
+ }
1404
+
1405
+ if (
1406
+ allowBaselineFallback &&
1407
+ (await tryFallbackToBaseline(code, signal, msAlive))
1408
+ ) {
1409
+ return
1410
+ }
1411
+
1412
+ resetTerminal({
1413
+ exitAlternateScreen: shouldExitAlternateScreen(code, signal),
1414
+ })
1415
+ printCrashDiagnostics(code, signal, { msAlive, stderrTail })
1416
+ process.exit(signal ? 1 : code || 0)
1417
+ }
1418
+
1419
+ child.on('exit', exitListener)
1420
+ return exitListener
1421
+ }
1422
+
1423
+ async function main() {
1424
+ for (const line of startupBanner) {
1425
+ console.log(line)
1426
+ }
1427
+ if (startupBanner.length > 0) console.log('')
1428
+
1429
+ await ensureBinaryReady()
1430
+
1431
+ const child = spawnInstalledBinary()
1432
+ const exitListener = attachExitHandler(child)
1433
+
1434
+ setTimeout(() => {
1435
+ checkForUpdates(child, exitListener)
1436
+ }, 100)
1437
+ }
1438
+
1439
+ return {
1440
+ config: productConfig,
1441
+ main,
1442
+ stopRunningProcess,
1443
+ // Internals exposed for tests only. The AVX2 path is optimistic-then-
1444
+ // corrected (see detectMachineHasAvx2), so the correction has to be
1445
+ // exercised directly — there is no way to make a CI runner lack AVX2.
1446
+ __testing: {
1447
+ detectMachineHasAvx2,
1448
+ recordMachineLacksAvx2,
1449
+ readCachedAvx2,
1450
+ isIllegalInstructionExit,
1451
+ isStartupCpuFeatureCrash,
1452
+ tryFallbackToBaseline,
1453
+ printCrashDiagnostics,
1454
+ checkForUpdates,
1455
+ spawnInstalledBinary,
1456
+ attachExitHandler,
1457
+ getDefaultTargetKey,
1458
+ getCpuFeatureCachePath,
1459
+ getCurrentVersion,
1460
+ getMetadataVersion,
1461
+ getRequiredWrapperVersion,
1462
+ ensureBinaryReady,
1463
+ isTargetAllowedForThisMachine,
1464
+ CONFIG,
1465
+ },
1466
+ }
1467
+ }
1468
+
1469
+ module.exports = { createLauncher }