π File detail
utils/shellConfig.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes CLAUDE_ALIAS_REGEX, getShellConfigPaths, filterClaudeAliases, readFileLines, and writeFileLines (and more) β mainly functions, hooks, or classes. Dependencies touch Node filesystem, Node OS/process metadata, and Node path helpers. It composes internal code from errors and localInstaller (relative imports). What the file header says: Utilities for managing shell configuration files (like .bashrc, .zshrc) Used for managing claude aliases and PATH entries.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Utilities for managing shell configuration files (like .bashrc, .zshrc) Used for managing claude aliases and PATH entries
π€ Exports (heuristic)
CLAUDE_ALIAS_REGEXgetShellConfigPathsfilterClaudeAliasesreadFileLineswriteFileLinesfindClaudeAliasfindValidClaudeAlias
π External import roots
Package roots from from "β¦" (relative paths omitted).
fsospath
π₯οΈ Source preview
/**
* Utilities for managing shell configuration files (like .bashrc, .zshrc)
* Used for managing claude aliases and PATH entries
*/
import { open, readFile, stat } from 'fs/promises'
import { homedir as osHomedir } from 'os'
import { join } from 'path'
import { isFsInaccessible } from './errors.js'
import { getLocalClaudePath } from './localInstaller.js'
export const CLAUDE_ALIAS_REGEX = /^\s*alias\s+claude\s*=/
type EnvLike = Record<string, string | undefined>
type ShellConfigOptions = {
env?: EnvLike
homedir?: string
}
/**
* Get the paths to shell configuration files
* Respects ZDOTDIR for zsh users
* @param options Optional overrides for testing (env, homedir)
*/
export function getShellConfigPaths(
options?: ShellConfigOptions,
): Record<string, string> {
const home = options?.homedir ?? osHomedir()
const env = options?.env ?? process.env
const zshConfigDir = env.ZDOTDIR || home
return {
zsh: join(zshConfigDir, '.zshrc'),
bash: join(home, '.bashrc'),
fish: join(home, '.config/fish/config.fish'),
}
}
/**
* Filter out installer-created claude aliases from an array of lines
* Only removes aliases pointing to $HOME/.claude/local/claude
* Preserves custom user aliases that point to other locations
* Returns the filtered lines and whether our default installer alias was found
*/
export function filterClaudeAliases(lines: string[]): {
filtered: string[]
hadAlias: boolean
} {
let hadAlias = false
const filtered = lines.filter(line => {
// Check if this is a claude alias
if (CLAUDE_ALIAS_REGEX.test(line)) {
// Extract the alias target - handle spaces, quotes, and various formats
// First try with quotes
let match = line.match(/alias\s+claude\s*=\s*["']([^"']+)["']/)
if (!match) {
// Try without quotes (capturing until end of line or comment)
match = line.match(/alias\s+claude\s*=\s*([^#\n]+)/)
}
if (match && match[1]) {
const target = match[1].trim()
// Only remove if it points to the installer location
// The installer always creates aliases with the full expanded path
if (target === getLocalClaudePath()) {
hadAlias = true
return false // Remove this line
}
}
// Keep custom aliases that don't point to the installer location
}
return true
})
return { filtered, hadAlias }
}
/**
* Read a file and split it into lines
* Returns null if file doesn't exist or can't be read
*/
export async function readFileLines(
filePath: string,
): Promise<string[] | null> {
try {
const content = await readFile(filePath, { encoding: 'utf8' })
return content.split('\n')
} catch (e: unknown) {
if (isFsInaccessible(e)) return null
throw e
}
}
/**
* Write lines back to a file
*/
export async function writeFileLines(
filePath: string,
lines: string[],
): Promise<void> {
const fh = await open(filePath, 'w')
try {
await fh.writeFile(lines.join('\n'), { encoding: 'utf8' })
await fh.datasync()
} finally {
await fh.close()
}
}
/**
* Check if a claude alias exists in any shell config file
* Returns the alias target if found, null otherwise
* @param options Optional overrides for testing (env, homedir)
*/
export async function findClaudeAlias(
options?: ShellConfigOptions,
): Promise<string | null> {
const configs = getShellConfigPaths(options)
for (const configPath of Object.values(configs)) {
const lines = await readFileLines(configPath)
if (!lines) continue
for (const line of lines) {
if (CLAUDE_ALIAS_REGEX.test(line)) {
// Extract the alias target
const match = line.match(/alias\s+claude=["']?([^"'\s]+)/)
if (match && match[1]) {
return match[1]
}
}
}
}
return null
}
/**
* Check if a claude alias exists and points to a valid executable
* Returns the alias target if valid, null otherwise
* @param options Optional overrides for testing (env, homedir)
*/
export async function findValidClaudeAlias(
options?: ShellConfigOptions,
): Promise<string | null> {
const aliasTarget = await findClaudeAlias(options)
if (!aliasTarget) return null
const home = options?.homedir ?? osHomedir()
// Expand ~ to home directory
const expandedPath = aliasTarget.startsWith('~')
? aliasTarget.replace('~', home)
: aliasTarget
// Check if the target exists and is executable
try {
const stats = await stat(expandedPath)
// Check if it's a file (could be executable or symlink)
if (stats.isFile() || stats.isSymbolicLink()) {
return aliasTarget
}
} catch {
// Target doesn't exist or can't be accessed
}
return null
}