Created
August 5, 2025 22:05
-
-
Save zx0r/018f9da05ced6b25cf6b1256a709b295 to your computer and use it in GitHub Desktop.
VSCode Marketplace API, download and install extensions latest stable version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
# License : MIT License | |
# Author : zx0r | |
# Description : | |
# A Python script that interacts with the Visual Studio Code Marketplace API | |
# to automatically fetch and download the latest stable and optionally pre-release .vsix versions of given extensions. | |
# It also detects if an extension is an Extension Pack and recursively downloads all included extensions. | |
# Features | |
# - Fetch the latest stable version of any VSCode extension | |
# - Automatically resolve and download all extensions from Extension Packs | |
# - Optionally fetch pre-release versions (if available) | |
# - Save .vsix files for offline installation or backup | |
# - Supports both single and batch extension queries | |
# pip install requests | |
# python vscode_extension_fetcher.py | |
# Enter choice [1/2]: 2 | |
# Enter extension identifier (e.g. GitHub.copilot or full URL): Github.copilot | |
# 📦 Found extensionPack: ['GitHub.copilot-chat', 'GitHub.copilot'] | |
# 📦 GitHub.copilot-1.350.0 | |
# 📦 GitHub.copilot-chat-0.29.1 | |
# 📦 GitHub.copilot-1.350.0 | |
# ✅ Saved metadata to: Github.copilot\Github.copilot.csv | |
# 📥 Do you want to download all .vsix files? [y/N]: y | |
# ✅ All VSIX files saved in: Github.copilot | |
# Vsix files are in the folder Github.copilot | |
# codium --install-extension GitHub.copilot-1.350.0.vsix --force | |
# codium --install-extension GitHub.copilot-chat-0.29.1.vsix --force | |
# Check this https://github.com/zx0r/VSCodium-Setup/blob/main/scripts/install_copilot.sh | |
# Key feature for full functionality of GitHub.copilot and GitHub.copilot-chat on VSCodium | |
# update_product_json() { ... } | |
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | |
import csv | |
import requests | |
from requests.adapters import HTTPAdapter, Retry | |
import os | |
import sys | |
from typing import List | |
def create_retry_session(): | |
"""Create a requests session with retry capability.""" | |
retry_strategy = Retry( | |
total=3, | |
backoff_factor=1, | |
status_forcelist=[429, 500, 502, 503, 504], | |
allowed_methods=["GET", "POST"], | |
) | |
adapter = HTTPAdapter(max_retries=retry_strategy) | |
session = requests.Session() | |
session.mount("https://", adapter) | |
# Add user agent to avoid being blocked | |
session.headers.update( | |
{ | |
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1" | |
} | |
) | |
return session | |
def build_flags(): | |
"""Build flags for the VS Code Marketplace API.""" | |
return 0x1 | 0x2 | 0x4 | 0x8 | 0x10 | 0x40 | 0x80 | 0x100 | 0x8000 | |
def get_vscode_extensions( | |
session, filter_type, filter_value, page_size=500, max_page=10000, flags=None | |
): | |
"""Query the VS Code Marketplace API for extensions.""" | |
headers = {"Accept": "application/json; charset=utf-8; api-version=7.2-preview.1"} | |
for page in range(1, max_page + 1): | |
body = { | |
"filters": [ | |
{ | |
"criteria": [{"filterType": filter_type, "value": filter_value}], | |
"pageNumber": page, | |
"pageSize": page_size, | |
"sortBy": 0, | |
"sortOrder": 0, | |
} | |
], | |
"assetTypes": [], | |
"flags": flags, | |
} | |
response = session.post( | |
"https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", | |
json=body, | |
headers=headers, | |
) | |
response.raise_for_status() | |
extensions = response.json()["results"][0]["extensions"] | |
if not extensions: | |
break | |
yield from extensions | |
def extract_versions(extension, is_pack=False): | |
"""Extract latest stable and matching pre-release versions of an extension.""" | |
name = extension["extensionName"] | |
publisher = extension["publisher"]["publisherName"] | |
extension_id = f"{publisher}.{name}" | |
stats = {s["statisticName"]: s["value"] for s in extension.get("statistics", [])} | |
installs = stats.get("install", 0) | |
versions = extension["versions"] | |
# Custom sort to handle non-standard version formats like 0.8.2023092902 | |
def version_sort_key(v): | |
version = v["version"] | |
# Try to split and convert parts to integers for accurate comparison | |
try: | |
parts = [] | |
for part in version.split("."): | |
parts.append(int(part)) | |
return parts | |
except (ValueError, TypeError): | |
# If conversion fails, use string comparison as fallback | |
return [0, 0, 0] | |
# Sort versions - latest first | |
versions_sorted = sorted(versions, key=version_sort_key, reverse=True) | |
stable, prerelease = None, None | |
# First find the latest stable version | |
for v in versions_sorted: | |
props = {p["key"]: p["value"] for p in v.get("properties", [])} | |
is_prerelease = props.get("Microsoft.VisualStudio.Code.PreRelease") == "true" | |
version = v["version"] | |
# For stable versions | |
if not is_prerelease: | |
download_url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/{publisher}/vsextensions/{name}/{version}/vspackage" | |
stable = { | |
"ExtensionId": extension_id, | |
"Installs": installs, | |
"ReleaseChannel": "Stable", | |
"Version": version, | |
"IsExtensionPack": is_pack, | |
"DownloadURL": download_url, | |
} | |
break | |
# If no stable version found, return empty list | |
if not stable: | |
return [] | |
# Find the latest pre-release version | |
# Attempt to get major.minor version from stable | |
try: | |
version_parts = stable["Version"].split(".") | |
major_minor = f"{version_parts[0]}.{version_parts[1]}" | |
except (IndexError, ValueError): | |
major_minor = stable["Version"] # Fallback to full version | |
for v in versions_sorted: | |
props = {p["key"]: p["value"] for p in v.get("properties", [])} | |
is_prerelease = props.get("Microsoft.VisualStudio.Code.PreRelease") == "true" | |
version = v["version"] | |
# Check if this is a pre-release version from the same major.minor version line | |
if is_prerelease and version.startswith(major_minor): | |
download_url = f"https://marketplace.visualstudio.com/_apis/public/gallery/publishers/{publisher}/vsextensions/{name}/{version}/vspackage" | |
prerelease = { | |
"ExtensionId": extension_id, | |
"Installs": installs, | |
"ReleaseChannel": "Pre-release", | |
"Version": version, | |
"IsExtensionPack": is_pack, | |
"DownloadURL": download_url, | |
} | |
break | |
# Return both stable and pre-release if found, otherwise just stable | |
result = [stable] | |
if prerelease: | |
result.append(prerelease) | |
return result | |
def handle_extension_pack(version_obj): | |
"""Extract extension pack dependencies.""" | |
for prop in version_obj.get("properties", []): | |
if prop.get("key") == "Microsoft.VisualStudio.Code.ExtensionPack": | |
return prop.get("value").split(",") | |
return [] | |
def save_csv(filename, rows): | |
"""Save metadata to CSV file.""" | |
if not rows: | |
print("⚠️ No data to write.") | |
return | |
with open(filename, "w", newline="", encoding="utf-8") as f: | |
writer = csv.DictWriter(f, fieldnames=rows[0].keys()) | |
writer.writeheader() | |
writer.writerows(rows) | |
print(f"✅ Saved metadata to: {filename}") | |
def download_vsix(rows, folder): | |
"""Download VSIX files for the given extensions.""" | |
os.makedirs(folder, exist_ok=True) | |
session = create_retry_session() | |
for row in rows: | |
name = f"{row['ExtensionId']}-{row['Version']}.vsix" | |
url = row["DownloadURL"] | |
dest = os.path.join(folder, name) | |
print(f"⬇️ Downloading {row['ExtensionId']}-{row['Version']}.vsix ...") | |
r = session.get(url) | |
with open(dest, "wb") as f: | |
f.write(r.content) | |
print(f"✅ All VSIX files saved in: {folder}") | |
def interactive_mode(): | |
"""Handle interactive user input.""" | |
print("📦 VSCode Extension Metadata Fetcher") | |
print("1. Fetch metadata for ALL extensions (Latest stable only)") | |
print("2. Fetch metadata for a SPECIFIC extension") | |
choice = input("Enter choice [1/2]: ").strip() | |
if choice == "1": | |
return (1, 8, "Microsoft.VisualStudio.Code") | |
elif choice == "2": | |
val = input( | |
"Enter extension identifier (e.g. GitHub.copilot or full URL): " | |
).strip() | |
if "marketplace.visualstudio.com/items" in val: | |
val = val.split("itemName=")[-1] | |
return (2, 7, val) | |
else: | |
print("Invalid choice.") | |
exit(1) | |
def fetch_specific_extension(name, session, flags): | |
"""Fetch a specific extension and its dependencies.""" | |
ext = next(get_vscode_extensions(session, 7, name, flags=flags), None) | |
if not ext: | |
print(f"Extension not found: {name}") | |
return [], name | |
all_rows = extract_versions(ext, is_pack=False) | |
# Handle extension pack dependencies | |
pack = handle_extension_pack(ext["versions"][0]) | |
if pack: | |
print(f"📦 Found extensionPack: {pack}") | |
for p in pack: | |
p_ext = next( | |
get_vscode_extensions(session, 7, p.strip(), flags=flags), None | |
) | |
if p_ext: | |
# For each dependency, get versions | |
dep_rows = extract_versions(p_ext, is_pack=True) | |
if dep_rows: | |
all_rows.extend(dep_rows) | |
# Print summary of extensions found | |
if all_rows: | |
for row in all_rows: | |
if row["ReleaseChannel"] == "Stable": | |
print(f"📦 {row['ExtensionId']}-{row['Version']}") | |
return all_rows, name | |
def main(): | |
"""Main function.""" | |
session = create_retry_session() | |
flags = build_flags() | |
result = interactive_mode() | |
all_data = [] | |
if result[0] == 1: | |
# Handle "all extensions" mode | |
output_name = "all_extensions_metadata" | |
print("🔍 Parsing all extensions...") | |
csv_file = f"{output_name}.csv" | |
with open(csv_file, "w", newline="", encoding="utf-8") as f: | |
writer = None | |
for ext in get_vscode_extensions( | |
session, result[1], result[2], flags=flags | |
): | |
rows = extract_versions(ext, is_pack=False) | |
if rows: | |
if not writer: | |
writer = csv.DictWriter(f, fieldnames=rows[0].keys()) | |
writer.writeheader() | |
writer.writerows(rows) | |
all_data.extend(rows) | |
sys.stdout.write(f"\rParsed: {len(all_data)} extensions") | |
sys.stdout.flush() | |
print(f"\n✅ Metadata saved to: {csv_file}") | |
else: | |
# Handle specific extension mode | |
_, _, ext_name = result | |
all_data, output_name = fetch_specific_extension(ext_name, session, flags) | |
if all_data: | |
# Ensure output directory exists | |
os.makedirs(output_name, exist_ok=True) | |
# Save CSV in the extension folder | |
csv_path = os.path.join(output_name, f"{output_name}.csv") | |
save_csv(csv_path, all_data) | |
dl = input("📥 Do you want to download all .vsix files? [y/N]: ").lower() | |
if dl == "y": | |
download_vsix(all_data, folder=output_name) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment