freebuff 0.2.9 → 0.2.11
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 +225 -9
- package/package.json +9 -9
package/launcher.js
CHANGED
|
@@ -1336,10 +1336,17 @@ function createLauncher(productConfig) {
|
|
|
1336
1336
|
|
|
1337
1337
|
function installStagedBinary(
|
|
1338
1338
|
{ tempBinaryPath, version, targetKey },
|
|
1339
|
-
|
|
1339
|
+
// keepStagingOnFailure: leave a staged update that could not be moved
|
|
1340
|
+
// into place (a file locked by antivirus or another session on Windows)
|
|
1341
|
+
// for the next launch to adopt, instead of downloading it all again.
|
|
1342
|
+
{ quiet = false, keepStagingOnFailure = false } = {},
|
|
1340
1343
|
) {
|
|
1341
1344
|
const replacements = []
|
|
1345
|
+
let installed = false
|
|
1342
1346
|
const metadataTempPath = `${CONFIG.metadataPath}.new.${process.pid}`
|
|
1347
|
+
// The directory the binary was staged in: this launcher's own, or one
|
|
1348
|
+
// adopted from a launcher that died before it could install it.
|
|
1349
|
+
const stagingDir = path.dirname(tempBinaryPath)
|
|
1343
1350
|
|
|
1344
1351
|
try {
|
|
1345
1352
|
fs.writeFileSync(
|
|
@@ -1356,10 +1363,7 @@ function createLauncher(productConfig) {
|
|
|
1356
1363
|
// pre-date this change won't have the wasm and will still install —
|
|
1357
1364
|
// they'll just hit the same crash they had before, which is fine.
|
|
1358
1365
|
if (includeTreeSitterWasm) {
|
|
1359
|
-
const tempWasmPath = path.join(
|
|
1360
|
-
CONFIG.tempDownloadDir,
|
|
1361
|
-
'tree-sitter.wasm',
|
|
1362
|
-
)
|
|
1366
|
+
const tempWasmPath = path.join(stagingDir, 'tree-sitter.wasm')
|
|
1363
1367
|
if (fs.existsSync(tempWasmPath)) {
|
|
1364
1368
|
const targetWasmPath = path.join(
|
|
1365
1369
|
path.dirname(CONFIG.binaryPath),
|
|
@@ -1375,13 +1379,14 @@ function createLauncher(productConfig) {
|
|
|
1375
1379
|
replacements,
|
|
1376
1380
|
)
|
|
1377
1381
|
commitReplacements(replacements)
|
|
1382
|
+
installed = true
|
|
1378
1383
|
} catch (error) {
|
|
1379
1384
|
rollbackReplacements(replacements)
|
|
1380
1385
|
throw error
|
|
1381
1386
|
} finally {
|
|
1382
1387
|
removeFileIfPresent(metadataTempPath)
|
|
1383
|
-
if (fs.existsSync(
|
|
1384
|
-
fs.rmSync(
|
|
1388
|
+
if ((installed || !keepStagingOnFailure) && fs.existsSync(stagingDir)) {
|
|
1389
|
+
fs.rmSync(stagingDir, { recursive: true })
|
|
1385
1390
|
}
|
|
1386
1391
|
}
|
|
1387
1392
|
|
|
@@ -1545,8 +1550,15 @@ function createLauncher(productConfig) {
|
|
|
1545
1550
|
if (
|
|
1546
1551
|
runningProcess.exitCode === null &&
|
|
1547
1552
|
runningProcess.signalCode === null
|
|
1548
|
-
)
|
|
1553
|
+
) {
|
|
1549
1554
|
pendingUpdates.set(runningProcess, stagedBinary)
|
|
1555
|
+
// Closing the terminal window kills this launcher too (SIGHUP, or
|
|
1556
|
+
// CTRL_CLOSE_EVENT on Windows) before the exit handler can
|
|
1557
|
+
// install. Mark the staged update ready so the next launch adopts
|
|
1558
|
+
// it instead of a user who never quits from inside the CLI
|
|
1559
|
+
// staying on this build forever.
|
|
1560
|
+
await writeStagedManifest(stagedBinary)
|
|
1561
|
+
}
|
|
1550
1562
|
return
|
|
1551
1563
|
}
|
|
1552
1564
|
|
|
@@ -1605,7 +1617,7 @@ function createLauncher(productConfig) {
|
|
|
1605
1617
|
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
|
|
1606
1618
|
return
|
|
1607
1619
|
}
|
|
1608
|
-
installStagedBinary(staged, { quiet: true })
|
|
1620
|
+
installStagedBinary(staged, { quiet: true, keepStagingOnFailure: true })
|
|
1609
1621
|
console.log(
|
|
1610
1622
|
`Updated ${displayName} to ${staged.version}. Ready for your next launch.`,
|
|
1611
1623
|
)
|
|
@@ -1616,6 +1628,202 @@ function createLauncher(productConfig) {
|
|
|
1616
1628
|
}
|
|
1617
1629
|
}
|
|
1618
1630
|
|
|
1631
|
+
/**
|
|
1632
|
+
* Deferred staging directories are `<tempDownloadDirName>-<pid>`, plus
|
|
1633
|
+
* `<…>-<pid>.adopting` while a later launcher installs one it adopted. The
|
|
1634
|
+
* pid is the launcher that owns the directory.
|
|
1635
|
+
*/
|
|
1636
|
+
const STAGED_MANIFEST_NAME = 'staged.json'
|
|
1637
|
+
|
|
1638
|
+
function parseStagingDirOwner(name) {
|
|
1639
|
+
const prefix = `${tempDownloadDirName}-`
|
|
1640
|
+
if (!name.startsWith(prefix)) return null
|
|
1641
|
+
const match = /^(\d{1,10})(?:\.adopting)?$/.exec(name.slice(prefix.length))
|
|
1642
|
+
return match ? Number(match[1]) : null
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function isProcessAlive(pid) {
|
|
1646
|
+
try {
|
|
1647
|
+
process.kill(pid, 0)
|
|
1648
|
+
return true
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
// EPERM: it exists, it just is not ours to signal.
|
|
1651
|
+
return error?.code === 'EPERM'
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Record that a staged update is complete: the version, target and sha256 of
|
|
1657
|
+
* every file installStagedBinary() will move. Written last and renamed into
|
|
1658
|
+
* place, so a launcher killed mid-download or mid-extraction leaves a
|
|
1659
|
+
* directory without one, which is only ever deleted. Best effort: a failure
|
|
1660
|
+
* here leaves the exit-time install exactly as it was.
|
|
1661
|
+
*
|
|
1662
|
+
* The hashes are this launcher's own record, checked again before adoption
|
|
1663
|
+
* so a file truncated or altered on disk since is never installed. They are
|
|
1664
|
+
* not a second trust root: the archive was verified against npm's sha256 at
|
|
1665
|
+
* download, and anything able to write this directory can already write the
|
|
1666
|
+
* installed binary next to it.
|
|
1667
|
+
*/
|
|
1668
|
+
async function writeStagedManifest({ tempBinaryPath, version, targetKey }) {
|
|
1669
|
+
const stagingDir = path.dirname(tempBinaryPath)
|
|
1670
|
+
const manifestPath = path.join(stagingDir, STAGED_MANIFEST_NAME)
|
|
1671
|
+
const tempManifestPath = `${manifestPath}.tmp`
|
|
1672
|
+
try {
|
|
1673
|
+
const files = {
|
|
1674
|
+
[CONFIG.binaryName]: await computeFileSha256(tempBinaryPath),
|
|
1675
|
+
}
|
|
1676
|
+
const wasmPath = path.join(stagingDir, 'tree-sitter.wasm')
|
|
1677
|
+
if (includeTreeSitterWasm && fs.existsSync(wasmPath)) {
|
|
1678
|
+
files['tree-sitter.wasm'] = await computeFileSha256(wasmPath)
|
|
1679
|
+
}
|
|
1680
|
+
fs.writeFileSync(
|
|
1681
|
+
tempManifestPath,
|
|
1682
|
+
JSON.stringify({ version, target: targetKey, files }),
|
|
1683
|
+
)
|
|
1684
|
+
fs.renameSync(tempManifestPath, manifestPath)
|
|
1685
|
+
} catch {
|
|
1686
|
+
try {
|
|
1687
|
+
removeFileIfPresent(tempManifestPath)
|
|
1688
|
+
} catch {
|
|
1689
|
+
// The directory may already be gone: installed at exit, or removed.
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function readStagedManifest(stagingDir) {
|
|
1695
|
+
try {
|
|
1696
|
+
const manifest = JSON.parse(
|
|
1697
|
+
fs.readFileSync(path.join(stagingDir, STAGED_MANIFEST_NAME), 'utf8'),
|
|
1698
|
+
)
|
|
1699
|
+
const files = manifest?.files
|
|
1700
|
+
if (
|
|
1701
|
+
!isStrictReleaseVersion(manifest?.version) ||
|
|
1702
|
+
!PLATFORM_TARGETS[manifest?.target] ||
|
|
1703
|
+
!files ||
|
|
1704
|
+
typeof files !== 'object' ||
|
|
1705
|
+
!isSha256Hex(files[CONFIG.binaryName]) ||
|
|
1706
|
+
Object.entries(files).some(
|
|
1707
|
+
([name, digest]) =>
|
|
1708
|
+
!isAllowedArchiveEntry(name, null) || !isSha256Hex(digest),
|
|
1709
|
+
)
|
|
1710
|
+
) {
|
|
1711
|
+
return null
|
|
1712
|
+
}
|
|
1713
|
+
return { version: manifest.version, targetKey: manifest.target, files }
|
|
1714
|
+
} catch {
|
|
1715
|
+
return null
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
async function stagedFilesMatch(stagingDir, files) {
|
|
1720
|
+
for (const [name, digest] of Object.entries(files)) {
|
|
1721
|
+
const filePath = path.join(stagingDir, name)
|
|
1722
|
+
if (!fs.existsSync(filePath)) return false
|
|
1723
|
+
const verification = await verifyFileSha256(filePath, digest)
|
|
1724
|
+
if (!verification.ok) return false
|
|
1725
|
+
}
|
|
1726
|
+
// Nothing unlisted may ride along into the install (the wasm is moved
|
|
1727
|
+
// whenever it is present).
|
|
1728
|
+
return fs
|
|
1729
|
+
.readdirSync(stagingDir)
|
|
1730
|
+
.every((name) => name === STAGED_MANIFEST_NAME || name in files)
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Install an update a previous launcher downloaded but never installed, and
|
|
1735
|
+
* delete what dead launchers left behind.
|
|
1736
|
+
*
|
|
1737
|
+
* A deferred update is installed by the launcher's exit handler, which runs
|
|
1738
|
+
* only when the CLI exits on its own. Closing the terminal window or tab
|
|
1739
|
+
* (SIGHUP; CTRL_CLOSE_EVENT on Windows) kills the launcher first, so a user
|
|
1740
|
+
* who always leaves that way never updated: each launch downloaded the same
|
|
1741
|
+
* release again and left another full copy of the binary in a
|
|
1742
|
+
* `<tempDownloadDirName>-<pid>` directory.
|
|
1743
|
+
*
|
|
1744
|
+
* Runs before the binary starts, so nothing of ours holds the files. A
|
|
1745
|
+
* directory whose owner is still running belongs to a live session that
|
|
1746
|
+
* will install it on exit and is left alone. Claiming by rename means two
|
|
1747
|
+
* launchers starting together never install the same directory twice.
|
|
1748
|
+
* Never throws: a failure here must not stop the CLI from starting.
|
|
1749
|
+
*/
|
|
1750
|
+
function removeDirQuietly(dir) {
|
|
1751
|
+
try {
|
|
1752
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
1753
|
+
} catch {
|
|
1754
|
+
// Locked (Windows) or already gone; the next launch tries again.
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
async function adoptOrphanedStagedUpdates() {
|
|
1759
|
+
if (!deferUpdatesUntilExit) return null
|
|
1760
|
+
try {
|
|
1761
|
+
const orphans = []
|
|
1762
|
+
for (const name of fs.readdirSync(CONFIG.configDir)) {
|
|
1763
|
+
const owner = parseStagingDirOwner(name)
|
|
1764
|
+
if (owner === null) continue
|
|
1765
|
+
// Our own pid was just assigned to us, so a directory under it is a
|
|
1766
|
+
// leftover from an earlier process that had the same pid.
|
|
1767
|
+
if (owner !== process.pid && isProcessAlive(owner)) continue
|
|
1768
|
+
const dir = path.join(CONFIG.configDir, name)
|
|
1769
|
+
orphans.push({ dir, staged: readStagedManifest(dir) })
|
|
1770
|
+
}
|
|
1771
|
+
if (orphans.length === 0) return null
|
|
1772
|
+
|
|
1773
|
+
orphans.sort((a, b) =>
|
|
1774
|
+
a.staged && b.staged
|
|
1775
|
+
? compareVersions(b.staged.version, a.staged.version)
|
|
1776
|
+
: a.staged
|
|
1777
|
+
? -1
|
|
1778
|
+
: b.staged
|
|
1779
|
+
? 1
|
|
1780
|
+
: 0,
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
const installedVersion = getCurrentVersion()
|
|
1784
|
+
let adoptedVersion = null
|
|
1785
|
+
for (const { dir, staged } of orphans) {
|
|
1786
|
+
if (
|
|
1787
|
+
adoptedVersion === null &&
|
|
1788
|
+
staged &&
|
|
1789
|
+
(installedVersion === null ||
|
|
1790
|
+
compareVersions(installedVersion, staged.version) < 0) &&
|
|
1791
|
+
isTargetAllowedForThisMachine(staged.targetKey)
|
|
1792
|
+
) {
|
|
1793
|
+
const claimedDir = `${CONFIG.tempDownloadDir}.adopting`
|
|
1794
|
+
try {
|
|
1795
|
+
fs.renameSync(dir, claimedDir)
|
|
1796
|
+
} catch {
|
|
1797
|
+
continue // Another launcher claimed it first.
|
|
1798
|
+
}
|
|
1799
|
+
try {
|
|
1800
|
+
if (await stagedFilesMatch(claimedDir, staged.files)) {
|
|
1801
|
+
installStagedBinary(
|
|
1802
|
+
{
|
|
1803
|
+
tempBinaryPath: path.join(claimedDir, CONFIG.binaryName),
|
|
1804
|
+
version: staged.version,
|
|
1805
|
+
targetKey: staged.targetKey,
|
|
1806
|
+
},
|
|
1807
|
+
{ quiet: true, keepStagingOnFailure: true },
|
|
1808
|
+
)
|
|
1809
|
+
adoptedVersion = staged.version
|
|
1810
|
+
} else {
|
|
1811
|
+
removeDirQuietly(claimedDir)
|
|
1812
|
+
}
|
|
1813
|
+
} catch {
|
|
1814
|
+
// Rolled back by installStagedBinary, which kept the directory.
|
|
1815
|
+
// It is ours until we exit, then any later launch retries it.
|
|
1816
|
+
}
|
|
1817
|
+
continue
|
|
1818
|
+
}
|
|
1819
|
+
removeDirQuietly(dir)
|
|
1820
|
+
}
|
|
1821
|
+
return adoptedVersion
|
|
1822
|
+
} catch {
|
|
1823
|
+
return null
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1619
1827
|
/**
|
|
1620
1828
|
* Make captured output safe to print back to the terminal.
|
|
1621
1829
|
*
|
|
@@ -1845,6 +2053,12 @@ function createLauncher(productConfig) {
|
|
|
1845
2053
|
...process.env,
|
|
1846
2054
|
...optionEnv,
|
|
1847
2055
|
CODEBUFF_LAUNCHER_PID: String(process.pid),
|
|
2056
|
+
// Which npm wrapper started this binary. The wrapper never updates
|
|
2057
|
+
// itself, so this is how launch telemetry tells an install that
|
|
2058
|
+
// cannot install deferred updates from one that can.
|
|
2059
|
+
...(wrapperVersion
|
|
2060
|
+
? { CODEBUFF_LAUNCHER_VERSION: String(wrapperVersion) }
|
|
2061
|
+
: {}),
|
|
1848
2062
|
},
|
|
1849
2063
|
})
|
|
1850
2064
|
} catch (err) {
|
|
@@ -2069,6 +2283,7 @@ function createLauncher(productConfig) {
|
|
|
2069
2283
|
}
|
|
2070
2284
|
if (startupBanner.length > 0) console.log('')
|
|
2071
2285
|
|
|
2286
|
+
await adoptOrphanedStagedUpdates()
|
|
2072
2287
|
await ensureBinaryReady()
|
|
2073
2288
|
|
|
2074
2289
|
const child = spawnInstalledBinary()
|
|
@@ -2099,6 +2314,7 @@ function createLauncher(productConfig) {
|
|
|
2099
2314
|
isStartupCpuFeatureCrash,
|
|
2100
2315
|
stderrReportsMissingCpuFeature,
|
|
2101
2316
|
tryFallbackToBaseline,
|
|
2317
|
+
adoptOrphanedStagedUpdates,
|
|
2102
2318
|
printCrashDiagnostics,
|
|
2103
2319
|
checkForUpdates,
|
|
2104
2320
|
spawnInstalledBinary,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freebuff",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "The world's strongest free coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -44,13 +44,13 @@
|
|
|
44
44
|
"provenance": false
|
|
45
45
|
},
|
|
46
46
|
"binaryChecksums": {
|
|
47
|
-
"linux-x64": "
|
|
48
|
-
"linux-x64-baseline": "
|
|
49
|
-
"linux-arm64": "
|
|
50
|
-
"darwin-x64": "
|
|
51
|
-
"darwin-x64-baseline": "
|
|
52
|
-
"darwin-arm64": "
|
|
53
|
-
"win32-x64": "
|
|
54
|
-
"win32-x64-baseline": "
|
|
47
|
+
"linux-x64": "b7b9a1895573fc217054ef9e038b6b4007bd090454a3a7e8aa440ee87fa3431f",
|
|
48
|
+
"linux-x64-baseline": "f285aad92a017f9e8a44c41514ee61dfd0170418c7eda6f36cf95e3b19a30ca0",
|
|
49
|
+
"linux-arm64": "83c0f126f1f6d69b14855a3bafa84f97dd0ef209a6bcbcb175592c53fc87c5f3",
|
|
50
|
+
"darwin-x64": "d74ebb1fdb7f01be838bcc90c7691c7b8a53b7a4efc17c51a42e50f491a66cc2",
|
|
51
|
+
"darwin-x64-baseline": "67e7b4afa784497eb30e122e202ba68866554f2f87fb3ef4c8b73177405ff5b8",
|
|
52
|
+
"darwin-arm64": "8ce3e35caf2edf826ac99c9b416fdb636db0ce8a83f1e0b3588f89634df96c93",
|
|
53
|
+
"win32-x64": "64abf928dc43a6d5cd783a8beedc5ff6820bf4275f1b9056079d1afe5d644773",
|
|
54
|
+
"win32-x64-baseline": "ddce1008c6c151a734a45e628df4f75ee673f6c2984118974b8f6f1c7d087e83"
|
|
55
55
|
}
|
|
56
56
|
}
|