(version: str)
| 150 | |
| 151 | |
| 152 | def _download(version: str) -> Path: |
| 153 | os_name = _os_name() |
| 154 | arch = _arch() |
| 155 | ext = "zip" if os_name == "windows" else "tar.gz" |
| 156 | # Linux ships a fully-static "-portable" build; the standard linux binary |
| 157 | # dynamically links glibc 2.38+ and fails on older distros. macOS/Windows |
| 158 | # have no such variant. Keep in sync with install.sh / install.js / cli.c. |
| 159 | variant = "-portable" if os_name == "linux" else "" |
| 160 | archive = f"codebase-memory-mcp-{os_name}-{arch}{variant}.{ext}" |
| 161 | url = f"https://github.com/{REPO}/releases/download/v{version}/{archive}" |
| 162 | _validate_url_scheme(url) |
| 163 | |
| 164 | dest = _bin_path(version) |
| 165 | dest.parent.mkdir(parents=True, exist_ok=True) |
| 166 | |
| 167 | print( |
| 168 | f"codebase-memory-mcp: downloading v{version} for {os_name}/{arch}...", |
| 169 | file=sys.stderr, |
| 170 | ) |
| 171 | |
| 172 | with tempfile.TemporaryDirectory() as tmp: |
| 173 | tmp_archive = os.path.join(tmp, f"cbm.{ext}") |
| 174 | try: |
| 175 | urllib.request.urlretrieve(url, tmp_archive) # noqa: S310 — scheme validated above |
| 176 | except urllib.error.HTTPError as e: |
| 177 | sys.exit( |
| 178 | f"codebase-memory-mcp: download failed ({e})\n" |
| 179 | f"URL: {url}\n" |
| 180 | f"See https://github.com/{REPO}/releases for available versions." |
| 181 | ) |
| 182 | |
| 183 | _verify_checksum(tmp_archive, archive, version) |
| 184 | |
| 185 | if ext == "tar.gz": |
| 186 | import tarfile |
| 187 | with tarfile.open(tmp_archive) as tf: |
| 188 | _safe_extract_tar(tf, tmp) |
| 189 | else: |
| 190 | import zipfile |
| 191 | with zipfile.ZipFile(tmp_archive) as zf: |
| 192 | _safe_extract_zip(zf, tmp) |
| 193 | |
| 194 | bin_name = "codebase-memory-mcp.exe" if os_name == "windows" else "codebase-memory-mcp" |
| 195 | extracted = os.path.join(tmp, bin_name) |
| 196 | if not os.path.exists(extracted): |
| 197 | sys.exit("codebase-memory-mcp: binary not found after extraction") |
| 198 | |
| 199 | shutil.copy2(extracted, dest) |
| 200 | current = dest.stat().st_mode |
| 201 | dest.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) |
| 202 | |
| 203 | return dest |
| 204 | |
| 205 | |
| 206 | def main() -> None: |
no test coverage detected