From 1f96921f49de8ac1f83a48420c1b1a1c9c6ac2ce Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 12:55:18 +0100 Subject: [PATCH 1/9] push --- .../schemas}/extension.schema.json | 2 +- {scripts => .github/scripts}/generate.py | 12 +++++------- {scripts => .github/scripts}/requirements.txt | 0 {scripts => .github/scripts}/validate.py | 4 ++-- .github/workflows/generate-gallery.yml | 8 ++++---- .github/workflows/validate-pr.yml | 4 ++-- README.md | 15 ++++++--------- docs/CONTRIBUTING.md | 8 ++++---- generated/extensions.json => extensions.json | 4 ++-- .../microsoft.sample-extension/extension.json | 2 +- 10 files changed, 27 insertions(+), 32 deletions(-) rename {schemas => .github/schemas}/extension.schema.json (98%) rename {scripts => .github/scripts}/generate.py (91%) rename {scripts => .github/scripts}/requirements.txt (100%) rename {scripts => .github/scripts}/validate.py (98%) rename generated/extensions.json => extensions.json (89%) diff --git a/schemas/extension.schema.json b/.github/schemas/extension.schema.json similarity index 98% rename from schemas/extension.schema.json rename to .github/schemas/extension.schema.json index 54500fd..f2a474f 100644 --- a/schemas/extension.schema.json +++ b/.github/schemas/extension.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/schemas/extension.schema.json", + "$id": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/extension.schema.json", "title": "Command Palette Extension", "description": "Schema for a Command Palette extension submission.", "type": "object", diff --git a/scripts/generate.py b/.github/scripts/generate.py similarity index 91% rename from scripts/generate.py rename to .github/scripts/generate.py index e173434..5808718 100644 --- a/scripts/generate.py +++ b/.github/scripts/generate.py @@ -1,10 +1,10 @@ """Generate the aggregate gallery JSON for CmdPal-Extensions. Scans extensions/*/extension.json, merges them into a single -generated/extensions.json that the Command Palette app fetches at runtime. +extensions.json at the repo root that the Command Palette app fetches at runtime. Usage: - python scripts/generate.py + python .github/scripts/generate.py """ import glob @@ -13,16 +13,15 @@ import sys from datetime import datetime, timezone -REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) +REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) EXTENSIONS_DIR = os.path.join(REPO_ROOT, "extensions") -OUTPUT_DIR = os.path.join(REPO_ROOT, "generated") -OUTPUT_FILE = os.path.join(OUTPUT_DIR, "extensions.json") +OUTPUT_FILE = os.path.join(REPO_ROOT, "extensions.json") BASE_RAW_URL = ( "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main" ) GALLERY_SCHEMA_URL = ( - f"{BASE_RAW_URL}/schemas/gallery.schema.json" + f"{BASE_RAW_URL}/.github/schemas/gallery.schema.json" ) # Fields from extension.json that should not appear in the gallery output. @@ -118,7 +117,6 @@ def generate_gallery() -> dict: def write_gallery(gallery: dict) -> None: """Write the gallery JSON to the output file.""" - os.makedirs(OUTPUT_DIR, exist_ok=True) with open(OUTPUT_FILE, "w", encoding="utf-8", newline="\n") as f: json.dump(gallery, f, indent=2, ensure_ascii=False) f.write("\n") diff --git a/scripts/requirements.txt b/.github/scripts/requirements.txt similarity index 100% rename from scripts/requirements.txt rename to .github/scripts/requirements.txt diff --git a/scripts/validate.py b/.github/scripts/validate.py similarity index 98% rename from scripts/validate.py rename to .github/scripts/validate.py index 230191f..b968241 100644 --- a/scripts/validate.py +++ b/.github/scripts/validate.py @@ -38,9 +38,9 @@ # Constants # --------------------------------------------------------------------------- -REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent.parent EXTENSIONS_DIR = REPO_ROOT / "extensions" -SCHEMA_PATH = REPO_ROOT / "schemas" / "extension.schema.json" +SCHEMA_PATH = REPO_ROOT / ".github" / "schemas" / "extension.schema.json" VALID_CATEGORIES = { "Developer Tools", diff --git a/.github/workflows/generate-gallery.yml b/.github/workflows/generate-gallery.yml index 42d088a..c1dcbe9 100644 --- a/.github/workflows/generate-gallery.yml +++ b/.github/workflows/generate-gallery.yml @@ -24,16 +24,16 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install -r scripts/requirements.txt + run: pip install -r .github/scripts/requirements.txt - name: Generate extensions gallery - run: python scripts/generate.py + run: python .github/scripts/generate.py - name: Commit and push if changed run: | - git diff --quiet generated/extensions.json && exit 0 + git diff --quiet extensions.json && exit 0 git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add generated/extensions.json + git add extensions.json git commit -m "chore: regenerate extensions gallery [skip ci]" git push diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index bcf14c2..9af36e5 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -21,7 +21,7 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install -r scripts/requirements.txt + run: pip install -r .github/scripts/requirements.txt - name: Get changed extension files id: changed @@ -33,4 +33,4 @@ jobs: - name: Validate extension submission if: steps.changed.outputs.files != '' - run: python scripts/validate.py ${{ steps.changed.outputs.files }} + run: python .github/scripts/validate.py ${{ steps.changed.outputs.files }} diff --git a/README.md b/README.md index bdcced9..f75edc7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The community gallery of extensions for [Microsoft Command Palette](https://gith ## How it works -Each extension is represented by a folder under `extensions/` containing an `extension.json` metadata file and an icon. A CI pipeline aggregates all individual submissions into a single [`generated/extensions.json`](generated/extensions.json) file that the Command Palette app fetches at runtime to populate its extension gallery. +Each extension is represented by a folder under `extensions/` containing an `extension.json` metadata file and an icon. A CI pipeline aggregates all individual submissions into a single [`extensions.json`](extensions.json) file at the repo root that the Command Palette app fetches at runtime to populate its extension gallery. ## For extension developers @@ -15,7 +15,7 @@ Want to list your extension in the gallery? We'd love to have you! Check out the The Command Palette app fetches the gallery from: ``` -https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/generated/extensions.json +https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions.json ``` ## Repository structure @@ -26,13 +26,10 @@ CmdPal-Extensions/ │ └── ./ │ ├── extension.json # Extension metadata │ └── icon.png # Extension icon -├── generated/ -│ └── extensions.json # Auto-generated aggregate (do not edit) -├── schemas/ -│ └── extension.schema.json # JSON Schema for extension.json -├── scripts/ # CI/build scripts -└── docs/ - └── CONTRIBUTING.md # Submission guide for developers +├── extensions.json # Auto-generated aggregate (do not edit) +├── docs/ +│ └── CONTRIBUTING.md # Submission guide for developers +└── .github/ # CI workflows, scripts, and schemas ``` ## Contributing diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 63cf7a4..67cf7a0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -4,7 +4,7 @@ Welcome! We're excited that you want to share your extension with the Command Pa ## Overview -This repository is the **community gallery** for [Microsoft Command Palette](https://github.com/microsoft/PowerToys) extensions. Each extension is represented by a folder containing metadata and an icon. A CI pipeline aggregates all submissions into a single `generated/extensions.json` file that the Command Palette app fetches at runtime. +This repository is the **community gallery** for [Microsoft Command Palette](https://github.com/microsoft/PowerToys) extensions. Each extension is represented by a folder containing metadata and an icon. A CI pipeline aggregates all submissions into a single `extensions.json` file at the repo root that the Command Palette app fetches at runtime. ## Prerequisites @@ -60,7 +60,7 @@ Create an `extension.json` file inside your folder. Here is the full template wi ```json { - "$schema": "../../schemas/extension.schema.json", + "$schema": "../../.github/schemas/extension.schema.json", "id": "publisher.extension-id", "name": "My Extension", "description": "A short description of what the extension does (max 200 characters).", @@ -82,7 +82,7 @@ Create an `extension.json` file inside your folder. Here is the full template wi | Field | Required | Description | |-------|----------|-------------| -| `$schema` | Optional | Path to the JSON schema. Enables editor autocompletion and validation. Use `"../../schemas/extension.schema.json"`. | +| `$schema` | Optional | Path to the JSON schema. Enables editor autocompletion and validation. Use `"../../.github/schemas/extension.schema.json"`. | | `id` | **Required** | Unique identifier in `.` format. **Must match your folder name exactly.** | | `name` | **Required** | Human-readable display name (max 100 characters). | | `description` | **Required** | Short description of the extension (max 200 characters). | @@ -174,7 +174,7 @@ For editor autocompletion and inline validation, add the `$schema` property to t ```json { - "$schema": "../../schemas/extension.schema.json" + "$schema": "../../.github/schemas/extension.schema.json" } ``` diff --git a/generated/extensions.json b/extensions.json similarity index 89% rename from generated/extensions.json rename to extensions.json index b6d11b6..2a25251 100644 --- a/generated/extensions.json +++ b/extensions.json @@ -1,7 +1,7 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/schemas/gallery.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json", "version": "1.0", - "generatedAt": "2026-03-27T11:35:12Z", + "generatedAt": "2026-03-27T12:59:39Z", "extensionCount": 1, "extensions": [ { diff --git a/extensions/microsoft.sample-extension/extension.json b/extensions/microsoft.sample-extension/extension.json index 8a2fa68..d6e67b7 100644 --- a/extensions/microsoft.sample-extension/extension.json +++ b/extensions/microsoft.sample-extension/extension.json @@ -1,5 +1,5 @@ { - "$schema": "../../schemas/extension.schema.json", + "$schema": "../../.github/schemas/extension.schema.json", "id": "microsoft.sample-extension", "name": "Sample Extension", "description": "A sample Command Palette extension to demonstrate the gallery submission format.", From 6a0c455700e7a5cd7e2c8904a19600c2bb3f1268 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 14:00:26 +0100 Subject: [PATCH 2/9] feat: update extension schema to V2 format - Rename 'name' to 'title', 'publisher' to 'author' object - Support multiple install sources (installSources array) - Install types: winget, msstore, url with type-specific keys - Simplify id format to slug (no publisher prefix) - Drop version and license fields - Make category and tags optional - Rename sample extension folder accordingly - Update docs, schema, scripts, and PR template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/schemas/extension.schema.json | 108 +++++++++++------- .github/scripts/validate.py | 25 ++-- README.md | 2 +- docs/CONTRIBUTING.md | 72 ++++++------ extensions.json | 27 +++-- .../extension.json | 25 ++-- .../icon.png | Bin 8 files changed, 150 insertions(+), 113 deletions(-) rename extensions/{microsoft.sample-extension => sample-extension}/extension.json (55%) rename extensions/{microsoft.sample-extension => sample-extension}/icon.png (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c984ed7..14f5f4e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,12 +5,12 @@ Please fill out the checklist below to help us review your PR quickly. ### Checklist -- [ ] My folder is named `.` (lowercase, alphanumeric + hyphens only) +- [ ] My folder is named `` (lowercase, alphanumeric + hyphens only) - [ ] I have added an `extension.json` with all required fields - [ ] The `id` field in my `extension.json` matches my folder name - [ ] I have added an icon file (PNG or SVG, under 100 KB) - [ ] The `icon` field in `extension.json` matches my icon filename -- [ ] My extension is available at the install source I specified (winget/GitHub/Store) +- [ ] My extension is available at the install source I specified (winget/MS Store/URL) - [ ] I have read the [Contributing Guide](../docs/CONTRIBUTING.md) ### Additional context diff --git a/.github/schemas/extension.schema.json b/.github/schemas/extension.schema.json index f2a474f..969b503 100644 --- a/.github/schemas/extension.schema.json +++ b/.github/schemas/extension.schema.json @@ -4,7 +4,7 @@ "title": "Command Palette Extension", "description": "Schema for a Command Palette extension submission.", "type": "object", - "required": ["id", "name", "description", "publisher", "version", "category", "icon", "installSource"], + "required": ["id", "title", "description", "author", "icon", "installSources"], "additionalProperties": false, "properties": { "$schema": { @@ -13,11 +13,11 @@ }, "id": { "type": "string", - "description": "Unique extension identifier in . format. Must match the folder name.", - "pattern": "^[a-z0-9]+(-[a-z0-9]+)*\\.[a-z0-9]+(-[a-z0-9]+)*$", - "examples": ["contoso.quick-notes", "microsoft.sample-extension"] + "description": "Unique extension identifier (slug). Must match the folder name.", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "examples": ["media-controls", "quick-notes", "sample-extension"] }, - "name": { + "title": { "type": "string", "description": "Human-readable display name for the extension.", "minLength": 1, @@ -29,17 +29,34 @@ "minLength": 1, "maxLength": 200 }, - "publisher": { + "author": { + "type": "object", + "description": "Extension author information.", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Author display name.", + "minLength": 1, + "maxLength": 100 + }, + "url": { + "type": "string", + "description": "URL to the author's website or profile.", + "format": "uri" + } + } + }, + "icon": { "type": "string", - "description": "Publisher display name.", - "minLength": 1, - "maxLength": 100 + "description": "Filename of the icon in the same folder. Must be a .png or .svg file.", + "pattern": "^[\\w.-]+\\.(png|svg)$" }, - "version": { + "homepage": { "type": "string", - "description": "Semantic version string.", - "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$", - "examples": ["1.0.0", "2.1.0-beta.1"] + "description": "URL to the project homepage or repository.", + "format": "uri" }, "category": { "type": "string", @@ -68,38 +85,41 @@ "maxItems": 5, "uniqueItems": true }, - "icon": { - "type": "string", - "description": "Filename of the icon in the same folder. Must be a .png or .svg file.", - "pattern": "^[\\w.-]+\\.(png|svg)$" - }, - "installSource": { - "type": "object", - "description": "Where and how to install this extension.", - "required": ["type", "value"], - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "description": "Installation source type.", - "enum": ["winget", "github", "store"] - }, - "value": { - "type": "string", - "description": "Package identifier, GitHub repo URL, or Microsoft Store link.", - "minLength": 1 - } + "installSources": { + "type": "array", + "description": "One or more installation sources for this extension.", + "minItems": 1, + "items": { + "type": "object", + "description": "An installation source.", + "required": ["type"], + "oneOf": [ + { + "properties": { + "type": { "const": "winget" }, + "id": { "type": "string", "description": "Winget package identifier.", "minLength": 1 } + }, + "required": ["type", "id"], + "additionalProperties": false + }, + { + "properties": { + "type": { "const": "msstore" }, + "id": { "type": "string", "description": "Microsoft Store product ID.", "minLength": 1 } + }, + "required": ["type", "id"], + "additionalProperties": false + }, + { + "properties": { + "type": { "const": "url" }, + "uri": { "type": "string", "description": "Direct download or release page URL.", "format": "uri" } + }, + "required": ["type", "uri"], + "additionalProperties": false + } + ] } - }, - "homepage": { - "type": "string", - "description": "URL to the project homepage or repository.", - "format": "uri" - }, - "license": { - "type": "string", - "description": "SPDX license identifier (e.g., MIT, Apache-2.0).", - "maxLength": 50 } } } diff --git a/.github/scripts/validate.py b/.github/scripts/validate.py index b968241..f81d908 100644 --- a/.github/scripts/validate.py +++ b/.github/scripts/validate.py @@ -7,7 +7,7 @@ Usage: # Validate extensions touched by specific changed files (CI mode) - python scripts/validate.py extensions/contoso.quick-notes/extension.json + python scripts/validate.py extensions/quick-notes/extension.json # Validate ALL extensions in the gallery python scripts/validate.py @@ -22,6 +22,7 @@ import json import os import pathlib +import re import subprocess import sys from typing import List, Set @@ -60,6 +61,9 @@ MAX_ICON_SIZE_KB = 100 VALID_ICON_EXTENSIONS = {".png", ".svg"} +# V2 id format: lowercase alphanumeric slug with hyphens (e.g. "media-controls") +ID_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") + # --------------------------------------------------------------------------- # Helpers @@ -81,7 +85,7 @@ def discover_extension_folders_from_files(changed_files: List[str]) -> Set[pathl for raw in changed_files: p = pathlib.Path(raw).resolve() # Walk up to find a path whose parent is the extensions/ dir. - # e.g. extensions/contoso.quick-notes/extension.json -> extensions/contoso.quick-notes + # e.g. extensions/quick-notes/extension.json -> extensions/quick-notes try: rel = p.relative_to(EXTENSIONS_DIR) except ValueError: @@ -177,7 +181,14 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p f"does not match folder name \"{folder_name}\"" ) - # 5. Icon file must exist, be PNG/SVG, and ≤100 KB + # 5. id must be a valid V2 slug (lowercase alphanumeric + hyphens) + if ext_id and not ID_PATTERN.match(ext_id): + errors.append( + f"{folder_name}/extension.json: 'id' field \"{ext_id}\" is not a valid slug. " + f"Must be lowercase alphanumeric with hyphens (e.g. \"quick-notes\")." + ) + + # 6. Icon file must exist, be PNG/SVG, and ≤100 KB icon_filename = data.get("icon", "") if icon_filename: icon_path = folder / icon_filename @@ -200,7 +211,7 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p f"exceeds {MAX_ICON_SIZE_KB} KB limit" ) - # 6. Category must be from the predefined list + # 7. Category must be from the predefined list (when provided) category = data.get("category", "") if category and category not in VALID_CATEGORIES: errors.append( @@ -208,7 +219,7 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p f"Must be one of: {', '.join(sorted(VALID_CATEGORIES))}" ) - # 7. Tags validation + # 8. Tags validation (when provided) tags = data.get("tags", []) if isinstance(tags, list): if len(tags) > MAX_TAGS: @@ -223,7 +234,7 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p f"exceeds {MAX_TAG_LENGTH} character limit ({len(tag)} chars)" ) - # 8. Duplicate ID check across the gallery + # 9. Duplicate ID check across the gallery if ext_id and ext_id in id_index: other = id_index[ext_id] errors.append( @@ -246,7 +257,7 @@ def main() -> int: parser.add_argument( "files", nargs="*", - help="Changed file paths (e.g. extensions/contoso.quick-notes/extension.json). " + help="Changed file paths (e.g. extensions/quick-notes/extension.json). " "If omitted, validates ALL extensions under extensions/.", ) parser.add_argument( diff --git a/README.md b/README.md index f75edc7..acf4b1c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions.js ``` CmdPal-Extensions/ ├── extensions/ # One folder per extension submission -│ └── ./ +│ └── / │ ├── extension.json # Extension metadata │ └── icon.png # Extension icon ├── extensions.json # Auto-generated aggregate (do not edit) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 67cf7a0..cc056c5 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -14,8 +14,8 @@ Before you begin, make sure you have: - A **GitHub account** - Your extension **already published** on one of the supported install sources: - [winget](https://github.com/microsoft/winget-pkgs) (Windows Package Manager) - - [GitHub Releases](https://docs.github.com/en/repositories/releasing-projects-on-github) - [Microsoft Store](https://apps.microsoft.com/) + - Direct download URL ## Step-by-step submission guide @@ -34,25 +34,24 @@ git checkout -b add-my-extension Create a folder under `extensions/` using the naming convention: ``` -extensions/./ +extensions// ``` **Naming rules:** | Rule | Detail | |------|--------| -| Format | `.` | +| Format | `` — a simple slug | | Characters | Lowercase alphanumeric characters and hyphens only (`a-z`, `0-9`, `-`) | -| Separator | A single dot (`.`) separates publisher from extension name | | Hyphens | Use hyphens to separate words (e.g., `quick-notes`, not `quicknotes`) | -| No leading/trailing hyphens | Each segment must start and end with an alphanumeric character | +| No leading/trailing hyphens | Must start and end with an alphanumeric character | **Examples:** -- ✅ `contoso.quick-notes` -- ✅ `my-company.clipboard-manager` -- ❌ `Contoso.QuickNotes` (uppercase not allowed) -- ❌ `contoso_quick_notes` (underscores not allowed, missing dot separator) +- ✅ `media-controls` +- ✅ `clipboard-manager` +- ❌ `MediaControls` (uppercase not allowed) +- ❌ `clipboard_manager` (underscores not allowed) ### 4. Add `extension.json` @@ -61,20 +60,23 @@ Create an `extension.json` file inside your folder. Here is the full template wi ```json { "$schema": "../../.github/schemas/extension.schema.json", - "id": "publisher.extension-id", - "name": "My Extension", + "id": "my-extension", + "title": "My Extension", "description": "A short description of what the extension does (max 200 characters).", - "publisher": "Publisher Display Name", - "version": "1.0.0", + "author": { + "name": "Publisher Display Name", + "url": "https://github.com/publisher" + }, "category": "Utilities", "tags": ["tag1", "tag2"], "icon": "icon.png", - "installSource": { - "type": "winget", - "value": "Publisher.PackageName" - }, - "homepage": "https://github.com/publisher/extension", - "license": "MIT" + "installSources": [ + { + "type": "winget", + "id": "Publisher.PackageName" + } + ], + "homepage": "https://github.com/publisher/extension" } ``` @@ -83,17 +85,15 @@ Create an `extension.json` file inside your folder. Here is the full template wi | Field | Required | Description | |-------|----------|-------------| | `$schema` | Optional | Path to the JSON schema. Enables editor autocompletion and validation. Use `"../../.github/schemas/extension.schema.json"`. | -| `id` | **Required** | Unique identifier in `.` format. **Must match your folder name exactly.** | -| `name` | **Required** | Human-readable display name (max 100 characters). | +| `id` | **Required** | Unique identifier slug (e.g., `media-controls`). **Must match your folder name exactly.** | +| `title` | **Required** | Human-readable display name (max 100 characters). | | `description` | **Required** | Short description of the extension (max 200 characters). | -| `publisher` | **Required** | Publisher display name (max 100 characters). | -| `version` | **Required** | Semantic version string (e.g., `1.0.0`, `2.1.0-beta.1`). | -| `category` | **Required** | Primary category. Must be one of the valid categories listed below. | +| `author` | **Required** | Object with `name` (required, max 100 characters) and `url` (optional). | +| `category` | Optional | Primary category. Must be one of the valid categories listed below. | | `tags` | Optional | Up to 5 freeform tags for filtering (each max 30 characters). | | `icon` | **Required** | Filename of the icon in the same folder (e.g., `icon.png`). | -| `installSource` | **Required** | Object with `type` and `value` describing where to install the extension. | +| `installSources` | **Required** | Array of install source objects. Each has a `type` and a type-specific identifier. See below. | | `homepage` | Optional | URL to the project homepage or repository. | -| `license` | Optional | SPDX license identifier (e.g., `MIT`, `Apache-2.0`). | #### Valid categories @@ -110,13 +110,13 @@ Create an `extension.json` file inside your folder. Here is the full template wi #### Install source types -The `installSource` object has two fields — `type` and `value`: +Each object in the `installSources` array has a `type` and a type-specific field: -| Type | Value | Example | -|------|-------|---------| -| `winget` | The winget package identifier | `"Publisher.PackageName"` | -| `github` | Full URL to the GitHub repository or releases page | `"https://github.com/publisher/repo"` | -| `store` | Microsoft Store link or product ID | `"https://apps.microsoft.com/detail/..."` | +| Type | Field | Description | Example | +|------|-------|-------------|---------| +| `winget` | `id` | The winget package identifier | `"Publisher.PackageName"` | +| `msstore` | `id` | Microsoft Store product ID | `"9n3bq81g19k7"` | +| `url` | `uri` | Direct download URL | `"https://example.com/extension.msix"` | ### 5. Add an icon @@ -132,8 +132,8 @@ Place an icon file in your extension folder alongside `extension.json`. Push your branch to your fork and open a pull request targeting the `main` branch of this repository. ```bash -git add extensions/my-publisher.my-extension/ -git commit -m "Add my-publisher.my-extension to gallery" +git add extensions/my-extension/ +git commit -m "Add my-extension to gallery" git push origin add-my-extension ``` @@ -159,14 +159,14 @@ A maintainer will review your PR. Once approved and merged, your extension will To update an already-published extension (e.g., bump the version, update the description, or change the icon): 1. Create a new branch in your fork -2. Update the files in your existing `extensions/./` folder +2. Update the files in your existing `extensions//` folder 3. Open a new pull request targeting `main` The same CI validation and review process applies. ## Reference -See [`extensions/microsoft.sample-extension/`](../extensions/microsoft.sample-extension/) for a complete working example of a gallery submission. +See [`extensions/sample-extension/`](../extensions/sample-extension/) for a complete working example of a gallery submission. ## Schema reference diff --git a/extensions.json b/extensions.json index 2a25251..d79563b 100644 --- a/extensions.json +++ b/extensions.json @@ -1,28 +1,31 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json", "version": "1.0", - "generatedAt": "2026-03-27T12:59:39Z", + "generatedAt": "2026-03-27T13:00:10Z", "extensionCount": 1, "extensions": [ { - "id": "microsoft.sample-extension", - "name": "Sample Extension", + "id": "sample-extension", + "title": "Sample Extension", "description": "A sample Command Palette extension to demonstrate the gallery submission format.", - "publisher": "Microsoft", - "version": "1.0.0", + "author": { + "name": "Microsoft", + "url": "https://github.com/microsoft" + }, + "homepage": "https://github.com/microsoft/CmdPal-Extensions", "category": "Developer Tools", "tags": [ "sample", "demo", "template" ], - "installSource": { - "type": "github", - "value": "https://github.com/microsoft/CmdPal-Extensions" - }, - "homepage": "https://github.com/microsoft/CmdPal-Extensions", - "license": "MIT", - "iconUrl": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions/microsoft.sample-extension/icon.png" + "installSources": [ + { + "type": "url", + "uri": "https://github.com/microsoft/CmdPal-Extensions" + } + ], + "iconUrl": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions/sample-extension/icon.png" } ] } diff --git a/extensions/microsoft.sample-extension/extension.json b/extensions/sample-extension/extension.json similarity index 55% rename from extensions/microsoft.sample-extension/extension.json rename to extensions/sample-extension/extension.json index d6e67b7..020906e 100644 --- a/extensions/microsoft.sample-extension/extension.json +++ b/extensions/sample-extension/extension.json @@ -1,17 +1,20 @@ { "$schema": "../../.github/schemas/extension.schema.json", - "id": "microsoft.sample-extension", - "name": "Sample Extension", + "id": "sample-extension", + "title": "Sample Extension", "description": "A sample Command Palette extension to demonstrate the gallery submission format.", - "publisher": "Microsoft", - "version": "1.0.0", - "category": "Developer Tools", - "tags": ["sample", "demo", "template"], - "icon": "icon.png", - "installSource": { - "type": "github", - "value": "https://github.com/microsoft/CmdPal-Extensions" + "author": { + "name": "Microsoft", + "url": "https://github.com/microsoft" }, + "icon": "icon.png", "homepage": "https://github.com/microsoft/CmdPal-Extensions", - "license": "MIT" + "category": "Developer Tools", + "tags": ["sample", "demo", "template"], + "installSources": [ + { + "type": "url", + "uri": "https://github.com/microsoft/CmdPal-Extensions" + } + ] } diff --git a/extensions/microsoft.sample-extension/icon.png b/extensions/sample-extension/icon.png similarity index 100% rename from extensions/microsoft.sample-extension/icon.png rename to extensions/sample-extension/icon.png From 8d32c02ac55c7cfdbe11044c5e303f80310d8868 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 14:10:20 +0100 Subject: [PATCH 3/9] fix: handle multiline file list in PR validation workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/validate-pr.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 9af36e5..5809930 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -23,14 +23,11 @@ jobs: - name: Install dependencies run: pip install -r .github/scripts/requirements.txt - - name: Get changed extension files - id: changed + - name: Validate extension submission run: | FILES=$(git diff --name-only origin/main...HEAD -- 'extensions/') - echo "files<> "$GITHUB_OUTPUT" - echo "$FILES" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - - name: Validate extension submission - if: steps.changed.outputs.files != '' - run: python .github/scripts/validate.py ${{ steps.changed.outputs.files }} + if [ -n "$FILES" ]; then + python .github/scripts/validate.py $FILES + else + echo "No extension files changed." + fi From 37c6c18ad54fa80928900b1bd20bf60a108a5c18 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 14:26:24 +0100 Subject: [PATCH 4/9] Update docs/CONTRIBUTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cc056c5..f23f4aa 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -15,7 +15,7 @@ Before you begin, make sure you have: - Your extension **already published** on one of the supported install sources: - [winget](https://github.com/microsoft/winget-pkgs) (Windows Package Manager) - [Microsoft Store](https://apps.microsoft.com/) - - Direct download URL + - Direct download or release page URL ## Step-by-step submission guide From e35b7f9af8eb8cd4c8934fd758bec65c20975ee9 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 15:23:18 +0100 Subject: [PATCH 5/9] docs: add examples annotations to extension schema Adds examples to all fields in the JSON Schema for better editor autocompletion and documentation (id, title, description, author, icon, homepage, tags, and all installSources types). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/schemas/extension.schema.json | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/schemas/extension.schema.json b/.github/schemas/extension.schema.json index 969b503..39b08f3 100644 --- a/.github/schemas/extension.schema.json +++ b/.github/schemas/extension.schema.json @@ -21,13 +21,15 @@ "type": "string", "description": "Human-readable display name for the extension.", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "examples": ["Media Controls for Command Palette", "Quick Notes", "Clipboard Manager"] }, "description": { "type": "string", "description": "Short description of the extension.", "minLength": 1, - "maxLength": 200 + "maxLength": 200, + "examples": ["Manage audio or video playback and switch between media apps directly from Command Palette."] }, "author": { "type": "object", @@ -39,24 +41,28 @@ "type": "string", "description": "Author display name.", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "examples": ["Jiri Polasek", "Microsoft"] }, "url": { "type": "string", "description": "URL to the author's website or profile.", - "format": "uri" + "format": "uri", + "examples": ["https://jiripolasek.com", "https://github.com/microsoft"] } } }, "icon": { "type": "string", "description": "Filename of the icon in the same folder. Must be a .png or .svg file.", - "pattern": "^[\\w.-]+\\.(png|svg)$" + "pattern": "^[\\w.-]+\\.(png|svg)$", + "examples": ["icon.png", "icon.svg"] }, "homepage": { "type": "string", "description": "URL to the project homepage or repository.", - "format": "uri" + "format": "uri", + "examples": ["https://github.com/jiripolasek/MediaControlsExtension"] }, "category": { "type": "string", @@ -83,7 +89,8 @@ "maxLength": 30 }, "maxItems": 5, - "uniqueItems": true + "uniqueItems": true, + "examples": [["media", "playback", "audio"]] }, "installSources": { "type": "array", @@ -97,7 +104,7 @@ { "properties": { "type": { "const": "winget" }, - "id": { "type": "string", "description": "Winget package identifier.", "minLength": 1 } + "id": { "type": "string", "description": "Winget package identifier.", "minLength": 1, "examples": ["JiriPolasek.MediaControlsforCommandPalette"] } }, "required": ["type", "id"], "additionalProperties": false @@ -105,7 +112,7 @@ { "properties": { "type": { "const": "msstore" }, - "id": { "type": "string", "description": "Microsoft Store product ID.", "minLength": 1 } + "id": { "type": "string", "description": "Microsoft Store product ID.", "minLength": 1, "examples": ["9n3bq81g19k7"] } }, "required": ["type", "id"], "additionalProperties": false @@ -113,7 +120,7 @@ { "properties": { "type": { "const": "url" }, - "uri": { "type": "string", "description": "Direct download or release page URL.", "format": "uri" } + "uri": { "type": "string", "description": "Direct download or release page URL.", "format": "uri", "examples": ["https://github.com/jiripolasek/MediaControlsExtension/releases"] } }, "required": ["type", "uri"], "additionalProperties": false From 71ac31440f55be29765605a9ccc309d3e5c6938c Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 15:25:12 +0100 Subject: [PATCH 6/9] refactor: remove category field from extension schema Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/schemas/extension.schema.json | 16 ---------------- .github/scripts/validate.py | 22 +--------------------- docs/CONTRIBUTING.md | 15 --------------- extensions.json | 3 +-- extensions/sample-extension/extension.json | 1 - 5 files changed, 2 insertions(+), 55 deletions(-) diff --git a/.github/schemas/extension.schema.json b/.github/schemas/extension.schema.json index 39b08f3..b5608a4 100644 --- a/.github/schemas/extension.schema.json +++ b/.github/schemas/extension.schema.json @@ -64,22 +64,6 @@ "format": "uri", "examples": ["https://github.com/jiripolasek/MediaControlsExtension"] }, - "category": { - "type": "string", - "description": "Primary category for the extension.", - "enum": [ - "Developer Tools", - "Productivity", - "Utilities", - "System", - "Media", - "Communication", - "Education", - "Entertainment", - "Security", - "Other" - ] - }, "tags": { "type": "array", "description": "Optional freeform tags for filtering. Maximum 5 tags.", diff --git a/.github/scripts/validate.py b/.github/scripts/validate.py index f81d908..441b823 100644 --- a/.github/scripts/validate.py +++ b/.github/scripts/validate.py @@ -43,18 +43,6 @@ EXTENSIONS_DIR = REPO_ROOT / "extensions" SCHEMA_PATH = REPO_ROOT / ".github" / "schemas" / "extension.schema.json" -VALID_CATEGORIES = { - "Developer Tools", - "Productivity", - "Utilities", - "System", - "Media", - "Communication", - "Education", - "Entertainment", - "Security", - "Other", -} MAX_TAGS = 5 MAX_TAG_LENGTH = 30 @@ -211,15 +199,7 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p f"exceeds {MAX_ICON_SIZE_KB} KB limit" ) - # 7. Category must be from the predefined list (when provided) - category = data.get("category", "") - if category and category not in VALID_CATEGORIES: - errors.append( - f"{folder_name}/extension.json: Invalid category \"{category}\". " - f"Must be one of: {', '.join(sorted(VALID_CATEGORIES))}" - ) - - # 8. Tags validation (when provided) + # 7. Tags validation (when provided) tags = data.get("tags", []) if isinstance(tags, list): if len(tags) > MAX_TAGS: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f23f4aa..bab92c1 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -67,7 +67,6 @@ Create an `extension.json` file inside your folder. Here is the full template wi "name": "Publisher Display Name", "url": "https://github.com/publisher" }, - "category": "Utilities", "tags": ["tag1", "tag2"], "icon": "icon.png", "installSources": [ @@ -89,25 +88,11 @@ Create an `extension.json` file inside your folder. Here is the full template wi | `title` | **Required** | Human-readable display name (max 100 characters). | | `description` | **Required** | Short description of the extension (max 200 characters). | | `author` | **Required** | Object with `name` (required, max 100 characters) and `url` (optional). | -| `category` | Optional | Primary category. Must be one of the valid categories listed below. | | `tags` | Optional | Up to 5 freeform tags for filtering (each max 30 characters). | | `icon` | **Required** | Filename of the icon in the same folder (e.g., `icon.png`). | | `installSources` | **Required** | Array of install source objects. Each has a `type` and a type-specific identifier. See below. | | `homepage` | Optional | URL to the project homepage or repository. | -#### Valid categories - -- `Developer Tools` -- `Productivity` -- `Utilities` -- `System` -- `Media` -- `Communication` -- `Education` -- `Entertainment` -- `Security` -- `Other` - #### Install source types Each object in the `installSources` array has a `type` and a type-specific field: diff --git a/extensions.json b/extensions.json index d79563b..3ac57ac 100644 --- a/extensions.json +++ b/extensions.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json", "version": "1.0", - "generatedAt": "2026-03-27T13:00:10Z", + "generatedAt": "2026-03-27T14:25:06Z", "extensionCount": 1, "extensions": [ { @@ -13,7 +13,6 @@ "url": "https://github.com/microsoft" }, "homepage": "https://github.com/microsoft/CmdPal-Extensions", - "category": "Developer Tools", "tags": [ "sample", "demo", diff --git a/extensions/sample-extension/extension.json b/extensions/sample-extension/extension.json index 020906e..32831c6 100644 --- a/extensions/sample-extension/extension.json +++ b/extensions/sample-extension/extension.json @@ -9,7 +9,6 @@ }, "icon": "icon.png", "homepage": "https://github.com/microsoft/CmdPal-Extensions", - "category": "Developer Tools", "tags": ["sample", "demo", "template"], "installSources": [ { From cc80d4b127f116108320332c88b73b2121bcac2b Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 15:26:56 +0100 Subject: [PATCH 7/9] Update extensions/sample-extension/extension.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- extensions/sample-extension/extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/sample-extension/extension.json b/extensions/sample-extension/extension.json index 32831c6..93f0f77 100644 --- a/extensions/sample-extension/extension.json +++ b/extensions/sample-extension/extension.json @@ -13,7 +13,7 @@ "installSources": [ { "type": "url", - "uri": "https://github.com/microsoft/CmdPal-Extensions" + "uri": "https://github.com/microsoft/CmdPal-Extensions/releases/latest" } ] } From 6580f4feb3a211f52dfbcd914690c950849e8fdb Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 15:26:58 +0100 Subject: [PATCH 8/9] docs: align url install source description with schema Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bab92c1..3af98d7 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -101,7 +101,7 @@ Each object in the `installSources` array has a `type` and a type-specific field |------|-------|-------------|---------| | `winget` | `id` | The winget package identifier | `"Publisher.PackageName"` | | `msstore` | `id` | Microsoft Store product ID | `"9n3bq81g19k7"` | -| `url` | `uri` | Direct download URL | `"https://example.com/extension.msix"` | +| `url` | `uri` | Direct download or release page URL | `"https://github.com/publisher/extension/releases"` | ### 5. Add an icon From 0c1d78b5de897cb399debec1958a17a0bbd8eda1 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Fri, 27 Mar 2026 15:34:39 +0100 Subject: [PATCH 9/9] feat: namespace extensions under author folders to prevent name squatting Extensions now live at extensions/// with id format author.extension-name (e.g., jiripolasek.media-controls). - Schema id pattern updated to require dot-separated author.name - Validate script discovers extensions two levels deep - Generate script globs two levels deep, icon URLs include author path - Sample extension moved to extensions/microsoft/sample-extension/ - Docs, PR template, and README updated with new convention Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/schemas/extension.schema.json | 6 +- .github/scripts/generate.py | 10 +- .github/scripts/validate.py | 105 ++++++++++-------- README.md | 9 +- docs/CONTRIBUTING.md | 38 +++---- extensions.json | 8 +- .../sample-extension/extension.json | 4 +- .../{ => microsoft}/sample-extension/icon.png | Bin 9 files changed, 102 insertions(+), 82 deletions(-) rename extensions/{ => microsoft}/sample-extension/extension.json (82%) rename extensions/{ => microsoft}/sample-extension/icon.png (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 14f5f4e..03cab13 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,9 +5,9 @@ Please fill out the checklist below to help us review your PR quickly. ### Checklist -- [ ] My folder is named `` (lowercase, alphanumeric + hyphens only) +- [ ] My folder follows the `/` convention (lowercase, alphanumeric + hyphens only) - [ ] I have added an `extension.json` with all required fields -- [ ] The `id` field in my `extension.json` matches my folder name +- [ ] The `id` field in my `extension.json` matches my folder path (`author.extension-name`) - [ ] I have added an icon file (PNG or SVG, under 100 KB) - [ ] The `icon` field in `extension.json` matches my icon filename - [ ] My extension is available at the install source I specified (winget/MS Store/URL) diff --git a/.github/schemas/extension.schema.json b/.github/schemas/extension.schema.json index b5608a4..3549b32 100644 --- a/.github/schemas/extension.schema.json +++ b/.github/schemas/extension.schema.json @@ -13,9 +13,9 @@ }, "id": { "type": "string", - "description": "Unique extension identifier (slug). Must match the folder name.", - "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", - "examples": ["media-controls", "quick-notes", "sample-extension"] + "description": "Unique extension identifier in author.extension-name format. Must match the folder path (author/extension-name).", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*\\.[a-z0-9]+(-[a-z0-9]+)*$", + "examples": ["jiripolasek.media-controls", "microsoft.sample-extension"] }, "title": { "type": "string", diff --git a/.github/scripts/generate.py b/.github/scripts/generate.py index 5808718..e2a299f 100644 --- a/.github/scripts/generate.py +++ b/.github/scripts/generate.py @@ -29,8 +29,8 @@ def discover_extension_paths() -> list[str]: - """Return sorted paths to every extension.json under extensions/.""" - pattern = os.path.join(EXTENSIONS_DIR, "*", "extension.json") + """Return sorted paths to every extension.json under extensions///.""" + pattern = os.path.join(EXTENSIONS_DIR, "*", "*", "extension.json") return sorted(glob.glob(pattern)) @@ -60,8 +60,10 @@ def load_extension(path: str) -> dict | None: def build_icon_url(extension_id: str, icon_filename: str) -> str: - """Build the absolute raw GitHub URL for an extension's icon.""" - return f"{BASE_RAW_URL}/extensions/{extension_id}/{icon_filename}" + """Build the absolute raw GitHub URL for an extension's icon. + The id is author.extension-name, mapping to extensions/author/extension-name/.""" + author, ext_name = extension_id.split(".", 1) + return f"{BASE_RAW_URL}/extensions/{author}/{ext_name}/{icon_filename}" def transform_extension(data: dict) -> dict: diff --git a/.github/scripts/validate.py b/.github/scripts/validate.py index 441b823..0505105 100644 --- a/.github/scripts/validate.py +++ b/.github/scripts/validate.py @@ -49,8 +49,8 @@ MAX_ICON_SIZE_KB = 100 VALID_ICON_EXTENSIONS = {".png", ".svg"} -# V2 id format: lowercase alphanumeric slug with hyphens (e.g. "media-controls") -ID_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +# V2 id format: author.extension-name (e.g. "jiripolasek.media-controls") +ID_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*\.[a-z0-9]+(-[a-z0-9]+)*$") # --------------------------------------------------------------------------- @@ -68,27 +68,35 @@ def load_schema() -> dict: def discover_extension_folders_from_files(changed_files: List[str]) -> Set[pathlib.Path]: """Given a list of changed file paths, return the set of extension folder - paths (absolute) that were touched.""" + paths (absolute) that were touched. + Extensions live at extensions///.""" folders: Set[pathlib.Path] = set() for raw in changed_files: p = pathlib.Path(raw).resolve() - # Walk up to find a path whose parent is the extensions/ dir. - # e.g. extensions/quick-notes/extension.json -> extensions/quick-notes try: rel = p.relative_to(EXTENSIONS_DIR) except ValueError: continue # not under extensions/ - top_folder = EXTENSIONS_DIR / rel.parts[0] - if top_folder.is_dir(): - folders.add(top_folder) + # Need at least 2 levels: author/extension-name + if len(rel.parts) >= 2: + ext_folder = EXTENSIONS_DIR / rel.parts[0] / rel.parts[1] + if ext_folder.is_dir(): + folders.add(ext_folder) return folders def discover_all_extension_folders() -> Set[pathlib.Path]: - """Return every immediate sub-directory of extensions/.""" + """Return every extension folder under extensions///.""" if not EXTENSIONS_DIR.is_dir(): return set() - return {d for d in EXTENSIONS_DIR.iterdir() if d.is_dir()} + folders: Set[pathlib.Path] = set() + for author_dir in EXTENSIONS_DIR.iterdir(): + if not author_dir.is_dir(): + continue + for ext_dir in author_dir.iterdir(): + if ext_dir.is_dir(): + folders.add(ext_dir) + return folders def git_diff_changed_files() -> List[str]: @@ -112,21 +120,24 @@ def build_id_index(exclude_folder: pathlib.Path | None = None) -> dict[str, path index: dict[str, pathlib.Path] = {} if not EXTENSIONS_DIR.is_dir(): return index - for folder in EXTENSIONS_DIR.iterdir(): - if not folder.is_dir(): - continue - if exclude_folder and folder.resolve() == exclude_folder.resolve(): + for author_dir in EXTENSIONS_DIR.iterdir(): + if not author_dir.is_dir(): continue - ext_json = folder / "extension.json" - if ext_json.exists(): - try: - with open(ext_json, encoding="utf-8") as fh: - data = json.load(fh) - ext_id = data.get("id") - if ext_id: - index[ext_id] = folder - except (json.JSONDecodeError, OSError): - pass # skip broken files; they'll be caught during their own validation + for folder in author_dir.iterdir(): + if not folder.is_dir(): + continue + if exclude_folder and folder.resolve() == exclude_folder.resolve(): + continue + ext_json = folder / "extension.json" + if ext_json.exists(): + try: + with open(ext_json, encoding="utf-8") as fh: + data = json.load(fh) + ext_id = data.get("id") + if ext_id: + index[ext_id] = folder + except (json.JSONDecodeError, OSError): + pass return index @@ -139,11 +150,13 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p """Validate a single extension folder. Returns a list of error strings.""" errors: List[str] = [] folder_name = folder.name + author_name = folder.parent.name + display_path = f"{author_name}/{folder_name}" ext_json_path = folder / "extension.json" # 1. extension.json must exist if not ext_json_path.exists(): - errors.append(f"{folder_name}: Missing required file extension.json") + errors.append(f"{display_path}: Missing required file extension.json") return errors # nothing more to check # 2. Must be valid JSON @@ -151,7 +164,7 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p with open(ext_json_path, encoding="utf-8") as fh: data = json.load(fh) except json.JSONDecodeError as exc: - errors.append(f"{folder_name}/extension.json: Invalid JSON – {exc}") + errors.append(f"{display_path}/extension.json: Invalid JSON – {exc}") return errors # 3. JSON Schema validation @@ -159,21 +172,23 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p jsonschema.validate(instance=data, schema=schema) except jsonschema.ValidationError as exc: path = " -> ".join(str(p) for p in exc.absolute_path) if exc.absolute_path else "(root)" - errors.append(f"{folder_name}/extension.json: Schema validation error at '{path}': {exc.message}") + errors.append(f"{display_path}/extension.json: Schema validation error at '{path}': {exc.message}") - # 4. id must match folder name + # 4. id must match folder path (author/extension-name -> author.extension-name) ext_id = data.get("id", "") - if ext_id != folder_name: + author_dir = folder.parent.name + expected_id = f"{author_dir}.{folder_name}" + if ext_id != expected_id: errors.append( - f"{folder_name}/extension.json: 'id' field \"{ext_id}\" " - f"does not match folder name \"{folder_name}\"" + f"{author_dir}/{folder_name}/extension.json: 'id' field \"{ext_id}\" " + f"does not match expected \"{expected_id}\" (from folder path {author_dir}/{folder_name}/)" ) - # 5. id must be a valid V2 slug (lowercase alphanumeric + hyphens) + # 5. id must be a valid author.extension-name format if ext_id and not ID_PATTERN.match(ext_id): errors.append( - f"{folder_name}/extension.json: 'id' field \"{ext_id}\" is not a valid slug. " - f"Must be lowercase alphanumeric with hyphens (e.g. \"quick-notes\")." + f"{author_dir}/{folder_name}/extension.json: 'id' field \"{ext_id}\" is not valid. " + f"Must be author.extension-name format (e.g. \"jiripolasek.media-controls\")." ) # 6. Icon file must exist, be PNG/SVG, and ≤100 KB @@ -182,20 +197,20 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p icon_path = folder / icon_filename if not icon_path.exists(): errors.append( - f"{folder_name}/extension.json: Icon file \"{icon_filename}\" " - f"not found in {folder_name}/" + f"{display_path}/extension.json: Icon file \"{icon_filename}\" " + f"not found in {display_path}/" ) else: suffix = icon_path.suffix.lower() if suffix not in VALID_ICON_EXTENSIONS: errors.append( - f"{folder_name}/{icon_filename}: Icon must be .png or .svg " + f"{display_path}/{icon_filename}: Icon must be .png or .svg " f"(got \"{suffix}\")" ) size_kb = icon_path.stat().st_size / 1024 if size_kb > MAX_ICON_SIZE_KB: errors.append( - f"{folder_name}/{icon_filename}: Icon is {size_kb:.1f} KB, " + f"{display_path}/{icon_filename}: Icon is {size_kb:.1f} KB, " f"exceeds {MAX_ICON_SIZE_KB} KB limit" ) @@ -204,22 +219,23 @@ def validate_extension(folder: pathlib.Path, schema: dict, id_index: dict[str, p if isinstance(tags, list): if len(tags) > MAX_TAGS: errors.append( - f"{folder_name}/extension.json: Too many tags ({len(tags)}). " + f"{display_path}/extension.json: Too many tags ({len(tags)}). " f"Maximum is {MAX_TAGS}." ) for i, tag in enumerate(tags): if isinstance(tag, str) and len(tag) > MAX_TAG_LENGTH: errors.append( - f"{folder_name}/extension.json: Tag #{i + 1} \"{tag}\" " + f"{display_path}/extension.json: Tag #{i + 1} \"{tag}\" " f"exceeds {MAX_TAG_LENGTH} character limit ({len(tag)} chars)" ) # 9. Duplicate ID check across the gallery if ext_id and ext_id in id_index: other = id_index[ext_id] + other_display = f"{other.parent.name}/{other.name}" errors.append( - f"{folder_name}/extension.json: Duplicate id \"{ext_id}\" — " - f"already used by {other.name}/" + f"{display_path}/extension.json: Duplicate id \"{ext_id}\" — " + f"already used by {other_display}/" ) return errors @@ -269,7 +285,8 @@ def main() -> int: validated_count = 0 for folder in sorted(folders): - print(f"Validating {folder.name}/ ...") + display = f"{folder.parent.name}/{folder.name}" + print(f"Validating {display}/ ...") # Build ID index excluding the current folder to detect duplicates elsewhere id_index = build_id_index(exclude_folder=folder) errors = validate_extension(folder, schema, id_index) @@ -278,7 +295,7 @@ def main() -> int: print(f" ❌ {err}") total_errors.extend(errors) else: - print(f" ✅ {folder.name} is valid") + print(f" ✅ {display} is valid") validated_count += 1 # Summary diff --git a/README.md b/README.md index acf4b1c..1a56cb5 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,11 @@ https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions.js ``` CmdPal-Extensions/ -├── extensions/ # One folder per extension submission -│ └── / -│ ├── extension.json # Extension metadata -│ └── icon.png # Extension icon +├── extensions/ # All extension submissions +│ └── / +│ └── / +│ ├── extension.json # Extension metadata +│ └── icon.png # Extension icon ├── extensions.json # Auto-generated aggregate (do not edit) ├── docs/ │ └── CONTRIBUTING.md # Submission guide for developers diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3af98d7..c116dae 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -34,24 +34,24 @@ git checkout -b add-my-extension Create a folder under `extensions/` using the naming convention: ``` -extensions// +extensions/// ``` **Naming rules:** | Rule | Detail | |------|--------| -| Format | `` — a simple slug | -| Characters | Lowercase alphanumeric characters and hyphens only (`a-z`, `0-9`, `-`) | -| Hyphens | Use hyphens to separate words (e.g., `quick-notes`, not `quicknotes`) | -| No leading/trailing hyphens | Must start and end with an alphanumeric character | +| Structure | `/` — author folder containing extension folder | +| Author | Your name or org, lowercase alphanumeric + hyphens (e.g., `jiripolasek`, `microsoft`) | +| Extension name | Lowercase alphanumeric + hyphens (e.g., `media-controls`) | +| No leading/trailing hyphens | Each segment must start and end with an alphanumeric character | **Examples:** -- ✅ `media-controls` -- ✅ `clipboard-manager` -- ❌ `MediaControls` (uppercase not allowed) -- ❌ `clipboard_manager` (underscores not allowed) +- ✅ `jiripolasek/media-controls` +- ✅ `my-company/clipboard-manager` +- ❌ `MediaControls` (missing author folder, uppercase) +- ❌ `my_company/clipboard_manager` (underscores not allowed) ### 4. Add `extension.json` @@ -59,8 +59,8 @@ Create an `extension.json` file inside your folder. Here is the full template wi ```json { - "$schema": "../../.github/schemas/extension.schema.json", - "id": "my-extension", + "$schema": "../../../.github/schemas/extension.schema.json", + "id": "publisher.my-extension", "title": "My Extension", "description": "A short description of what the extension does (max 200 characters).", "author": { @@ -83,8 +83,8 @@ Create an `extension.json` file inside your folder. Here is the full template wi | Field | Required | Description | |-------|----------|-------------| -| `$schema` | Optional | Path to the JSON schema. Enables editor autocompletion and validation. Use `"../../.github/schemas/extension.schema.json"`. | -| `id` | **Required** | Unique identifier slug (e.g., `media-controls`). **Must match your folder name exactly.** | +| `$schema` | Optional | Path to the JSON schema. Enables editor autocompletion and validation. Use `"../../../.github/schemas/extension.schema.json"`. | +| `id` | **Required** | Unique identifier in `author.extension-name` format (e.g., `jiripolasek.media-controls`). **Must match your folder path** (`author/extension-name`). | | `title` | **Required** | Human-readable display name (max 100 characters). | | `description` | **Required** | Short description of the extension (max 200 characters). | | `author` | **Required** | Object with `name` (required, max 100 characters) and `url` (optional). | @@ -117,8 +117,8 @@ Place an icon file in your extension folder alongside `extension.json`. Push your branch to your fork and open a pull request targeting the `main` branch of this repository. ```bash -git add extensions/my-extension/ -git commit -m "Add my-extension to gallery" +git add extensions/my-publisher/my-extension/ +git commit -m "Add my-publisher.my-extension to gallery" git push origin add-my-extension ``` @@ -130,7 +130,7 @@ Our CI pipeline automatically validates your submission. It checks that: - Your `extension.json` conforms to the schema - Required fields are present and correctly formatted -- The `id` matches the folder name +- The `id` matches the folder path (`author.extension-name` ↔ `author/extension-name/`) - The icon file exists and is within size limits If the CI reports errors, review the logs, fix the issues, and push updated commits to your PR branch. @@ -144,14 +144,14 @@ A maintainer will review your PR. Once approved and merged, your extension will To update an already-published extension (e.g., bump the version, update the description, or change the icon): 1. Create a new branch in your fork -2. Update the files in your existing `extensions//` folder +2. Update the files in your existing `extensions///` folder 3. Open a new pull request targeting `main` The same CI validation and review process applies. ## Reference -See [`extensions/sample-extension/`](../extensions/sample-extension/) for a complete working example of a gallery submission. +See [`extensions/microsoft/sample-extension/`](../extensions/microsoft/sample-extension/) for a complete working example of a gallery submission. ## Schema reference @@ -159,7 +159,7 @@ For editor autocompletion and inline validation, add the `$schema` property to t ```json { - "$schema": "../../.github/schemas/extension.schema.json" + "$schema": "../../../.github/schemas/extension.schema.json" } ``` diff --git a/extensions.json b/extensions.json index 3ac57ac..0a73f83 100644 --- a/extensions.json +++ b/extensions.json @@ -1,11 +1,11 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json", "version": "1.0", - "generatedAt": "2026-03-27T14:25:06Z", + "generatedAt": "2026-03-27T14:34:28Z", "extensionCount": 1, "extensions": [ { - "id": "sample-extension", + "id": "microsoft.sample-extension", "title": "Sample Extension", "description": "A sample Command Palette extension to demonstrate the gallery submission format.", "author": { @@ -21,10 +21,10 @@ "installSources": [ { "type": "url", - "uri": "https://github.com/microsoft/CmdPal-Extensions" + "uri": "https://github.com/microsoft/CmdPal-Extensions/releases/latest" } ], - "iconUrl": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions/sample-extension/icon.png" + "iconUrl": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions/microsoft/sample-extension/icon.png" } ] } diff --git a/extensions/sample-extension/extension.json b/extensions/microsoft/sample-extension/extension.json similarity index 82% rename from extensions/sample-extension/extension.json rename to extensions/microsoft/sample-extension/extension.json index 93f0f77..7c58750 100644 --- a/extensions/sample-extension/extension.json +++ b/extensions/microsoft/sample-extension/extension.json @@ -1,6 +1,6 @@ { - "$schema": "../../.github/schemas/extension.schema.json", - "id": "sample-extension", + "$schema": "../../../.github/schemas/extension.schema.json", + "id": "microsoft.sample-extension", "title": "Sample Extension", "description": "A sample Command Palette extension to demonstrate the gallery submission format.", "author": { diff --git a/extensions/sample-extension/icon.png b/extensions/microsoft/sample-extension/icon.png similarity index 100% rename from extensions/sample-extension/icon.png rename to extensions/microsoft/sample-extension/icon.png