|
| 1 | +# docs: docs/cli.md |
| 2 | +import pathlib |
| 3 | +import sys |
| 4 | + |
| 5 | +from finecode.wm_client import ApiClient, ApiError # ApiError used for start_runners check |
| 6 | +from finecode.wm_server import wm_lifecycle |
| 7 | +from finecode.cli_app.commands._env_setup import create_and_install_envs, EnvSetupFailed |
| 8 | +from loguru import logger |
| 9 | + |
| 10 | + |
| 11 | +class BootstrapFailed(Exception): |
| 12 | + def __init__(self, message: str) -> None: |
| 13 | + self.message = message |
| 14 | + |
| 15 | + |
| 16 | +async def bootstrap( |
| 17 | + workdir_path: pathlib.Path, |
| 18 | + recreate: bool = False, |
| 19 | + log_level: str = "INFO", |
| 20 | +) -> None: |
| 21 | + """Create the dev_workspace environment for the workspace root. |
| 22 | +
|
| 23 | + Uses the current Python process (sys.executable) as a temporary runner so |
| 24 | + that ``create_envs`` and ``install_envs`` can run before the venv exists. |
| 25 | + After bootstrap, run ``finecode prepare-envs`` to set up all other envs. |
| 26 | + """ |
| 27 | + port_file = None |
| 28 | + try: |
| 29 | + port_file = wm_lifecycle.start_own_server(workdir_path, log_level=log_level) |
| 30 | + try: |
| 31 | + port = await wm_lifecycle.wait_until_ready_from_file(port_file) |
| 32 | + except TimeoutError as exc: |
| 33 | + raise BootstrapFailed(str(exc)) from exc |
| 34 | + |
| 35 | + client = ApiClient() |
| 36 | + await client.connect("127.0.0.1", port) |
| 37 | + try: |
| 38 | + await _run(client, workdir_path, recreate) |
| 39 | + finally: |
| 40 | + await client.close() |
| 41 | + finally: |
| 42 | + if port_file is not None and port_file.exists(): |
| 43 | + port_file.unlink(missing_ok=True) |
| 44 | + |
| 45 | + |
| 46 | +async def _run( |
| 47 | + client: ApiClient, |
| 48 | + workdir_path: pathlib.Path, |
| 49 | + recreate: bool, |
| 50 | +) -> None: |
| 51 | + venv_dir = workdir_path / ".venvs" / "dev_workspace" |
| 52 | + workdir_str = str(workdir_path) |
| 53 | + |
| 54 | + if venv_dir.exists() and not recreate: |
| 55 | + logger.info( |
| 56 | + f"dev_workspace already exists at '{venv_dir}'. " |
| 57 | + "Use --recreate to delete and recreate it." |
| 58 | + ) |
| 59 | + return |
| 60 | + |
| 61 | + # Discover projects (no runners — venv may not exist yet). |
| 62 | + logger.info("Discovering projects...") |
| 63 | + result = await client.add_dir(workdir_path, start_runners=False) |
| 64 | + projects: list[dict] = result.get("projects", []) |
| 65 | + |
| 66 | + current_project = next((p for p in projects if p["path"] == workdir_str), None) |
| 67 | + if current_project is None: |
| 68 | + raise BootstrapFailed( |
| 69 | + "bootstrap must be run from the workspace/project root" |
| 70 | + ) |
| 71 | + if current_project["status"] == "CONFIG_INVALID": |
| 72 | + raise BootstrapFailed( |
| 73 | + f"Project '{current_project['name']}' has invalid configuration" |
| 74 | + ) |
| 75 | + |
| 76 | + if venv_dir.exists(): |
| 77 | + # recreate=True (already handled False above) |
| 78 | + logger.info(f"Removing existing dev_workspace at '{venv_dir}'...") |
| 79 | + try: |
| 80 | + await client.remove_env(workdir_str, "dev_workspace") |
| 81 | + except ApiError as exc: |
| 82 | + raise BootstrapFailed( |
| 83 | + f"Failed to remove existing dev_workspace: {exc}" |
| 84 | + ) from exc |
| 85 | + |
| 86 | + # Start the dev_workspace runner using the current Python executable. |
| 87 | + # This works even though the venv doesn't exist yet because sys.executable |
| 88 | + # already has finecode and all handlers installed (e.g. via pipx/uvx). |
| 89 | + logger.info(f"Starting temporary runner using {sys.executable}...") |
| 90 | + try: |
| 91 | + await client.start_runners( |
| 92 | + projects=[workdir_str], |
| 93 | + python_overrides={"dev_workspace": sys.executable}, |
| 94 | + ) |
| 95 | + except ApiError as exc: |
| 96 | + raise BootstrapFailed(f"Failed to start runner: {exc}") from exc |
| 97 | + |
| 98 | + dw_env = { |
| 99 | + "name": "dev_workspace", |
| 100 | + "venv_dir_path": venv_dir.as_uri(), |
| 101 | + "project_def_path": (workdir_path / "pyproject.toml").as_uri(), |
| 102 | + } |
| 103 | + |
| 104 | + logger.info("Creating dev_workspace virtualenv and installing dependencies...") |
| 105 | + try: |
| 106 | + await create_and_install_envs( |
| 107 | + client=client, |
| 108 | + project_path=workdir_str, |
| 109 | + envs=[dw_env], |
| 110 | + dev_env="cli", |
| 111 | + ) |
| 112 | + except EnvSetupFailed as exc: |
| 113 | + raise BootstrapFailed(exc.message) from exc |
| 114 | + |
| 115 | + logger.info( |
| 116 | + f"Bootstrap complete. dev_workspace created at '{venv_dir}'.\n" |
| 117 | + "Next step: run 'finecode prepare-envs' to set up all other environments." |
| 118 | + ) |
| 119 | + |
| 120 | + |
| 121 | +__all__ = ["bootstrap", "BootstrapFailed"] |
0 commit comments