diff --git a/comfy_cli/command/install.py b/comfy_cli/command/install.py index 7dc8d86..92e93da 100755 --- a/comfy_cli/command/install.py +++ b/comfy_cli/command/install.py @@ -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 @@ -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) @@ -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]") @@ -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 @@ -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 diff --git a/tests/comfy_cli/command/test_frontend_pr.py b/tests/comfy_cli/command/test_frontend_pr.py index a557324..51035a4 100644 --- a/tests/comfy_cli/command/test_frontend_pr.py +++ b/tests/comfy_cli/command/test_frontend_pr.py @@ -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" @@ -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): @@ -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""" diff --git a/tests/comfy_cli/command/test_launch_frontend_pr.py b/tests/comfy_cli/command/test_launch_frontend_pr.py index 74871f6..b798c03 100644 --- a/tests/comfy_cli/command/test_launch_frontend_pr.py +++ b/tests/comfy_cli/command/test_launch_frontend_pr.py @@ -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 ]