diff --git a/src/lightcone/cli/commands.py b/src/lightcone/cli/commands.py index 79471ff1..e528c6fd 100644 --- a/src/lightcone/cli/commands.py +++ b/src/lightcone/cli/commands.py @@ -222,12 +222,26 @@ def init( # venv if not no_venv: - subprocess.run(["python", "-m", "venv", ".venv"], cwd=directory, check=False) - subprocess.run( - [".venv/bin/python", "-m", "pip", "install", "-q", "lightcone-cli"], - cwd=directory, - check=False, - ) + if shutil.which("uv"): + with console.status("[dim]Creating virtual environment…[/dim]"): + subprocess.run(["uv", "venv", "--python", "3.12", ".venv"], cwd=directory, check=False, capture_output=True) + with console.status("[dim]Installing lightcone-cli…[/dim]"): + subprocess.run( + ["uv", "pip", "install", "--python", ".venv/bin/python", "lightcone-cli"], + cwd=directory, + check=False, + capture_output=True, + ) + else: + with console.status("[dim]Creating virtual environment…[/dim]"): + subprocess.run(["python", "-m", "venv", ".venv"], cwd=directory, check=False, capture_output=True) + with console.status("[dim]Installing lightcone-cli…[/dim]"): + subprocess.run( + [".venv/bin/python", "-m", "pip", "install", "-q", "lightcone-cli"], + cwd=directory, + check=False, + capture_output=True, + ) console.print(f"\n[green]Project initialized at[/green] {directory}") diff --git a/tests/test_cli.py b/tests/test_cli.py index 0bd6472b..5f5f0053 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,10 @@ """Tests for the redesigned lightcone CLI.""" from __future__ import annotations +import shutil +import subprocess from pathlib import Path +from unittest.mock import MagicMock import pytest from click.testing import CliRunner @@ -83,6 +86,46 @@ def test_init_refuses_when_astra_yaml_exists( assert "already exists" in result.output +def test_init_venv_uses_uv_when_available( + runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + calls: list[list[str]] = [] + + def _fake_run(cmd: list[str], **kwargs: object) -> MagicMock: + calls.append(list(cmd)) + return MagicMock(returncode=0) + + monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/uv" if name == "uv" else None) + monkeypatch.setattr(subprocess, "run", _fake_run) + + project = tmp_path / "proj" + result = runner.invoke(main, ["init", str(project), "--no-git"]) + assert result.exit_code == 0, result.output + + assert ["uv", "venv", "--python", "3.12", ".venv"] in calls + assert ["uv", "pip", "install", "--python", ".venv/bin/python", "lightcone-cli"] in calls + + +def test_init_venv_falls_back_to_python_when_uv_missing( + runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + calls: list[list[str]] = [] + + def _fake_run(cmd: list[str], **kwargs: object) -> MagicMock: + calls.append(list(cmd)) + return MagicMock(returncode=0) + + monkeypatch.setattr(shutil, "which", lambda _: None) + monkeypatch.setattr(subprocess, "run", _fake_run) + + project = tmp_path / "proj" + result = runner.invoke(main, ["init", str(project), "--no-git"]) + assert result.exit_code == 0, result.output + + assert ["python", "-m", "venv", ".venv"] in calls + assert [".venv/bin/python", "-m", "pip", "install", "-q", "lightcone-cli"] in calls + + # ---- lc verify ------------------------------------------------------------