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
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ This desktop app is a packaged way to use [ComfyUI](https://github.com/comfyanon

- Stable version of ComfyUI from [releases](https://github.com/comfyanonymous/ComfyUI/releases)
- [ComfyUI_frontend](https://github.com/Comfy-Org/ComfyUI_frontend)
- [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager)
- [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager) (installed via pip when `--enable-manager` is set; legacy custom node is only cloned for older ComfyUI versions)
- [uv](https://github.com/astral-sh/uv)

On startup, it will install all the necessary python dependencies with uv and start the ComfyUI server. The app will automatically update with stable releases of ComfyUI, ComfyUI-Manager, and the uv executable as well as some desktop specific features.
On startup, it will install all the necessary python dependencies with uv and start the ComfyUI server. The app will automatically update with stable releases of ComfyUI, ComfyUI-Manager (pip), and the uv executable as well as some desktop-specific features.

Developers, read on.

Expand All @@ -33,7 +33,7 @@ Developers, read on.
The desktop application comes bundled with:

- ComfyUI source code
- ComfyUI-Manager
- ComfyUI-Manager (pip package or legacy custom node, depending on the bundled ComfyUI version)
- Electron, Chromium binaries, and node modules

**Windows**
Expand Down Expand Up @@ -170,7 +170,7 @@ Before you can start the electron application, you need to download the ComfyUI

First, initialize the application resources by running `yarn make:assets`:

This command will install ComfyUI and ComfyUI-Manager under `assets/`. The exact versions of each package is defined in `package.json`.
This command will install ComfyUI under `assets/`. If the bundled ComfyUI version contains `manager_requirements.txt`, ComfyUI-Manager will be installed via pip at runtime; otherwise the legacy custom node is cloned for compatibility. The exact versions of each package is defined in `package.json`.

You can then run `start` to build and launch the app. A watcher will also be started; it will automatically rebuild the app when a source file is changed:

Expand Down Expand Up @@ -198,23 +198,25 @@ You can generate the compiled requirements files by running the following comman

```powershell
## Nvidia Cuda requirements
uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\custom_nodes\ComfyUI-Manager\requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_nvidia.compiled --override assets\override.txt `
uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_nvidia.compiled --override assets\override.txt `
--index-url https://pypi.org/simple `
--extra-index-url https://download.pytorch.org/whl/cu129

## CPU requirements
uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\custom_nodes\ComfyUI-Manager\requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_cpu.compiled `
uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets\requirements\windows_cpu.compiled `
--index-url https://pypi.org/simple
```

#### macOS

```bash
## macOS requirements
uv pip compile assets/ComfyUI/requirements.txt assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets/requirements/macos.compiled --override assets/override.txt \
uv pip compile assets/ComfyUI/requirements.txt assets/ComfyUI/manager_requirements.txt --emit-index-annotation --emit-index-url --index-strategy unsafe-best-match -o assets/requirements/macos.compiled --override assets/override.txt \
--index-url https://pypi.org/simple
```

If you are working with a legacy ComfyUI bundle that does not include `manager_requirements.txt`, replace that path in the commands above with `assets/ComfyUI/custom_nodes/ComfyUI-Manager/requirements.txt`.

### Troubleshooting

If you get an error similar to:
Expand Down
17 changes: 13 additions & 4 deletions scripts/makeComfy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

import pkg from './getPackage.js';

Expand All @@ -15,10 +17,17 @@ if (pkg.config.comfyUI.optionalBranch) {
// Checkout tag as branch.
execAndLog(`git ${noWarning} clone ${comfyRepo} --depth 1 --branch v${pkg.config.comfyUI.version} assets/ComfyUI`);
}
execAndLog(`git clone ${managerRepo} assets/ComfyUI/custom_nodes/ComfyUI-Manager`);
execAndLog(
`cd assets/ComfyUI/custom_nodes/ComfyUI-Manager && git ${noWarning} checkout ${pkg.config.managerCommit} && cd ../../..`
);
const assetsComfyPath = path.join('assets', 'ComfyUI');
const managerRequirementsPath = path.join(assetsComfyPath, 'manager_requirements.txt');

if (fs.existsSync(managerRequirementsPath)) {
console.log('Detected manager_requirements.txt, skipping legacy ComfyUI-Manager clone.');
} else {
execAndLog(`git clone ${managerRepo} assets/ComfyUI/custom_nodes/ComfyUI-Manager`);
execAndLog(
`cd assets/ComfyUI/custom_nodes/ComfyUI-Manager && git ${noWarning} checkout ${pkg.config.managerCommit} && cd ../../..`
);
}
execAndLog(`yarn run make:frontend`);
execAndLog(`yarn run download:uv all`);
execAndLog(`yarn run patch:core:frontend`);
Expand Down
29 changes: 22 additions & 7 deletions scripts/verifyBuild.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import fs from 'node:fs';
import path from 'node:path';

/**
* Verify the app build for the current platform.
* Check that all required paths are present.
*/
const PATHS = {
/**
* @typedef {{ base: string; required: string[] }} VerifyConfig
*/

const PATHS = /** @type {Record<'mac' | 'windows', VerifyConfig>} */ ({
mac: {
base: 'dist/mac-arm64/ComfyUI.app/Contents/Resources',
required: ['ComfyUI', 'ComfyUI/custom_nodes/ComfyUI-Manager', 'UI', 'uv/macos/uv', 'uv/macos/uvx'],
required: ['ComfyUI', 'UI', 'uv/macos/uv', 'uv/macos/uvx'],
},
windows: {
base: 'dist/win-unpacked/resources',
required: [
// Add Windows-specific paths here
'ComfyUI',
'ComfyUI/custom_nodes/ComfyUI-Manager',
'UI',
'uv/win/uv.exe',
'uv/win/uvx.exe',
],
},
};
});

/**
* @param {VerifyConfig} config
*/
function verifyConfig(config) {
const required = [...config.required];
const managerRequirementsPath = path.join(config.base, 'ComfyUI', 'manager_requirements.txt');
const legacyManagerPath = path.join(config.base, 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager');
if (fs.existsSync(managerRequirementsPath)) {
required.push('ComfyUI/manager_requirements.txt');
} else if (fs.existsSync(legacyManagerPath)) {
required.push('ComfyUI/custom_nodes/ComfyUI-Manager');
} else {
required.push('ComfyUI/manager_requirements.txt');
}
Comment on lines +34 to +42
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use path.join() for cross-platform path construction.

Lines 37, 39, and 41 push strings with hardcoded forward slashes. While path.join() normalizes these later, explicitly constructing these paths ensures consistency across platforms.

Based on learnings, prefer path.join() for all path construction to ensure cross-platform compatibility.

Apply this diff:

   const managerRequirementsPath = path.join(config.base, 'ComfyUI', 'manager_requirements.txt');
   const legacyManagerPath = path.join(config.base, 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager');
   if (fs.existsSync(managerRequirementsPath)) {
-    required.push('ComfyUI/manager_requirements.txt');
+    required.push(path.join('ComfyUI', 'manager_requirements.txt'));
   } else if (fs.existsSync(legacyManagerPath)) {
-    required.push('ComfyUI/custom_nodes/ComfyUI-Manager');
+    required.push(path.join('ComfyUI', 'custom_nodes', 'ComfyUI-Manager'));
   } else {
-    required.push('ComfyUI/manager_requirements.txt');
+    required.push(path.join('ComfyUI', 'manager_requirements.txt'));
   }
🤖 Prompt for AI Agents
In scripts/verifyBuild.js around lines 34 to 42, the code pushes hardcoded path
strings with forward slashes into the required array; replace those string
literals with path.join calls (e.g.,
path.join('ComfyUI','manager_requirements.txt') and
path.join('ComfyUI','custom_nodes','ComfyUI-Manager')) so all pushed paths are
constructed via path.join for consistent cross-platform path separators and
normalization.

Comment on lines +40 to +42
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a comment to clarify fallback intent.

The else block adds manager_requirements.txt to required even if it doesn't exist, which will cause verification to fail. This enforces that at least one manager configuration must be present. A brief comment would make this intent explicit.

   } else {
+    // Fallback: require new manager format, which will fail verification if neither config exists
     required.push(path.join('ComfyUI', 'manager_requirements.txt'));
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In scripts/verifyBuild.js around lines 40-42, the else branch unconditionally
pushes 'ComfyUI/manager_requirements.txt' into the required array as an
intentional fallback to enforce at least one manager configuration; add a brief
inline comment explaining this fallback intent (e.g., that the script requires
at least one manager config and will fail verification if none of the recognized
files are present) so future readers understand why a non-existent file is being
added and that the behavior is deliberate.


const missingPaths = [];

for (const requiredPath of config.required) {
for (const requiredPath of required) {
const fullPath = path.join(config.base, requiredPath);
if (!fs.existsSync(fullPath)) {
missingPaths.push(requiredPath);
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface ServerArgs {
export const DEFAULT_SERVER_ARGS: ServerArgs = {
listen: '127.0.0.1',
port: '8000',
'enable-manager': '',
};

export enum DownloadStatus {
Expand Down
3 changes: 1 addition & 2 deletions src/main-process/comfyDesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export class ComfyDesktopApp implements HasTelemetry {
async buildServerArgs({ useExternalServer, COMFY_HOST, COMFY_PORT }: DevOverrides): Promise<ServerArgs> {
// Shallow-clone the setting launch args to avoid mutation.
const serverArgs: ServerArgs = {
listen: DEFAULT_SERVER_ARGS.listen,
port: DEFAULT_SERVER_ARGS.port,
...DEFAULT_SERVER_ARGS,
...useComfySettings().get('Comfy.Server.LaunchArgs'),
};

Expand Down
11 changes: 10 additions & 1 deletion src/services/cmCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ import { HasTelemetry, ITelemetry, trackEvent } from './telemetry';

export class CmCli implements HasTelemetry {
private readonly cliPath: string;
private readonly moduleName = 'comfyui_manager.cm_cli';
constructor(
private readonly virtualEnvironment: VirtualEnvironment,
readonly telemetry: ITelemetry
) {
this.cliPath = path.join(getAppResourcesPath(), 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager', 'cm-cli.py');
}

private async buildCommandArgs(args: string[]): Promise<string[]> {
if (await pathAccessible(this.cliPath)) {
return [this.cliPath, ...args];
}
return ['-m', this.moduleName, ...args];
}

public async runCommandAsync(
args: string[],
callbacks?: ProcessCallbacks,
Expand All @@ -31,8 +39,9 @@ export class CmCli implements HasTelemetry {
COMFYUI_PATH: this.virtualEnvironment.basePath,
...env,
};
const commandArgs = await this.buildCommandArgs(args);
const { exitCode } = await this.virtualEnvironment.runPythonCommandAsync(
[this.cliPath, ...args],
commandArgs,
{
onStdout: (message) => {
output += message;
Expand Down
31 changes: 30 additions & 1 deletion src/virtualEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { app } from 'electron';
import log from 'electron-log/main';
import pty from 'node-pty';
import { ChildProcess, spawn } from 'node:child_process';
import { existsSync } from 'node:fs';
import { readdir, rm } from 'node:fs/promises';
import os, { EOL } from 'node:os';
import path from 'node:path';
Expand Down Expand Up @@ -116,6 +117,7 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor {
readonly pythonInterpreterPath: string;
readonly comfyUIRequirementsPath: string;
readonly comfyUIManagerRequirementsPath: string;
readonly legacyComfyUIManagerRequirementsPath: string;
readonly selectedDevice: TorchDeviceType;
readonly telemetry: ITelemetry;
readonly pythonMirror?: string;
Expand Down Expand Up @@ -186,13 +188,18 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor {
this.venvPath = path.join(basePath, '.venv');
const resourcesPath = app.isPackaged ? path.join(process.resourcesPath) : path.join(app.getAppPath(), 'assets');
this.comfyUIRequirementsPath = path.join(resourcesPath, 'ComfyUI', 'requirements.txt');
this.comfyUIManagerRequirementsPath = path.join(
const managerRequirementsPath = path.join(resourcesPath, 'ComfyUI', 'manager_requirements.txt');
this.legacyComfyUIManagerRequirementsPath = path.join(
resourcesPath,
'ComfyUI',
'custom_nodes',
'ComfyUI-Manager',
'requirements.txt'
);
this.comfyUIManagerRequirementsPath = this.resolveManagerRequirementsPath(
managerRequirementsPath,
this.legacyComfyUIManagerRequirementsPath
);

this.cacheDir = path.join(basePath, 'uv-cache');

Expand Down Expand Up @@ -231,6 +238,12 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor {
}
}

private resolveManagerRequirementsPath(primary: string, legacy: string) {
if (existsSync(primary)) return primary;
if (existsSync(legacy)) return legacy;
return primary;
}

public async create(callbacks?: ProcessCallbacks): Promise<void> {
try {
await this.createEnvironment(callbacks);
Expand Down Expand Up @@ -377,6 +390,9 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor {
);
return this.manualInstall(callbacks);
}

// Ensure Manager requirements are installed even if the compiled file did not include them.
await this.installComfyUIManagerRequirements(callbacks);
}

/**
Expand Down Expand Up @@ -641,6 +657,13 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor {
})
);

if (!(await pathAccessible(this.comfyUIManagerRequirementsPath))) {
throw new Error(
`Manager requirements file was not found at ${this.comfyUIManagerRequirementsPath}. ` +
`If you are using a legacy build, ensure the ComfyUI-Manager custom node is present at ${this.legacyComfyUIManagerRequirementsPath}.`
);
}
Comment on lines +660 to +665
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

This error message is duplicated on lines 764-769. Consider extracting this to a private method or constant to maintain consistency and make future updates easier. For example:

private getManagerRequirementsNotFoundError(): Error {
  return new Error(
    `Manager requirements file was not found at ${this.comfyUIManagerRequirementsPath}. ` +
      `If you are using a legacy build, ensure the ComfyUI-Manager custom node is present at ${this.legacyComfyUIManagerRequirementsPath}.`
  );
}

Then use throw this.getManagerRequirementsNotFoundError(); in both locations.

Copilot uses AI. Check for mistakes.

log.info(`Installing ComfyUIManager requirements from ${this.comfyUIManagerRequirementsPath}`);
const installCmd = getPipInstallArgs({
requirementsFile: this.comfyUIManagerRequirementsPath,
Expand Down Expand Up @@ -738,6 +761,12 @@ export class VirtualEnvironment implements HasTelemetry, PythonExecutor {
};

const coreOutput = await checkRequirements(this.comfyUIRequirementsPath);
if (!(await pathAccessible(this.comfyUIManagerRequirementsPath))) {
throw new Error(
`Manager requirements file was not found at ${this.comfyUIManagerRequirementsPath}. ` +
`If you are using a legacy build, ensure the ComfyUI-Manager custom node is present at ${this.legacyComfyUIManagerRequirementsPath}.`
);
}
const managerOutput = await checkRequirements(this.comfyUIManagerRequirementsPath);

const coreOk = hasAllPackages(coreOutput);
Expand Down
1 change: 1 addition & 0 deletions tests/resources/ComfyUI/manager_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Dummy manager requirements file for tests
1 change: 1 addition & 0 deletions tests/unit/install/installationManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ vi.mock('@/virtualEnvironment', () => {
venvPath: 'valid/venv',
comfyUIRequirementsPath: 'valid/requirements.txt',
comfyUIManagerRequirementsPath: 'valid/manager-requirements.txt',
legacyComfyUIManagerRequirementsPath: 'valid/legacy-manager-requirements.txt',
})),
};
});
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/main-process/comfyDesktopApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe('ComfyDesktopApp', () => {
expect(result).toEqual({
listen: DEFAULT_SERVER_ARGS.listen,
port: '8188',
'enable-manager': '',
});
});

Expand All @@ -138,6 +139,7 @@ describe('ComfyDesktopApp', () => {
expect(result).toEqual({
listen: 'localhost',
port: '8188', // Still uses findAvailablePort result
'enable-manager': '',
});
});

Expand All @@ -152,6 +154,7 @@ describe('ComfyDesktopApp', () => {
expect(result).toEqual({
listen: DEFAULT_SERVER_ARGS.listen,
port: DEFAULT_SERVER_ARGS.port,
'enable-manager': '',
});
});
});
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/virtualEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import log from 'electron-log/main';
import { type ChildProcess, spawn } from 'node:child_process';
import path from 'node:path';
import { test as baseTest, describe, expect, vi } from 'vitest';

import type { ITelemetry } from '@/services/telemetry';
Expand Down Expand Up @@ -27,10 +28,12 @@ const mockTelemetry: ITelemetry = {

const test = baseTest.extend<TestFixtures>({
virtualEnv: async ({}, use) => {
const resourcesPath = path.join(__dirname, '../resources');

// Mock process.resourcesPath since app.isPackaged is true
vi.stubGlobal('process', {
...process,
resourcesPath: '/test/resources',
resourcesPath,
});

const virtualEnv = new VirtualEnvironment('/mock/venv', {
Expand Down
Loading