Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,4 @@ data/
.planning/
.mcp.json
.planning
.bg-shell/
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@automaker/prompts": "1.0.0",
"@automaker/types": "1.0.0",
"@automaker/utils": "1.0.0",
"@github/copilot-sdk": "^0.1.16",
"@github/copilot-sdk": "0.1.16",
"@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.98.0",
"cookie-parser": "1.4.7",
Expand Down
17 changes: 15 additions & 2 deletions apps/server/src/lib/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Version utility - Reads version from package.json
*/

import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createLogger } from '@automaker/utils';
Expand All @@ -24,7 +24,20 @@ export function getVersion(): string {
}

try {
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
const candidatePaths = [
// Development via tsx: src/lib -> project root
join(__dirname, '..', '..', 'package.json'),
// Packaged/build output: lib -> server bundle root
join(__dirname, '..', 'package.json'),
];

const packageJsonPath = candidatePaths.find((candidate) => existsSync(candidate));
if (!packageJsonPath) {
throw new Error(
`package.json not found in any expected location: ${candidatePaths.join(', ')}`
);
}

const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version || '0.0.0';
cachedVersion = version;
Expand Down
62 changes: 39 additions & 23 deletions apps/ui/src/electron/server/backend-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,46 @@ const serverLogger = createLogger('Server');
export async function startServer(): Promise<void> {
const isDev = !app.isPackaged;

// Find Node.js executable (handles desktop launcher scenarios)
const nodeResult = findNodeExecutable({
skipSearch: isDev,
logger: (msg: string) => logger.info(msg),
});
const command = nodeResult.nodePath;
let command: string;
let commandSource: string;
let args: string[];
let serverPath: string;

// Validate that the found Node executable actually exists
// systemPathExists is used because node-finder returns system paths
if (command !== 'node') {
let exists: boolean;
try {
exists = systemPathExists(command);
} catch (error) {
const originalError = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
);
}
if (!exists) {
throw new Error(`Node.js executable not found at: ${command} (source: ${nodeResult.source})`);
if (isDev) {
// In development, run the TypeScript server via the user's Node.js.
const nodeResult = findNodeExecutable({
skipSearch: true,
logger: (msg: string) => logger.info(msg),
});
command = nodeResult.nodePath;
commandSource = nodeResult.source;

// Validate that the found Node executable actually exists
// systemPathExists is used because node-finder returns system paths
if (command !== 'node') {
let exists: boolean;
try {
exists = systemPathExists(command);
} catch (error) {
const originalError = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
);
Comment on lines +52 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better debuggability, it's recommended to preserve the original error's context (including stack trace) when re-throwing. The cause property on Error is perfect for this (available in Node.js v16.9.0+). This change also provides an opportunity to use the new commandSource variable for consistency instead of nodeResult.source.

Suggested change
const originalError = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
);
throw new Error(
`Failed to verify Node.js executable at: ${command} (source: ${commandSource}).`,
{ cause: error }
);

}
if (!exists) {
throw new Error(
`Node.js executable not found at: ${command} (source: ${nodeResult.source})`
);
}
}
Comment on lines +36 to 62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that the call site disables discovery and that the helper returns the fallback literal.
rg -n -C2 'skipSearch:\s*true' apps/ui/src/electron/server/backend-server.ts
sed -n '275,300p' libs/platform/src/node-finder.ts

Repository: AutoMaker-Org/automaker

Length of output: 952


🏁 Script executed:

# Check other call sites of findNodeExecutable to understand the pattern
rg -n 'findNodeExecutable' apps/ui/src/electron/server/backend-server.ts libs/platform/src/node-finder.ts --type ts

Repository: AutoMaker-Org/automaker

Length of output: 751


🏁 Script executed:

# Check if there are comments or documentation explaining the skipSearch choice
rg -B5 -A5 'skipSearch.*true' apps/ui/src/electron/server/ --type ts

Repository: AutoMaker-Org/automaker

Length of output: 887


🏁 Script executed:

# Verify the function signature and default behavior
rg -B3 -A10 'export function findNodeExecutable' libs/platform/src/node-finder.ts --type ts

Repository: AutoMaker-Org/automaker

Length of output: 477


🏁 Script executed:

# Check the packaged mode branch to see the alternative approach
rg -B5 -A15 'if (isDev)' apps/ui/src/electron/server/backend-server.ts --type ts

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

# Look for any environment setup or PATH configuration in the Electron context
rg -n 'NODE_PATH|process.env.PATH|ELECTRON' apps/ui/src/electron/ --type ts | head -20

Repository: AutoMaker-Org/automaker

Length of output: 574


🏁 Script executed:

# Get the full context of the backend server startup including buildEnhancedPath usage
sed -n '1,170p' apps/ui/src/electron/server/backend-server.ts | tail -40

Repository: AutoMaker-Org/automaker

Length of output: 1614


🏁 Script executed:

# Look for buildEnhancedPath implementation and documentation
rg -B3 -A15 'export.*buildEnhancedPath|function buildEnhancedPath' libs/platform/src/ --type ts

Repository: AutoMaker-Org/automaker

Length of output: 1418


Don't bypass findNodeExecutable() in dev mode.

With skipSearch: true, the function returns the fallback literal 'node' and skips all platform discovery. Additionally, buildEnhancedPath() explicitly skips PATH enhancement when receiving the fallback value, and validation is skipped at lines 47–62. In dev mode, Electron will rely solely on the current process.env.PATH to resolve node, which is often unavailable outside shell-launched sessions. Use the same discovery logic as packaged mode by removing skipSearch: true.

♻️ Proposed fix
     const nodeResult = findNodeExecutable({
-      skipSearch: true,
       logger: (msg: string) => logger.info(msg),
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/electron/server/backend-server.ts` around lines 36 - 62, The
dev-mode startup is bypassing full Node discovery by calling
findNodeExecutable({ skipSearch: true, ... }), which returns the literal 'node'
and prevents buildEnhancedPath/validation from running; remove the skipSearch:
true option so findNodeExecutable runs platform discovery (keep the logger
usage), then preserve the existing validation logic that checks command !==
'node' and uses systemPathExists to verify the resolved node path in the isDev
branch (references: findNodeExecutable, buildEnhancedPath, isDev, command,
commandSource).

} else {
// In packaged builds, use Electron's bundled Node runtime instead of a system Node.
// This makes the desktop app self-contained and avoids incompatibilities with whatever
// Node version the user happens to have installed globally.
command = process.execPath;
commandSource = 'electron';
}

let args: string[];
let serverPath: string;

// __dirname is apps/ui/dist-electron (Vite bundles all into single file)
if (isDev) {
serverPath = path.join(__dirname, '../../server/src/index.ts');
Expand Down Expand Up @@ -133,6 +146,8 @@ export async function startServer(): Promise<void> {
PORT: state.serverPort.toString(),
DATA_DIR: dataDir,
NODE_PATH: serverNodeModules,
// Run packaged backend with Electron's embedded Node runtime.
...(app.isPackaged && { ELECTRON_RUN_AS_NODE: '1' }),
// Pass API key to server for CSRF protection
AUTOMAKER_API_KEY: state.apiKey!,
// Only set ALLOWED_ROOT_DIRECTORY if explicitly provided in environment
Expand All @@ -146,6 +161,7 @@ export async function startServer(): Promise<void> {
logger.info('[DATA_DIR_SPAWN] env.DATA_DIR=', env.DATA_DIR);

logger.info('Starting backend server...');
logger.info('Runtime command:', command, `(source: ${commandSource})`);
logger.info('Server path:', serverPath);
logger.info('Server root (cwd):', serverRoot);
logger.info('NODE_PATH:', serverNodeModules);
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading