diff --git a/apps/frontend/src/main/cli-tool-manager.ts b/apps/frontend/src/main/cli-tool-manager.ts index e54dd43b..4e0c6d0a 100644 --- a/apps/frontend/src/main/cli-tool-manager.ts +++ b/apps/frontend/src/main/cli-tool-manager.ts @@ -27,6 +27,7 @@ import os from 'os'; import { app } from 'electron'; import { findExecutable } from './env-utils'; import type { ToolDetectionResult } from '../shared/types'; +import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-python'; /** * Supported CLI tools managed by this system @@ -731,37 +732,15 @@ class CLIToolManager { /** * Find Homebrew Python on macOS - * - * Checks both Apple Silicon and Intel Homebrew locations. - * Searches for python3, python3.13, python3.12, etc. in order. + * Delegates to shared utility function. * * @returns Path to Homebrew Python or null if not found */ private findHomebrewPython(): string | null { - const homebrewDirs = [ - '/opt/homebrew/bin', // Apple Silicon - '/usr/local/bin', // Intel Mac - ]; - - // Check for generic python3 first, then specific versions (newest first) - const pythonNames = [ - 'python3', - 'python3.13', - 'python3.12', - 'python3.11', - 'python3.10', - ]; - - for (const dir of homebrewDirs) { - for (const name of pythonNames) { - const pythonPath = path.join(dir, name); - if (existsSync(pythonPath)) { - return pythonPath; - } - } - } - - return null; + return findHomebrewPythonUtil( + (pythonPath) => this.validatePython(pythonPath), + '[CLI Tools]' + ); } /** diff --git a/apps/frontend/src/main/python-detector.ts b/apps/frontend/src/main/python-detector.ts index f8c80d20..e15bcd45 100644 --- a/apps/frontend/src/main/python-detector.ts +++ b/apps/frontend/src/main/python-detector.ts @@ -2,6 +2,7 @@ import { execSync, execFileSync } from 'child_process'; import { existsSync, accessSync, constants } from 'fs'; import path from 'path'; import { app } from 'electron'; +import { findHomebrewPython as findHomebrewPythonUtil } from './utils/homebrew-python'; /** * Get the path to the bundled Python executable. @@ -34,23 +35,12 @@ export function getBundledPythonPath(): string | null { /** * Find the first existing Homebrew Python installation. - * Checks common Homebrew paths for Python 3. + * Delegates to shared utility function. * * @returns The path to Homebrew Python, or null if not found */ function findHomebrewPython(): string | null { - const homebrewPaths = [ - '/opt/homebrew/bin/python3', // Apple Silicon (M1/M2/M3) - '/usr/local/bin/python3' // Intel Mac - ]; - - for (const pythonPath of homebrewPaths) { - if (existsSync(pythonPath)) { - return pythonPath; - } - } - - return null; + return findHomebrewPythonUtil(validatePythonVersion, '[Python]'); } /** @@ -308,6 +298,7 @@ const ALLOWED_PATH_PATTERNS: RegExp[] = [ /** * Known safe Python commands (not full paths). * These are resolved by the shell/OS and are safe. + * Note: Update this list when new Python versions are released. */ const SAFE_PYTHON_COMMANDS = new Set([ 'python', @@ -316,6 +307,7 @@ const SAFE_PYTHON_COMMANDS = new Set([ 'python3.11', 'python3.12', 'python3.13', + 'python3.14', 'py', 'py -3', ]); diff --git a/apps/frontend/src/main/utils/homebrew-python.ts b/apps/frontend/src/main/utils/homebrew-python.ts new file mode 100644 index 00000000..9891c123 --- /dev/null +++ b/apps/frontend/src/main/utils/homebrew-python.ts @@ -0,0 +1,77 @@ +/** + * Homebrew Python Detection Utility + * + * Shared logic for finding Python installations in Homebrew directories. + * Used by both python-detector.ts and cli-tool-manager.ts to ensure + * consistent Python detection across the application. + */ + +import { existsSync } from 'fs'; +import path from 'path'; + +/** + * Validation result for a Python installation. + */ +export interface PythonValidation { + valid: boolean; + version?: string; + message: string; +} + +/** + * Find the first valid Homebrew Python installation. + * Checks common Homebrew paths for Python 3, including versioned installations. + * Prioritizes newer Python versions (3.14, 3.13, 3.12, 3.11, 3.10). + * + * Note: This list should be updated when new Python versions are released. + * Check for specific versions first to ensure we find the latest available version. + * + * @param validateFn - Function to validate a Python path and return validation result + * @param logPrefix - Prefix for log messages (e.g., '[Python]', '[CLI Tools]') + * @returns The path to Homebrew Python, or null if not found + */ +export function findHomebrewPython( + validateFn: (pythonPath: string) => PythonValidation, + logPrefix: string +): string | null { + const homebrewDirs = [ + '/opt/homebrew/bin', // Apple Silicon (M1/M2/M3) + '/usr/local/bin' // Intel Mac + ]; + + // Check for specific Python versions first (newest to oldest), then fall back to generic python3. + // This ensures we find the latest available version that meets our requirements. + const pythonNames = [ + 'python3.14', + 'python3.13', + 'python3.12', + 'python3.11', + 'python3.10', + 'python3', + ]; + + for (const dir of homebrewDirs) { + for (const name of pythonNames) { + const pythonPath = path.join(dir, name); + if (existsSync(pythonPath)) { + try { + // Validate that this Python meets version requirements + const validation = validateFn(pythonPath); + if (validation.valid) { + console.log(`${logPrefix} Found valid Homebrew Python: ${pythonPath} (${validation.version})`); + return pythonPath; + } else { + console.warn(`${logPrefix} ${pythonPath} rejected: ${validation.message}`); + } + } catch (error) { + // Version check failed (e.g., timeout, permission issue), try next candidate + console.warn(`${logPrefix} Failed to validate ${pythonPath}: ${error}`); + continue; + } + } + } + } + + console.log(`${logPrefix} No valid Homebrew Python found in ${homebrewDirs.join(', ')}`); + return null; +}