Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.
Draft
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
4 changes: 2 additions & 2 deletions src/install/installationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { CmCli } from '../services/cmCli';
import { captureSentryException } from '../services/sentry';
import { type HasTelemetry, ITelemetry, trackEvent } from '../services/telemetry';
import { type DesktopConfig, useDesktopConfig } from '../store/desktopConfig';
import { canExecuteShellCommand, compareVersions, validateHardware } from '../utils';
import { canExecuteGit, compareVersions, validateHardware } from '../utils';
import type { ProcessCallbacks, VirtualEnvironment } from '../virtualEnvironment';
import { createProcessCallbacks } from './createProcessCallbacks';
import { InstallWizard } from './installWizard';
Expand Down Expand Up @@ -176,7 +176,7 @@ export class InstallationManager implements HasTelemetry {
// Check if git is installed
log.verbose('Checking if git is installed.');
appState.setInstallStage(createInstallStageInfo(InstallStage.GIT_CHECK, { progress: 5 }));
const gitInstalled = await canExecuteShellCommand('git --version');
const gitInstalled = await canExecuteGit();
if (!gitInstalled) {
log.verbose('git not detected in path, loading download-git page.');

Expand Down
4 changes: 2 additions & 2 deletions src/main-process/comfyInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { DesktopInstallState } from '../main_types';
import type { InstallValidation } from '../preload';
import { type ITelemetry, getTelemetry } from '../services/telemetry';
import { useDesktopConfig } from '../store/desktopConfig';
import { canExecute, canExecuteShellCommand, pathAccessible } from '../utils';
import { canExecute, canExecuteGit, pathAccessible } from '../utils';
import { VirtualEnvironment } from '../virtualEnvironment';

/**
Expand Down Expand Up @@ -188,7 +188,7 @@ export class ComfyInstallation {
}

// Git
validation.git = (await canExecuteShellCommand('git --help')) ? 'OK' : 'error';
validation.git = (await canExecuteGit()) ? 'OK' : 'error';
if (validation.git !== 'OK') log.warn('git not found in path.');
this.onUpdate?.(validation);

Expand Down
31 changes: 29 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,41 @@ export async function canExecute(path: string): Promise<boolean> {
export async function canExecuteShellCommand(command: string, timeout = 5000): Promise<boolean> {
const proc = exec(command);
return new Promise((resolve, reject) => {
setTimeout(() => {
const timeoutHandle = setTimeout(() => {
proc.kill();
reject(new Error('Timed out attempting to execute git'));
}, timeout);
proc.on('exit', (code) => resolve(code === 0));
proc.on('exit', (code) => {
clearTimeout(timeoutHandle);
resolve(code === 0);
});
});
}

const quoteShellPath = (value: string) => `"${value.replaceAll('"', '')}"`;

function getWindowsGitCommands(): string[] {
const programFiles = [process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter(
(value): value is string => !!value
);

return programFiles.flatMap((basePath) => [
`${quoteShellPath(path.join(basePath, 'Git', 'cmd', 'git.exe'))} --help`,
`${quoteShellPath(path.join(basePath, 'Git', 'bin', 'git.exe'))} --help`,
]);
}

export async function canExecuteGit(): Promise<boolean> {
if (await canExecuteShellCommand('git --help')) return true;
if (process.platform !== 'win32') return false;

for (const command of getWindowsGitCommands()) {
if (await canExecuteShellCommand(command)) return true;
}

return false;
}

export async function containsDirectory(path: string, contains: string): Promise<boolean> {
if (await pathAccessible(path)) {
const contents = await fsPromises.readdir(path, { withFileTypes: true });
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/install/installationManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ vi.mock('@/utils', async () => {
}),
canExecute: vi.fn(() => Promise.resolve(true)),
canExecuteShellCommand: vi.fn(() => Promise.resolve(true)),
canExecuteGit: vi.fn(() => Promise.resolve(true)),
};
});

Expand Down Expand Up @@ -212,7 +213,7 @@ describe('InstallationManager', () => {
{
scenario: 'detects missing git',
mockSetup: () => {
vi.mocked(utils.canExecuteShellCommand).mockResolvedValue(false);
vi.mocked(utils.canExecuteGit).mockResolvedValue(false);
},
expectedErrors: ['git'],
},
Expand Down
34 changes: 33 additions & 1 deletion tests/unit/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Systeminformation } from 'systeminformation';
import si from 'systeminformation';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { validateHardware } from '@/utils';
import { canExecuteGit, validateHardware } from '@/utils';

vi.mock('node:child_process', () => ({
exec: vi.fn(),
Expand All @@ -21,6 +21,8 @@ const createChildProcess = (): ChildProcess =>

type ExecResponse = { error?: Error | null; stdout?: string; stderr?: string };

type ExitResponse = { code: number };

const withExecResponses = (responses: Array<[RegExp, ExecResponse]>, fallback: ExecResponse = {}) => {
execMock.mockImplementation(((
command: string,
Expand All @@ -33,6 +35,20 @@ const withExecResponses = (responses: Array<[RegExp, ExecResponse]>, fallback: E
}) as typeof exec);
};

const withExitResponses = (responses: Array<[RegExp, ExitResponse]>, fallback?: ExitResponse) => {
execMock.mockImplementation((command: string) => {
const match = responses.find(([pattern]) => pattern.test(command));
const { code } = match?.[1] ?? fallback ?? { code: 1 };

return {
kill: vi.fn(),
on: vi.fn((event: string, callback: (code: number) => void) => {
if (event === 'exit') setImmediate(() => callback(code));
}),
} as unknown as ChildProcess;
});
};

beforeEach(() => {
execMock.mockReset();
});
Expand Down Expand Up @@ -99,3 +115,19 @@ describe('validateHardware', () => {
});
});
});

describe('canExecuteGit', () => {
it('falls back to the standard Git for Windows install path when git is missing from PATH', async () => {
vi.stubGlobal('process', {
...process,
platform: 'win32',
env: { ...process.env, ProgramFiles: String.raw`C:\Program Files` },
});
withExitResponses([
[/^git --help$/, { code: 1 }],
[/^"C:\\Program Files\\Git\\cmd\\git\.exe" --help$/, { code: 0 }],
]);

await expect(canExecuteGit()).resolves.toBe(true);
});
});