freebuff 0.1.2 → 0.1.5

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/README.md +2 -2
  2. package/launcher.js +298 -46
  3. package/package.json +9 -9
package/README.md CHANGED
@@ -33,7 +33,7 @@ and keep the extracted `freebuff` binary and `tree-sitter.wasm` together.
33
33
 
34
34
  ## Why Freebuff?
35
35
 
36
- **Choice of leading models** — The regular CLI picker includes GLM 5.3 Flash, DeepSeek, GPT-6 Luna, MiMo 2.6 Flash, MiMo 2.6 Pro, Solar Mini 4, Solar Pro 4, Space Bunny Alpha, Gemini 3.8 Flash, and Muse Spark 1.2, with GLM 5.3 Flash selected by default.
36
+ **Choice of leading models** — The regular CLI picker includes GLM 5.3 Flash, DeepSeek, GPT-6 Luna, MiMo 2.6 Flash, MiMo 2.6 Pro, Solar Mini 4, Solar Pro 4, Space Bunny Alpha, Gemini 3.8 Flash, and Muse Spark 1.3, with GLM 5.3 Flash selected by default.
37
37
 
38
38
  **Fast** — 5–10× speed up. 3–5× tokens per second compared to Claude, plus context gathering in seconds.
39
39
 
@@ -43,7 +43,7 @@ and keep the extracted `freebuff` binary and `tree-sitter.wasm` together.
43
43
 
44
44
  **How can it be free?** Freebuff is supported by text ads.
45
45
 
46
- **What models do you use?** In full mode, the regular picker includes GLM 5.3 Flash, DeepSeek V4.1 Flash, GPT-6 Luna, MiMo 2.6 Flash, MiMo 2.6 Pro, Solar Mini 4, Solar Pro 4, Space Bunny Alpha, Gemini 3.8 Flash, and Muse Spark 1.2. Space Bunny Alpha is a beta stealth model from an anonymous provider that retains prompts. Gemini 3.8 Flash requires a paid plan: without one it is listed but locked, and selecting it opens the plans page. GPT-6 Luna and MiMo 2.6 Pro are open to every full-access account. Muse Spark 1.2 has a 1M-token context and is rate limited across all users, so at busy times it queues and then answers on DeepSeek V4 Flash instead of leaving you waiting. GLM 5.3 Flash is the CLI default, and it costs no session at all, so the default is always available no matter how much you have used. Limited mode uses GLM 5.3 Flash, DeepSeek V4.1 Flash, MiMo 2.6 Flash, Solar Mini 4, and Solar Pro 4. Most models draw on your normal daily sessions rather than a separate limit; GLM 5.3 Flash, DeepSeek V4.1 Flash, MiMo 2.6 Flash, Solar Mini 4, and Solar Pro 4 are unmetered at full access. Models may still serve from a quantized (Q8_0) build.
46
+ **What models do you use?** In full mode, the regular picker includes GLM 5.3 Flash, DeepSeek V4.1 Flash, GPT-6 Luna, MiMo 2.6 Flash, MiMo 2.6 Pro, Solar Mini 4, Solar Pro 4, Space Bunny Alpha, Gemini 3.8 Flash, and Muse Spark 1.3. Space Bunny Alpha is a beta stealth model from an anonymous provider that retains prompts. Gemini 3.8 Flash and Muse Spark 1.3 require a paid plan: without one they are listed but locked, and selecting one opens the plans page. GPT-6 Luna and MiMo 2.6 Pro are open to every full-access account. Muse Spark 1.3 has a 1M-token context and is rate limited across all users, so at busy times it queues and then answers on DeepSeek V4 Flash instead of leaving you waiting. GLM 5.3 Flash is the CLI default, and it costs no session at all, so the default is always available no matter how much you have used. Limited mode uses GLM 5.3 Flash, DeepSeek V4.1 Flash, MiMo 2.6 Flash, Solar Mini 4, and Solar Pro 4. Most models draw on your normal daily sessions rather than a separate limit; GLM 5.3 Flash, DeepSeek V4.1 Flash, MiMo 2.6 Flash, Solar Mini 4, and Solar Pro 4 are unmetered at full access. Models may still serve from a quantized (Q8_0) build.
47
47
 
48
48
  **Which countries is Freebuff available in?** All countries. Freebuff runs in "full" mode in the US, Canada, UK, EU, and other select countries, and in "limited" mode everywhere else (or while using a VPN). See [freebuff.com](https://freebuff.com) for the full list.
49
49
 
package/launcher.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn } = require('child_process')
3
+ const { execFileSync, spawn } = require('child_process')
4
4
  const crypto = require('crypto')
5
5
  const fs = require('fs')
6
6
  const http = require('http')
@@ -35,15 +35,51 @@ const PLATFORM_TARGET_KEYS = [
35
35
 
36
36
  const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]'])
37
37
 
38
+ /**
39
+ * Registrable domains that serve release downloads. An https override must
40
+ * name one of these or a subdomain of one.
41
+ */
42
+ const RELEASE_ORIGIN_DOMAINS = ['codebuff.com', 'freebuff.com']
43
+
44
+ function isReleaseOriginHost(hostname) {
45
+ const host = hostname.toLowerCase()
46
+ return RELEASE_ORIGIN_DOMAINS.some(
47
+ (domain) => host === domain || host.endsWith(`.${domain}`),
48
+ )
49
+ }
50
+
51
+ /**
52
+ * A release version the launcher will put into a download URL or a file
53
+ * name: strict `MAJOR.MINOR.PATCH` with an optional dot-separated
54
+ * alphanumeric prerelease, and nothing else — no build metadata, no
55
+ * leading `v`, no whitespace, `/`, `..`, `%` or `?`. The version reaches
56
+ * `/api/releases/download/<version>/<file>` and the partial-archive path
57
+ * from the npm registry document or the local metadata file, and neither
58
+ * is a place that should be able to rewrite a URL path or escape a
59
+ * directory.
60
+ */
61
+ const STRICT_RELEASE_VERSION =
62
+ /^(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})(?:-(?:0|[1-9]\d{0,8}|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d{0,8}|\d*[A-Za-z-][0-9A-Za-z-]*))*)?$/
63
+
64
+ function isStrictReleaseVersion(version) {
65
+ return (
66
+ typeof version === 'string' &&
67
+ version.length <= 64 &&
68
+ STRICT_RELEASE_VERSION.test(version)
69
+ )
70
+ }
71
+
38
72
  /**
39
73
  * Decide where release archives are downloaded from.
40
74
  *
41
75
  * NEXT_PUBLIC_CODEBUFF_APP_URL is read at runtime, so anyone able to set an
42
- * environment variable could point the launcher at a plain-http origin and
43
- * let a network attacker substitute the archive. The override is honoured
44
- * only over https, or over http on a loopback host (local release servers in
45
- * development and tests). Anything else is ignored with a warning and the
46
- * default origin is used.
76
+ * environment variable could point the launcher at another origin. The
77
+ * archive's sha256 comes from npm, not from that origin, so a substituted
78
+ * archive is refused either way; the origin is still held to the hosts that
79
+ * actually serve releases. The override is honoured only over https to a
80
+ * codebuff.com / freebuff.com host, or over http/https on a loopback host
81
+ * (local release servers in development and tests). Anything else is
82
+ * ignored with a warning and the default origin is used.
47
83
  */
48
84
  function resolveDownloadOrigin(configuredOrigin, { warn = () => {} } = {}) {
49
85
  if (typeof configuredOrigin !== 'string' || configuredOrigin.trim() === '') {
@@ -62,15 +98,25 @@ function resolveDownloadOrigin(configuredOrigin, { warn = () => {} } = {}) {
62
98
  }
63
99
 
64
100
  const isLocal = LOCAL_HOSTNAMES.has(parsed.hostname.toLowerCase())
101
+ const isHttp = parsed.protocol === 'http:' || parsed.protocol === 'https:'
102
+ // Only an origin: credentials or a query would ride along on every request.
103
+ const isBareOrigin =
104
+ !parsed.username && !parsed.password && !parsed.search && !parsed.hash
105
+ if (isHttp && isLocal && isBareOrigin) {
106
+ return trimmed
107
+ }
65
108
  if (
66
- parsed.protocol === 'https:' ||
67
- (parsed.protocol === 'http:' && isLocal)
109
+ parsed.protocol === 'https:' &&
110
+ isReleaseOriginHost(parsed.hostname) &&
111
+ isBareOrigin
68
112
  ) {
69
113
  return trimmed
70
114
  }
71
115
 
72
116
  warn(
73
- `Ignoring NEXT_PUBLIC_CODEBUFF_APP_URL=${configuredOrigin}: release downloads must use https (http is allowed only for localhost). Downloading from ${DEFAULT_DOWNLOAD_ORIGIN}.`,
117
+ parsed.protocol === 'https:'
118
+ ? `Ignoring NEXT_PUBLIC_CODEBUFF_APP_URL=${parsed.protocol}//${parsed.host}: release downloads come only from codebuff.com or freebuff.com (or localhost). Downloading from ${DEFAULT_DOWNLOAD_ORIGIN}.`
119
+ : `Ignoring NEXT_PUBLIC_CODEBUFF_APP_URL=${parsed.protocol}//${parsed.host}: release downloads must use https (http is allowed only for localhost). Downloading from ${DEFAULT_DOWNLOAD_ORIGIN}.`,
74
120
  )
75
121
  return DEFAULT_DOWNLOAD_ORIGIN
76
122
  }
@@ -146,6 +192,9 @@ function createLauncher(productConfig) {
146
192
  // Tests only. os.homedir() ignores $HOME under `bun test`, so pointing HOME
147
193
  // at a temp dir is not enough to keep a test off the real ~/.config.
148
194
  configDir: configDirOverride = null,
195
+ // Tests only: stands in for the OS AVX2 probe (/proc/cpuinfo, sysctl),
196
+ // returning true, false, or null for "could not tell".
197
+ cpuAvx2Probe: cpuAvx2ProbeOverride = null,
149
198
  } = productConfig
150
199
 
151
200
  /**
@@ -282,6 +331,19 @@ function createLauncher(productConfig) {
282
331
  const BUN_ILLEGAL_INSTRUCTION_PANIC =
283
332
  /^panic(?:\([^)\n]*\))?: Illegal instruction at address 0x[0-9a-f]+/im
284
333
 
334
+ /**
335
+ * Bun's `CPU:` header line lists the features it detected (`CPU: sse42
336
+ * popcnt avx avx2`). avx2 in that list is Bun's own CPUID saying the CPU has
337
+ * it, so an illegal instruction on such a machine is not a missing AVX2.
338
+ */
339
+ const BUN_CPU_LINE = /^CPU:([^\n]*)$/m
340
+
341
+ function stderrReportsAvx2Present(stderrTail) {
342
+ if (!stderrTail) return false
343
+ const match = BUN_CPU_LINE.exec(sanitizeForReplay(stderrTail))
344
+ return Boolean(match && /(?:^|\s)avx2(?:\s|$)/i.test(match[1]))
345
+ }
346
+
285
347
  function stderrReportsMissingCpuFeature(code, signal, stderrTail) {
286
348
  if (!stderrTail || (code === 0 && !signal)) return false
287
349
  const text = sanitizeForReplay(stderrTail)
@@ -442,12 +504,75 @@ function createLauncher(productConfig) {
442
504
  return null
443
505
  }
444
506
 
445
- function linuxCpuHasAvx2() {
446
- try {
447
- return /\bavx2\b/i.test(fs.readFileSync('/proc/cpuinfo', 'utf8'))
448
- } catch {
449
- return true
507
+ /**
508
+ * `/proc/cpuinfo` answers for the kernel's view of the CPU: true or false
509
+ * from its `flags` lines, null when it has none to read.
510
+ */
511
+ function parseCpuinfoAvx2(text) {
512
+ if (typeof text !== 'string' || !/^flags\s*:/m.test(text)) return null
513
+ return /^flags\s*:.*\bavx2\b/m.test(text)
514
+ }
515
+
516
+ /** `sysctl -n hw.optional.avx2_0` prints 1 or 0 on an Intel Mac. */
517
+ function parseSysctlAvx2(output) {
518
+ const value = typeof output === 'string' ? output.trim() : ''
519
+ if (value === '1') return true
520
+ if (value === '0') return false
521
+ return null
522
+ }
523
+
524
+ // Where probeCpuAvx2() gets its answer, recorded as the evidence for a pin.
525
+ function cpuProbeSource() {
526
+ if (process.platform === 'linux') return 'cpuinfo'
527
+ if (process.platform === 'darwin') return 'sysctl'
528
+ return 'probe'
529
+ }
530
+
531
+ let _cpuProbeCache
532
+
533
+ /**
534
+ * Ask the OS whether this CPU has AVX2: true, false, or null for "could not
535
+ * tell". Linux reads /proc/cpuinfo. macOS asks sysctl, which costs a
536
+ * subprocess, so this runs only on the crash path, when re-checking an old
537
+ * pin, and when picking a download target for a baseline install, never on
538
+ * an ordinary launch. Windows has no probe: the one we had tripped Defender
539
+ * (see detectMachineHasAvx2), so there Bun's crash report is the evidence.
540
+ */
541
+ function probeCpuAvx2() {
542
+ if (_cpuProbeCache === undefined) {
543
+ _cpuProbeCache = cpuAvx2ProbeOverride
544
+ ? cpuAvx2ProbeOverride()
545
+ : probeCpuAvx2FromOs()
450
546
  }
547
+ return _cpuProbeCache
548
+ }
549
+
550
+ function probeCpuAvx2FromOs() {
551
+ if (process.arch !== 'x64') return null
552
+ if (process.platform === 'linux') {
553
+ try {
554
+ return parseCpuinfoAvx2(fs.readFileSync('/proc/cpuinfo', 'utf8'))
555
+ } catch {
556
+ return null
557
+ }
558
+ }
559
+ if (process.platform === 'darwin') {
560
+ // x64 node on Apple Silicon runs under Rosetta, and sysctl (arm64 only)
561
+ // then describes the host, not what Rosetta emulates. No answer there.
562
+ if (/\bApple\b/.test(os.cpus()[0]?.model || '')) return null
563
+ try {
564
+ return parseSysctlAvx2(
565
+ execFileSync('/usr/sbin/sysctl', ['-n', 'hw.optional.avx2_0'], {
566
+ encoding: 'utf8',
567
+ stdio: ['ignore', 'pipe', 'ignore'],
568
+ timeout: 2000,
569
+ }),
570
+ )
571
+ } catch {
572
+ return null
573
+ }
574
+ }
575
+ return null
451
576
  }
452
577
 
453
578
  let _hasAvx2Cache
@@ -464,17 +589,33 @@ function createLauncher(productConfig) {
464
589
  return true
465
590
  }
466
591
 
467
- // A recorded illegal-instruction crash outranks everything below it: the
468
- // binary actually failed on this machine, which beats any inference we
469
- // could make about the CPU.
470
- const recorded = readCachedAvx2()
471
- if (recorded !== null) {
472
- return recorded
592
+ // A recorded verdict outranks everything below it. Every record now names
593
+ // its evidence: Bun's crash report, the OS probe, or 'crash' for an
594
+ // illegal instruction nothing could explain.
595
+ //
596
+ // A record WITHOUT evidence was written by an older launcher, which pinned
597
+ // baseline on any SIGILL, including a bug on a CPU that has AVX2. Re-check
598
+ // those against the OS once: a CPU with AVX2 gets its record dropped and
599
+ // the optimized build back on the next update (see getDownloadTargetKey).
600
+ const record = readCpuFeatureRecord()
601
+ if (record && (record.avx2 || record.evidence)) {
602
+ return record.avx2
603
+ }
604
+ if (record) {
605
+ const probed = probeCpuAvx2()
606
+ if (probed !== true) {
607
+ writeCpuFeatureRecord(
608
+ false,
609
+ probed === false ? cpuProbeSource() : 'crash',
610
+ )
611
+ return false
612
+ }
613
+ clearCpuFeatureRecord()
473
614
  }
474
615
 
475
616
  // Linux can just ask, and a file read is cheap enough not to cache.
476
617
  if (process.platform === 'linux') {
477
- return linuxCpuHasAvx2()
618
+ return probeCpuAvx2() ?? true
478
619
  }
479
620
 
480
621
  // Everything else assumes AVX2 — true of every x64 CPU since ~2013.
@@ -492,42 +633,77 @@ function createLauncher(productConfig) {
492
633
  return true
493
634
  }
494
635
 
495
- // Called from the illegal-instruction fallback. Persisting here is what keeps
496
- // the optimistic assumption above from costing more than a single crash — and
497
- // it is deliberately separate from the metadata target, so a lost or rewritten
498
- // metadata file can't resurrect the AVX2 build on a CPU that can't run it.
499
- function recordMachineLacksAvx2() {
636
+ /**
637
+ * Positive evidence that this CPU runs the optimized build: a recorded
638
+ * verdict with evidence, or the OS probe. The optimistic default does not
639
+ * count. This is what lets a baseline install move back to the optimized
640
+ * build; without evidence, baseline stays put.
641
+ */
642
+ function knownToHaveAvx2() {
643
+ if (!machineHasAvx2()) return false
644
+ const record = readCpuFeatureRecord()
645
+ if (record?.avx2 === true && record.evidence) return true
646
+ return probeCpuAvx2() === true
647
+ }
648
+
649
+ // Called from the illegal-instruction fallback, only when the CPU is shown
650
+ // to lack AVX2 or nothing can say either way (see tryFallbackToBaseline).
651
+ // Persisting here is what keeps the optimistic assumption above from costing
652
+ // more than a single crash — and it is deliberately separate from the
653
+ // metadata target, so a lost or rewritten metadata file can't resurrect the
654
+ // AVX2 build on a CPU that can't run it.
655
+ function recordMachineLacksAvx2(evidence = 'crash') {
500
656
  _hasAvx2Cache = false
501
- writeCachedAvx2(false)
657
+ writeCpuFeatureRecord(false, evidence)
502
658
  }
503
659
 
504
660
  function getCpuFeatureCachePath() {
505
661
  return path.join(CONFIG.configDir, 'cpu-features.json')
506
662
  }
507
663
 
508
- function readCachedAvx2() {
664
+ /** `{ avx2, evidence }`; evidence is null on a record from an older launcher. */
665
+ function readCpuFeatureRecord() {
509
666
  try {
510
667
  const cache = JSON.parse(
511
668
  fs.readFileSync(getCpuFeatureCachePath(), 'utf8'),
512
669
  )
513
- return typeof cache.avx2 === 'boolean' ? cache.avx2 : null
670
+ if (typeof cache.avx2 !== 'boolean') return null
671
+ return {
672
+ avx2: cache.avx2,
673
+ evidence:
674
+ typeof cache.evidence === 'string' && cache.evidence
675
+ ? cache.evidence
676
+ : null,
677
+ }
514
678
  } catch {
515
679
  return null
516
680
  }
517
681
  }
518
682
 
519
- function writeCachedAvx2(value) {
683
+ function readCachedAvx2() {
684
+ return readCpuFeatureRecord()?.avx2 ?? null
685
+ }
686
+
687
+ function writeCpuFeatureRecord(value, evidence) {
520
688
  try {
521
689
  fs.mkdirSync(CONFIG.configDir, { recursive: true })
522
690
  fs.writeFileSync(
523
691
  getCpuFeatureCachePath(),
524
- JSON.stringify({ avx2: value }),
692
+ JSON.stringify({ avx2: value, evidence }),
525
693
  )
526
694
  } catch {
527
695
  // Best effort; we'll just re-probe next launch.
528
696
  }
529
697
  }
530
698
 
699
+ function clearCpuFeatureRecord() {
700
+ try {
701
+ fs.unlinkSync(getCpuFeatureCachePath())
702
+ } catch {
703
+ // Already gone, or unwritable; either way nothing pins baseline now.
704
+ }
705
+ }
706
+
531
707
  function getDefaultTargetKey() {
532
708
  const override = getTargetOverride()
533
709
  if (override) {
@@ -580,6 +756,17 @@ function createLauncher(productConfig) {
580
756
 
581
757
  const metadata = getCurrentMetadata()
582
758
  if (metadata?.target && isTargetAllowedForThisMachine(metadata.target)) {
759
+ // An installed baseline keeps running (isTargetAllowedForThisMachine
760
+ // always allows it), but it is not a life sentence: once there is
761
+ // evidence the CPU has AVX2, the next download goes back to the optimized
762
+ // build. That is how a machine sent to baseline by a crash that was not a
763
+ // missing AVX2 re-probes on the next version.
764
+ if (
765
+ metadata.target === getBaselineFallbackTargetKey() &&
766
+ knownToHaveAvx2()
767
+ ) {
768
+ return getDefaultTargetKey()
769
+ }
583
770
  return metadata.target
584
771
  }
585
772
 
@@ -606,7 +793,7 @@ function createLauncher(productConfig) {
606
793
 
607
794
  const body = await streamToString(res)
608
795
  const packageData = JSON.parse(body)
609
- if (typeof packageData.version !== 'string' || !packageData.version) {
796
+ if (!isStrictReleaseVersion(packageData.version)) {
610
797
  return null
611
798
  }
612
799
 
@@ -795,6 +982,9 @@ function createLauncher(productConfig) {
795
982
  }
796
983
 
797
984
  function getPartialArchivePath(version, targetKey) {
985
+ if (!isStrictReleaseVersion(version)) {
986
+ throw new Error(`Invalid release version for an archive path: ${version}`)
987
+ }
798
988
  return path.join(
799
989
  CONFIG.configDir,
800
990
  `.${packageName}-${version}-${targetKey}.tar.gz.part`,
@@ -1027,6 +1217,21 @@ function createLauncher(productConfig) {
1027
1217
  ) {
1028
1218
  const fileName = PLATFORM_TARGETS[targetKey]
1029
1219
 
1220
+ // Before anything is built from it: the version becomes a URL path
1221
+ // segment and part of a file name below.
1222
+ if (!isStrictReleaseVersion(version)) {
1223
+ const error = new Error(
1224
+ `Refusing to download ${packageName}: ${JSON.stringify(String(version)).slice(0, 80)} is not a release version.`,
1225
+ )
1226
+ error.code = 'EBADVERSION'
1227
+ error.retryable = false
1228
+ trackUpdateFailed(error.message, 'invalid', {
1229
+ stage: 'version_check',
1230
+ target: targetKey,
1231
+ })
1232
+ throw error
1233
+ }
1234
+
1030
1235
  if (!fileName) {
1031
1236
  const error = new Error(
1032
1237
  `Unsupported platform: ${process.platform} ${process.arch}`,
@@ -1713,7 +1918,7 @@ function createLauncher(productConfig) {
1713
1918
  // feature (see stderrReportsMissingCpuFeature), is proof this CPU cannot
1714
1919
  // run this build; a Windows startup abort is a strong suspicion (see
1715
1920
  // isStartupCpuFeatureCrash).
1716
- const confirmed =
1921
+ let confirmed =
1717
1922
  isIllegalInstructionExit(code, signal) ||
1718
1923
  stderrReportsMissingCpuFeature(code, signal, stderrTail)
1719
1924
  if (!confirmed && !isStartupCpuFeatureCrash(code, signal, msAlive)) {
@@ -1736,16 +1941,35 @@ function createLauncher(productConfig) {
1736
1941
  return false
1737
1942
  }
1738
1943
 
1739
- // Only a confirmed illegal instruction gets written down. Persisting it
1740
- // before the download is what caps the cost at one crash: even if the
1741
- // download or the relaunch fails, we never optimistically pick the AVX2
1742
- // build again. A suspected crash records nothing — installing the baseline
1743
- // already keeps this machine on it, so a guess that turns out to be wrong
1744
- // costs the slower build instead of leaving cpu-features.json asserting a
1745
- // CPU limitation we never observed.
1746
- if (confirmed) {
1747
- recordMachineLacksAvx2()
1748
- }
1944
+ // An illegal instruction proves the binary used an instruction this
1945
+ // machine refused, not that the instruction was AVX2: a Bun or JIT bug
1946
+ // raises SIGILL too. So ask for evidence before pinning baseline, which
1947
+ // otherwise sticks for good.
1948
+ //
1949
+ // - The CPU lacks AVX2 (Bun's `Features: no_avx2`, or the OS probe):
1950
+ // record it. Persisting before the download is what caps the cost at
1951
+ // one crash: even if the download or the relaunch fails, we never
1952
+ // optimistically pick the AVX2 build again.
1953
+ // - The CPU has AVX2 (the OS probe, or avx2 in Bun's `CPU:` line): this
1954
+ // crash is something else. Run baseline for now, since it may still
1955
+ // start, and record the evidence so the next update goes back to the
1956
+ // optimized build (getDownloadTargetKey). At worst one crash per release.
1957
+ // - No evidence either way (Windows without Bun's report, an unreadable
1958
+ // /proc/cpuinfo, Rosetta): a confirmed crash is pinned as before.
1959
+ // A suspected crash records nothing: installing the baseline already keeps
1960
+ // this machine on it, and cpu-features.json should not assert a CPU
1961
+ // limitation we never observed.
1962
+ const verdict = crashAvx2Verdict(code, signal, stderrTail)
1963
+ if (verdict.avx2 === true) {
1964
+ writeCpuFeatureRecord(true, verdict.evidence)
1965
+ confirmed = false
1966
+ } else if (verdict.avx2 === false) {
1967
+ recordMachineLacksAvx2(verdict.evidence)
1968
+ confirmed = true
1969
+ } else if (confirmed) {
1970
+ recordMachineLacksAvx2('crash')
1971
+ }
1972
+ const cpuHasAvx2 = verdict.avx2 === true
1749
1973
 
1750
1974
  let version = metadata?.version || null
1751
1975
  let binaryChecksums = null
@@ -1763,9 +1987,11 @@ function createLauncher(productConfig) {
1763
1987
  })
1764
1988
  console.error('')
1765
1989
  console.error(
1766
- confirmed
1767
- ? `${packageName} is switching to the older-CPU binary for this machine.`
1768
- : `${packageName} crashed on startup; trying the older-CPU binary.`,
1990
+ cpuHasAvx2
1991
+ ? `${packageName} crashed, but this CPU supports AVX2; using the older-CPU binary until the next update.`
1992
+ : confirmed
1993
+ ? `${packageName} is switching to the older-CPU binary for this machine.`
1994
+ : `${packageName} crashed on startup; trying the older-CPU binary.`,
1769
1995
  )
1770
1996
 
1771
1997
  try {
@@ -1781,6 +2007,27 @@ function createLauncher(productConfig) {
1781
2007
  return true
1782
2008
  }
1783
2009
 
2010
+ /**
2011
+ * What the evidence says about AVX2 after a crash: `{ avx2, evidence }`,
2012
+ * avx2 null when nothing can tell. Bun's own verdict that AVX2 is missing
2013
+ * wins, then the OS probe, then avx2 in Bun's `CPU:` line.
2014
+ */
2015
+ function crashAvx2Verdict(code, signal, stderrTail) {
2016
+ if (
2017
+ stderrTail &&
2018
+ !(code === 0 && !signal) &&
2019
+ BUN_NO_AVX2_FEATURE.test(sanitizeForReplay(stderrTail))
2020
+ ) {
2021
+ return { avx2: false, evidence: 'bun-report' }
2022
+ }
2023
+ const probed = probeCpuAvx2()
2024
+ if (probed !== null) return { avx2: probed, evidence: cpuProbeSource() }
2025
+ if (stderrReportsAvx2Present(stderrTail)) {
2026
+ return { avx2: true, evidence: 'bun-report' }
2027
+ }
2028
+ return { avx2: null, evidence: null }
2029
+ }
2030
+
1784
2031
  function attachExitHandler(child, allowBaselineFallback = true) {
1785
2032
  const exitListener = async (code, signal) => {
1786
2033
  // A child we never watched (only reachable from a test) reports no age
@@ -1843,6 +2090,11 @@ function createLauncher(productConfig) {
1843
2090
  detectMachineHasAvx2,
1844
2091
  recordMachineLacksAvx2,
1845
2092
  readCachedAvx2,
2093
+ readCpuFeatureRecord,
2094
+ parseCpuinfoAvx2,
2095
+ parseSysctlAvx2,
2096
+ stderrReportsAvx2Present,
2097
+ getDownloadTargetKey,
1846
2098
  isIllegalInstructionExit,
1847
2099
  isStartupCpuFeatureCrash,
1848
2100
  stderrReportsMissingCpuFeature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freebuff",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "The world's strongest free coding agent",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -41,13 +41,13 @@
41
41
  "provenance": false
42
42
  },
43
43
  "binaryChecksums": {
44
- "linux-x64": "2a149ec595d8f27ec8654b5ab072c22020d5e4f4bd0a0d2c0a1a334b6a063f71",
45
- "linux-x64-baseline": "22b13efa9eb52d9a074ed2e67feb6474a551796e22d0fa0e7a9a3081943ef16b",
46
- "linux-arm64": "ef43f44264cae55d86f5d0df45f4c948543d0bf92525c1b296b910badc2f0c71",
47
- "darwin-x64": "cba39ca908625a8b43335942879fa6c9bd91a2a623f4a26d8a9e0d3e39b080e3",
48
- "darwin-x64-baseline": "8be9ca67c635dd4bac21d1e726353c24df5d421d598a2c94fff43540a8062369",
49
- "darwin-arm64": "529b4b2352690c9d3cfc8436e8b15649bc043eb58e07781d6986c6f0f2577f4e",
50
- "win32-x64": "fc97aa41e2a59304afa49e27bb2f22aa8d7b47f39d182a3abd99c5ca33c5ddd2",
51
- "win32-x64-baseline": "d280003c00ce1d91b19cbeee3ead6fee516f9aba665c175a8e19553d0a6dfe8c"
44
+ "linux-x64": "2c0f5b23b563830cbfb99f4fcc57e2657524f77159f2a8088b67f8279f7db3f9",
45
+ "linux-x64-baseline": "194b9ea6667ef7503412f4edca2d0e256cf8dc592e658bc147a46105f15aa44e",
46
+ "linux-arm64": "7609d2cad1055d0df2c9d73b95a10c98439eee77a78d739084af408c33d8e6b4",
47
+ "darwin-x64": "a1c7aa7fc361d1f5095d5c50dab409101a411b14951933c55b8f03d777600f5f",
48
+ "darwin-x64-baseline": "165d9077f04af2d30063d06445f0ddc2aa1feafadb034140d2f66435df4ae1c6",
49
+ "darwin-arm64": "2e99d58f7805c5ff8975e94ee6c31d23bfd2d0dd1094806659a1fa6c4c9c68e0",
50
+ "win32-x64": "ba239c2aae754927e11655cbc534c6c5dcfc3c41e6f8a8e9c4ede256acf23fc3",
51
+ "win32-x64-baseline": "cb1d911d296d481ffb1165bda1999c6e5633bb0d183c584fc121da2db7cf8abc"
52
52
  }
53
53
  }