diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c984ed7..03cab13 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 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/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 new file mode 100644 index 0000000..3549b32 --- /dev/null +++ b/.github/schemas/extension.schema.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$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", + "required": ["id", "title", "description", "author", "icon", "installSources"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "Reference to this schema file." + }, + "id": { + "type": "string", + "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", + "description": "Human-readable display name for the extension.", + "minLength": 1, + "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, + "examples": ["Manage audio or video playback and switch between media apps directly from Command Palette."] + }, + "author": { + "type": "object", + "description": "Extension author information.", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Author display name.", + "minLength": 1, + "maxLength": 100, + "examples": ["Jiri Polasek", "Microsoft"] + }, + "url": { + "type": "string", + "description": "URL to the author's website or profile.", + "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)$", + "examples": ["icon.png", "icon.svg"] + }, + "homepage": { + "type": "string", + "description": "URL to the project homepage or repository.", + "format": "uri", + "examples": ["https://github.com/jiripolasek/MediaControlsExtension"] + }, + "tags": { + "type": "array", + "description": "Optional freeform tags for filtering. Maximum 5 tags.", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 30 + }, + "maxItems": 5, + "uniqueItems": true, + "examples": [["media", "playback", "audio"]] + }, + "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, "examples": ["JiriPolasek.MediaControlsforCommandPalette"] } + }, + "required": ["type", "id"], + "additionalProperties": false + }, + { + "properties": { + "type": { "const": "msstore" }, + "id": { "type": "string", "description": "Microsoft Store product ID.", "minLength": 1, "examples": ["9n3bq81g19k7"] } + }, + "required": ["type", "id"], + "additionalProperties": false + }, + { + "properties": { + "type": { "const": "url" }, + "uri": { "type": "string", "description": "Direct download or release page URL.", "format": "uri", "examples": ["https://github.com/jiripolasek/MediaControlsExtension/releases"] } + }, + "required": ["type", "uri"], + "additionalProperties": false + } + ] + } + } + } +} diff --git a/scripts/generate.py b/.github/scripts/generate.py similarity index 86% rename from scripts/generate.py rename to .github/scripts/generate.py index e173434..e2a299f 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. @@ -30,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)) @@ -61,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: @@ -118,7 +119,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 65% rename from scripts/validate.py rename to .github/scripts/validate.py index 230191f..0505105 100644 --- a/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 @@ -38,28 +39,19 @@ # 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" - -VALID_CATEGORIES = { - "Developer Tools", - "Productivity", - "Utilities", - "System", - "Media", - "Communication", - "Education", - "Entertainment", - "Security", - "Other", -} +SCHEMA_PATH = REPO_ROOT / ".github" / "schemas" / "extension.schema.json" + MAX_TAGS = 5 MAX_TAG_LENGTH = 30 MAX_ICON_SIZE_KB = 100 VALID_ICON_EXTENSIONS = {".png", ".svg"} +# 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]+)*$") + # --------------------------------------------------------------------------- # Helpers @@ -76,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/contoso.quick-notes/extension.json -> extensions/contoso.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]: @@ -120,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 @@ -147,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 @@ -159,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 @@ -167,68 +172,70 @@ 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. Icon file must exist, be PNG/SVG, and ≤100 KB + # 5. id must be a valid author.extension-name format + if ext_id and not ID_PATTERN.match(ext_id): + errors.append( + 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 icon_filename = data.get("icon", "") if icon_filename: 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" ) - # 6. Category must be from the predefined list - 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))}" - ) - - # 7. Tags validation + # 7. Tags validation (when provided) tags = data.get("tags", []) 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)" ) - # 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] + 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 @@ -246,7 +253,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( @@ -278,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) @@ -287,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/.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..5809930 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -21,16 +21,13 @@ 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 + - 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 scripts/validate.py ${{ steps.changed.outputs.files }} + if [ -n "$FILES" ]; then + python .github/scripts/validate.py $FILES + else + echo "No extension files changed." + fi diff --git a/README.md b/README.md index bdcced9..1a56cb5 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,24 +15,22 @@ 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 ``` CmdPal-Extensions/ -├── extensions/ # One folder per extension submission -│ └── ./ -│ ├── 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/ # 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 +└── .github/ # CI workflows, scripts, and schemas ``` ## Contributing diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 63cf7a4..c116dae 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 @@ -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 or release page 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 | `.` | -| 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`) | +| 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:** -- ✅ `contoso.quick-notes` -- ✅ `my-company.clipboard-manager` -- ❌ `Contoso.QuickNotes` (uppercase not allowed) -- ❌ `contoso_quick_notes` (underscores not allowed, missing dot separator) +- ✅ `jiripolasek/media-controls` +- ✅ `my-company/clipboard-manager` +- ❌ `MediaControls` (missing author folder, uppercase) +- ❌ `my_company/clipboard_manager` (underscores not allowed) ### 4. Add `extension.json` @@ -60,21 +59,23 @@ Create an `extension.json` file inside your folder. Here is the full template wi ```json { - "$schema": "../../schemas/extension.schema.json", - "id": "publisher.extension-id", - "name": "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).", - "publisher": "Publisher Display Name", - "version": "1.0.0", - "category": "Utilities", + "author": { + "name": "Publisher Display Name", + "url": "https://github.com/publisher" + }, "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" } ``` @@ -82,41 +83,25 @@ 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"`. | -| `id` | **Required** | Unique identifier in `.` format. **Must match your folder name exactly.** | -| `name` | **Required** | Human-readable display name (max 100 characters). | +| `$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). | -| `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). | | `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 - -- `Developer Tools` -- `Productivity` -- `Utilities` -- `System` -- `Media` -- `Communication` -- `Education` -- `Entertainment` -- `Security` -- `Other` #### 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 or release page URL | `"https://github.com/publisher/extension/releases"` | ### 5. Add an icon @@ -132,7 +117,7 @@ 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 add extensions/my-publisher/my-extension/ git commit -m "Add my-publisher.my-extension to gallery" git push origin add-my-extension ``` @@ -145,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. @@ -159,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/microsoft.sample-extension/`](../extensions/microsoft.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 @@ -174,7 +159,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 53% rename from generated/extensions.json rename to extensions.json index b6d11b6..0a73f83 100644 --- a/generated/extensions.json +++ b/extensions.json @@ -1,28 +1,30 @@ { - "$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-27T14:34:28Z", "extensionCount": 1, "extensions": [ { "id": "microsoft.sample-extension", - "name": "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", + "author": { + "name": "Microsoft", + "url": "https://github.com/microsoft" + }, + "homepage": "https://github.com/microsoft/CmdPal-Extensions", "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/releases/latest" + } + ], + "iconUrl": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/extensions/microsoft/sample-extension/icon.png" } ] } diff --git a/extensions/microsoft.sample-extension/extension.json b/extensions/microsoft.sample-extension/extension.json deleted file mode 100644 index 8a2fa68..0000000 --- a/extensions/microsoft.sample-extension/extension.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "../../schemas/extension.schema.json", - "id": "microsoft.sample-extension", - "name": "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" - }, - "homepage": "https://github.com/microsoft/CmdPal-Extensions", - "license": "MIT" -} diff --git a/extensions/microsoft/sample-extension/extension.json b/extensions/microsoft/sample-extension/extension.json new file mode 100644 index 0000000..7c58750 --- /dev/null +++ b/extensions/microsoft/sample-extension/extension.json @@ -0,0 +1,19 @@ +{ + "$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": { + "name": "Microsoft", + "url": "https://github.com/microsoft" + }, + "icon": "icon.png", + "homepage": "https://github.com/microsoft/CmdPal-Extensions", + "tags": ["sample", "demo", "template"], + "installSources": [ + { + "type": "url", + "uri": "https://github.com/microsoft/CmdPal-Extensions/releases/latest" + } + ] +} diff --git a/extensions/microsoft.sample-extension/icon.png b/extensions/microsoft/sample-extension/icon.png similarity index 100% rename from extensions/microsoft.sample-extension/icon.png rename to extensions/microsoft/sample-extension/icon.png diff --git a/schemas/extension.schema.json b/schemas/extension.schema.json deleted file mode 100644 index 54500fd..0000000 --- a/schemas/extension.schema.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/schemas/extension.schema.json", - "title": "Command Palette Extension", - "description": "Schema for a Command Palette extension submission.", - "type": "object", - "required": ["id", "name", "description", "publisher", "version", "category", "icon", "installSource"], - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "description": "Reference to this schema file." - }, - "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"] - }, - "name": { - "type": "string", - "description": "Human-readable display name for the extension.", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "description": "Short description of the extension.", - "minLength": 1, - "maxLength": 200 - }, - "publisher": { - "type": "string", - "description": "Publisher display name.", - "minLength": 1, - "maxLength": 100 - }, - "version": { - "type": "string", - "description": "Semantic version string.", - "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.]+)?$", - "examples": ["1.0.0", "2.1.0-beta.1"] - }, - "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.", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 30 - }, - "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 - } - } - }, - "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 - } - } -}