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
4,927 changes: 3,452 additions & 1,475 deletions frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/comfygit-panel.css

Large diffs are not rendered by default.

22,054 changes: 11,810 additions & 10,244 deletions js/comfygit-panel.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "comfygit-manager"
version = "0.0.9"
version = "0.0.10"
description = "ComfyGit Manager - Node for managing comfygit environments in ComfyUI"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
30 changes: 26 additions & 4 deletions scripts/comfygit-dev
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,34 @@

set -e

# Resolve symlinks to get actual script location
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
# Resolve symlinks to get actual script location (portable across macOS/Linux)
if command -v realpath &> /dev/null; then
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
elif command -v readlink &> /dev/null && readlink -f / &> /dev/null; then
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
else
# Fallback for macOS without coreutils
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
fi
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
MANAGER_PATH="$(dirname "$SCRIPT_DIR")"
CORE_PATH="/home/akatzfey/projects/comfyhub/comfygit/packages/core"
DEFAULT_COMFYUI="/home/akatzfey/projects/ComfyUI_dev/ComfyUI"

# Auto-detect CORE_PATH relative to manager (sibling repo)
# Expected structure: comfyhub/comfygit-manager and comfyhub/comfygit/packages/core
CORE_PATH="$(dirname "$MANAGER_PATH")/comfygit/packages/core"
if [[ ! -d "$CORE_PATH" ]]; then
echo "Error: comfygit-core not found at $CORE_PATH"
echo "Expected directory structure:"
echo " comfyhub/"
echo " comfygit-manager/ (this repo)"
echo " comfygit/"
echo " packages/"
echo " core/"
exit 1
fi

# Default ComfyUI path (can be overridden by first argument)
DEFAULT_COMFYUI="$HOME/ComfyUI"
RUNPOD_IMAGE="runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04"

# Parse arguments
Expand Down
72 changes: 61 additions & 11 deletions server/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,29 +539,75 @@ def _get_venv_executables(venv_path: Path) -> tuple[Path, Path]:
)


def ensure_orchestrator_venv(venv_path: Path) -> None:
def _create_venv_with_uv(venv_path: Path, python_exe: Path) -> bool:
"""
Create dedicated virtual environment for orchestrator.
This runs once when custom node first loads.
Create venv using UVCommand from comfygit-core.

Uses bundled uv binary which creates symlinks on macOS,
avoiding the @executable_path dylib issue.

Returns True on success, False if uv unavailable.
"""
python_exe, pip_exe = _get_venv_executables(venv_path)
try:
from comfygit_core.integrations.uv_command import UVCommand

if venv_path.exists() and python_exe.exists():
return # Already set up
uv = UVCommand()
uv.venv(venv_path, python="3.12")

print("[ComfyGit] Setting up orchestrator environment...")
# Install comfygit-core using uv pip (doesn't need pip in venv)
dev_core_path = os.environ.get("COMFYGIT_DEV_CORE_PATH")
if dev_core_path:
print(f"[ComfyGit] Dev mode: installing core from {dev_core_path}")
uv.pip_install(["-e", dev_core_path], python=python_exe)
else:
uv.pip_install(["comfygit-core"], python=python_exe)

return True

# Create venv
venv.create(venv_path, with_pip=True, clear=True)
except ImportError:
print("[ComfyGit] UVCommand not available, falling back to venv module")
return False
except Exception as e:
print(f"[ComfyGit] uv venv failed ({e}), falling back to venv module")
return False


def _create_venv_with_stdlib(venv_path: Path, pip_exe: Path) -> None:
"""
Create venv using stdlib venv module with symlinks=True.

Symlinks avoid the macOS @executable_path dylib issue by keeping
the python binary in its original location where the dylib exists.
"""
venv.create(venv_path, with_pip=True, clear=True, symlinks=True)

# Dev mode: install from local path as editable
dev_core_path = os.environ.get("COMFYGIT_DEV_CORE_PATH")
if dev_core_path:
print(f"[ComfyGit] Dev mode: installing core from {dev_core_path}")
subprocess.run([str(pip_exe), "install", "-e", dev_core_path, "--quiet"], check=True)
else:
subprocess.run([str(pip_exe), "install", "comfygit-core", "--quiet"], check=True)


def ensure_orchestrator_venv(venv_path: Path) -> None:
"""
Create dedicated virtual environment for orchestrator.
This runs once when custom node first loads.

Uses uv (bundled with comfygit-core) if available, falls back to stdlib venv.
Both methods use symlinks to avoid macOS dylib loading issues.
"""
python_exe, pip_exe = _get_venv_executables(venv_path)

if venv_path.exists() and python_exe.exists():
return # Already set up

print("[ComfyGit] Setting up orchestrator environment...")

# Try uv first (preferred), fall back to stdlib venv
if not _create_venv_with_uv(venv_path, python_exe):
_create_venv_with_stdlib(venv_path, pip_exe)

print("[ComfyGit] Orchestrator environment ready")


Expand Down Expand Up @@ -884,8 +930,12 @@ def _get_comfyui_backend_flags(self, env: Environment) -> list[str]:
backend = version.split('+')[1]
print(f"[Orchestrator] Detected PyTorch backend: {backend}")
return []
elif sys.platform == "darwin":
# macOS: no suffix means MPS build (Apple Silicon GPU)
print("[Orchestrator] Detected macOS PyTorch (MPS)")
return []
else:
# No suffix = CPU-only build
# Linux/Windows without suffix = CPU-only build
print("[Orchestrator] Detected CPU-only PyTorch, adding --cpu flag")
return ["--cpu"]

Expand Down
69 changes: 35 additions & 34 deletions testing/unit/test_orchestrator_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,6 @@
class TestOrchestratorVenv:
"""Test orchestrator venv creation and setup."""

def test_ensure_orchestrator_venv_creates_venv(self, temp_dir, mocker):
"""Should create orchestrator venv if it doesn't exist."""
from server.orchestrator import ensure_orchestrator_venv

venv_path = temp_dir / ".orchestrator_venv"

# Mock venv.create and subprocess.run to avoid actual venv creation
mocker.patch("venv.create")
mocker.patch("subprocess.run")

# Create the expected directory structure to simulate venv creation
venv_path.mkdir()
(venv_path / "bin").mkdir()
(venv_path / "bin" / "python").touch()
(venv_path / "bin" / "pip").touch()

ensure_orchestrator_venv(venv_path)

# Verify venv was not recreated (idempotent check)
assert venv_path.exists()
assert (venv_path / "bin" / "python").exists()
assert (venv_path / "bin" / "pip").exists()

def test_ensure_orchestrator_venv_idempotent(self, mock_orchestrator_venv):
"""Should not recreate venv if it already exists."""
from server.orchestrator import ensure_orchestrator_venv
Expand All @@ -46,30 +23,54 @@ def test_ensure_orchestrator_venv_idempotent(self, mock_orchestrator_venv):
assert marker.exists()
assert marker.stat().st_mtime == original_mtime

def test_ensure_orchestrator_venv_installs_comfygit_core(self, temp_dir, mocker):
"""Should install comfygit-core in orchestrator venv."""
def test_ensure_orchestrator_venv_tries_uv_first(self, temp_dir, mocker):
"""Should try UVCommand first, fall back to venv module on failure."""
from server.orchestrator import ensure_orchestrator_venv

venv_path = temp_dir / ".orchestrator_venv"

# Mock venv.create to avoid actual venv creation
# Mock _create_venv_with_uv to succeed
mock_uv_create = mocker.patch(
"server.orchestrator._create_venv_with_uv",
return_value=True
)

# Mock venv.create as fallback (shouldn't be called if uv succeeds)
mock_venv_create = mocker.patch("venv.create")

# Mock subprocess to capture pip install
mock_run = mocker.patch("subprocess.run")
ensure_orchestrator_venv(venv_path)

# _create_venv_with_uv should have been called
mock_uv_create.assert_called_once()
# venv.create should NOT have been called (uv succeeded)
mock_venv_create.assert_not_called()

def test_ensure_orchestrator_venv_falls_back_to_stdlib(self, temp_dir, mocker):
"""Should fall back to venv module if UVCommand fails."""
from server.orchestrator import ensure_orchestrator_venv

# Create minimal directory structure so function doesn't early return
# (but don't create python executable so it goes through creation path)
venv_path = temp_dir / ".orchestrator_venv"

# Make UVCommand raise an exception to trigger fallback
mocker.patch(
"server.orchestrator._create_venv_with_uv",
return_value=False
)

# Mock venv.create and subprocess.run for fallback
mock_venv_create = mocker.patch("venv.create")
mock_run = mocker.patch("subprocess.run")

ensure_orchestrator_venv(venv_path)

# Verify venv.create was called
mock_venv_create.assert_called_once_with(venv_path, with_pip=True, clear=True)
# venv.create should have been called with symlinks=True
mock_venv_create.assert_called_once_with(
venv_path, with_pip=True, clear=True, symlinks=True
)

# Verify pip install was called with comfygit-core
# pip install should have been called
mock_run.assert_called_once()
args = mock_run.call_args[0][0]
assert "pip" in str(args[0]) # First arg should be pip executable
assert "install" in args
assert "comfygit-core" in args

Expand Down
Loading
Loading