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
57 changes: 49 additions & 8 deletions comfy_cli/command/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from rich import print as rprint
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm

from comfy_cli import constants, ui, utils
from comfy_cli.command.custom_nodes.command import update_node_id_cache
Expand Down Expand Up @@ -634,7 +635,7 @@ def find_pr_by_branch(repo_owner: str, repo_name: str, username: str, branch: st


def verify_node_tools() -> bool:
"""Verify that Node.js, npm, and vite are available"""
"""Verify that Node.js, npm, and pnpm are available for frontend building"""
try:
# Check Node.js
node_result = subprocess.run(["node", "--version"], capture_output=True, text=True, check=False)
Expand All @@ -651,7 +652,7 @@ def verify_node_tools() -> bool:
node_version = node_result.stdout.strip()
rprint(f"[green]Found Node.js {node_version}[/green]")

# Check npm
# Check npm (needed for pnpm installation)
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]")
Expand All @@ -661,9 +662,49 @@ def verify_node_tools() -> bool:
npm_version = npm_result.stdout.strip()
rprint(f"[green]Found npm {npm_version}[/green]")

# Check pnpm (required for modern frontend)
pnpm_result = subprocess.run(["pnpm", "--version"], capture_output=True, text=True, check=False)
if pnpm_result.returncode != 0:
rprint("[yellow]pnpm is not installed but is required for the modern frontend.[/yellow]")

# Ask user permission to install pnpm
install_pnpm = Confirm.ask(
"[bold yellow]Install pnpm automatically using npm?[/bold yellow] (This will run: npm install -g pnpm)"
)

if not install_pnpm:
rprint("[bold red]Cannot build frontend without pnpm.[/bold red]")
rprint("[yellow]To install manually:[/yellow]")
rprint(" npm install -g pnpm")
return False

# Install pnpm
rprint("[yellow]Installing pnpm...[/yellow]")
install_result = subprocess.run(
["npm", "install", "-g", "pnpm"], capture_output=True, text=True, check=False
)

if install_result.returncode != 0:
rprint("[bold red]Failed to install pnpm automatically.[/bold red]")
rprint(f"[red]Error: {install_result.stderr}[/red]")
rprint("[yellow]Please install manually: npm install -g pnpm[/yellow]")
return False

# Verify pnpm installation
verify_result = subprocess.run(["pnpm", "--version"], capture_output=True, text=True, check=False)
if verify_result.returncode != 0:
rprint("[bold red]pnpm installation failed to verify.[/bold red]")
return False

pnpm_version = verify_result.stdout.strip()
rprint(f"[green]Successfully installed pnpm {pnpm_version}[/green]")
else:
pnpm_version = pnpm_result.stdout.strip()
rprint(f"[green]Found pnpm {pnpm_version}[/green]")

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


Expand Down Expand Up @@ -743,11 +784,11 @@ def handle_temporary_frontend_pr(frontend_pr: str) -> Optional[str]:
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}")
# Run pnpm install
rprint("Running pnpm install...")
pnpm_install = subprocess.run(["pnpm", "install"], capture_output=True, text=True, check=False)
if pnpm_install.returncode != 0:
rprint(f"[bold red]pnpm install failed:[/bold red]\n{pnpm_install.stderr}")
return None

# Build with vite
Expand Down
69 changes: 65 additions & 4 deletions tests/comfy_cli/command/test_frontend_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ class TestNodeToolsVerification:

@patch("subprocess.run")
def test_verify_node_tools_success(self, mock_run):
"""Test successful Node.js and npm verification"""
# Mock successful node and npm commands
"""Test successful Node.js, npm, and pnpm verification"""
# Mock successful node, npm, and pnpm commands
node_result = Mock()
node_result.returncode = 0
node_result.stdout = "v18.0.0"
Expand All @@ -88,10 +88,14 @@ def test_verify_node_tools_success(self, mock_run):
npm_result.returncode = 0
npm_result.stdout = "9.0.0"

mock_run.side_effect = [node_result, npm_result]
pnpm_result = Mock()
pnpm_result.returncode = 0
pnpm_result.stdout = "8.0.0"

mock_run.side_effect = [node_result, npm_result, pnpm_result]

assert verify_node_tools() is True
assert mock_run.call_count == 2
assert mock_run.call_count == 3

@patch("subprocess.run")
def test_verify_node_tools_missing_node(self, mock_run):
Expand Down Expand Up @@ -119,6 +123,63 @@ def test_verify_node_tools_missing_npm(self, mock_run):
assert verify_node_tools() is False
assert mock_run.call_count == 2

@patch("rich.prompt.Confirm.ask")
@patch("subprocess.run")
def test_verify_node_tools_auto_install_pnpm(self, mock_run, mock_confirm):
"""Test automatic pnpm installation when user agrees"""
# Mock successful node and npm
node_result = Mock()
node_result.returncode = 0
node_result.stdout = "v18.0.0"

npm_result = Mock()
npm_result.returncode = 0
npm_result.stdout = "9.0.0"

# Mock pnpm not found initially
pnpm_missing = Mock()
pnpm_missing.returncode = 1

# Mock successful pnpm installation
install_result = Mock()
install_result.returncode = 0

# Mock pnpm verification after install
pnpm_verify = Mock()
pnpm_verify.returncode = 0
pnpm_verify.stdout = "8.0.0"

mock_run.side_effect = [node_result, npm_result, pnpm_missing, install_result, pnpm_verify]
mock_confirm.return_value = True # User agrees to install

assert verify_node_tools() is True
assert mock_run.call_count == 5
mock_confirm.assert_called_once()

@patch("rich.prompt.Confirm.ask")
@patch("subprocess.run")
def test_verify_node_tools_user_declines_pnpm_install(self, mock_run, mock_confirm):
"""Test when user declines pnpm installation"""
# Mock successful node and npm
node_result = Mock()
node_result.returncode = 0
node_result.stdout = "v18.0.0"

npm_result = Mock()
npm_result.returncode = 0
npm_result.stdout = "9.0.0"

# Mock pnpm not found
pnpm_missing = Mock()
pnpm_missing.returncode = 1

mock_run.side_effect = [node_result, npm_result, pnpm_missing]
mock_confirm.return_value = False # User declines install

assert verify_node_tools() is False
assert mock_run.call_count == 3
mock_confirm.assert_called_once()

@patch("subprocess.run")
def test_verify_node_tools_file_not_found(self, mock_run):
"""Test when commands are not found"""
Expand Down
2 changes: 1 addition & 1 deletion tests/comfy_cli/command/test_launch_frontend_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def test_launch_frontend_pr_cache_miss_builds(

# Mock successful build
mock_run.side_effect = [
Mock(returncode=0), # npm install
Mock(returncode=0), # pnpm install
Mock(returncode=0), # vite build
]

Expand Down
Loading