From 92c6f2786d8cce2f87b5e97748a59462f6a4787f Mon Sep 17 00:00:00 2001 From: Alex <63423455+AlexMadera@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:14:57 +0100 Subject: [PATCH] fix(python): use venv Python for all services to fix dotenv errors (#311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(python): use venv Python for all services to fix dotenv errors Services were spawning Python processes using findPythonCommand() which returns the bundled Python directly. However, dependencies like python-dotenv are only installed in the venv created from the bundled Python. Changes: - Add getConfiguredPythonPath() helper that returns venv Python when ready - Update all services to use venv Python instead of bundled Python directly: - memory-service.ts - memory-handlers.ts - agent-process.ts - changelog-service.ts - title-generator.ts - insights/config.ts - project-context-handlers.ts - worktree-handlers.ts - Fix availability checks to use findPythonCommand() (can return null) - Add python:verify script for bundling verification The flow now works correctly: 1. App starts → findPythonCommand() finds bundled Python 2. pythonEnvManager creates venv using bundled Python 3. pip installs dependencies (dotenv, claude-agent-sdk, etc.) 4. All services use venv Python → has all dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix lintin and test --------- Co-authored-by: Claude --- apps/frontend/package-lock.json | 54 +++++----- apps/frontend/package.json | 1 + .../scripts/verify-python-bundling.cjs | 102 ++++++++++++++++++ apps/frontend/src/main/agent/agent-process.ts | 23 ++-- .../src/main/changelog/changelog-service.ts | 20 +++- apps/frontend/src/main/insights/config.ts | 20 ++-- .../context/project-context-handlers.ts | 23 ++-- .../src/main/ipc-handlers/memory-handlers.ts | 16 ++- .../ipc-handlers/task/worktree-handlers.ts | 10 +- apps/frontend/src/main/memory-service.ts | 16 +-- apps/frontend/src/main/python-env-manager.ts | 25 +++++ apps/frontend/src/main/title-generator.ts | 21 +++- 12 files changed, 244 insertions(+), 87 deletions(-) create mode 100644 apps/frontend/scripts/verify-python-bundling.cjs diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index e06c0d46..9378a744 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -195,6 +195,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -579,6 +580,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -622,6 +624,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -661,6 +664,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1067,7 +1071,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1089,7 +1092,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -4212,8 +4214,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4400,6 +4401,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4410,6 +4412,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4501,6 +4504,7 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -4913,6 +4917,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4973,6 +4978,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5145,7 +5151,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5540,6 +5545,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6210,8 +6216,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -6561,6 +6566,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -6618,8 +6624,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dotenv": { "version": "16.6.1", @@ -6695,6 +6700,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -6823,7 +6829,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -6844,7 +6849,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -6860,7 +6864,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -6871,7 +6874,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -7241,6 +7243,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8486,6 +8489,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -9276,6 +9280,7 @@ "integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10207,7 +10212,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12398,6 +12402,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12495,6 +12500,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12531,7 +12537,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -12549,7 +12554,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -12570,7 +12574,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -12586,7 +12589,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12599,8 +12601,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/proc-log": { "version": "2.0.1", @@ -12704,6 +12705,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12713,6 +12715,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14032,7 +14035,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -14089,7 +14093,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -14116,7 +14119,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14138,7 +14140,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14152,7 +14153,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -14167,7 +14167,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14484,6 +14483,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14833,6 +14833,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15875,6 +15876,7 @@ "integrity": "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index bc7545f6..8f1b6a47 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -30,6 +30,7 @@ "rebuild": "electron-rebuild", "python:download": "node scripts/download-python.cjs", "python:download:all": "node scripts/download-python.cjs --all", + "python:verify": "node scripts/verify-python-bundling.cjs", "package": "npm run python:download && electron-vite build && electron-builder", "package:mac": "npm run python:download && electron-vite build && electron-builder --mac", "package:win": "npm run python:download && electron-vite build && electron-builder --win", diff --git a/apps/frontend/scripts/verify-python-bundling.cjs b/apps/frontend/scripts/verify-python-bundling.cjs new file mode 100644 index 00000000..00699614 --- /dev/null +++ b/apps/frontend/scripts/verify-python-bundling.cjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * Verify Python bundling configuration is correct. + * Run this before packaging to ensure Python will be properly bundled. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync, spawnSync } = require('child_process'); + +const FRONTEND_DIR = path.resolve(__dirname, '..'); +const PYTHON_RUNTIME_DIR = path.join(FRONTEND_DIR, 'python-runtime'); + +console.log('=== Python Bundling Verification ===\n'); + +// Check 1: Python runtime downloaded? +console.log('1. Checking if Python runtime is downloaded...'); +const platform = process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux'; +const arch = process.arch; +const runtimePath = path.join(PYTHON_RUNTIME_DIR, `${platform}-${arch}`, 'python'); + +if (fs.existsSync(runtimePath)) { + const pythonExe = process.platform === 'win32' + ? path.join(runtimePath, 'python.exe') + : path.join(runtimePath, 'bin', 'python3'); + + if (fs.existsSync(pythonExe)) { + console.log(` ✓ Found bundled Python at: ${pythonExe}`); + + // Test version + try { + const version = execSync(`"${pythonExe}" --version`, { encoding: 'utf8' }).trim(); + console.log(` ✓ Version: ${version}`); + } catch (e) { + console.log(` ✗ Failed to get version: ${e.message}`); + } + } else { + console.log(` ✗ Python executable not found at: ${pythonExe}`); + } +} else { + console.log(` ✗ Python runtime not downloaded. Run: npm run python:download`); +} + +// Check 2: package.json extraResources configured? +console.log('\n2. Checking package.json extraResources configuration...'); +const packageJson = require(path.join(FRONTEND_DIR, 'package.json')); +const extraResources = packageJson.build?.extraResources || []; + +const pythonResource = extraResources.find(r => + (typeof r === 'string' && r.includes('python')) || + (typeof r === 'object' && r.from?.includes('python')) +); + +if (pythonResource) { + console.log(' ✓ Python is configured in extraResources:'); + console.log(` ${JSON.stringify(pythonResource)}`); +} else { + console.log(' ✗ Python not found in extraResources configuration'); +} + +// Check 3: Test venv creation simulation +console.log('\n3. Checking venv creation capability...'); +try { + // Find system Python for testing + let pythonCmd = process.platform === 'win32' ? 'python' : 'python3'; + + const result = spawnSync(pythonCmd, ['-m', 'venv', '--help'], { encoding: 'utf8' }); + if (result.status === 0) { + console.log(` ✓ venv module is available`); + } else { + console.log(` ✗ venv module not available: ${result.stderr}`); + } +} catch (e) { + console.log(` ✗ Failed to check venv: ${e.message}`); +} + +// Check 4: Verify requirements.txt exists +console.log('\n4. Checking requirements.txt...'); +const backendDir = path.join(FRONTEND_DIR, '..', 'backend'); +const requirementsPath = path.join(backendDir, 'requirements.txt'); + +if (fs.existsSync(requirementsPath)) { + const content = fs.readFileSync(requirementsPath, 'utf8'); + const hasDotenv = content.includes('python-dotenv'); + const hasSDK = content.includes('claude-agent-sdk'); + + console.log(` ✓ requirements.txt found`); + console.log(` ${hasDotenv ? '✓' : '✗'} python-dotenv: ${hasDotenv ? 'present' : 'MISSING!'}`); + console.log(` ${hasSDK ? '✓' : '✗'} claude-agent-sdk: ${hasSDK ? 'present' : 'MISSING!'}`); +} else { + console.log(` ✗ requirements.txt not found at: ${requirementsPath}`); +} + +// Summary +console.log('\n=== Summary ==='); +console.log('To fully test Python bundling:'); +console.log('1. Run: npm run python:download'); +console.log('2. Run: npm run package:win (or :mac/:linux)'); +console.log('3. Launch the packaged app and check Dev Tools console for:'); +console.log(' - "[Python] Found bundled Python at: ..."'); +console.log(' - "[PythonEnvManager] Ready with Python path: ..."'); +console.log('4. Try creating and running a task - should work without dotenv errors'); diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index c606bf01..c3442180 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -9,7 +9,8 @@ import { ProcessType, ExecutionProgressData } from './types'; import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv, detectAuthFailure } from '../rate-limit-detector'; import { projectStore } from '../project-store'; import { getClaudeProfileManager } from '../claude-profile-manager'; -import { findPythonCommand, parsePythonCommand } from '../python-detector'; +import { parsePythonCommand } from '../python-detector'; +import { getConfiguredPythonPath } from '../python-env-manager'; /** * Process spawning and lifecycle management @@ -18,8 +19,9 @@ export class AgentProcessManager { private state: AgentState; private events: AgentEvents; private emitter: EventEmitter; - // Auto-detect Python command on initialization - private pythonPath: string = findPythonCommand() || 'python'; + // Python path will be configured by pythonEnvManager after venv is ready + // Use null to indicate not yet configured - getPythonPath() will use fallback + private _pythonPath: string | null = null; private autoBuildSourcePath: string = ''; constructor(state: AgentState, events: AgentEvents, emitter: EventEmitter) { @@ -33,7 +35,7 @@ export class AgentProcessManager { */ configure(pythonPath?: string, autoBuildSourcePath?: string): void { if (pythonPath) { - this.pythonPath = pythonPath; + this._pythonPath = pythonPath; } if (autoBuildSourcePath) { this.autoBuildSourcePath = autoBuildSourcePath; @@ -41,10 +43,17 @@ export class AgentProcessManager { } /** - * Get the configured Python path + * Get the configured Python path. + * Returns explicitly configured path, or falls back to getConfiguredPythonPath() + * which uses the venv Python if ready. */ getPythonPath(): string { - return this.pythonPath; + // If explicitly configured (by pythonEnvManager), use that + if (this._pythonPath) { + return this._pythonPath; + } + // Otherwise use the global configured path (venv if ready, else bundled/system) + return getConfiguredPythonPath(); } /** @@ -168,7 +177,7 @@ export class AgentProcessManager { const profileEnv = getProfileEnv(); // Parse Python command to handle space-separated commands like "py -3" - const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath); + const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.getPythonPath()); const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], { cwd, env: { diff --git a/apps/frontend/src/main/changelog/changelog-service.ts b/apps/frontend/src/main/changelog/changelog-service.ts index 214e0ccb..049cfc3a 100644 --- a/apps/frontend/src/main/changelog/changelog-service.ts +++ b/apps/frontend/src/main/changelog/changelog-service.ts @@ -27,15 +27,15 @@ import { getCommits, getBranchDiffCommits } from './git-integration'; -import { findPythonCommand } from '../python-detector'; +import { getConfiguredPythonPath } from '../python-env-manager'; /** * Main changelog service - orchestrates all changelog operations * Delegates to specialized modules for specific concerns */ export class ChangelogService extends EventEmitter { - // Auto-detect Python command on initialization - private pythonPath: string = findPythonCommand() || 'python'; + // Python path will be configured by pythonEnvManager after venv is ready + private _pythonPath: string | null = null; private claudePath: string = 'claude'; private autoBuildSourcePath: string = ''; private cachedEnv: Record | null = null; @@ -130,13 +130,25 @@ export class ChangelogService extends EventEmitter { */ configure(pythonPath?: string, autoBuildSourcePath?: string): void { if (pythonPath) { - this.pythonPath = pythonPath; + this._pythonPath = pythonPath; } if (autoBuildSourcePath) { this.autoBuildSourcePath = autoBuildSourcePath; } } + /** + * Get the configured Python path. + * Returns explicitly configured path, or falls back to getConfiguredPythonPath() + * which uses the venv Python if ready. + */ + private get pythonPath(): string { + if (this._pythonPath) { + return this._pythonPath; + } + return getConfiguredPythonPath(); + } + /** * Get the auto-claude source path (detects automatically if not configured) */ diff --git a/apps/frontend/src/main/insights/config.ts b/apps/frontend/src/main/insights/config.ts index a937dbed..aa75c487 100644 --- a/apps/frontend/src/main/insights/config.ts +++ b/apps/frontend/src/main/insights/config.ts @@ -2,15 +2,16 @@ import path from 'path'; import { existsSync, readFileSync } from 'fs'; import { app } from 'electron'; import { getProfileEnv } from '../rate-limit-detector'; -import { findPythonCommand } from '../python-detector'; +import { getConfiguredPythonPath } from '../python-env-manager'; /** * Configuration manager for insights service * Handles path detection and environment variable loading */ export class InsightsConfig { - // Auto-detect Python command on initialization - private pythonPath: string = findPythonCommand() || 'python'; + // Python path will be configured by pythonEnvManager after venv is ready + // Use getter to always get current configured path + private _pythonPath: string | null = null; private autoBuildSourcePath: string = ''; /** @@ -18,7 +19,7 @@ export class InsightsConfig { */ configure(pythonPath?: string, autoBuildSourcePath?: string): void { if (pythonPath) { - this.pythonPath = pythonPath; + this._pythonPath = pythonPath; } if (autoBuildSourcePath) { this.autoBuildSourcePath = autoBuildSourcePath; @@ -26,10 +27,17 @@ export class InsightsConfig { } /** - * Get configured Python path + * Get configured Python path. + * Returns explicitly configured path, or falls back to getConfiguredPythonPath() + * which uses the venv Python if ready. */ getPythonPath(): string { - return this.pythonPath; + // If explicitly configured (by pythonEnvManager), use that + if (this._pythonPath) { + return this._pythonPath; + } + // Otherwise use the global configured path (venv if ready, else bundled/system) + return getConfiguredPythonPath(); } /** diff --git a/apps/frontend/src/main/ipc-handlers/context/project-context-handlers.ts b/apps/frontend/src/main/ipc-handlers/context/project-context-handlers.ts index 24878d63..f632b6de 100644 --- a/apps/frontend/src/main/ipc-handlers/context/project-context-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/context/project-context-handlers.ts @@ -1,4 +1,4 @@ -import { ipcMain, app } from 'electron'; +import { ipcMain } from 'electron'; import type { BrowserWindow } from 'electron'; import path from 'path'; import { existsSync, readFileSync } from 'fs'; @@ -21,7 +21,8 @@ import { buildMemoryStatus } from './memory-status-handlers'; import { loadFileBasedMemories } from './memory-data-handlers'; -import { findPythonCommand, parsePythonCommand } from '../../python-detector'; +import { parsePythonCommand } from '../../python-detector'; +import { getConfiguredPythonPath } from '../../python-env-manager'; import { getAugmentedEnv } from '../../env-utils'; /** @@ -159,20 +160,10 @@ export function registerProjectContextHandlers( const analyzerPath = path.join(autoBuildSource, 'analyzer.py'); const indexOutputPath = path.join(project.path, AUTO_BUILD_PATHS.PROJECT_INDEX); - // Get Python command directly from settings file (not pythonEnvManager which creates NEW venv) - let pythonCmd = findPythonCommand() || 'python3'; - try { - const settingsPath = path.join(app.getPath('userData'), 'settings.json'); - if (existsSync(settingsPath)) { - const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - if (settings.pythonPath && existsSync(settings.pythonPath)) { - pythonCmd = settings.pythonPath; - console.log('[project-context] Using Python from settings:', pythonCmd); - } - } - } catch (err) { - console.warn('[project-context] Could not read Python from settings:', err); - } + // Get configured Python path (venv if ready, otherwise bundled/system) + // This ensures we use the venv Python which has dependencies installed + const pythonCmd = getConfiguredPythonPath(); + console.log('[project-context] Using Python:', pythonCmd); const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonCmd); diff --git a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts index 965696a7..b106ea37 100644 --- a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts @@ -23,7 +23,8 @@ import { isKuzuAvailable, } from '../memory-service'; import { validateOpenAIApiKey } from '../api-validation-service'; -import { findPythonCommand, parsePythonCommand } from '../python-detector'; +import { parsePythonCommand } from '../python-detector'; +import { getConfiguredPythonPath } from '../python-env-manager'; /** * Ollama Service Status @@ -104,10 +105,9 @@ async function executeOllamaDetector( command: string, baseUrl?: string ): Promise<{ success: boolean; data?: unknown; error?: string }> { - const pythonCmd = findPythonCommand(); - if (!pythonCmd) { - return { success: false, error: 'Python not found' }; - } + // Use configured Python path (venv if ready, otherwise bundled/system) + // Note: ollama_model_detector.py doesn't require dotenv, but using venv is safer + const pythonCmd = getConfiguredPythonPath(); // Find the ollama_model_detector.py script const possiblePaths = [ @@ -543,10 +543,8 @@ export function registerMemoryHandlers(): void { baseUrl?: string ): Promise> => { try { - const pythonCmd = findPythonCommand(); - if (!pythonCmd) { - return { success: false, error: 'Python not found' }; - } + // Use configured Python path (venv if ready, otherwise bundled/system) + const pythonCmd = getConfiguredPythonPath(); // Find the ollama_model_detector.py script const possiblePaths = [ diff --git a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts index 4b937a8b..f159983b 100644 --- a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts @@ -5,11 +5,11 @@ import path from 'path'; import { existsSync, readdirSync, statSync, readFileSync } from 'fs'; import { execSync, spawn, spawnSync } from 'child_process'; import { projectStore } from '../../project-store'; -import { PythonEnvManager } from '../../python-env-manager'; +import { getConfiguredPythonPath, PythonEnvManager } from '../../python-env-manager'; import { getEffectiveSourcePath } from '../../auto-claude-updater'; import { getProfileEnv } from '../../rate-limit-detector'; import { findTaskAndProject } from './shared'; -import { findPythonCommand, parsePythonCommand } from '../../python-detector'; +import { parsePythonCommand } from '../../python-detector'; /** * Read the stored base branch from task_metadata.json @@ -354,7 +354,8 @@ export function registerWorktreeHandlers( debug('Using stored base branch:', taskBaseBranch); } - const pythonPath = pythonEnvManager.getPythonPath() || findPythonCommand() || 'python'; + // Use configured Python path (venv if ready, otherwise bundled/system) + const pythonPath = getConfiguredPythonPath(); debug('Running command:', pythonPath, args.join(' ')); debug('Working directory:', sourcePath); @@ -711,7 +712,8 @@ export function registerWorktreeHandlers( console.warn('[IPC] Using stored base branch for preview:', taskBaseBranch); } - const pythonPath = pythonEnvManager.getPythonPath() || findPythonCommand() || 'python'; + // Use configured Python path (venv if ready, otherwise bundled/system) + const pythonPath = getConfiguredPythonPath(); console.warn('[IPC] Running merge preview:', pythonPath, args.join(' ')); // Get profile environment for consistency diff --git a/apps/frontend/src/main/memory-service.ts b/apps/frontend/src/main/memory-service.ts index 006284c8..0f7a7064 100644 --- a/apps/frontend/src/main/memory-service.ts +++ b/apps/frontend/src/main/memory-service.ts @@ -9,10 +9,10 @@ import { spawn } from 'child_process'; import * as path from 'path'; -import * as os from 'os'; import * as fs from 'fs'; import { app } from 'electron'; import { findPythonCommand, parsePythonCommand } from './python-detector'; +import { getConfiguredPythonPath } from './python-env-manager'; import { getMemoriesDir } from './config-paths'; import type { MemoryEpisode } from '../shared/types'; @@ -120,10 +120,7 @@ async function executeQuery( args: string[], timeout: number = 10000 ): Promise { - const pythonCmd = findPythonCommand(); - if (!pythonCmd) { - return { success: false, error: 'Python not found' }; - } + const pythonCmd = getConfiguredPythonPath(); const scriptPath = getQueryScriptPath(); if (!scriptPath) { @@ -186,10 +183,7 @@ async function executeSemanticQuery( embedderConfig: EmbedderConfig, timeout: number = 30000 // Longer timeout for embedding operations ): Promise { - const pythonCmd = findPythonCommand(); - if (!pythonCmd) { - return { success: false, error: 'Python not found' }; - } + const pythonCmd = getConfiguredPythonPath(); const scriptPath = getQueryScriptPath(); if (!scriptPath) { @@ -591,7 +585,7 @@ export async function closeMemoryService(): Promise { * Check if Python with LadybugDB is available */ export function isKuzuAvailable(): boolean { - // Check if Python is available + // Check if Python is available (findPythonCommand can return null) const pythonCmd = findPythonCommand(); if (!pythonCmd) { return false; @@ -619,7 +613,7 @@ export function getMemoryServiceStatus(dbPath?: string): MemoryServiceStatus { ? fs.readdirSync(basePath).filter((name) => !name.startsWith('.')) : []; - // Check if Python and script are available + // Check if Python and script are available (findPythonCommand can return null) const pythonAvailable = findPythonCommand() !== null; const scriptAvailable = getQueryScriptPath() !== null; diff --git a/apps/frontend/src/main/python-env-manager.ts b/apps/frontend/src/main/python-env-manager.ts index 726a3b0a..eb1eabed 100644 --- a/apps/frontend/src/main/python-env-manager.ts +++ b/apps/frontend/src/main/python-env-manager.ts @@ -432,3 +432,28 @@ export class PythonEnvManager extends EventEmitter { // Singleton instance export const pythonEnvManager = new PythonEnvManager(); + +/** + * Get the configured venv Python path if ready, otherwise fall back to system Python. + * This should be used by ALL services that need to spawn Python processes. + * + * Priority: + * 1. If venv is ready -> return venv Python (has all dependencies installed) + * 2. Fall back to findPythonCommand() -> bundled or system Python + * + * Note: For scripts that require dependencies (dotenv, claude-agent-sdk, etc.), + * the venv Python MUST be used. Only use this fallback for scripts that + * don't have external dependencies (like ollama_model_detector.py). + */ +export function getConfiguredPythonPath(): string { + // If venv is ready, always prefer it (has dependencies installed) + if (pythonEnvManager.isEnvReady()) { + const venvPath = pythonEnvManager.getPythonPath(); + if (venvPath) { + return venvPath; + } + } + + // Fall back to system/bundled Python + return findPythonCommand() || 'python'; +} diff --git a/apps/frontend/src/main/title-generator.ts b/apps/frontend/src/main/title-generator.ts index d4e1f712..2963b5f4 100644 --- a/apps/frontend/src/main/title-generator.ts +++ b/apps/frontend/src/main/title-generator.ts @@ -4,7 +4,8 @@ import { spawn } from 'child_process'; import { app } from 'electron'; import { EventEmitter } from 'events'; import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from './rate-limit-detector'; -import { findPythonCommand, parsePythonCommand } from './python-detector'; +import { parsePythonCommand } from './python-detector'; +import { getConfiguredPythonPath } from './python-env-manager'; /** * Debug logging - only logs when DEBUG=true or in development mode @@ -21,8 +22,8 @@ function debug(...args: unknown[]): void { * Service for generating task titles from descriptions using Claude AI */ export class TitleGenerator extends EventEmitter { - // Auto-detect Python command on initialization - private pythonPath: string = findPythonCommand() || 'python'; + // Python path will be configured by pythonEnvManager after venv is ready + private _pythonPath: string | null = null; private autoBuildSourcePath: string = ''; constructor() { @@ -35,13 +36,25 @@ export class TitleGenerator extends EventEmitter { */ configure(pythonPath?: string, autoBuildSourcePath?: string): void { if (pythonPath) { - this.pythonPath = pythonPath; + this._pythonPath = pythonPath; } if (autoBuildSourcePath) { this.autoBuildSourcePath = autoBuildSourcePath; } } + /** + * Get the configured Python path. + * Returns explicitly configured path, or falls back to getConfiguredPythonPath() + * which uses the venv Python if ready. + */ + private get pythonPath(): string { + if (this._pythonPath) { + return this._pythonPath; + } + return getConfiguredPythonPath(); + } + /** * Get the auto-claude source path (detects automatically if not configured) */