Skip to content

Commit dae6795

Browse files
Add frontend PR support for testing ComfyUI_frontend pull requests (#309)
1 parent a23c0e8 commit dae6795

File tree

9 files changed

+970
-19
lines changed

9 files changed

+970
-19
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,30 @@ Comfy provides commands that allow you to easily run the installed ComfyUI.
123123
- 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.
124124
- Important: When using --pr, any --version and --commit parameters are ignored. The PR branch will be checked out regardless of version settings.
125125

126+
- To test a frontend pull request:
127+
128+
```
129+
comfy launch --frontend-pr "#456"
130+
comfy launch --frontend-pr "username:branch-name"
131+
comfy launch --frontend-pr "https://github.com/Comfy-Org/ComfyUI_frontend/pull/456"
132+
```
133+
134+
- The `--frontend-pr` option allows you to test frontend PRs by automatically cloning, building, and using the frontend for that session.
135+
- Requirements: Node.js and npm must be installed to build the frontend.
136+
- Builds are cached for quick switching between PRs - subsequent uses of the same PR are instant.
137+
- Each PR is used only for that launch session. Normal launches use the default frontend.
138+
139+
**Managing PR cache**:
140+
```
141+
comfy pr-cache list # List cached PR builds
142+
comfy pr-cache clean # Clean all cached builds
143+
comfy pr-cache clean 456 # Clean specific PR cache
144+
```
145+
146+
- Cache automatically expires after 7 days
147+
- Maximum of 10 PR builds are kept (oldest are removed automatically)
148+
- Cache limits help manage disk space while keeping recent builds available
149+
126150
### Managing Custom Nodes
127151

128152
comfy provides a convenient way to manage custom nodes for extending ComfyUI's functionality. Here are some examples:

comfy_cli/cmdline.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rich.console import Console
1111

1212
from comfy_cli import constants, env_checker, logging, tracking, ui, utils
13-
from comfy_cli.command import custom_nodes
13+
from comfy_cli.command import custom_nodes, pr_command
1414
from comfy_cli.command import install as install_inner
1515
from comfy_cli.command import run as run_inner
1616
from comfy_cli.command.install import validate_version
@@ -485,10 +485,18 @@ def stop():
485485
@app.command(help="Launch ComfyUI: ?[--background] ?[-- <extra args ...>]")
486486
@tracking.track_command()
487487
def launch(
488-
background: Annotated[bool, typer.Option(help="Launch ComfyUI in background")] = False,
489488
extra: list[str] = typer.Argument(None),
489+
background: Annotated[bool, typer.Option(help="Launch ComfyUI in background")] = False,
490+
frontend_pr: Annotated[
491+
Optional[str],
492+
typer.Option(
493+
"--frontend-pr",
494+
show_default=False,
495+
help="Use a specific frontend PR. Supports formats: username:branch, #123, or PR URL",
496+
),
497+
] = None,
490498
):
491-
launch_command(background, extra)
499+
launch_command(background, extra, frontend_pr)
492500

493501

494502
@app.command("set-default", help="Set default ComfyUI path")
@@ -658,4 +666,7 @@ def standalone(
658666
app.add_typer(models_command.app, name="model", help="Manage models.")
659667
app.add_typer(custom_nodes.app, name="node", help="Manage custom nodes.")
660668
app.add_typer(custom_nodes.manager_app, name="manager", help="Manage ComfyUI-Manager.")
669+
670+
app.add_typer(pr_command.app, name="pr-cache", help="Manage PR cache.")
671+
661672
app.add_typer(tracking.app, name="tracking", help="Manage analytics tracking settings.")

comfy_cli/command/install.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,172 @@ def find_pr_by_branch(repo_owner: str, repo_name: str, username: str, branch: st
631631

632632
except requests.RequestException:
633633
return None
634+
635+
636+
def verify_node_tools() -> bool:
637+
"""Verify that Node.js, npm, and vite are available"""
638+
try:
639+
# Check Node.js
640+
node_result = subprocess.run(["node", "--version"], capture_output=True, text=True, check=False)
641+
if node_result.returncode != 0:
642+
rprint("[bold red]Node.js is not installed.[/bold red]")
643+
rprint("[yellow]To use --frontend-pr, please install Node.js first:[/yellow]")
644+
rprint(" • Download from: https://nodejs.org/")
645+
rprint(" • Or use a package manager:")
646+
rprint(" - macOS: brew install node")
647+
rprint(" - Ubuntu/Debian: sudo apt install nodejs npm")
648+
rprint(" - Windows: winget install OpenJS.NodeJS")
649+
return False
650+
651+
node_version = node_result.stdout.strip()
652+
rprint(f"[green]Found Node.js {node_version}[/green]")
653+
654+
# Check npm
655+
npm_result = subprocess.run(["npm", "--version"], capture_output=True, text=True, check=False)
656+
if npm_result.returncode != 0:
657+
rprint("[bold red]npm is not installed.[/bold red]")
658+
rprint("[yellow]npm usually comes with Node.js. Try reinstalling Node.js.[/yellow]")
659+
return False
660+
661+
npm_version = npm_result.stdout.strip()
662+
rprint(f"[green]Found npm {npm_version}[/green]")
663+
664+
return True
665+
except FileNotFoundError as e:
666+
rprint(f"[bold red]Error checking Node.js tools: {e}[/bold red]")
667+
return False
668+
669+
670+
def handle_temporary_frontend_pr(frontend_pr: str) -> Optional[str]:
671+
"""Handle temporary frontend PR for launch - returns path to built frontend"""
672+
from comfy_cli.pr_cache import PRCache
673+
674+
rprint("\n[bold blue]Preparing frontend PR for launch...[/bold blue]")
675+
676+
# Verify Node.js tools first
677+
if not verify_node_tools():
678+
rprint("[bold red]Cannot build frontend without Node.js and npm[/bold red]")
679+
return None
680+
681+
# Parse frontend PR reference
682+
try:
683+
repo_owner, repo_name, pr_number = parse_frontend_pr_reference(frontend_pr)
684+
except ValueError as e:
685+
rprint(f"[bold red]Error parsing frontend PR reference: {e}[/bold red]")
686+
return None
687+
688+
# Fetch PR info
689+
try:
690+
if pr_number:
691+
pr_info = fetch_pr_info(repo_owner, repo_name, pr_number)
692+
else:
693+
username, branch = frontend_pr.split(":", 1)
694+
pr_info = find_pr_by_branch(repo_owner, repo_name, username, branch)
695+
696+
if not pr_info:
697+
rprint(f"[bold red]Frontend PR not found: {frontend_pr}[/bold red]")
698+
return None
699+
except Exception as e:
700+
rprint(f"[bold red]Error fetching frontend PR information: {e}[/bold red]")
701+
return None
702+
703+
# Check cache first
704+
cache = PRCache()
705+
cached_path = cache.get_cached_frontend_path(pr_info)
706+
if cached_path:
707+
rprint(f"[bold green]Using cached frontend build for PR #{pr_info.number}[/bold green]")
708+
rprint(f"[bold green]PR #{pr_info.number}: {pr_info.title} by {pr_info.user}[/bold green]")
709+
return str(cached_path)
710+
711+
# Need to build - show PR info
712+
console.print(
713+
Panel(
714+
f"[bold]Frontend PR #{pr_info.number}[/bold]: {pr_info.title}\n"
715+
f"[yellow]Author[/yellow]: {pr_info.user}\n"
716+
f"[yellow]Branch[/yellow]: {pr_info.head_branch}\n"
717+
f"[yellow]Source[/yellow]: {pr_info.head_repo_url}",
718+
title="[bold blue]Building Frontend PR[/bold blue]",
719+
border_style="blue",
720+
)
721+
)
722+
723+
# Build in cache directory
724+
cache_path = cache.get_frontend_cache_path(pr_info)
725+
cache_path.mkdir(parents=True, exist_ok=True)
726+
727+
# Clone or update repository
728+
repo_path = cache_path / "repo"
729+
if not (repo_path / ".git").exists():
730+
rprint("Cloning frontend repository...")
731+
clone_comfyui(url=pr_info.base_repo_url, repo_dir=str(repo_path))
732+
733+
# Checkout PR
734+
rprint(f"Checking out PR #{pr_info.number}...")
735+
success = checkout_pr(str(repo_path), pr_info)
736+
if not success:
737+
rprint("[bold red]Failed to checkout frontend PR[/bold red]")
738+
return None
739+
740+
# Build frontend
741+
rprint("\n[bold yellow]Building frontend (this may take a moment)...[/bold yellow]")
742+
original_dir = os.getcwd()
743+
try:
744+
os.chdir(repo_path)
745+
746+
# Run npm install
747+
rprint("Running npm install...")
748+
npm_install = subprocess.run(["npm", "install"], capture_output=True, text=True, check=False)
749+
if npm_install.returncode != 0:
750+
rprint(f"[bold red]npm install failed:[/bold red]\n{npm_install.stderr}")
751+
return None
752+
753+
# Build with vite
754+
rprint("Building with vite...")
755+
vite_build = subprocess.run(["npx", "vite", "build"], capture_output=True, text=True, check=False)
756+
if vite_build.returncode != 0:
757+
rprint(f"[bold red]vite build failed:[/bold red]\n{vite_build.stderr}")
758+
return None
759+
760+
# Check if dist exists
761+
dist_path = repo_path / "dist"
762+
if dist_path.exists():
763+
# Save cache info
764+
cache.save_cache_info(pr_info, cache_path)
765+
rprint("[bold green]✓ Frontend built and cached successfully[/bold green]")
766+
rprint(f"[bold green]Using frontend from PR #{pr_info.number}: {pr_info.title}[/bold green]")
767+
rprint(f"[dim]Cache will expire in {cache.DEFAULT_MAX_CACHE_AGE_DAYS} days[/dim]")
768+
return str(dist_path)
769+
else:
770+
rprint("[bold red]Frontend build completed but dist folder not found[/bold red]")
771+
return None
772+
773+
finally:
774+
os.chdir(original_dir)
775+
776+
777+
def parse_frontend_pr_reference(pr_ref: str) -> tuple[str, str, Optional[int]]:
778+
"""
779+
Parse frontend PR reference. Similar to parse_pr_reference but defaults to Comfy-Org/ComfyUI_frontend
780+
"""
781+
pr_ref = pr_ref.strip()
782+
783+
if pr_ref.startswith("https://github.com/"):
784+
parsed = urlparse(pr_ref)
785+
if "/pull/" in parsed.path:
786+
path_parts = parsed.path.strip("/").split("/")
787+
if len(path_parts) >= 4:
788+
repo_owner = path_parts[0]
789+
repo_name = path_parts[1]
790+
pr_number = int(path_parts[3])
791+
return repo_owner, repo_name, pr_number
792+
793+
elif pr_ref.startswith("#"):
794+
pr_number = int(pr_ref[1:])
795+
return "Comfy-Org", "ComfyUI_frontend", pr_number
796+
797+
elif ":" in pr_ref:
798+
username, branch = pr_ref.split(":", 1)
799+
return "Comfy-Org", "ComfyUI_frontend", None
800+
801+
else:
802+
raise ValueError(f"Invalid frontend PR reference format: {pr_ref}")

comfy_cli/command/launch.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
console = Console()
2323

2424

25-
def launch_comfyui(extra):
25+
def launch_comfyui(extra, frontend_pr=None):
2626
reboot_path = None
2727

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

3737
extra = extra if extra is not None else []
3838

39+
# Handle temporary frontend PR
40+
if frontend_pr:
41+
from comfy_cli.command.install import handle_temporary_frontend_pr
42+
43+
try:
44+
frontend_path = handle_temporary_frontend_pr(frontend_pr)
45+
if frontend_path:
46+
# Check if --front-end-root is not already specified
47+
if not any(arg.startswith("--front-end-root") for arg in extra):
48+
extra = ["--front-end-root", frontend_path] + extra
49+
except Exception as e:
50+
print(f"[bold red]Failed to prepare frontend PR: {e}[/bold red]")
51+
# Continue with default frontend
52+
3953
process = None
4054

4155
if "COMFY_CLI_BACKGROUND" not in os.environ:
@@ -107,6 +121,7 @@ def redirector_stdout():
107121
def launch(
108122
background: bool = False,
109123
extra: list[str] | None = None,
124+
frontend_pr: str | None = None,
110125
):
111126
check_for_updates()
112127
resolved_workspace = workspace_manager.workspace_path
@@ -133,12 +148,12 @@ def launch(
133148

134149
os.chdir(resolved_workspace)
135150
if background:
136-
background_launch(extra)
151+
background_launch(extra, frontend_pr)
137152
else:
138-
launch_comfyui(extra)
153+
launch_comfyui(extra, frontend_pr)
139154

140155

141-
def background_launch(extra):
156+
def background_launch(extra, frontend_pr=None):
142157
config_background = ConfigManager().background
143158
if config_background is not None and utils.is_running(config_background[2]):
144159
console.print(
@@ -171,7 +186,13 @@ def background_launch(extra):
171186
"comfy",
172187
f"--workspace={os.path.abspath(os.getcwd())}",
173188
"launch",
174-
] + extra
189+
]
190+
191+
# Add frontend PR option if specified
192+
if frontend_pr:
193+
cmd.extend(["--frontend-pr", frontend_pr])
194+
195+
cmd.extend(extra)
175196

176197
loop = asyncio.get_event_loop()
177198
log = loop.run_until_complete(launch_and_monitor(cmd, listen, port))

comfy_cli/command/pr_command.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""PR cache management commands.
2+
3+
This module provides CLI commands for managing the PR cache, including:
4+
- Listing cached PR builds
5+
- Cleaning specific or all cached builds
6+
- Displaying cache information in a user-friendly format
7+
"""
8+
9+
import typer
10+
from rich import print as rprint
11+
from rich.console import Console
12+
from rich.table import Table
13+
14+
from comfy_cli import tracking
15+
from comfy_cli.pr_cache import PRCache
16+
17+
app = typer.Typer(help="Manage PR cache")
18+
console = Console()
19+
20+
21+
@app.command("list", help="List cached PR builds")
22+
@tracking.track_command()
23+
def list_cached() -> None:
24+
"""List all cached PR builds."""
25+
cache = PRCache()
26+
cached_frontends = cache.list_cached_frontends()
27+
28+
if not cached_frontends:
29+
rprint("[yellow]No cached PR builds found[/yellow]")
30+
return
31+
32+
table = Table(title="Cached Frontend PR Builds")
33+
table.add_column("PR #", style="cyan")
34+
table.add_column("Title", style="white")
35+
table.add_column("Author", style="green")
36+
table.add_column("Age", style="yellow")
37+
table.add_column("Size (MB)", style="magenta")
38+
39+
for info in cached_frontends:
40+
age = cache.get_cache_age(info.get("cached_at", ""))
41+
table.add_row(
42+
str(info.get("pr_number", "?")),
43+
info.get("pr_title", "Unknown")[:50], # Truncate long titles
44+
info.get("user", "Unknown"),
45+
age,
46+
f"{info.get('size_mb', 0):.1f}",
47+
)
48+
49+
console.print(table)
50+
51+
# Show cache settings
52+
rprint(
53+
f"\n[dim]Cache settings: Max age: {cache.DEFAULT_MAX_CACHE_AGE_DAYS} days, "
54+
f"Max items: {cache.DEFAULT_MAX_CACHE_ITEMS}[/dim]"
55+
)
56+
57+
58+
@app.command("clean", help="Clean PR cache")
59+
@tracking.track_command()
60+
def clean_cache(
61+
pr_number: int = typer.Argument(None, help="Specific PR number to clean (omit to clean all)"),
62+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
63+
) -> None:
64+
"""Clean cached PR builds."""
65+
cache = PRCache()
66+
67+
if pr_number:
68+
if not yes:
69+
confirm = typer.confirm(f"Remove cache for PR #{pr_number}?")
70+
if not confirm:
71+
rprint("[yellow]Cancelled[/yellow]")
72+
return
73+
cache.clean_frontend_cache(pr_number)
74+
rprint(f"[green]✓ Cleaned cache for PR #{pr_number}[/green]")
75+
else:
76+
if not yes:
77+
cached = cache.list_cached_frontends()
78+
if cached:
79+
rprint(f"[yellow]This will remove {len(cached)} cached PR build(s)[/yellow]")
80+
confirm = typer.confirm("Remove all cached PR builds?")
81+
if not confirm:
82+
rprint("[yellow]Cancelled[/yellow]")
83+
return
84+
cache.clean_frontend_cache()
85+
rprint("[green]✓ Cleaned all PR cache[/green]")

0 commit comments

Comments
 (0)