diff --git a/README.md b/README.md index f37befa5a..75126fb44 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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** @@ -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: @@ -198,12 +198,12 @@ 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 ``` @@ -211,10 +211,12 @@ uv pip compile assets\ComfyUI\requirements.txt assets\ComfyUI\custom_nodes\Comfy ```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: diff --git a/scripts/makeComfy.js b/scripts/makeComfy.js index 75b8272de..12141a720 100644 --- a/scripts/makeComfy.js +++ b/scripts/makeComfy.js @@ -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'; @@ -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`); diff --git a/scripts/verifyBuild.js b/scripts/verifyBuild.js index 9902b28ce..0fbb4be7d 100644 --- a/scripts/verifyBuild.js +++ b/scripts/verifyBuild.js @@ -1,5 +1,3 @@ -/* 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'; @@ -7,28 +5,45 @@ 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'); + } + 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); diff --git a/src/constants.ts b/src/constants.ts index 37466e2ea..2e1057775 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 { diff --git a/src/main-process/comfyDesktopApp.ts b/src/main-process/comfyDesktopApp.ts index add9ff890..c9d6bd6b3 100644 --- a/src/main-process/comfyDesktopApp.ts +++ b/src/main-process/comfyDesktopApp.ts @@ -46,8 +46,7 @@ export class ComfyDesktopApp implements HasTelemetry { async buildServerArgs({ useExternalServer, COMFY_HOST, COMFY_PORT }: DevOverrides): Promise { // 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'), }; diff --git a/src/services/cmCli.ts b/src/services/cmCli.ts index f6a6b59de..bfd13483f 100644 --- a/src/services/cmCli.ts +++ b/src/services/cmCli.ts @@ -11,6 +11,7 @@ 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 @@ -18,6 +19,13 @@ export class CmCli implements HasTelemetry { this.cliPath = path.join(getAppResourcesPath(), 'ComfyUI', 'custom_nodes', 'ComfyUI-Manager', 'cm-cli.py'); } + private async buildCommandArgs(args: string[]): Promise { + if (await pathAccessible(this.cliPath)) { + return [this.cliPath, ...args]; + } + return ['-m', this.moduleName, ...args]; + } + public async runCommandAsync( args: string[], callbacks?: ProcessCallbacks, @@ -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; diff --git a/src/virtualEnvironment.ts b/src/virtualEnvironment.ts index 6ee9f388b..3f51a763d 100644 --- a/src/virtualEnvironment.ts +++ b/src/virtualEnvironment.ts @@ -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'; @@ -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; @@ -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'); @@ -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 { try { await this.createEnvironment(callbacks); @@ -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); } /** @@ -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}.` + ); + } + log.info(`Installing ComfyUIManager requirements from ${this.comfyUIManagerRequirementsPath}`); const installCmd = getPipInstallArgs({ requirementsFile: this.comfyUIManagerRequirementsPath, @@ -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); diff --git a/tests/resources/ComfyUI/manager_requirements.txt b/tests/resources/ComfyUI/manager_requirements.txt new file mode 100644 index 000000000..d47ad480e --- /dev/null +++ b/tests/resources/ComfyUI/manager_requirements.txt @@ -0,0 +1 @@ +# Dummy manager requirements file for tests diff --git a/tests/unit/install/installationManager.test.ts b/tests/unit/install/installationManager.test.ts index 7ea282702..651a95e30 100644 --- a/tests/unit/install/installationManager.test.ts +++ b/tests/unit/install/installationManager.test.ts @@ -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', })), }; }); diff --git a/tests/unit/main-process/comfyDesktopApp.test.ts b/tests/unit/main-process/comfyDesktopApp.test.ts index 69c38901b..cb657a5f5 100644 --- a/tests/unit/main-process/comfyDesktopApp.test.ts +++ b/tests/unit/main-process/comfyDesktopApp.test.ts @@ -125,6 +125,7 @@ describe('ComfyDesktopApp', () => { expect(result).toEqual({ listen: DEFAULT_SERVER_ARGS.listen, port: '8188', + 'enable-manager': '', }); }); @@ -138,6 +139,7 @@ describe('ComfyDesktopApp', () => { expect(result).toEqual({ listen: 'localhost', port: '8188', // Still uses findAvailablePort result + 'enable-manager': '', }); }); @@ -152,6 +154,7 @@ describe('ComfyDesktopApp', () => { expect(result).toEqual({ listen: DEFAULT_SERVER_ARGS.listen, port: DEFAULT_SERVER_ARGS.port, + 'enable-manager': '', }); }); }); diff --git a/tests/unit/virtualEnvironment.test.ts b/tests/unit/virtualEnvironment.test.ts index 0d02cb75f..986a9c5aa 100644 --- a/tests/unit/virtualEnvironment.test.ts +++ b/tests/unit/virtualEnvironment.test.ts @@ -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'; @@ -27,10 +28,12 @@ const mockTelemetry: ITelemetry = { const test = baseTest.extend({ 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', {