Skip to content

Instantly share code, notes, and snippets.

@zx0r
Created August 5, 2025 22:05
Show Gist options
  • Save zx0r/018f9da05ced6b25cf6b1256a709b295 to your computer and use it in GitHub Desktop.
Save zx0r/018f9da05ced6b25cf6b1256a709b295 to your computer and use it in GitHub Desktop.
VSCode Marketplace API, download and install extensions latest stable version
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 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