diff --git a/.gitignore b/.gitignore index 1c4bbe2ca7..825b68f63b 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,5 @@ OPUS_ANALYSIS_AND_IDEAS.md # Auto Claude generated files .security-key /shared_docs -Agents.md \ No newline at end of file +Agents.md +logs/security/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 55704e2bd9..034c42577c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,91 @@ +## Unreleased - WSL2/WSLg Compatibility + +### ✨ New Platform Support + +- **Full WSL2/WSLg compatibility** - Auto Claude Electron desktop app now runs natively on Windows 11 WSL2 with WSLg (Windows Subsystem for Linux Graphics) + +- Added comprehensive WSL2 setup guide and troubleshooting documentation ([guides/WSL2_SETUP.md](guides/WSL2_SETUP.md)) + +### 🛠️ Technical Improvements + +**Lazy Initialization Pattern:** +- Implemented lazy initialization for all Electron app access to handle delayed initialization on WSL2 +- Replaced direct `@electron-toolkit/utils` usage with safe platform detection +- Added Proxy-based lazy initialization for singleton services (ProjectStore, ChangelogService, TitleGenerator) + +**Sentry Integration:** +- Fixed Sentry initialization timing with safe version detection and package.json fallback +- Sentry now initializes before app.whenReady() while remaining WSL2-compatible +- Temporarily disabled Sentry environment propagation to subprocesses for WSL2 compatibility + +**electron-log:** +- Disabled preload script to avoid WSL2 path resolution issues +- Main logging functionality preserved and working correctly + +**electron-updater:** +- Implemented lazy loading pattern with module-level variable +- Added null checks to all autoUpdater access functions +- Changed `getCurrentVersion()` to use `app.getVersion()` instead of autoUpdater for better reliability + +**Settings Management:** +- Fixed settings path resolution by using `getSettingsPath()` function calls instead of module-level constants + +**Build Configuration:** +- Ensured CJS format with `.js` extensions for main and preload bundles +- Fixed preload script path from `.mjs` to `.js` to match build output +- Externalized Sentry packages to avoid bundling issues + +**Backend Path Detection:** +- Added safe `app.getAppPath()` access with try-catch for WSL2 compatibility +- Multiple fallback paths for backend detection + +### 🐛 Bug Fixes + +- Fixed "app.getVersion() is not a function" errors on WSL2 startup +- Fixed "autoUpdater is not defined" errors when accessing update functions +- Fixed "settingsPath is not defined" error in setup wizard +- Fixed preload script "index.mjs" not found error +- Fixed Sentry initialization timing error: "SDK should be initialized before app ready event" +- Fixed electron-log preload script path resolution failures +- Fixed module-level constant initialization issues on WSL2 +- Fixed singleton service initialization timing on WSL2 + +### 📚 Documentation + +- Added [WSL2_SETUP.md](guides/WSL2_SETUP.md) with: + - Prerequisites and installation steps + - Step-by-step WSLg verification guide + - Comprehensive troubleshooting section + - Technical explanations of all fixes + - Architecture patterns for WSL2 compatibility + - Testing checklist for WSL2 development + +### 🔧 Files Changed + +**Main Process:** +- `apps/frontend/src/main/index.ts` - Lazy platform detection, safe app initialization +- `apps/frontend/src/main/sentry.ts` - Safe version detection with fallbacks +- `apps/frontend/src/main/app-logger.ts` - Disabled preload for WSL2 +- `apps/frontend/src/main/app-updater.ts` - Lazy loading with null checks +- `apps/frontend/src/main/project-store.ts` - Proxy-based lazy initialization +- `apps/frontend/src/main/changelog/changelog-service.ts` - Proxy-based lazy initialization, safe path detection +- `apps/frontend/src/main/title-generator.ts` - Proxy-based lazy initialization, safe path detection +- `apps/frontend/src/main/env-utils.ts` - Disabled Sentry subprocess env for WSL2 + +**IPC Handlers:** +- `apps/frontend/src/main/ipc-handlers/settings-handlers.ts` - Function-based path resolution +- `apps/frontend/src/main/ipc-handlers/context/utils.ts` - WSL2-safe path handling +- `apps/frontend/src/main/ipc-handlers/project-handlers.ts` - WSL2-safe initialization + +**Build Configuration:** +- `apps/frontend/electron.vite.config.ts` - CJS format, external dependencies +- `apps/frontend/package.json` - Updated for WSL2 compatibility + +**Other:** +- `.gitignore` - Added logs/security/ exclusion + +--- + ## 2.7.4 - Terminal & Workflow Enhancements ### ✨ New Features diff --git a/apps/backend/agents/tools_pkg/tools/qa.py b/apps/backend/agents/tools_pkg/tools/qa.py index d0448c55c8..1f1d5e4f1c 100644 --- a/apps/backend/agents/tools_pkg/tools/qa.py +++ b/apps/backend/agents/tools_pkg/tools/qa.py @@ -12,7 +12,6 @@ from typing import Any from core.file_utils import write_json_atomic -from spec.validate_pkg.auto_fix import auto_fix_plan try: from claude_agent_sdk import tool @@ -159,6 +158,9 @@ async def update_qa_status(args: dict[str, Any]) -> dict[str, Any]: except json.JSONDecodeError as e: # Attempt to auto-fix the plan and retry + # Lazy import to avoid circular dependency + from spec.validate_pkg.auto_fix import auto_fix_plan + if auto_fix_plan(spec_dir): # Retry after fix try: diff --git a/apps/backend/agents/tools_pkg/tools/subtask.py b/apps/backend/agents/tools_pkg/tools/subtask.py index ac79be9864..81ffb02f25 100644 --- a/apps/backend/agents/tools_pkg/tools/subtask.py +++ b/apps/backend/agents/tools_pkg/tools/subtask.py @@ -12,7 +12,6 @@ from typing import Any from core.file_utils import write_json_atomic -from spec.validate_pkg.auto_fix import auto_fix_plan try: from claude_agent_sdk import tool @@ -142,6 +141,9 @@ async def update_subtask_status(args: dict[str, Any]) -> dict[str, Any]: except json.JSONDecodeError as e: # Attempt to auto-fix the plan and retry + # Lazy import to avoid circular dependency + from spec.validate_pkg.auto_fix import auto_fix_plan + if auto_fix_plan(spec_dir): # Retry after fix try: diff --git a/apps/frontend/electron.vite.config.ts b/apps/frontend/electron.vite.config.ts index 6ceaa51fd5..9cbbc77de3 100644 --- a/apps/frontend/electron.vite.config.ts +++ b/apps/frontend/electron.vite.config.ts @@ -20,31 +20,24 @@ const sentryDefines = { export default defineConfig({ main: { define: sentryDefines, - plugins: [externalizeDepsPlugin({ - // Bundle these packages into the main process (they won't be in node_modules in packaged app) - exclude: [ - 'uuid', - 'chokidar', - 'kuzu', - 'electron-updater', - '@electron-toolkit/utils', - // Sentry and its transitive dependencies (opentelemetry -> debug -> ms) - '@sentry/electron', - '@sentry/core', - '@sentry/node', - '@sentry/utils', - '@opentelemetry/instrumentation', - 'debug', - 'ms' - ] - })], + plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/main/index.ts') }, - // Only node-pty needs to be external (native module rebuilt by electron-builder) - external: ['@lydell/node-pty'] + output: { + format: 'cjs', + entryFileNames: '[name].js' + }, + // External modules that should not be bundled + external: [ + '@lydell/node-pty', // Native module + '@sentry/electron', // Sentry main (causes WSL2 issues when bundled) + '@sentry/core', + '@sentry/node', + '@electron-toolkit/utils' // Electron utilities (access app before ready) + ] } } }, @@ -54,6 +47,10 @@ export default defineConfig({ rollupOptions: { input: { index: resolve(__dirname, 'src/preload/index.ts') + }, + output: { + format: 'cjs', + entryFileNames: '[name].js' } } } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 745f92e0e0..7369cd727f 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -18,10 +18,13 @@ "node": ">=24.0.0", "npm": ">=10.0.0" }, + "//": "NOTE: Use 'dev:wsl2' scripts for WSL2 environments. The --no-sandbox flag disables Chromium's sandbox security feature and should ONLY be used in WSL2. Regular 'dev' scripts are safe for macOS/Windows/Linux. Production builds never use --no-sandbox.", "scripts": { "postinstall": "node scripts/postinstall.cjs", "dev": "electron-vite dev", "dev:debug": "cross-env DEBUG=true electron-vite dev", + "dev:wsl2": "node scripts/check-wsl2.cjs && electron-vite dev -- --no-sandbox", + "dev:debug:wsl2": "node scripts/check-wsl2.cjs && cross-env DEBUG=true electron-vite dev -- --no-sandbox", "dev:mcp": "electron-vite dev -- --remote-debugging-port=9222", "build": "electron-vite build", "start": "electron .", diff --git a/apps/frontend/scripts/check-wsl2.cjs b/apps/frontend/scripts/check-wsl2.cjs new file mode 100644 index 0000000000..d0269c3679 --- /dev/null +++ b/apps/frontend/scripts/check-wsl2.cjs @@ -0,0 +1,78 @@ +/** + * WSL2 Detection Script + * + * Validates that the development environment is running in WSL2 before allowing + * --no-sandbox flag usage. This prevents accidental use of disabled Chromium + * sandbox outside of WSL2 environments where it's required. + * + * Usage: node scripts/check-wsl2.cjs + * Exit codes: + * 0 - Running in WSL2 (safe to use --no-sandbox) + * 1 - Not running in WSL2 (should not use --no-sandbox) + */ + +const fs = require('fs'); + +/** + * Check if running in WSL2 environment + * @returns {boolean} true if in WSL2, false otherwise + */ +function isWSL2() { + // Method 1: Check WSL_DISTRO_NAME environment variable + if (process.env.WSL_DISTRO_NAME) { + return true; + } + + // Method 2: Check /proc/version for WSL2 signature (Linux only) + if (process.platform === 'linux') { + try { + const versionInfo = fs.readFileSync('/proc/version', 'utf8').toLowerCase(); + // WSL2 typically contains 'microsoft' and 'wsl2' in kernel version + if (versionInfo.includes('microsoft') && versionInfo.includes('wsl2')) { + return true; + } + // Older WSL2 versions might only have 'microsoft' + if (versionInfo.includes('microsoft')) { + return true; + } + } catch { + // /proc/version doesn't exist or can't be read + return false; + } + } + + // Method 3: Check for WSL interop (wsl.exe in PATH) + if (process.platform === 'linux') { + try { + const { execSync } = require('child_process'); + execSync('which wsl.exe', { stdio: 'ignore' }); + return true; + } catch { + // wsl.exe not found + } + } + + return false; +} + +// Main execution +const isWsl2 = isWSL2(); + +if (isWsl2) { + console.log('✓ WSL2 environment detected - --no-sandbox flag is safe to use'); + process.exit(0); +} else { + console.error('✗ Not running in WSL2!'); + console.error(''); + console.error('The dev:wsl2 script is designed for WSL2 environments only.'); + console.error('It disables Chromium sandbox (--no-sandbox) which is a security risk outside WSL2.'); + console.error(''); + console.error('Please use one of these alternatives:'); + console.error(' • npm run dev - Development mode with Chromium sandbox enabled'); + console.error(' • npm run dev:debug - Debug mode with Chromium sandbox enabled'); + console.error(''); + console.error('If you are in WSL2 but seeing this error, check:'); + console.error(' 1. WSL_DISTRO_NAME environment variable is set'); + console.error(' 2. Running WSL2 (not WSL1): wsl.exe --list --verbose'); + process.exit(1); +} diff --git a/apps/frontend/src/main/app-logger.ts b/apps/frontend/src/main/app-logger.ts index 07429c1953..452248a2ff 100644 --- a/apps/frontend/src/main/app-logger.ts +++ b/apps/frontend/src/main/app-logger.ts @@ -22,7 +22,7 @@ import os from 'os'; // Configure electron-log (wrapped in try-catch for re-import scenarios in tests) try { - log.initialize(); + log.initialize({ preload: false }); // Disable preload to avoid WSL2 path issues } catch { // Already initialized, ignore } @@ -42,15 +42,19 @@ log.transports.console.format = '[{h}:{i}:{s}] [{level}] {text}'; // Determine if this is a beta version function isBetaVersion(): boolean { try { + // WSL2 compatibility: app may not be ready yet at module load time + if (!app || typeof app.getVersion !== 'function') { + return false; + } const version = app.getVersion(); return version.includes('-beta') || version.includes('-alpha') || version.includes('-rc'); } catch (error) { - log.warn('Failed to detect beta version:', error); + // Silently fail if app is not ready yet - this is called at module load time return false; } } -// Enhanced logging for beta versions +// Enhanced logging for beta versions (lazy check - safe for WSL2) if (isBetaVersion()) { log.transports.file.level = 'debug'; log.info('Beta version detected - enhanced logging enabled'); diff --git a/apps/frontend/src/main/app-updater.ts b/apps/frontend/src/main/app-updater.ts index ffab241ed0..5c9df281a8 100644 --- a/apps/frontend/src/main/app-updater.ts +++ b/apps/frontend/src/main/app-updater.ts @@ -17,9 +17,9 @@ * - APP_UPDATE_ERROR: Error during update process */ -import { autoUpdater } from 'electron-updater'; import { app, net } from 'electron'; import type { BrowserWindow } from 'electron'; +import type { AppUpdater, UpdateInfo, ProgressInfo, UpdateDownloadedEvent } from 'electron-updater'; import { IPC_CHANNELS } from '../shared/constants'; import type { AppUpdateInfo } from '../shared/types'; import { compareVersions } from './updater/version-manager'; @@ -31,10 +31,6 @@ const GITHUB_REPO = 'Auto-Claude'; // Debug mode - DEBUG_UPDATER=true or development mode const DEBUG_UPDATER = process.env.DEBUG_UPDATER === 'true' || process.env.NODE_ENV === 'development'; -// Configure electron-updater -autoUpdater.autoDownload = true; // Automatically download updates when available -autoUpdater.autoInstallOnAppQuit = true; // Automatically install on app quit - // Update channels: 'latest' for stable, 'beta' for pre-release type UpdateChannel = 'latest' | 'beta'; @@ -48,7 +44,8 @@ let periodicCheckIntervalId: ReturnType | null = null; * * @param channel - The update channel to use */ -export function setUpdateChannel(channel: UpdateChannel): void { +export function setUpdateChannel(channel: UpdateChannel, updater?: any): void { + const autoUpdater = updater || require('electron-updater').autoUpdater; autoUpdater.channel = channel; // Clear any downloaded update info when channel changes to prevent showing // an Install button for an update from a different channel @@ -56,21 +53,14 @@ export function setUpdateChannel(channel: UpdateChannel): void { console.warn(`[app-updater] Update channel set to: ${channel}`); } -// Enable more verbose logging in debug mode -if (DEBUG_UPDATER) { - autoUpdater.logger = { - info: (msg: string) => console.warn('[app-updater:debug]', msg), - warn: (msg: string) => console.warn('[app-updater:debug]', msg), - error: (msg: string) => console.error('[app-updater:debug]', msg), - debug: (msg: string) => console.warn('[app-updater:debug]', msg) - }; -} - let mainWindow: BrowserWindow | null = null; // Track downloaded update state so it persists across Settings page navigations let downloadedUpdateInfo: AppUpdateInfo | null = null; +// Lazy-loaded autoUpdater instance (WSL2 compatibility) +let autoUpdater: AppUpdater | null = null; + /** * Initialize the app updater system * @@ -81,11 +71,36 @@ let downloadedUpdateInfo: AppUpdateInfo | null = null; * @param betaUpdates - Whether to receive beta/pre-release updates */ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false): void { + // Lazy-load electron-updater to avoid initialization before app is ready (WSL2 compatibility) + if (!autoUpdater) { + const updaterModule = require('electron-updater'); + autoUpdater = updaterModule.autoUpdater; + } + + // TypeScript guard: autoUpdater is guaranteed to be non-null after initialization above + if (!autoUpdater) { + throw new Error('[app-updater] Failed to initialize autoUpdater'); + } + + // Configure electron-updater + autoUpdater.autoDownload = true; // Automatically download updates when available + autoUpdater.autoInstallOnAppQuit = true; // Automatically install on app quit + + // Enable more verbose logging in debug mode + if (DEBUG_UPDATER) { + autoUpdater.logger = { + info: (msg: string) => console.warn('[app-updater:debug]', msg), + warn: (msg: string) => console.warn('[app-updater:debug]', msg), + error: (msg: string) => console.error('[app-updater:debug]', msg), + debug: (msg: string) => console.warn('[app-updater:debug]', msg) + }; + } + mainWindow = window; // Set update channel based on user preference const channel = betaUpdates ? 'beta' : 'latest'; - setUpdateChannel(channel); + setUpdateChannel(channel, autoUpdater); // Log updater configuration console.warn('[app-updater] ========================================'); @@ -102,7 +117,7 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) // ============================================ // Update available - new version found - autoUpdater.on('update-available', (info) => { + autoUpdater.on('update-available', (info: UpdateInfo) => { console.warn('[app-updater] Update available:', info.version); if (mainWindow) { mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_AVAILABLE, { @@ -114,7 +129,7 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) }); // Update downloaded - ready to install - autoUpdater.on('update-downloaded', (info) => { + autoUpdater.on('update-downloaded', (info: UpdateDownloadedEvent) => { console.warn('[app-updater] Update downloaded:', info.version); // Store downloaded update info so it persists across Settings page navigations // releaseNotes can be string | ReleaseNoteInfo[] | null | undefined, only use if string @@ -133,7 +148,7 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) }); // Download progress - autoUpdater.on('download-progress', (progress) => { + autoUpdater.on('download-progress', (progress: ProgressInfo) => { console.warn(`[app-updater] Download progress: ${progress.percent.toFixed(2)}%`); if (mainWindow) { mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_PROGRESS, { @@ -146,7 +161,7 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) }); // Error handling - autoUpdater.on('error', (error) => { + autoUpdater.on('error', (error: Error) => { console.error('[app-updater] Update error:', error); if (mainWindow) { mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_ERROR, { @@ -157,7 +172,7 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) }); // No update available - autoUpdater.on('update-not-available', (info) => { + autoUpdater.on('update-not-available', (info: UpdateInfo) => { console.warn('[app-updater] No updates available - you are on the latest version'); console.warn('[app-updater] Current version:', info.version); if (DEBUG_UPDATER) { @@ -180,7 +195,11 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) setTimeout(() => { console.warn('[app-updater] Performing initial update check'); - autoUpdater.checkForUpdates().catch((error) => { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized for initial check'); + return; + } + autoUpdater.checkForUpdates().catch((error: Error) => { console.error('[app-updater] ❌ Initial update check failed:', error.message); if (DEBUG_UPDATER) { console.error('[app-updater:debug] Full error:', error); @@ -194,7 +213,11 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) periodicCheckIntervalId = setInterval(() => { console.warn('[app-updater] Performing periodic update check'); - autoUpdater.checkForUpdates().catch((error) => { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized for periodic check'); + return; + } + autoUpdater.checkForUpdates().catch((error: Error) => { console.error('[app-updater] ❌ Periodic update check failed:', error.message); if (DEBUG_UPDATER) { console.error('[app-updater:debug] Full error:', error); @@ -210,6 +233,10 @@ export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false) * Called from IPC handler when user requests manual check */ export async function checkForUpdates(): Promise { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized'); + return null; + } try { console.warn('[app-updater] Manual update check requested'); const result = await autoUpdater.checkForUpdates(); @@ -248,6 +275,10 @@ export async function checkForUpdates(): Promise { * Called from IPC handler when user requests manual download */ export async function downloadUpdate(): Promise { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized'); + throw new Error('autoUpdater not initialized'); + } try { console.warn('[app-updater] Manual update download requested'); await autoUpdater.downloadUpdate(); @@ -262,15 +293,20 @@ export async function downloadUpdate(): Promise { * Called from IPC handler when user confirms installation */ export function quitAndInstall(): void { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized'); + return; + } console.warn('[app-updater] Quitting and installing update'); autoUpdater.quitAndInstall(false, true); } /** * Get current app version + * WSL2 compatibility: Use app.getVersion() instead of autoUpdater since autoUpdater is lazy-loaded */ export function getCurrentVersion(): string { - return autoUpdater.currentVersion.version; + return app.getVersion(); } /** @@ -447,6 +483,10 @@ export async function setUpdateChannelWithDowngradeCheck( channel: UpdateChannel, triggerDowngradeCheck = false ): Promise { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized'); + return null; + } autoUpdater.channel = channel; // Clear any downloaded update info when channel changes to prevent showing // an Install button for an update from a different channel @@ -473,6 +513,10 @@ export async function setUpdateChannelWithDowngradeCheck( * Uses electron-updater with allowDowngrade enabled to download older stable versions */ export async function downloadStableVersion(): Promise { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized'); + throw new Error('autoUpdater not initialized'); + } // Switch to stable channel autoUpdater.channel = 'latest'; // Enable downgrade to allow downloading older versions (e.g., stable when on beta) diff --git a/apps/frontend/src/main/changelog/changelog-service.ts b/apps/frontend/src/main/changelog/changelog-service.ts index 4d3bd42ef0..11932c14be 100644 --- a/apps/frontend/src/main/changelog/changelog-service.ts +++ b/apps/frontend/src/main/changelog/changelog-service.ts @@ -68,8 +68,6 @@ export class ChangelogService extends EventEmitter { // Check process.env first if ( - process.env.DEBUG === 'true' || - process.env.DEBUG === '1' || process.env.DEBUG === 'true' || process.env.DEBUG === '1' ) { @@ -121,11 +119,21 @@ export class ChangelogService extends EventEmitter { return this.autoBuildSourcePath; } + const appPathSegment: string[] = []; + // Add app path if app is ready (WSL2 compatibility) + try { + if (app && app.getAppPath) { + appPathSegment.push(path.resolve(app.getAppPath(), '..', 'backend')); + } + } catch (e) { + // App not ready yet, continue without app path + } + const possiblePaths = [ // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), - path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend') + ...appPathSegment, + path.resolve(process.cwd(), 'apps', 'backend'), ]; for (const p of possiblePaths) { @@ -522,5 +530,15 @@ export class ChangelogService extends EventEmitter { } } -// Export singleton instance -export const changelogService = new ChangelogService(); +// Lazy-initialized singleton instance (WSL2 compatible) +let _changelogService: ChangelogService | null = null; + +export const changelogService = new Proxy({} as ChangelogService, { + get(target, prop) { + if (!_changelogService) { + _changelogService = new ChangelogService(); + } + const value = _changelogService[prop as keyof ChangelogService]; + return typeof value === 'function' ? value.bind(_changelogService) : value; + } +}); diff --git a/apps/frontend/src/main/env-utils.ts b/apps/frontend/src/main/env-utils.ts index d30e293a3e..c192db9fed 100644 --- a/apps/frontend/src/main/env-utils.ts +++ b/apps/frontend/src/main/env-utils.ts @@ -16,7 +16,7 @@ import { promises as fsPromises } from 'fs'; import { execFileSync, execFile } from 'child_process'; import { promisify } from 'util'; import { getSentryEnvForSubprocess } from './sentry'; -import { isWindows, isUnix, getPathDelimiter } from './platform'; +import { isWindows, isUnix, getPathDelimiter, isWSL2 } from './platform'; const execFileAsync = promisify(execFile); @@ -247,8 +247,11 @@ export function getAugmentedEnv(additionalPaths?: string[]): Record { + // Set app name (in case pre-init failed on WSL2) + try { + app.setName('Auto Claude'); + if (is.mac) { + app.name = 'Auto Claude'; + } + } catch (e) { + // Ignore - already set + } + // Set app user model id for Windows - electronApp.setAppUserModelId('com.autoclaude.ui'); + app.setAppUserModelId('com.autoclaude.ui'); // Clear cache on Windows to prevent permission errors from stale cache - if (process.platform === 'win32') { + if (is.windows) { session.defaultSession.clearCache() .then(() => console.log('[main] Cleared cache on startup')) .catch((err) => console.warn('[main] Failed to clear cache:', err)); @@ -264,7 +290,7 @@ app.whenReady().then(() => { cleanupStaleUpdateMetadata(); // Set dock icon on macOS - if (process.platform === 'darwin') { + if (is.mac) { const iconPath = getIconPath(); try { const icon = nativeImage.createFromPath(iconPath); @@ -279,7 +305,22 @@ app.whenReady().then(() => { // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. app.on('browser-window-created', (_, window) => { - optimizer.watchWindowShortcuts(window); + // F12 toggles DevTools in development + if (is.dev) { + window.webContents.on('before-input-event', (_, input) => { + if (input.type === 'keyDown' && input.key === 'F12') { + window.webContents.toggleDevTools(); + } + }); + } + // Disable Ctrl/Cmd+R refresh in production + if (!is.dev) { + window.webContents.on('before-input-event', (event, input) => { + if (input.type === 'keyDown' && input.key === 'r' && (input.control || input.meta)) { + event.preventDefault(); + } + }); + } }); // Initialize agent manager @@ -448,7 +489,7 @@ app.whenReady().then(() => { // Quit when all windows are closed (except on macOS) app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { + if (!is.mac) { app.quit(); } }); diff --git a/apps/frontend/src/main/ipc-handlers/context/utils.ts b/apps/frontend/src/main/ipc-handlers/context/utils.ts index 6611e99740..35a2393d4d 100644 --- a/apps/frontend/src/main/ipc-handlers/context/utils.ts +++ b/apps/frontend/src/main/ipc-handlers/context/utils.ts @@ -1,6 +1,7 @@ import { app } from 'electron'; import path from 'path'; import { existsSync, readFileSync } from 'fs'; +import { getSettingsPath } from '../../settings-utils'; export interface EnvironmentVars { [key: string]: string; @@ -11,12 +12,11 @@ export interface GlobalSettings { globalOpenAIApiKey?: string; } -const settingsPath = path.join(app.getPath('userData'), 'settings.json'); - /** * Get the auto-build source path from settings */ export function getAutoBuildSourcePath(): string | null { + const settingsPath = getSettingsPath(); if (existsSync(settingsPath)) { try { const content = readFileSync(settingsPath, 'utf-8'); @@ -85,6 +85,7 @@ export function loadProjectEnvVars(projectPath: string, autoBuildPath?: string): * Load global settings from user data directory */ export function loadGlobalSettings(): GlobalSettings { + const settingsPath = getSettingsPath(); if (!existsSync(settingsPath)) { return {}; } diff --git a/apps/frontend/src/main/ipc-handlers/project-handlers.ts b/apps/frontend/src/main/ipc-handlers/project-handlers.ts index d752be8d7f..e047a3a39c 100644 --- a/apps/frontend/src/main/ipc-handlers/project-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/project-handlers.ts @@ -2,7 +2,6 @@ import { ipcMain, app } from 'electron'; import { existsSync, readFileSync } from 'fs'; import path from 'path'; import { execFileSync } from 'child_process'; -import { is } from '@electron-toolkit/utils'; import { IPC_CHANNELS } from '../../shared/constants'; import type { Project, @@ -142,8 +141,6 @@ function detectMainBranch(projectPath: string): string | null { return branches[0] || null; } -const settingsPath = path.join(app.getPath('userData'), 'settings.json'); - /** * Configure all Python-dependent services with the managed Python path */ diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 968c44def7..6bc62f0fc9 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -3,7 +3,16 @@ import { existsSync, writeFileSync, mkdirSync, statSync, readFileSync } from 'fs import { execFileSync } from 'node:child_process'; import path from 'path'; import { fileURLToPath } from 'url'; -import { is } from '@electron-toolkit/utils'; +import { isDev, isMacOS, isWindows, isLinux, getPlatformDescription } from '../platform'; + +// Platform detection wrapper for backward compatibility +// Uses centralized platform module (apps/frontend/src/main/platform/) +const is = { + get dev() { return isDev(); }, + get mac() { return isMacOS(); }, + get windows() { return isWindows(); }, + get linux() { return isLinux(); } +}; // ESM-compatible __dirname const __filename = fileURLToPath(import.meta.url); @@ -22,8 +31,6 @@ import { getSettingsPath, readSettingsFile } from '../settings-utils'; import { configureTools, getToolPath, getToolInfo, isPathFromWrongPlatform, preWarmToolCache } from '../cli-tool-manager'; import { parseEnvFile } from './utils'; -const settingsPath = getSettingsPath(); - /** * Auto-detect the auto-claude source path relative to the app location. * Works across platforms (macOS, Windows, Linux) in both dev and production modes. @@ -61,7 +68,7 @@ const detectAutoBuildSourcePath = (): string | null => { const debug = process.env.DEBUG === '1' || process.env.DEBUG === 'true'; if (debug) { - console.warn('[detectAutoBuildSourcePath] Platform:', process.platform); + console.warn('[detectAutoBuildSourcePath] Platform:', getPlatformDescription()); console.warn('[detectAutoBuildSourcePath] Is dev:', is.dev); console.warn('[detectAutoBuildSourcePath] __dirname:', __dirname); console.warn('[detectAutoBuildSourcePath] app.getAppPath():', app.getAppPath()); @@ -161,7 +168,7 @@ export function registerSettingsHandlers( // Persist migration changes if (needsSave) { try { - writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2)); } catch (error) { console.error('[SETTINGS_GET] Failed to persist migration:', error); // Continue anyway - settings will be migrated in-memory for this session @@ -202,7 +209,7 @@ export function registerSettingsHandlers( } } - writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2)); + writeFileSync(getSettingsPath(), JSON.stringify(newSettings, null, 2)); // Apply Python path if changed if (settings.pythonPath || settings.autoBuildPath) { @@ -470,12 +477,10 @@ export function registerSettingsHandlers( }; } - const platform = process.platform; - - if (platform === 'darwin') { + if (is.mac) { // macOS: Use execFileSync with argument array to prevent injection execFileSync('open', ['-a', 'Terminal', resolvedPath], { stdio: 'ignore' }); - } else if (platform === 'win32') { + } else if (is.windows) { // Windows: Use cmd.exe directly with argument array // /C tells cmd to execute the command and terminate // /K keeps the window open after executing cd diff --git a/apps/frontend/src/main/platform/index.ts b/apps/frontend/src/main/platform/index.ts index ea5c14198c..4584a24860 100644 --- a/apps/frontend/src/main/platform/index.ts +++ b/apps/frontend/src/main/platform/index.ts @@ -13,8 +13,8 @@ import * as os from 'os'; import * as path from 'path'; -import { existsSync } from 'fs'; -import { spawn, ChildProcess } from 'child_process'; +import { existsSync, readFileSync } from 'fs'; +import { spawn, execSync, ChildProcess } from 'child_process'; import { OS, ShellType, PathConfig, ShellConfig, BinaryDirectories } from './types'; // Re-export from paths.ts for backward compatibility @@ -63,6 +63,64 @@ export function isUnix(): boolean { return !isWindows(); } +/** + * Check if running in development mode (not packaged) + * + * Note: Requires the 'app' module from Electron to be ready. + * Use lazy evaluation when calling from module-level code. + */ +export function isDev(): boolean { + try { + const { app } = require('electron'); + return !app.isPackaged; + } catch { + // If app is not ready, check NODE_ENV + return process.env.NODE_ENV !== 'production'; + } +} + +/** + * Check if running in WSL2 environment + * + * Detects Windows Subsystem for Linux 2 by checking: + * 1. WSL_DISTRO_NAME environment variable + * 2. /proc/version for WSL2 signature (Linux only) + * 3. wsl.exe in PATH (WSL interop) + */ +export function isWSL2(): boolean { + // Method 1: Check WSL_DISTRO_NAME environment variable + if (process.env.WSL_DISTRO_NAME) { + return true; + } + + // Method 2: Check /proc/version for WSL2 signature (Linux only) + if (isLinux()) { + try { + const versionInfo = existsSync('/proc/version') + ? readFileSync('/proc/version', 'utf8').toLowerCase() + : ''; + // WSL2 typically contains 'microsoft' in kernel version + if (versionInfo.includes('microsoft')) { + return true; + } + } catch { + // /proc/version doesn't exist or can't be read + } + } + + // Method 3: Check for WSL interop (wsl.exe in PATH) + if (isLinux()) { + try { + execSync('which wsl.exe', { stdio: 'ignore' }); + return true; + } catch { + // wsl.exe not found + } + } + + return false; +} + /** * Get path configuration for the current platform */ diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index 5f1d8e7276..69edf28946 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -33,10 +33,10 @@ export class ProjectStore { private tasksCache: Map = new Map(); private readonly CACHE_TTL_MS = 3000; // 3 seconds TTL for task cache - constructor() { - // Store in app's userData directory - const userDataPath = app.getPath('userData'); - const storeDir = path.join(userDataPath, 'store'); + constructor(userDataPath?: string) { + // Store in app's userData directory (lazy initialization for WSL2 compatibility) + const actualUserDataPath = userDataPath || app.getPath('userData'); + const storeDir = path.join(actualUserDataPath, 'store'); // Ensure directory exists if (!existsSync(storeDir)) { @@ -849,5 +849,15 @@ export class ProjectStore { } } -// Singleton instance -export const projectStore = new ProjectStore(); +// Lazy-initialized singleton instance (WSL2 compatible) +let _projectStore: ProjectStore | null = null; + +export const projectStore = new Proxy({} as ProjectStore, { + get(target, prop) { + if (!_projectStore) { + _projectStore = new ProjectStore(); + } + const value = _projectStore[prop as keyof ProjectStore]; + return typeof value === 'function' ? value.bind(_projectStore) : value; + } +}); diff --git a/apps/frontend/src/main/sentry.ts b/apps/frontend/src/main/sentry.ts index c15f823e60..0b93011e12 100644 --- a/apps/frontend/src/main/sentry.ts +++ b/apps/frontend/src/main/sentry.ts @@ -120,10 +120,24 @@ export function initSentryMain(): void { console.log('[Sentry] To enable: set SENTRY_DSN environment variable'); } + // Get version safely for WSL2 compatibility + let appVersion: string; + try { + appVersion = app.getVersion(); + } catch (error) { + // WSL2: app may not be ready yet, use fallback + try { + const pkg = require('../../../package.json'); + appVersion = pkg.version || 'unknown'; + } catch { + appVersion = 'dev'; + } + } + Sentry.init({ dsn: cachedDsn, environment: app.isPackaged ? 'production' : 'development', - release: `auto-claude@${app.getVersion()}`, + release: `auto-claude@${appVersion}`, beforeSend(event: Sentry.ErrorEvent) { if (!sentryEnabledState) { diff --git a/apps/frontend/src/main/title-generator.ts b/apps/frontend/src/main/title-generator.ts index ae809ba351..fdf3319254 100644 --- a/apps/frontend/src/main/title-generator.ts +++ b/apps/frontend/src/main/title-generator.ts @@ -65,11 +65,21 @@ export class TitleGenerator extends EventEmitter { return this.autoBuildSourcePath; } + const appPathSegment: string[] = []; + // Add app path if app is ready (WSL2 compatibility) + try { + if (app && app.getAppPath) { + appPathSegment.push(path.resolve(app.getAppPath(), '..', 'backend')); + } + } catch (e) { + // App not ready yet, continue without app path + } + const possiblePaths = [ // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), - path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend') + ...appPathSegment, + path.resolve(process.cwd(), 'apps', 'backend'), ]; for (const p of possiblePaths) { @@ -316,5 +326,15 @@ asyncio.run(generate_title()) } } -// Export singleton instance -export const titleGenerator = new TitleGenerator(); +// Lazy-initialized singleton instance (WSL2 compatible) +let _titleGenerator: TitleGenerator | null = null; + +export const titleGenerator = new Proxy({} as TitleGenerator, { + get(target, prop) { + if (!_titleGenerator) { + _titleGenerator = new TitleGenerator(); + } + const value = _titleGenerator[prop as keyof TitleGenerator]; + return typeof value === 'function' ? value.bind(_titleGenerator) : value; + } +}); diff --git a/guides/WSL2_SETUP.md b/guides/WSL2_SETUP.md new file mode 100644 index 0000000000..9a633da2fb --- /dev/null +++ b/guides/WSL2_SETUP.md @@ -0,0 +1,603 @@ +# WSL2/WSLg Setup Guide for Auto Claude + +This guide documents the setup process and fixes required to run Auto Claude Electron desktop app on Windows 11 WSL2 with WSLg (Windows Subsystem for Linux Graphics). + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [WSLg Installation and Verification](#wslg-installation-and-verification) +3. [Technical Challenges and Solutions](#technical-challenges-and-solutions) +4. [Running the Application](#running-the-application) +5. [Troubleshooting](#troubleshooting) +6. [Architecture Changes](#architecture-changes) + +## Prerequisites + +### System Requirements + +- Windows 11 (WSLg is built-in) +- WSL2 enabled with Ubuntu 20.04 or later +- WSLg (Windows Subsystem for Linux Graphics) v1.0.51 or later + +### Required Software + +```bash +# Update WSL to latest version +wsl --update + +# Install Node.js (v18 or later) +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Install Python 3.12+ +sudo apt-get install -y python3.12 python3.12-venv python3-pip + +# Install build tools +sudo apt-get install -y build-essential +``` + +## WSLg Installation and Verification + +### Step 1: Verify WSLg Installation + +WSLg comes pre-installed with Windows 11. Verify the installation: + +```bash +# Check WSLg version +ls /mnt/wslg/versions.txt && cat /mnt/wslg/versions.txt +``` + +Expected output should show WSLg version (e.g., v1.0.71 or later): +``` +WSLg 1.0.71 +Weston 10.0.1 +Mesa 24.0.5 +``` + +### Step 2: Verify Display Server + +Check if the display server is running: + +```bash +# Verify DISPLAY and WAYLAND_DISPLAY environment variables +echo $DISPLAY +echo $WAYLAND_DISPLAY + +# Should output something like: +# DISPLAY=:0 +# WAYLAND_DISPLAY=wayland-0 +``` + +### Step 3: Test GUI Applications + +Test with a simple GUI app: + +```bash +# Install x11-apps for testing +sudo apt-get install -y x11-apps + +# Test with xclock (should open a clock window) +xclock +``` + +If the clock appears in Windows, WSLg is working correctly. + +### Step 4: Restart WSL (if needed) + +If display variables are not set or GUI apps don't work: + +```powershell +# From PowerShell/CMD in Windows +wsl --shutdown + +# Wait 8-10 seconds, then restart WSL +wsl +``` + +## Technical Challenges and Solutions + +The following sections document the technical challenges encountered when running Electron on WSL2 and the solutions implemented. + +### Challenge 1: Electron App Initialization Timing + +**Problem:** On WSL2, the Electron `app` object is not immediately available at module load time, causing errors like "app.getVersion() is not a function" or "app.getPath() is not defined". + +**Root Cause:** WSLg requires additional initialization time for the graphics subsystem, which delays Electron's app initialization. + +**Solution:** Implement lazy initialization patterns and safe access wrappers. + +#### Files Changed: + +**[apps/frontend/src/main/index.ts](../apps/frontend/src/main/index.ts)** + +```typescript +// Lazy-loaded platform info to avoid @electron-toolkit/utils initialization issues +const is = { + get dev() { return !app.isPackaged; }, + get mac() { return process.platform === 'darwin'; }, + get windows() { return process.platform === 'win32'; }, + get linux() { return process.platform === 'linux'; } +}; + +// Wrap app.setName in try-catch +try { + app.setName('Auto Claude'); + if (process.platform === 'darwin') { + app.name = 'Auto Claude'; + } + + if (process.platform === 'win32') { + app.commandLine.appendSwitch('disable-gpu-shader-disk-cache'); + app.commandLine.appendSwitch('disable-gpu-program-cache'); + } +} catch (e) { + console.warn('[main] App not ready for pre-initialization, will set name after ready'); +} + +// Set app user model id using app.setAppUserModelId instead of electronApp utility +app.whenReady().then(() => { + app.setAppUserModelId('com.autoclaude.ui'); + // ... rest of initialization +}); +``` + +### Challenge 2: Sentry Initialization Timing + +**Problem:** Sentry SDK requires initialization before `app.whenReady()` but accessing `app.getVersion()` fails on WSL2 when app is not ready. + +**Error Message:** +``` +Error: Sentry SDK should be initialized before the Electron app 'ready' event is fired +``` + +**Solution:** Safe version detection with fallback to package.json. + +#### Files Changed: + +**[apps/frontend/src/main/sentry.ts](../apps/frontend/src/main/sentry.ts)** + +```typescript +export function initSentryMain(): void { + // ... DSN validation ... + + // Get version safely for WSL2 compatibility + let appVersion = 'unknown'; + try { + appVersion = app.getVersion(); + } catch (error) { + // WSL2: app may not be ready yet, use fallback + try { + const pkg = require('../../../package.json'); + appVersion = pkg.version || 'unknown'; + } catch { + appVersion = 'dev'; + } + } + + Sentry.init({ + dsn: cachedDsn, + environment: app.isPackaged ? 'production' : 'development', + release: `auto-claude@${appVersion}`, + // ... rest of config + }); +} +``` + +**[apps/frontend/src/main/index.ts](../apps/frontend/src/main/index.ts)** + +```typescript +// Initialize Sentry at module level (before app.whenReady()) +// WSL2 compatible: uses safe version detection with fallback to package.json +initSentryMain(); +``` + +### Challenge 3: electron-log Preload Script Path Issues + +**Problem:** electron-log's preload script path resolution fails on WSL2. + +**Error Message:** +``` +Unable to load preload script: /home/user/projects/Auto-Claude/node_modules/electron-log/src/renderer/electron-log-preload.js +``` + +**Solution:** Disable preload script (main logging functionality still works). + +#### Files Changed: + +**[apps/frontend/src/main/app-logger.ts](../apps/frontend/src/main/app-logger.ts)** + +```typescript +// Configure electron-log (wrapped in try-catch for re-import scenarios in tests) +try { + log.initialize({ preload: false }); // Disable preload to avoid WSL2 path issues +} catch { + // Already initialized, ignore +} +``` + +### Challenge 4: electron-updater Lazy Loading + +**Problem:** `autoUpdater` was accessed before initialization, causing "autoUpdater is not defined" errors. + +**Root Cause:** autoUpdater was imported at module level but needed to be lazy-loaded to avoid WSL2 initialization issues. + +**Solution:** Implement module-level variable with lazy loading and null checks. + +#### Files Changed: + +**[apps/frontend/src/main/app-updater.ts](../apps/frontend/src/main/app-updater.ts)** + +```typescript +// Lazy-loaded autoUpdater instance (WSL2 compatibility) +let autoUpdater: any = null; + +export function initializeAppUpdater(window: BrowserWindow, betaUpdates = false): void { + // Lazy-load electron-updater to avoid initialization before app is ready + if (!autoUpdater) { + const updaterModule = require('electron-updater'); + autoUpdater = updaterModule.autoUpdater; + } + + // Configure electron-updater + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; + + // ... rest of initialization +} + +export async function checkForUpdates(): Promise { + if (!autoUpdater) { + console.error('[app-updater] autoUpdater not initialized'); + return null; + } + // ... rest of function +} + +// Use app.getVersion() instead of autoUpdater.currentVersion.version +export function getCurrentVersion(): string { + return app.getVersion(); +} +``` + +All functions that access autoUpdater now include null checks: +- `checkForUpdates()` +- `downloadUpdate()` +- `quitAndInstall()` +- `setUpdateChannelWithDowngradeCheck()` +- `downloadStableVersion()` + +### Challenge 5: Settings Path Resolution + +**Problem:** Module-level `settingsPath` constant was undefined on WSL2. + +**Solution:** Use function call `getSettingsPath()` instead of module-level constant. + +#### Files Changed: + +**[apps/frontend/src/main/ipc-handlers/settings-handlers.ts](../apps/frontend/src/main/ipc-handlers/settings-handlers.ts)** + +```typescript +// Removed module-level constant +// const settingsPath = getSettingsPath(); // REMOVED + +// Use function call instead +writeFileSync(getSettingsPath(), JSON.stringify(newSettings, null, 2)); +``` + +### Challenge 6: Singleton Initialization + +**Problem:** Singleton instances (ProjectStore, ChangelogService, TitleGenerator) were created at module load time, causing WSL2 initialization errors. + +**Solution:** Implement lazy initialization using JavaScript Proxy pattern. + +#### Files Changed: + +**[apps/frontend/src/main/project-store.ts](../apps/frontend/src/main/project-store.ts)** +**[apps/frontend/src/main/changelog/changelog-service.ts](../apps/frontend/src/main/changelog/changelog-service.ts)** +**[apps/frontend/src/main/title-generator.ts](../apps/frontend/src/main/title-generator.ts)** + +```typescript +// Lazy-initialized singleton instance (WSL2 compatible) +let _projectStore: ProjectStore | null = null; + +export const projectStore = new Proxy({} as ProjectStore, { + get(target, prop) { + if (!_projectStore) { + _projectStore = new ProjectStore(); + } + const value = _projectStore[prop as keyof ProjectStore]; + return typeof value === 'function' ? value.bind(_projectStore) : value; + } +}); +``` + +Same pattern applied to: +- `changelogService` +- `titleGenerator` + +### Challenge 7: Preload Script Path Extension + +**Problem:** Code referenced `index.mjs` but build output was `index.js`. + +**Error:** Preload script not found. + +**Solution:** Update path to match build output. + +#### Files Changed: + +**[apps/frontend/src/main/index.ts](../apps/frontend/src/main/index.ts)** + +```typescript +webPreferences: { + preload: join(__dirname, '../preload/index.js'), // Changed from .mjs + sandbox: false, + contextIsolation: true, + nodeIntegration: false, + // ... +} +``` + +### Challenge 8: Backend Path Detection + +**Problem:** `app.getAppPath()` fails when app is not ready on WSL2. + +**Solution:** Safe path detection with try-catch. + +#### Files Changed: + +**[apps/frontend/src/main/changelog/changelog-service.ts](../apps/frontend/src/main/changelog/changelog-service.ts)** +**[apps/frontend/src/main/title-generator.ts](../apps/frontend/src/main/title-generator.ts)** + +```typescript +private getBackendPath(): string { + const possiblePaths = [ + path.resolve(__dirname, '..', '..', '..', 'backend'), + path.resolve(process.cwd(), 'apps', 'backend') + ]; + + // Add app path if app is ready (WSL2 compatibility) + try { + if (app && app.getAppPath) { + possiblePaths.splice(1, 0, path.resolve(app.getAppPath(), '..', 'backend')); + } + } catch (e) { + // App not ready yet, continue without app path + } + + for (const p of possiblePaths) { + if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { + return p; + } + } + // ... error handling +} +``` + +### Challenge 9: Build Configuration + +**Problem:** ESM/CJS compatibility issues with Electron on WSL2. + +**Solution:** Ensure CJS format with `.js` extensions for main and preload. + +#### Files Changed: + +**[apps/frontend/electron.vite.config.ts](../apps/frontend/electron.vite.config.ts)** + +```typescript +export default defineConfig({ + main: { + define: sentryDefines, + plugins: [externalizeDepsPlugin()], + build: { + rollupOptions: { + input: { + index: resolve(__dirname, 'src/main/index.ts') + }, + output: { + format: 'cjs', + entryFileNames: '[name].js' + }, + external: [ + '@lydell/node-pty', + '@sentry/electron', + '@sentry/core', + '@sentry/node', + '@electron-toolkit/utils' + ] + } + } + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + rollupOptions: { + input: { + index: resolve(__dirname, 'src/preload/index.ts') + }, + output: { + format: 'cjs', + entryFileNames: '[name].js' + } + } + } + }, + // ... renderer config +}); +``` + +### Challenge 10: Sentry for Subprocesses + +**Problem:** Sentry environment export for subprocesses fails on WSL2. + +**Solution:** Temporarily disable for WSL2 compatibility. + +#### Files Changed: + +**[apps/frontend/src/main/env-utils.ts](../apps/frontend/src/main/env-utils.ts)** + +```typescript +// Disabled for WSL2 compatibility +// const sentryEnv = getSentryEnvForSubprocess(); +// Object.assign(env, sentryEnv); +``` + +## Running the Application + +### Development Mode + +```bash +cd /path/to/Auto-Claude + +# Install dependencies (first time only) +npm run install:all + +# Start the Electron app in development mode +npm run dev + +# The app should open in a new window +``` + +### Production Build + +```bash +# Build the application +npm run build + +# Start the built app +npm start +``` + +## Troubleshooting + +### Issue: "app.getVersion() is not a function" + +**Symptoms:** Errors during initialization about app not being defined. + +**Solution:** All fixes from this PR implement lazy initialization. If you see this error, ensure you're using the latest code with all WSL2 compatibility patches. + +### Issue: GPU/Graphics Errors + +**Symptoms:** Console warnings about libGLESv2.so or GPU permission errors. + +**Impact:** Non-critical. The app uses software rendering and works fine. + +**Example:** +``` +libva error: vaGetDriverNameByIndex() failed with unknown libva error +``` + +**Solution:** These can be ignored. The app functions correctly with software rendering. + +### Issue: Display Not Working + +**Symptoms:** App doesn't appear or crashes with display errors. + +**Solution:** +1. Verify WSLg is installed: `cat /mnt/wslg/versions.txt` +2. Check display variables: `echo $DISPLAY` and `echo $WAYLAND_DISPLAY` +3. Restart WSL: `wsl --shutdown` (from Windows PowerShell), wait 10 seconds, then `wsl` + +### Issue: "Cannot find module" Errors + +**Symptoms:** Module not found errors during startup. + +**Solution:** +1. Delete node_modules and reinstall: + ```bash + rm -rf node_modules apps/frontend/node_modules apps/backend/.venv + npm run install:all + ``` +2. Rebuild: + ```bash + npm run build + ``` + +### Issue: Settings Not Persisting + +**Symptoms:** Settings reset after restart. + +**Check:** Settings are stored in `~/.config/Auto-Claude/settings.json`. Verify the directory exists and has write permissions: + +```bash +ls -la ~/.config/Auto-Claude/ +chmod 755 ~/.config/Auto-Claude +``` + +## Architecture Changes + +### Lazy Initialization Pattern + +All WSL2 fixes follow a consistent pattern: + +1. **Delay module initialization** - Don't access Electron APIs at module load time +2. **Safe access wrappers** - Wrap Electron API calls in try-catch blocks +3. **Fallback mechanisms** - Provide fallbacks when APIs aren't available (e.g., package.json for version) +4. **Null checks** - Always check if objects are initialized before use +5. **Proxy pattern for singletons** - Defer singleton creation until first access + +### Key Principles + +1. **Never assume app is ready** - Even in main process, app may not be initialized +2. **Always provide fallbacks** - Have alternative paths when primary method fails +3. **Fail gracefully** - Log warnings instead of crashing +4. **Lazy load dependencies** - Use `require()` inside functions instead of top-level imports +5. **Test on WSL2** - All changes should be tested on WSL2 to catch initialization issues + +## Testing on WSL2 + +### Verification Checklist + +- [ ] App starts without errors +- [ ] Setup wizard completes successfully +- [ ] Settings persist across restarts +- [ ] Project creation works +- [ ] Logs are written correctly +- [ ] Updates can be checked (or gracefully skipped in dev mode) +- [ ] Backend subprocess spawns correctly +- [ ] Sentry initialization succeeds (if configured) + +### Debug Mode + +Enable debug logging to troubleshoot issues: + +```bash +# Enable Electron debug output +export ELECTRON_ENABLE_LOGGING=1 + +# Enable updater debug output +export DEBUG_UPDATER=true + +# Enable Node environment debug +export NODE_ENV=development + +# Run the app +npm run dev +``` + +## Contributing + +When making changes that affect WSL2 compatibility: + +1. Test on WSL2/WSLg before submitting PR +2. Follow the lazy initialization pattern +3. Add comments explaining WSL2-specific workarounds +4. Update this document if adding new workarounds + +## Related Issues + +- WSL2 app initialization timing +- Electron app object availability +- Sentry initialization before app.whenReady() +- electron-log preload script paths +- Module-level constant initialization + +## Credits + +WSL2/WSLg compatibility work by: +- Initial testing and bug reports: @freakedbuntu +- Implementation and fixes: Claude Code AI Assistant +- Based on Auto Claude by: @AndyMik90 + +## License + +Same as Auto Claude project license (AGPL-3.0).