From 04775af561ea1170158d0b3bab313c6eea0f7a15 Mon Sep 17 00:00:00 2001 From: Kacper Date: Mon, 2 Feb 2026 14:26:59 +0100 Subject: [PATCH 1/2] fix(electron): Fix broken symlinks in server bundle preventing app startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #742 This commit resolves two critical issues that prevented the Electron app from starting: 1. **Broken symlinks in server bundle** - After npm install, local @automaker/* packages were symlinked in node_modules - These symlinks broke after electron-builder packaging since relative paths no longer existed - Solution: Added Step 6b in prepare-server.mjs to replace symlinks with real directory copies - Added lstatSync and resolve imports to support symlink detection and replacement 2. **electronUserDataWriteFileSync fails on first launch** - The userData directory doesn't exist on first app launch - Writing .api-key file would fail with ENOENT error - Solution: Added directory existence check and creation with { recursive: true } before writing Files modified: - apps/ui/scripts/prepare-server.mjs: Added symlink replacement logic after npm install - libs/platform/src/system-paths.ts: Added parent directory creation in electronUserDataWriteFileSync Verification: After these fixes, npm run build:electron produces a working app that starts without ERR_MODULE_NOT_FOUND errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- apps/ui/scripts/prepare-server.mjs | 19 +++++++++++++++++-- libs/platform/src/system-paths.ts | 5 +++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/ui/scripts/prepare-server.mjs b/apps/ui/scripts/prepare-server.mjs index 82309574..182ab20c 100644 --- a/apps/ui/scripts/prepare-server.mjs +++ b/apps/ui/scripts/prepare-server.mjs @@ -7,8 +7,8 @@ */ import { execSync } from 'child_process'; -import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs'; -import { join, dirname } from 'path'; +import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, lstatSync } from 'fs'; +import { join, dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); @@ -112,6 +112,21 @@ execSync('npm install --omit=dev', { }, }); +// Step 6b: Replace symlinks for local packages with real copies +// npm install creates symlinks for file: references, but these break when packaged by electron-builder +console.log('🔗 Replacing symlinks with real directory copies...'); +const nodeModulesAutomaker = join(BUNDLE_DIR, 'node_modules', '@automaker'); +for (const pkgName of LOCAL_PACKAGES) { + const pkgDir = pkgName.replace('@automaker/', ''); + const nmPkgPath = join(nodeModulesAutomaker, pkgDir); + if (existsSync(nmPkgPath) && lstatSync(nmPkgPath).isSymbolicLink()) { + const realPath = resolve(BUNDLE_DIR, 'libs', pkgDir); + rmSync(nmPkgPath); + cpSync(realPath, nmPkgPath, { recursive: true }); + console.log(` ✓ Replaced symlink: ${pkgName}`); + } +} + // Step 7: Rebuild native modules for current architecture // This is critical for modules like node-pty that have native bindings console.log('🔨 Rebuilding native modules for current architecture...'); diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts index 8c212561..676f8ee0 100644 --- a/libs/platform/src/system-paths.ts +++ b/libs/platform/src/system-paths.ts @@ -750,6 +750,11 @@ export function electronUserDataWriteFileSync( throw new Error('[SystemPaths] Electron userData path not initialized'); } const fullPath = path.join(electronUserDataPath, relativePath); + // Ensure parent directory exists (may not exist on first launch) + const dir = path.dirname(fullPath); + if (!fsSync.existsSync(dir)) { + fsSync.mkdirSync(dir, { recursive: true }); + } fsSync.writeFileSync(fullPath, data, options); } From 4b4ae04fbebf52138c751281cba234efcfc2c5f5 Mon Sep 17 00:00:00 2001 From: Kacper Date: Mon, 2 Feb 2026 14:36:24 +0100 Subject: [PATCH 2/2] refactor: Address PR review feedback for symlink and directory handling - Use lstatSync with try/catch for robust broken symlink detection - Remove redundant existsSync check before mkdirSync with recursive: true Co-Authored-By: Claude Opus 4.5 --- apps/ui/scripts/prepare-server.mjs | 18 +++++++++++++----- libs/platform/src/system-paths.ts | 4 +--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/ui/scripts/prepare-server.mjs b/apps/ui/scripts/prepare-server.mjs index 182ab20c..db92b385 100644 --- a/apps/ui/scripts/prepare-server.mjs +++ b/apps/ui/scripts/prepare-server.mjs @@ -119,11 +119,19 @@ const nodeModulesAutomaker = join(BUNDLE_DIR, 'node_modules', '@automaker'); for (const pkgName of LOCAL_PACKAGES) { const pkgDir = pkgName.replace('@automaker/', ''); const nmPkgPath = join(nodeModulesAutomaker, pkgDir); - if (existsSync(nmPkgPath) && lstatSync(nmPkgPath).isSymbolicLink()) { - const realPath = resolve(BUNDLE_DIR, 'libs', pkgDir); - rmSync(nmPkgPath); - cpSync(realPath, nmPkgPath, { recursive: true }); - console.log(` ✓ Replaced symlink: ${pkgName}`); + try { + // lstatSync does not follow symlinks, allowing us to check for broken ones + if (lstatSync(nmPkgPath).isSymbolicLink()) { + const realPath = resolve(BUNDLE_DIR, 'libs', pkgDir); + rmSync(nmPkgPath); + cpSync(realPath, nmPkgPath, { recursive: true }); + console.log(` ✓ Replaced symlink: ${pkgName}`); + } + } catch (error) { + // If the path doesn't exist, lstatSync throws ENOENT. We can safely ignore this. + if (error.code !== 'ENOENT') { + throw error; + } } } diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts index 676f8ee0..0d900dfa 100644 --- a/libs/platform/src/system-paths.ts +++ b/libs/platform/src/system-paths.ts @@ -752,9 +752,7 @@ export function electronUserDataWriteFileSync( const fullPath = path.join(electronUserDataPath, relativePath); // Ensure parent directory exists (may not exist on first launch) const dir = path.dirname(fullPath); - if (!fsSync.existsSync(dir)) { - fsSync.mkdirSync(dir, { recursive: true }); - } + fsSync.mkdirSync(dir, { recursive: true }); fsSync.writeFileSync(fullPath, data, options); }