Download the Copilot CLI binary and cache it. Args: version: CLI version to download. Defaults to the pinned CLI_VERSION. force: If True, re-download even if already cached. Returns: Path to the cached binary. Raises: RuntimeError: If the version is not
(version: str | None = None, *, force: bool = False)
| 205 | |
| 206 | |
| 207 | def download_cli(version: str | None = None, *, force: bool = False) -> str: |
| 208 | """Download the Copilot CLI binary and cache it. |
| 209 | |
| 210 | Args: |
| 211 | version: CLI version to download. Defaults to the pinned CLI_VERSION. |
| 212 | force: If True, re-download even if already cached. |
| 213 | |
| 214 | Returns: |
| 215 | Path to the cached binary. |
| 216 | |
| 217 | Raises: |
| 218 | RuntimeError: If the version is not set, download fails, or |
| 219 | checksum verification fails. |
| 220 | """ |
| 221 | ver = version or CLI_VERSION |
| 222 | if not ver: |
| 223 | raise RuntimeError( |
| 224 | "No CLI version pinned. This is a development install — " |
| 225 | "set COPILOT_CLI_PATH or install a published wheel." |
| 226 | ) |
| 227 | |
| 228 | archive_name, binary_name = get_asset_info() |
| 229 | cache_dir = get_cache_dir(ver) |
| 230 | binary_path = cache_dir / binary_name |
| 231 | |
| 232 | # Return cached binary if available (unless force) |
| 233 | if not force and binary_path.exists(): |
| 234 | return str(binary_path) |
| 235 | |
| 236 | # Fetch checksums |
| 237 | checksums = _fetch_checksums(ver) |
| 238 | expected_hash = checksums.get(archive_name) |
| 239 | if not expected_hash: |
| 240 | raise RuntimeError( |
| 241 | f"No checksum found for '{archive_name}' in SHA256SUMS.txt. " |
| 242 | f"Available files: {list(checksums.keys())}" |
| 243 | ) |
| 244 | |
| 245 | # Download archive with retries |
| 246 | url = get_download_url(ver, archive_name) |
| 247 | last_exc: Exception | None = None |
| 248 | data: bytes | None = None |
| 249 | for attempt in range(_MAX_RETRIES): |
| 250 | try: |
| 251 | with urlopen(url, timeout=120) as response: |
| 252 | data = response.read() |
| 253 | break |
| 254 | except (HTTPError, URLError) as exc: |
| 255 | last_exc = exc |
| 256 | if attempt < _MAX_RETRIES - 1: |
| 257 | time.sleep(2**attempt) |
| 258 | if data is None: |
| 259 | raise RuntimeError( |
| 260 | f"Failed to download runtime from {url}: {last_exc}\n\n" |
| 261 | "If you are in an offline or firewalled environment, you can:\n" |
| 262 | f"1. Manually download the archive from: {url}\n" |
| 263 | f"2. Extract the '{binary_name}' binary to: {binary_path}\n" |
| 264 | "Or set COPILOT_CLI_PATH to point to an existing binary." |
no test coverage detected
searching dependent graphs…