|
| 1 | +import json |
| 2 | +import shutil |
| 3 | +import tempfile |
| 4 | +import time |
| 5 | +import zipfile |
| 6 | +from contextlib import contextmanager |
| 7 | +from functools import lru_cache |
| 8 | +from pathlib import Path |
| 9 | +from typing import Any |
| 10 | + |
| 11 | +import requests |
| 12 | + |
| 13 | +from codeflash.cli_cmds.console import logger, progress_bar |
| 14 | + |
| 15 | +supported_editor_paths = [ |
| 16 | + (Path(Path.home()) / ".vscode", "VSCode"), |
| 17 | + (Path(Path.home()) / ".cursor", "Cursor"), |
| 18 | + (Path(Path.home()) / ".windsurf", "Windsurf"), |
| 19 | +] |
| 20 | + |
| 21 | + |
| 22 | +@lru_cache(maxsize=1) |
| 23 | +def get_extension_info() -> dict[str, Any]: |
| 24 | + url = "https://open-vsx.org/api/codeflash/codeflash/latest" |
| 25 | + try: |
| 26 | + response = requests.get(url, timeout=60) |
| 27 | + response.raise_for_status() |
| 28 | + return response.json() |
| 29 | + except Exception as e: |
| 30 | + logger.error("Failed to retrieve extension metadata from open-vsx.org: %s", e) |
| 31 | + return {} |
| 32 | + |
| 33 | + |
| 34 | +@contextmanager |
| 35 | +def download_and_extract_extension(download_url: str) -> Path: |
| 36 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 37 | + tmpdir_path = Path(tmpdir) |
| 38 | + zip_path = tmpdir_path / "extension.zip" |
| 39 | + |
| 40 | + resp = requests.get(download_url, stream=True, timeout=60) |
| 41 | + resp.raise_for_status() |
| 42 | + with zip_path.open("wb") as f: |
| 43 | + for chunk in resp.iter_content(chunk_size=8192): |
| 44 | + f.write(chunk) |
| 45 | + |
| 46 | + with zipfile.ZipFile(zip_path, "r") as zf: |
| 47 | + zf.extractall(tmpdir_path) |
| 48 | + |
| 49 | + extension_path = tmpdir_path / "extension" |
| 50 | + if not extension_path.is_dir(): |
| 51 | + raise FileNotFoundError("Extension folder not found in downloaded archive") |
| 52 | + |
| 53 | + yield extension_path |
| 54 | + |
| 55 | + |
| 56 | +@contextmanager |
| 57 | +def download_and_extract_extension_with_progress(download_url: str) -> Path: |
| 58 | + with ( |
| 59 | + progress_bar("Downloading CodeFlash extension from open-vsx.org..."), |
| 60 | + download_and_extract_extension(download_url) as extension_path, |
| 61 | + ): |
| 62 | + yield extension_path |
| 63 | + |
| 64 | + |
| 65 | +def copy_extension_artifacts(src: Path, dest: Path, version: str) -> bool: |
| 66 | + dst_extensions_dir = dest / "extensions" |
| 67 | + if not dst_extensions_dir.exists(): |
| 68 | + logger.warning("Extensions directory does not exist: %s", str(dst_extensions_dir)) |
| 69 | + return False |
| 70 | + |
| 71 | + dest_path = dst_extensions_dir / f"codeflash.codeflash-{version}" |
| 72 | + |
| 73 | + shutil.copytree(src, dest_path, dirs_exist_ok=True) |
| 74 | + return True |
| 75 | + |
| 76 | + |
| 77 | +def get_metadata_file_path(editor_path: Path) -> Path: |
| 78 | + return editor_path / "extensions" / "extensions.json" |
| 79 | + |
| 80 | + |
| 81 | +@lru_cache(maxsize=len(supported_editor_paths)) |
| 82 | +def get_cf_extension_metadata(editor_path: Path) -> list[dict[str, Any]]: |
| 83 | + metadata_file = get_metadata_file_path(editor_path) |
| 84 | + if not metadata_file.exists(): |
| 85 | + logger.warning("Extensions metadata file does not exist") |
| 86 | + return [] |
| 87 | + with metadata_file.open("r", encoding="utf-8") as f: |
| 88 | + return json.load(f) |
| 89 | + |
| 90 | + |
| 91 | +def write_cf_extension_metadata(editor_path: Path, version: str) -> bool: |
| 92 | + data = { |
| 93 | + "identifier": {"id": "codeflash.codeflash", "uuid": "7798581f-9eab-42be-a1b2-87f90973434d"}, |
| 94 | + "version": version, |
| 95 | + "location": {"$mid": 1, "path": f"{editor_path}/extensions/codeflash.codeflash-{version}", "scheme": "file"}, |
| 96 | + "relativeLocation": f"codeflash.codeflash-{version}", |
| 97 | + "metadata": { |
| 98 | + "installedTimestamp": int(time.time() * 1000), |
| 99 | + "pinned": False, |
| 100 | + "source": "gallery", |
| 101 | + "id": "7798581f-9eab-42be-a1b2-87f90973434d", |
| 102 | + "publisherId": "bc13551d-2729-4c35-84ce-1d3bd3baab45", |
| 103 | + "publisherDisplayName": "CodeFlash", |
| 104 | + "targetPlatform": "universal", |
| 105 | + "updated": True, |
| 106 | + "isPreReleaseVersion": False, |
| 107 | + "hasPreReleaseVersion": False, |
| 108 | + "isApplicationScoped": False, |
| 109 | + "isMachineScoped": False, |
| 110 | + "isBuiltin": False, |
| 111 | + "private": False, |
| 112 | + "preRelease": False, |
| 113 | + }, |
| 114 | + } |
| 115 | + installed_extensions = get_cf_extension_metadata(editor_path) |
| 116 | + if not installed_extensions: |
| 117 | + return False |
| 118 | + installed_extensions = [ |
| 119 | + ext for ext in installed_extensions if ext.get("identifier", {}).get("id") != data["identifier"]["id"] |
| 120 | + ] |
| 121 | + installed_extensions.append(data) |
| 122 | + with get_metadata_file_path(editor_path).open("w", encoding="utf-8") as f: |
| 123 | + json.dump(installed_extensions, f) |
| 124 | + return True |
| 125 | + |
| 126 | + |
| 127 | +def is_latest_version_installed(editor_path: Path, latest_version: str) -> bool: |
| 128 | + installed_extensions = get_cf_extension_metadata(editor_path) |
| 129 | + current_version = "" |
| 130 | + for ext in installed_extensions: |
| 131 | + if ext.get("identifier", {}).get("id") == "codeflash.codeflash": |
| 132 | + current_version = ext.get("version", "") |
| 133 | + break |
| 134 | + return current_version == latest_version |
| 135 | + |
| 136 | + |
| 137 | +def manually_install_vscode_extension(downloadable_paths: list[tuple[Path, str]]) -> None: |
| 138 | + with progress_bar("Fetching extension metadata..."): |
| 139 | + info = get_extension_info() |
| 140 | + |
| 141 | + download_url = info.get("files", {}).get("download", "") |
| 142 | + latest_version = info.get("version", "") |
| 143 | + |
| 144 | + if not download_url or not latest_version: |
| 145 | + logger.error("Failed to retrieve extension metadata") |
| 146 | + return |
| 147 | + |
| 148 | + successful_installs = [] |
| 149 | + with download_and_extract_extension_with_progress(download_url) as extension_path: |
| 150 | + for editor_path, editor in downloadable_paths: |
| 151 | + try: |
| 152 | + did_copy = copy_extension_artifacts(extension_path, editor_path, latest_version) |
| 153 | + if not did_copy: |
| 154 | + continue |
| 155 | + did_write_metadata = write_cf_extension_metadata(editor_path, latest_version) |
| 156 | + if not did_write_metadata: |
| 157 | + continue |
| 158 | + |
| 159 | + successful_installs.append(editor) |
| 160 | + except Exception as e: |
| 161 | + logger.error("Failed to install CodeFlash extension for %s: %s", editor, e) |
| 162 | + if successful_installs: |
| 163 | + logger.info("Successfully installed CodeFlash extension for: %s", ", ".join(successful_installs)) |
| 164 | + |
| 165 | + |
| 166 | +def install_vscode_extension() -> None: |
| 167 | + editors_installed = [] |
| 168 | + downloadable_paths = [] |
| 169 | + |
| 170 | + for editor_path, editor in supported_editor_paths: |
| 171 | + if not editor_path.exists(): |
| 172 | + continue |
| 173 | + |
| 174 | + editors_installed.append(editor) |
| 175 | + |
| 176 | + info = get_extension_info() |
| 177 | + latest_version = info.get("version", "") |
| 178 | + |
| 179 | + if not latest_version or is_latest_version_installed(editor_path, latest_version): |
| 180 | + continue |
| 181 | + |
| 182 | + downloadable_paths.append((editor_path, editor)) |
| 183 | + |
| 184 | + if not downloadable_paths: |
| 185 | + if editors_installed: |
| 186 | + logger.info("CodeFlash extension is already installed and up-to-date for: %s", ", ".join(editors_installed)) |
| 187 | + return |
| 188 | + |
| 189 | + logger.info("No supported editors found for CodeFlash extension installation") |
| 190 | + return |
| 191 | + |
| 192 | + downloadable_editors = ", ".join([editor for _, editor in downloadable_paths]) |
| 193 | + logger.info("Installing CodeFlash extension for %s...", downloadable_editors) |
| 194 | + manually_install_vscode_extension(downloadable_paths) |
0 commit comments