π File detail
utils/nativeInstaller/packageManagers.ts
π§© .tsπ 337 linesπΎ 8,963 bytesπ text
β Back to All Filesπ― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes PackageManager, getOsRelease, detectMise, detectAsdf, and detectHomebrew (and more) β mainly functions, hooks, or classes. Dependencies touch Node filesystem and lodash-es. It composes internal code from debug, execFileNoThrow, and platform (relative imports). What the file header says: Package manager detection for Claude CLI.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Package manager detection for Claude CLI
π€ Exports (heuristic)
PackageManagergetOsReleasedetectMisedetectAsdfdetectHomebrewdetectWingetdetectPacmandetectDebdetectRpmdetectApkgetPackageManager
π External import roots
Package roots from from "β¦" (relative paths omitted).
fslodash-es
π₯οΈ Source preview
/**
* Package manager detection for Claude CLI
*/
import { readFile } from 'fs/promises'
import memoize from 'lodash-es/memoize.js'
import { logForDebugging } from '../debug.js'
import { execFileNoThrow } from '../execFileNoThrow.js'
import { getPlatform } from '../platform.js'
export type PackageManager =
| 'homebrew'
| 'winget'
| 'pacman'
| 'deb'
| 'rpm'
| 'apk'
| 'mise'
| 'asdf'
| 'unknown'
/**
* Parses /etc/os-release to extract the distro ID and ID_LIKE fields.
* ID_LIKE identifies the distro family (e.g. Ubuntu has ID_LIKE=debian),
* letting us skip package manager execs on distros that can't have them.
* Returns null if the file is unreadable (pre-systemd or non-standard systems);
* callers fall through to the exec in that case as a conservative fallback.
*/
export const getOsRelease = memoize(
async (): Promise<{ id: string; idLike: string[] } | null> => {
try {
const content = await readFile('/etc/os-release', 'utf8')
const idMatch = content.match(/^ID=["']?(\S+?)["']?\s*$/m)
const idLikeMatch = content.match(/^ID_LIKE=["']?(.+?)["']?\s*$/m)
return {
id: idMatch?.[1] ?? '',
idLike: idLikeMatch?.[1]?.split(' ') ?? [],
}
} catch {
return null
}
},
)
function isDistroFamily(
osRelease: { id: string; idLike: string[] },
families: string[],
): boolean {
return (
families.includes(osRelease.id) ||
osRelease.idLike.some(like => families.includes(like))
)
}
/**
* Detects if the currently running Claude instance was installed via mise
* (a polyglot tool version manager) by checking if the executable path
* is within a mise installs directory.
*
* mise installs to: ~/.local/share/mise/installs/<tool>/<version>/
*/
export function detectMise(): boolean {
const execPath = process.execPath || process.argv[0] || ''
// Check if the executable is within a mise installs directory
if (/[/\\]mise[/\\]installs[/\\]/i.test(execPath)) {
logForDebugging(`Detected mise installation: ${execPath}`)
return true
}
return false
}
/**
* Detects if the currently running Claude instance was installed via asdf
* (another polyglot tool version manager) by checking if the executable path
* is within an asdf installs directory.
*
* asdf installs to: ~/.asdf/installs/<tool>/<version>/
*/
export function detectAsdf(): boolean {
const execPath = process.execPath || process.argv[0] || ''
// Check if the executable is within an asdf installs directory
if (/[/\\]\.?asdf[/\\]installs[/\\]/i.test(execPath)) {
logForDebugging(`Detected asdf installation: ${execPath}`)
return true
}
return false
}
/**
* Detects if the currently running Claude instance was installed via Homebrew
* by checking if the executable path is within a Homebrew Caskroom directory.
*
* Note: We specifically check for Caskroom because npm can also be installed via
* Homebrew, which would place npm global packages under the same Homebrew prefix
* (e.g., /opt/homebrew/lib/node_modules). We need to distinguish between:
* - Homebrew cask: /opt/homebrew/Caskroom/claude-code/...
* - npm-global (via Homebrew's npm): /opt/homebrew/lib/node_modules/@anthropic-ai/...
*/
export function detectHomebrew(): boolean {
const platform = getPlatform()
// Homebrew is only for macOS and Linux
if (platform !== 'macos' && platform !== 'linux' && platform !== 'wsl') {
return false
}
// Get the path of the currently running executable
const execPath = process.execPath || process.argv[0] || ''
// Check if the executable is within a Homebrew Caskroom directory
// This is specific to Homebrew cask installations
if (execPath.includes('/Caskroom/')) {
logForDebugging(`Detected Homebrew cask installation: ${execPath}`)
return true
}
return false
}
/**
* Detects if the currently running Claude instance was installed via winget
* by checking if the executable path is within a WinGet directory.
*
* Winget installs to:
* - User: %LOCALAPPDATA%\Microsoft\WinGet\Packages
* - System: C:\Program Files\WinGet\Packages
* And creates links at: %LOCALAPPDATA%\Microsoft\WinGet\Links\
*/
export function detectWinget(): boolean {
const platform = getPlatform()
// Winget is only for Windows
if (platform !== 'windows') {
return false
}
const execPath = process.execPath || process.argv[0] || ''
// Check for WinGet paths (handles both forward and backslashes)
const wingetPatterns = [
/Microsoft[/\\]WinGet[/\\]Packages/i,
/Microsoft[/\\]WinGet[/\\]Links/i,
]
for (const pattern of wingetPatterns) {
if (pattern.test(execPath)) {
logForDebugging(`Detected winget installation: ${execPath}`)
return true
}
}
return false
}
/**
* Detects if the currently running Claude instance was installed via pacman
* by querying pacman's database for file ownership.
*
* We gate on the Arch distro family before invoking pacman. On other distros
* like Ubuntu/Debian, 'pacman' in PATH may resolve to the pacman game
* (/usr/games/pacman) rather than the Arch package manager.
*/
export const detectPacman = memoize(async (): Promise<boolean> => {
const platform = getPlatform()
if (platform !== 'linux') {
return false
}
const osRelease = await getOsRelease()
if (osRelease && !isDistroFamily(osRelease, ['arch'])) {
return false
}
const execPath = process.execPath || process.argv[0] || ''
const result = await execFileNoThrow('pacman', ['-Qo', execPath], {
timeout: 5000,
useCwd: false,
})
if (result.code === 0 && result.stdout) {
logForDebugging(`Detected pacman installation: ${result.stdout.trim()}`)
return true
}
return false
})
/**
* Detects if the currently running Claude instance was installed via a .deb package
* by querying dpkg's database for file ownership.
*
* We use `dpkg -S <execPath>` to check if the executable is owned by a dpkg-managed package.
*/
export const detectDeb = memoize(async (): Promise<boolean> => {
const platform = getPlatform()
if (platform !== 'linux') {
return false
}
const osRelease = await getOsRelease()
if (osRelease && !isDistroFamily(osRelease, ['debian'])) {
return false
}
const execPath = process.execPath || process.argv[0] || ''
const result = await execFileNoThrow('dpkg', ['-S', execPath], {
timeout: 5000,
useCwd: false,
})
if (result.code === 0 && result.stdout) {
logForDebugging(`Detected deb installation: ${result.stdout.trim()}`)
return true
}
return false
})
/**
* Detects if the currently running Claude instance was installed via an RPM package
* by querying the RPM database for file ownership.
*
* We use `rpm -qf <execPath>` to check if the executable is owned by an RPM package.
*/
export const detectRpm = memoize(async (): Promise<boolean> => {
const platform = getPlatform()
if (platform !== 'linux') {
return false
}
const osRelease = await getOsRelease()
if (osRelease && !isDistroFamily(osRelease, ['fedora', 'rhel', 'suse'])) {
return false
}
const execPath = process.execPath || process.argv[0] || ''
const result = await execFileNoThrow('rpm', ['-qf', execPath], {
timeout: 5000,
useCwd: false,
})
if (result.code === 0 && result.stdout) {
logForDebugging(`Detected rpm installation: ${result.stdout.trim()}`)
return true
}
return false
})
/**
* Detects if the currently running Claude instance was installed via Alpine APK
* by querying apk's database for file ownership.
*
* We use `apk info --who-owns <execPath>` to check if the executable is owned
* by an apk-managed package.
*/
export const detectApk = memoize(async (): Promise<boolean> => {
const platform = getPlatform()
if (platform !== 'linux') {
return false
}
const osRelease = await getOsRelease()
if (osRelease && !isDistroFamily(osRelease, ['alpine'])) {
return false
}
const execPath = process.execPath || process.argv[0] || ''
const result = await execFileNoThrow(
'apk',
['info', '--who-owns', execPath],
{
timeout: 5000,
useCwd: false,
},
)
if (result.code === 0 && result.stdout) {
logForDebugging(`Detected apk installation: ${result.stdout.trim()}`)
return true
}
return false
})
/**
* Memoized function to detect which package manager installed Claude
* Returns 'unknown' if no package manager is detected
*/
export const getPackageManager = memoize(async (): Promise<PackageManager> => {
if (detectHomebrew()) {
return 'homebrew'
}
if (detectWinget()) {
return 'winget'
}
if (detectMise()) {
return 'mise'
}
if (detectAsdf()) {
return 'asdf'
}
if (await detectPacman()) {
return 'pacman'
}
if (await detectApk()) {
return 'apk'
}
if (await detectDeb()) {
return 'deb'
}
if (await detectRpm()) {
return 'rpm'
}
return 'unknown'
})