Skip to content

Commit 70c5c51

Browse files
zoidbergclawdclaude
andcommitted
Prefer system tools over bundled fallbacks to avoid conflicts
On machines with Git and Node.js already installed, the bundled MinGit and Electron node.exe shim were taking priority, breaking npm/npx and potentially causing version conflicts. Now checks for system git, bash, and node at startup. Bundled tools are only added to PATH when the system equivalents are missing. This means: - Developer machines: system Git + Node.js + npm all work normally - Fresh installs: bundled MinGit + Electron shim kick in automatically Bump version to 0.2.6. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent df1c1bf commit 70c5c51

6 files changed

Lines changed: 89 additions & 34 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "elisa-backend",
3-
"version": "0.2.5",
3+
"version": "0.2.6",
44
"private": true,
55
"license": "MIT",
66
"type": "module",

backend/src/services/agentRunner.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,11 @@ describe('AgentRunner', () => {
185185
expect(result.summary).toBe('No output');
186186
});
187187

188-
it('passes Electron executable config when ELISA_RESOURCES_PATH is set', async () => {
188+
it('uses Electron shim when ELECTRON_RUN_AS_NODE is set (no system node)', async () => {
189189
const origPath = process.env.ELISA_RESOURCES_PATH;
190+
const origShim = process.env.ELECTRON_RUN_AS_NODE;
190191
process.env.ELISA_RESOURCES_PATH = '/fake/resources';
192+
process.env.ELECTRON_RUN_AS_NODE = '1';
191193
try {
192194
mockQuery.mockReturnValue(asyncIterable(makeResultMessage()) as any);
193195

@@ -207,6 +209,37 @@ describe('AgentRunner', () => {
207209
} finally {
208210
if (origPath === undefined) delete process.env.ELISA_RESOURCES_PATH;
209211
else process.env.ELISA_RESOURCES_PATH = origPath;
212+
if (origShim === undefined) delete process.env.ELECTRON_RUN_AS_NODE;
213+
else process.env.ELECTRON_RUN_AS_NODE = origShim;
214+
}
215+
});
216+
217+
it('uses system node when available (no executable override)', async () => {
218+
const origPath = process.env.ELISA_RESOURCES_PATH;
219+
const origShim = process.env.ELECTRON_RUN_AS_NODE;
220+
process.env.ELISA_RESOURCES_PATH = '/fake/resources';
221+
delete process.env.ELECTRON_RUN_AS_NODE;
222+
try {
223+
mockQuery.mockReturnValue(asyncIterable(makeResultMessage()) as any);
224+
225+
const runner = new AgentRunner();
226+
await runner.execute({
227+
taskId: 'test-1',
228+
prompt: 'hello',
229+
systemPrompt: 'you are a bot',
230+
onOutput: vi.fn().mockResolvedValue(undefined),
231+
workingDir: '/tmp/test',
232+
});
233+
234+
const callArgs = mockQuery.mock.calls[0][0];
235+
expect(callArgs.options?.executable).toBeUndefined();
236+
expect(callArgs.options?.env).toBeUndefined();
237+
expect(typeof callArgs.options?.stderr).toBe('function');
238+
} finally {
239+
if (origPath === undefined) delete process.env.ELISA_RESOURCES_PATH;
240+
else process.env.ELISA_RESOURCES_PATH = origPath;
241+
if (origShim === undefined) delete process.env.ELECTRON_RUN_AS_NODE;
242+
else process.env.ELECTRON_RUN_AS_NODE = origShim;
210243
}
211244
});
212245

backend/src/services/agentRunner.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,18 @@ export class AgentRunner {
176176
abortController?: AbortController,
177177
allowedTools?: string[],
178178
): Promise<AgentResult> {
179-
// In Electron production, Node.js may not be installed on the system.
180-
// Use the Electron binary itself as the Node.js runtime for spawning cli.js.
179+
// In Electron production, capture stderr for diagnostics.
180+
// When no system Node.js is available (ELECTRON_RUN_AS_NODE is set by
181+
// electron/main.ts), use the Electron binary itself as the runtime.
182+
// When system node exists, let the SDK use it (preserves npm/npx).
181183
const isElectronProd = !!process.env.ELISA_RESOURCES_PATH;
184+
const useElectronShim = isElectronProd && process.env.ELECTRON_RUN_AS_NODE === '1';
182185
const stderrChunks: string[] = [];
183186
const electronExecConfig = isElectronProd
184187
? {
185-
executable: process.execPath,
186-
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
188+
...(useElectronShim
189+
? { executable: process.execPath, env: { ...process.env } }
190+
: {}),
187191
stderr: (data: string) => { stderrChunks.push(data); },
188192
}
189193
: {};

electron/main.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -183,40 +183,58 @@ async function startBackend(): Promise<void> {
183183
// Production: tell the backend where packaged resources live
184184
process.env.ELISA_RESOURCES_PATH = process.resourcesPath;
185185

186-
// MinGit: set up bundled Git + bash for Windows so Claude Code CLI works
187-
// without a system Git installation.
186+
// Set up bundled tool fallbacks on Windows.
187+
// Prefer system installations when available; fall back to bundled tools
188+
// only when the system doesn't have them. This avoids breaking npm/npx
189+
// on developer machines that already have Node.js installed.
188190
if (process.platform === 'win32') {
189191
const fs = require('fs') as typeof import('fs');
190192
const os = require('os') as typeof import('os');
191-
const mingitDir = path.join(process.resourcesPath, 'mingit');
192-
const bashExe = path.join(mingitDir, 'usr', 'bin', 'bash.exe');
193-
if (fs.existsSync(bashExe)) {
194-
process.env.CLAUDE_CODE_GIT_BASH_PATH = bashExe;
195-
const gitCmd = path.join(mingitDir, 'cmd');
196-
const gitUsrBin = path.join(mingitDir, 'usr', 'bin');
197-
process.env.PATH = `${gitCmd};${gitUsrBin};${process.env.PATH}`;
193+
const { execSync } = require('child_process') as typeof import('child_process');
194+
195+
const hasCommand = (cmd: string): boolean => {
196+
try { execSync(`where ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
197+
};
198+
199+
// Git + bash: use system Git if available, otherwise bundled MinGit
200+
if (!hasCommand('git')) {
201+
const mingitDir = path.join(process.resourcesPath, 'mingit');
202+
const bashExe = path.join(mingitDir, 'usr', 'bin', 'bash.exe');
203+
if (fs.existsSync(bashExe)) {
204+
const gitCmd = path.join(mingitDir, 'cmd');
205+
const gitUsrBin = path.join(mingitDir, 'usr', 'bin');
206+
process.env.PATH = `${process.env.PATH};${gitCmd};${gitUsrBin}`;
207+
console.log('[main] Using bundled MinGit (no system git found)');
208+
}
198209
}
199210

200-
// Create a node.exe shim so agents and test runners can execute JS.
201-
// Hardlink Elisa.exe -> node.exe (zero disk cost on NTFS).
202-
// ELECTRON_RUN_AS_NODE=1 makes it behave as plain Node.js.
203-
const nodeShimDir = path.join(os.tmpdir(), 'elisa-node');
204-
const shimNodeExe = path.join(nodeShimDir, 'node.exe');
205-
try {
206-
fs.mkdirSync(nodeShimDir, { recursive: true });
207-
try { fs.unlinkSync(shimNodeExe); } catch { /* may not exist */ }
208-
try {
209-
fs.linkSync(process.execPath, shimNodeExe);
210-
} catch {
211-
fs.copyFileSync(process.execPath, shimNodeExe);
211+
// Claude Code needs bash.exe. Set CLAUDE_CODE_GIT_BASH_PATH if not
212+
// already resolvable (system Git puts it in PATH automatically).
213+
if (!hasCommand('bash')) {
214+
const mingitBash = path.join(process.resourcesPath, 'mingit', 'usr', 'bin', 'bash.exe');
215+
if (fs.existsSync(mingitBash)) {
216+
process.env.CLAUDE_CODE_GIT_BASH_PATH = mingitBash;
212217
}
213-
process.env.PATH = `${nodeShimDir};${process.env.PATH}`;
214-
} catch (err) {
215-
console.error('Failed to create node.exe shim:', err);
216218
}
217219

218-
// Let all child processes use the Electron binary as Node.js.
219-
process.env.ELECTRON_RUN_AS_NODE = '1';
220+
// Node.js: use system node if available, otherwise create Electron shim.
221+
if (!hasCommand('node')) {
222+
const nodeShimDir = path.join(os.tmpdir(), 'elisa-node');
223+
const shimNodeExe = path.join(nodeShimDir, 'node.exe');
224+
try {
225+
fs.mkdirSync(nodeShimDir, { recursive: true });
226+
try { fs.unlinkSync(shimNodeExe); } catch { /* may not exist */ }
227+
try { fs.linkSync(process.execPath, shimNodeExe); }
228+
catch { fs.copyFileSync(process.execPath, shimNodeExe); }
229+
process.env.PATH = `${process.env.PATH};${nodeShimDir}`;
230+
// ELECTRON_RUN_AS_NODE makes the Electron binary act as Node.js.
231+
// Only set when using the shim; system node doesn't need it.
232+
process.env.ELECTRON_RUN_AS_NODE = '1';
233+
console.log('[main] Using Electron node.exe shim (no system node found)');
234+
} catch (err) {
235+
console.error('[main] Failed to create node.exe shim:', err);
236+
}
237+
}
220238
}
221239

222240
const backendDist = path.join(process.resourcesPath, 'backend-dist');

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "frontend",
33
"private": true,
4-
"version": "0.2.5",
4+
"version": "0.2.6",
55
"license": "MIT",
66
"type": "module",
77
"scripts": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "elisa",
3-
"version": "0.2.5",
3+
"version": "0.2.6",
44
"private": true,
55
"license": "MIT",
66
"author": "Elisa",

0 commit comments

Comments
 (0)