diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8fc97ab..aa5b1719 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -84,6 +89,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -147,6 +157,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -188,6 +203,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/apps/frontend/src/main/python-detector.ts b/apps/frontend/src/main/python-detector.ts index 8f6834a7..55cea55e 100644 --- a/apps/frontend/src/main/python-detector.ts +++ b/apps/frontend/src/main/python-detector.ts @@ -18,17 +18,18 @@ export function findPythonCommand(): string | null { for (const cmd of candidates) { try { - const version = execSync(`${cmd} --version`, { - stdio: 'pipe', - timeout: 5000, - windowsHide: true - }).toString(); - - if (version.includes('Python 3')) { + // Validate version meets minimum requirement (Python 3.10+) + const validation = validatePythonVersion(cmd); + if (validation.valid) { + console.log(`[Python] Found valid Python: ${cmd} (${validation.version})`); return cmd; + } else { + console.warn(`[Python] ${cmd} version too old: ${validation.message}`); + continue; } } catch { // Command not found or errored, try next + console.warn(`[Python] Command not found or errored: ${cmd}`); continue; } } @@ -37,6 +38,71 @@ export function findPythonCommand(): string | null { return isWindows ? 'python' : 'python3'; } +/** + * Extract Python version from a command. + * + * @param pythonCmd - The Python command to check (e.g., "python3", "py -3") + * @returns The version string (e.g., "3.10.5") or null if unable to detect + */ +function getPythonVersion(pythonCmd: string): string | null { + try { + const version = execSync(`${pythonCmd} --version`, { + stdio: 'pipe', + timeout: 5000, + windowsHide: true + }).toString().trim(); + + // Extract version number from "Python 3.10.5" format + const match = version.match(/Python (\d+\.\d+\.\d+)/); + return match ? match[1] : null; + } catch { + return null; + } +} + +/** + * Validate that a Python command meets minimum version requirements. + * + * @param pythonCmd - The Python command to validate + * @returns Validation result with status, version, and message + */ +function validatePythonVersion(pythonCmd: string): { + valid: boolean; + version?: string; + message: string; +} { + const MINIMUM_VERSION = '3.10.0'; + + const versionStr = getPythonVersion(pythonCmd); + if (!versionStr) { + return { + valid: false, + message: 'Unable to detect Python version' + }; + } + + // Parse version numbers for comparison + const [major, minor] = versionStr.split('.').map(Number); + const [reqMajor, reqMinor] = MINIMUM_VERSION.split('.').map(Number); + + const meetsRequirement = + major > reqMajor || (major === reqMajor && minor >= reqMinor); + + if (!meetsRequirement) { + return { + valid: false, + version: versionStr, + message: `Python ${versionStr} is too old. Requires Python ${MINIMUM_VERSION}+ (claude-agent-sdk requirement)` + }; + } + + return { + valid: true, + version: versionStr, + message: `Python ${versionStr} meets requirements` + }; +} + /** * Get the default Python command for the current platform. * This is a synchronous fallback that doesn't test if Python actually exists. diff --git a/apps/frontend/src/main/python-env-manager.ts b/apps/frontend/src/main/python-env-manager.ts index 5056b7cc..00e2737e 100644 --- a/apps/frontend/src/main/python-env-manager.ts +++ b/apps/frontend/src/main/python-env-manager.ts @@ -3,6 +3,7 @@ import { existsSync } from 'fs'; import path from 'path'; import { EventEmitter } from 'events'; import { app } from 'electron'; +import { findPythonCommand } from './python-detector'; export interface PythonEnvStatus { ready: boolean; @@ -96,61 +97,29 @@ export class PythonEnvManager extends EventEmitter { } /** - * Find system Python3 + * Find system Python 3.10+ + * Uses the shared python-detector logic which validates version requirements. */ private findSystemPython(): string | null { - const isWindows = process.platform === 'win32'; - - // Windows candidates - py launcher is handled specially - // Unix candidates - try python3 first, then python - const candidates = isWindows - ? ['python', 'python3'] - : ['python3', 'python']; - - // On Windows, try the py launcher first (most reliable) - if (isWindows) { - try { - // py -3 runs Python 3, verify it works - const version = execSync('py -3 --version', { - stdio: 'pipe', - timeout: 5000 - }).toString(); - if (version.includes('Python 3')) { - // Get the actual executable path - const pythonPath = execSync('py -3 -c "import sys; print(sys.executable)"', { - stdio: 'pipe', - timeout: 5000 - }).toString().trim(); - return pythonPath; - } - } catch { - // py launcher not available, continue with other candidates - } + const pythonCmd = findPythonCommand(); + if (!pythonCmd) { + return null; } - for (const cmd of candidates) { - try { - const version = execSync(`${cmd} --version`, { - stdio: 'pipe', - timeout: 5000 - }).toString(); - if (version.includes('Python 3')) { - // Get the actual path - // On Windows, use Python itself to get the path - // On Unix, use 'which' - const pathCmd = isWindows - ? `${cmd} -c "import sys; print(sys.executable)"` - : `which ${cmd}`; - const pythonPath = execSync(pathCmd, { stdio: 'pipe', timeout: 5000 }) - .toString() - .trim(); - return pythonPath; - } - } catch { - continue; - } + try { + // Get the actual executable path from the command + // For commands like "py -3", we need to resolve to the actual executable + const pythonPath = execSync(`${pythonCmd} -c "import sys; print(sys.executable)"`, { + stdio: 'pipe', + timeout: 5000 + }).toString().trim(); + + console.log(`[PythonEnvManager] Found Python at: ${pythonPath}`); + return pythonPath; + } catch (err) { + console.error(`[PythonEnvManager] Failed to get Python path for ${pythonCmd}:`, err); + return null; } - return null; } /** @@ -161,7 +130,11 @@ export class PythonEnvManager extends EventEmitter { const systemPython = this.findSystemPython(); if (!systemPython) { - this.emit('error', 'Python 3 not found. Please install Python 3.9+'); + this.emit( + 'error', + 'Python 3.10+ not found. Please install Python 3.10 or higher (required by claude-agent-sdk).\n\n' + + 'Download: https://www.python.org/downloads/' + ); return false; }