Skip to main content
Glama
shell-utils.ts7.27 kB
/** * @fileoverview Shell configuration utilities for tm-profiles * Provides functions to modify shell configuration files (bashrc, zshrc, etc.) */ import fs from 'fs'; import os from 'os'; import path from 'path'; export interface ShellConfigResult { success: boolean; shellConfigFile?: string; message: string; alreadyExists?: boolean; } /** * Detects the user's shell configuration file path * Supports: zsh, bash, fish, PowerShell (Windows) * @returns The path to the shell config file, or null if not detected */ export function getShellConfigPath(): string | null { const homeDir = os.homedir(); const shell = process.env.SHELL || ''; // Unix-like shells (Linux, macOS, WSL, Git Bash) if (shell.includes('zsh')) { return path.join(homeDir, '.zshrc'); } if (shell.includes('bash')) { // macOS uses .bash_profile for login shells - prefer it even if it doesn't exist yet const bashProfile = path.join(homeDir, '.bash_profile'); if (process.platform === 'darwin') { return bashProfile; } return path.join(homeDir, '.bashrc'); } if (shell.includes('fish')) { return path.join(homeDir, '.config', 'fish', 'config.fish'); } // Windows PowerShell - check $PROFILE env var or use default location if (process.platform === 'win32') { // PowerShell sets PSModulePath when running if (process.env.PSModulePath) { // Use $PROFILE if set, otherwise use default PowerShell profile path const psProfile = process.env.PROFILE || path.join( homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1' ); // Also check PowerShell Core location const pwshProfile = path.join( homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1' ); if (fs.existsSync(pwshProfile)) return pwshProfile; if (fs.existsSync(psProfile)) return psProfile; // Return PowerShell Core path as default (more modern) return pwshProfile; } // Git Bash on Windows - check for .bashrc const bashrc = path.join(homeDir, '.bashrc'); if (fs.existsSync(bashrc)) return bashrc; } // Fallback - check what exists (covers WSL and other edge cases) const zshrc = path.join(homeDir, '.zshrc'); if (fs.existsSync(zshrc)) return zshrc; const bashrc = path.join(homeDir, '.bashrc'); if (fs.existsSync(bashrc)) return bashrc; return null; } /** * Checks if a shell config file is a PowerShell profile */ function isPowerShellProfile(filePath: string): boolean { return filePath.endsWith('.ps1'); } /** * Checks if a shell config file is a fish config */ function isFishConfig(filePath: string): boolean { return filePath.includes('config.fish'); } /** * Adds an export statement to the user's shell configuration file * Handles both Unix-style shells (bash, zsh, fish) and PowerShell * @param envVar - The environment variable name * @param value - The value to set * @param comment - Optional comment to add above the export * @returns Result object with success status and details */ export function addShellExport( envVar: string, value: string, comment?: string ): ShellConfigResult { // Validate envVar contains only safe characters (prevents ReDoS attacks) if (!/^[A-Z_][A-Z0-9_]*$/i.test(envVar)) { return { success: false, message: `Invalid environment variable name: ${envVar}. Must start with a letter or underscore and contain only alphanumeric characters and underscores.` }; } // Validate value to prevent shell injection if ( typeof value !== 'string' || value.includes('\n') || value.includes('\r') ) { return { success: false, message: `Invalid value: must be a single-line string without newlines` }; } const shellConfigFile = getShellConfigPath(); if (!shellConfigFile) { return { success: false, message: 'Could not determine shell type (zsh, bash, fish, or PowerShell)' }; } try { // Create the profile directory if it doesn't exist (handles fish's nested ~/.config/fish/ and PowerShell) const profileDir = path.dirname(shellConfigFile); if (!fs.existsSync(profileDir)) { fs.mkdirSync(profileDir, { recursive: true }); } // Check if file exists, create empty if it doesn't (common for PowerShell and macOS .bash_profile) if (!fs.existsSync(shellConfigFile)) { const isBashProfileOnMac = process.platform === 'darwin' && shellConfigFile.endsWith('.bash_profile'); if (isPowerShellProfile(shellConfigFile) || isBashProfileOnMac) { // Create empty profile file fs.writeFileSync(shellConfigFile, ''); } else { return { success: false, shellConfigFile, message: `Shell config file ${shellConfigFile} not found` }; } } const content = fs.readFileSync(shellConfigFile, 'utf8'); // Check if the export already exists using precise regex patterns // This avoids false positives from comments or partial matches let alreadyExists: boolean; if (isPowerShellProfile(shellConfigFile)) { alreadyExists = new RegExp(`^\\s*\\$env:${envVar}\\s*=`, 'm').test( content ); } else if (isFishConfig(shellConfigFile)) { // Fish uses 'set -gx VAR value' or 'set -xg VAR value' syntax // Only match exported variables (must have 'x' flag) alreadyExists = new RegExp( `^\\s*set\\s+-[gux]*x[gux]*\\s+${envVar}\\s+`, 'm' ).test(content); } else { alreadyExists = new RegExp(`^\\s*export\\s+${envVar}\\s*=`, 'm').test( content ); } if (alreadyExists) { return { success: true, shellConfigFile, message: `${envVar} already configured`, alreadyExists: true }; } // Build the export block based on shell type let exportLine: string; let commentPrefix: string; if (isPowerShellProfile(shellConfigFile)) { // PowerShell syntax - escape quotes and backticks const escapedValue = value.replace(/["`$]/g, '`$&'); exportLine = `$env:${envVar} = "${escapedValue}"`; commentPrefix = '#'; } else if (isFishConfig(shellConfigFile)) { // Fish shell syntax - use 'set -gx VAR value' (global export) // Escape single quotes in fish by closing, escaping, reopening: 'foo'\''bar' const escapedValue = value.replace(/'/g, "'\\''"); exportLine = `set -gx ${envVar} '${escapedValue}'`; commentPrefix = '#'; } else { // Unix shell syntax (bash, zsh) - use single quotes and escape embedded single quotes const escapedValue = value.replace(/'/g, "'\\''"); exportLine = `export ${envVar}='${escapedValue}'`; commentPrefix = '#'; } const commentLine = comment ? `${commentPrefix} ${comment}\n` : ''; const block = `\n${commentLine}${exportLine}\n`; fs.appendFileSync(shellConfigFile, block); return { success: true, shellConfigFile, message: `Added ${envVar} to ${shellConfigFile}` }; } catch (error) { return { success: false, shellConfigFile, message: `Failed to modify shell config: ${(error as Error).message}` }; } } /** * Enables Claude Code deferred MCP loading by adding the required env var * @returns Result object with success status and details */ export function enableDeferredMcpLoading(): ShellConfigResult { return addShellExport( 'ENABLE_EXPERIMENTAL_MCP_CLI', 'true', 'Claude Code deferred MCP loading (added by Taskmaster)' ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server