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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ Comfy provides commands that allow you to easily run the installed ComfyUI.
- If you want to run ComfyUI with a specific pull request, you can use the `--pr` option. This will automatically install the specified pull request and run ComfyUI with it.
- Important: When using --pr, any --version and --commit parameters are ignored. The PR branch will be checked out regardless of version settings.

- To test a frontend pull request:

```
comfy launch --frontend-pr "#456"
comfy launch --frontend-pr "username:branch-name"
comfy launch --frontend-pr "https://github.com/Comfy-Org/ComfyUI_frontend/pull/456"
```

- The `--frontend-pr` option allows you to test frontend PRs by automatically cloning, building, and using the frontend for that session.
- Requirements: Node.js and npm must be installed to build the frontend.
- Builds are cached for quick switching between PRs - subsequent uses of the same PR are instant.
- Each PR is used only for that launch session. Normal launches use the default frontend.

**Managing PR cache**:
```
comfy pr-cache list # List cached PR builds
comfy pr-cache clean # Clean all cached builds
comfy pr-cache clean 456 # Clean specific PR cache
```

- Cache automatically expires after 7 days
- Maximum of 10 PR builds are kept (oldest are removed automatically)
- Cache limits help manage disk space while keeping recent builds available

### Managing Custom Nodes

comfy provides a convenient way to manage custom nodes for extending ComfyUI's functionality. Here are some examples:
Expand Down
17 changes: 14 additions & 3 deletions comfy_cli/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from rich.console import Console

from comfy_cli import constants, env_checker, logging, tracking, ui, utils
from comfy_cli.command import custom_nodes
from comfy_cli.command import custom_nodes, pr_command
from comfy_cli.command import install as install_inner
from comfy_cli.command import run as run_inner
from comfy_cli.command.install import validate_version
Expand Down Expand Up @@ -485,10 +485,18 @@ def stop():
@app.command(help="Launch ComfyUI: ?[--background] ?[-- <extra args ...>]")
@tracking.track_command()
def launch(
background: Annotated[bool, typer.Option(help="Launch ComfyUI in background")] = False,
extra: list[str] = typer.Argument(None),
background: Annotated[bool, typer.Option(help="Launch ComfyUI in background")] = False,
frontend_pr: Annotated[
Optional[str],
typer.Option(
"--frontend-pr",
show_default=False,
help="Use a specific frontend PR. Supports formats: username:branch, #123, or PR URL",
),
] = None,
):
launch_command(background, extra)
launch_command(background, extra, frontend_pr)


@app.command("set-default", help="Set default ComfyUI path")
Expand Down Expand Up @@ -658,4 +666,7 @@ def standalone(
app.add_typer(models_command.app, name="model", help="Manage models.")
app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.")
app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.")

app.add_typer(pr_command.app, name="pr-cache", help="Manage PR cache.")

app.add_typer(tracking.app, name="tracking", help="Manage analytics tracking settings.")
169 changes: 169 additions & 0 deletions comfy_cli/command/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,172 @@ def find_pr_by_branch(repo_owner: str, repo_name: str, username: str, branch: st

except requests.RequestException:
return None


def verify_node_tools() -> bool:
"""Verify that Node.js, npm, and vite are available"""
try:
# Check Node.js
node_result = subprocess.run(["node", "--version"], capture_output=True, text=True, check=False)
if node_result.returncode != 0:
rprint("[bold red]Node.js is not installed.[/bold red]")
rprint("[yellow]To use --frontend-pr, please install Node.js first:[/yellow]")
rprint(" • Download from: https://nodejs.org/")
rprint(" • Or use a package manager:")
rprint(" - macOS: brew install node")
rprint(" - Ubuntu/Debian: sudo apt install nodejs npm")
rprint(" - Windows: winget install OpenJS.NodeJS")
return False

node_version = node_result.stdout.strip()
rprint(f"[green]Found Node.js {node_version}[/green]")

# Check npm
npm_result = subprocess.run(["npm", "--version"], capture_output=True, text=True, check=False)
if npm_result.returncode != 0:
rprint("[bold red]npm is not installed.[/bold red]")
rprint("[yellow]npm usually comes with Node.js. Try reinstalling Node.js.[/yellow]")
return False

npm_version = npm_result.stdout.strip()
rprint(f"[green]Found npm {npm_version}[/green]")

return True
except FileNotFoundError as e:
rprint(f"[bold red]Error checking Node.js tools: {e}[/bold red]")
return False


def handle_temporary_frontend_pr(frontend_pr: str) -> Optional[str]:
"""Handle temporary frontend PR for launch - returns path to built frontend"""
from comfy_cli.pr_cache import PRCache

rprint("\n[bold blue]Preparing frontend PR for launch...[/bold blue]")

# Verify Node.js tools first
if not verify_node_tools():
rprint("[bold red]Cannot build frontend without Node.js and npm[/bold red]")
return None

# Parse frontend PR reference
try:
repo_owner, repo_name, pr_number = parse_frontend_pr_reference(frontend_pr)
except ValueError as e:
rprint(f"[bold red]Error parsing frontend PR reference: {e}[/bold red]")
return None

# Fetch PR info
try:
if pr_number:
pr_info = fetch_pr_info(repo_owner, repo_name, pr_number)
else:
username, branch = frontend_pr.split(":", 1)
pr_info = find_pr_by_branch(repo_owner, repo_name, username, branch)

if not pr_info:
rprint(f"[bold red]Frontend PR not found: {frontend_pr}[/bold red]")
return None
except Exception as e:
rprint(f"[bold red]Error fetching frontend PR information: {e}[/bold red]")
return None

# Check cache first
cache = PRCache()
cached_path = cache.get_cached_frontend_path(pr_info)
if cached_path:
rprint(f"[bold green]Using cached frontend build for PR #{pr_info.number}[/bold green]")
rprint(f"[bold green]PR #{pr_info.number}: {pr_info.title} by {pr_info.user}[/bold green]")
return str(cached_path)

# Need to build - show PR info
console.print(
Panel(
f"[bold]Frontend PR #{pr_info.number}[/bold]: {pr_info.title}\n"
f"[yellow]Author[/yellow]: {pr_info.user}\n"
f"[yellow]Branch[/yellow]: {pr_info.head_branch}\n"
f"[yellow]Source[/yellow]: {pr_info.head_repo_url}",
title="[bold blue]Building Frontend PR[/bold blue]",
border_style="blue",
)
)

# Build in cache directory
cache_path = cache.get_frontend_cache_path(pr_info)
cache_path.mkdir(parents=True, exist_ok=True)

# Clone or update repository
repo_path = cache_path / "repo"
if not (repo_path / ".git").exists():
rprint("Cloning frontend repository...")
clone_comfyui(url=pr_info.base_repo_url, repo_dir=str(repo_path))

# Checkout PR
rprint(f"Checking out PR #{pr_info.number}...")
success = checkout_pr(str(repo_path), pr_info)
if not success:
rprint("[bold red]Failed to checkout frontend PR[/bold red]")
return None

# Build frontend
rprint("\n[bold yellow]Building frontend (this may take a moment)...[/bold yellow]")
original_dir = os.getcwd()
try:
os.chdir(repo_path)

# Run npm install
rprint("Running npm install...")
npm_install = subprocess.run(["npm", "install"], capture_output=True, text=True, check=False)
if npm_install.returncode != 0:
rprint(f"[bold red]npm install failed:[/bold red]\n{npm_install.stderr}")
return None

# Build with vite
rprint("Building with vite...")
vite_build = subprocess.run(["npx", "vite", "build"], capture_output=True, text=True, check=False)
if vite_build.returncode != 0:
rprint(f"[bold red]vite build failed:[/bold red]\n{vite_build.stderr}")
return None

# Check if dist exists
dist_path = repo_path / "dist"
if dist_path.exists():
# Save cache info
cache.save_cache_info(pr_info, cache_path)
rprint("[bold green]✓ Frontend built and cached successfully[/bold green]")
rprint(f"[bold green]Using frontend from PR #{pr_info.number}: {pr_info.title}[/bold green]")
rprint(f"[dim]Cache will expire in {cache.DEFAULT_MAX_CACHE_AGE_DAYS} days[/dim]")
return str(dist_path)
else:
rprint("[bold red]Frontend build completed but dist folder not found[/bold red]")
return None

finally:
os.chdir(original_dir)


def parse_frontend_pr_reference(pr_ref: str) -> tuple[str, str, Optional[int]]:
"""
Parse frontend PR reference. Similar to parse_pr_reference but defaults to Comfy-Org/ComfyUI_frontend
"""
pr_ref = pr_ref.strip()

if pr_ref.startswith("https://github.com/"):
parsed = urlparse(pr_ref)
if "/pull/" in parsed.path:
path_parts = parsed.path.strip("/").split("/")
if len(path_parts) >= 4:
repo_owner = path_parts[0]
repo_name = path_parts[1]
pr_number = int(path_parts[3])
return repo_owner, repo_name, pr_number

elif pr_ref.startswith("#"):
pr_number = int(pr_ref[1:])
return "Comfy-Org", "ComfyUI_frontend", pr_number

elif ":" in pr_ref:
username, branch = pr_ref.split(":", 1)
return "Comfy-Org", "ComfyUI_frontend", None

else:
raise ValueError(f"Invalid frontend PR reference format: {pr_ref}")
31 changes: 26 additions & 5 deletions comfy_cli/command/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
console = Console()


def launch_comfyui(extra):
def launch_comfyui(extra, frontend_pr=None):
reboot_path = None

new_env = os.environ.copy()
Expand All @@ -36,6 +36,20 @@ def launch_comfyui(extra):

extra = extra if extra is not None else []

# Handle temporary frontend PR
if frontend_pr:
from comfy_cli.command.install import handle_temporary_frontend_pr

try:
frontend_path = handle_temporary_frontend_pr(frontend_pr)
if frontend_path:
# Check if --front-end-root is not already specified
if not any(arg.startswith("--front-end-root") for arg in extra):
extra = ["--front-end-root", frontend_path] + extra
except Exception as e:
print(f"[bold red]Failed to prepare frontend PR: {e}[/bold red]")
# Continue with default frontend

process = None

if "COMFY_CLI_BACKGROUND" not in os.environ:
Expand Down Expand Up @@ -107,6 +121,7 @@ def redirector_stdout():
def launch(
background: bool = False,
extra: list[str] | None = None,
frontend_pr: str | None = None,
):
check_for_updates()
resolved_workspace = workspace_manager.workspace_path
Expand All @@ -133,12 +148,12 @@ def launch(

os.chdir(resolved_workspace)
if background:
background_launch(extra)
background_launch(extra, frontend_pr)
else:
launch_comfyui(extra)
launch_comfyui(extra, frontend_pr)


def background_launch(extra):
def background_launch(extra, frontend_pr=None):
config_background = ConfigManager().background
if config_background is not None and utils.is_running(config_background[2]):
console.print(
Expand Down Expand Up @@ -171,7 +186,13 @@ def background_launch(extra):
"comfy",
f"--workspace={os.path.abspath(os.getcwd())}",
"launch",
] + extra
]

# Add frontend PR option if specified
if frontend_pr:
cmd.extend(["--frontend-pr", frontend_pr])

cmd.extend(extra)

loop = asyncio.get_event_loop()
log = loop.run_until_complete(launch_and_monitor(cmd, listen, port))
Expand Down
85 changes: 85 additions & 0 deletions comfy_cli/command/pr_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""PR cache management commands.

This module provides CLI commands for managing the PR cache, including:
- Listing cached PR builds
- Cleaning specific or all cached builds
- Displaying cache information in a user-friendly format
"""

import typer
from rich import print as rprint
from rich.console import Console
from rich.table import Table

from comfy_cli import tracking
from comfy_cli.pr_cache import PRCache

app = typer.Typer(help="Manage PR cache")
console = Console()


@app.command("list", help="List cached PR builds")
@tracking.track_command()
def list_cached() -> None:
"""List all cached PR builds."""
cache = PRCache()
cached_frontends = cache.list_cached_frontends()

if not cached_frontends:
rprint("[yellow]No cached PR builds found[/yellow]")
return

table = Table(title="Cached Frontend PR Builds")
table.add_column("PR #", style="cyan")
table.add_column("Title", style="white")
table.add_column("Author", style="green")
table.add_column("Age", style="yellow")
table.add_column("Size (MB)", style="magenta")

for info in cached_frontends:
age = cache.get_cache_age(info.get("cached_at", ""))
table.add_row(
str(info.get("pr_number", "?")),
info.get("pr_title", "Unknown")[:50], # Truncate long titles
info.get("user", "Unknown"),
age,
f"{info.get('size_mb', 0):.1f}",
)

console.print(table)

# Show cache settings
rprint(
f"\n[dim]Cache settings: Max age: {cache.DEFAULT_MAX_CACHE_AGE_DAYS} days, "
f"Max items: {cache.DEFAULT_MAX_CACHE_ITEMS}[/dim]"
)


@app.command("clean", help="Clean PR cache")
@tracking.track_command()
def clean_cache(
pr_number: int = typer.Argument(None, help="Specific PR number to clean (omit to clean all)"),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
) -> None:
"""Clean cached PR builds."""
cache = PRCache()

if pr_number:
if not yes:
confirm = typer.confirm(f"Remove cache for PR #{pr_number}?")
if not confirm:
rprint("[yellow]Cancelled[/yellow]")
return
cache.clean_frontend_cache(pr_number)
rprint(f"[green]✓ Cleaned cache for PR #{pr_number}[/green]")
else:
if not yes:
cached = cache.list_cached_frontends()
if cached:
rprint(f"[yellow]This will remove {len(cached)} cached PR build(s)[/yellow]")
confirm = typer.confirm("Remove all cached PR builds?")
if not confirm:
rprint("[yellow]Cancelled[/yellow]")
return
cache.clean_frontend_cache()
rprint("[green]✓ Cleaned all PR cache[/green]")
Loading
Loading