From ad591607eaef15b2c080c5b5622358ea9e84ac9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:06:18 +0000 Subject: [PATCH] docs: update RFC, user guide, and API reference for multi-catalog support - RFC: replace FUTURE FEATURE section with full implementation docs, add catalog stack resolution order, config file examples, merge conflict resolution, and install_allowed behavior - EXTENSION-USER-GUIDE.md: add multi-catalog section with CLI examples for catalogs/catalog-add/catalog-remove, update catalog config docs - EXTENSION-API-REFERENCE.md: add CatalogEntry class docs, update ExtensionCatalog docs with new methods and result annotations, add catalog CLI commands (catalogs, catalog add, catalog remove) Also fix extension_catalogs command to correctly show "Using built-in default catalog stack" when config file exists but has empty catalogs Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> --- extensions/EXTENSION-API-REFERENCE.md | 113 ++++++++++++++++++++++++-- extensions/EXTENSION-USER-GUIDE.md | 106 ++++++++++++++++++++---- extensions/RFC-EXTENSION-SYSTEM.md | 105 ++++++++++++++++++------ src/specify_cli/__init__.py | 4 +- 4 files changed, 281 insertions(+), 47 deletions(-) diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 9764ca83..4c2764c1 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -243,6 +243,32 @@ manager.check_compatibility( ) # Raises: CompatibilityError if incompatible ``` +### CatalogEntry + +**Module**: `specify_cli.extensions` + +Represents a single catalog in the active catalog stack. + +```python +from specify_cli.extensions import CatalogEntry + +entry = CatalogEntry( + url="https://example.com/catalog.json", + name="org-approved", + priority=1, + install_allowed=True, +) +``` + +**Fields**: + +| Field | Type | Description | +|-------|------|-------------| +| `url` | `str` | Catalog URL (must use HTTPS, or HTTP for localhost) | +| `name` | `str` | Human-readable catalog name | +| `priority` | `int` | Sort order (lower = higher priority, wins on conflicts) | +| `install_allowed` | `bool` | Whether extensions from this catalog can be installed | + ### ExtensionCatalog **Module**: `specify_cli.extensions` @@ -253,30 +279,65 @@ from specify_cli.extensions import ExtensionCatalog catalog = ExtensionCatalog(project_root) ``` +**Class attributes**: + +```python +ExtensionCatalog.DEFAULT_CATALOG_URL # org-approved catalog URL +ExtensionCatalog.COMMUNITY_CATALOG_URL # community catalog URL +``` + **Methods**: ```python -# Fetch catalog +# Get the ordered list of active catalogs +entries = catalog.get_active_catalogs() # List[CatalogEntry] + +# Fetch catalog (primary catalog, backward compat) catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict -# Search extensions +# Search extensions across all active catalogs +# Each result includes _catalog_name and _install_allowed results = catalog.search( query: Optional[str] = None, tag: Optional[str] = None, author: Optional[str] = None, verified_only: bool = False -) # Returns: List[Dict] +) # Returns: List[Dict] — each dict includes _catalog_name, _install_allowed -# Get extension info +# Get extension info (searches all active catalogs) +# Returns None if not found; includes _catalog_name and _install_allowed ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict] -# Check cache validity +# Check cache validity (primary catalog) is_valid = catalog.is_cache_valid() # bool -# Clear cache +# Clear all catalog caches catalog.clear_cache() ``` +**Result annotation fields**: + +Each extension dict returned by `search()` and `get_extension_info()` includes: + +| Field | Type | Description | +|-------|------|-------------| +| `_catalog_name` | `str` | Name of the source catalog | +| `_install_allowed` | `bool` | Whether installation is allowed from this catalog | + +**Catalog config file** (`.specify/extension-catalogs.yml`): + +```yaml +catalogs: + - name: "org-approved" + url: "https://example.com/catalog.json" + priority: 1 + install_allowed: true + - name: "community" + url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" + priority: 2 + install_allowed: false +``` + ### HookExecutor **Module**: `specify_cli.extensions` @@ -543,6 +604,38 @@ EXECUTE_COMMAND: {command} **Output**: List of installed extensions with metadata +### extension catalogs + +**Usage**: `specify extension catalogs` + +Lists all active catalogs in the current catalog stack, showing name, URL, priority, and `install_allowed` status. + +### extension catalog add + +**Usage**: `specify extension catalog add URL [OPTIONS]` + +**Options**: + +- `--name NAME` - Catalog name (required) +- `--priority INT` - Priority (lower = higher priority, default: 10) +- `--install-allowed / --no-install-allowed` - Allow installs from this catalog (default: false) + +**Arguments**: + +- `URL` - Catalog URL (must use HTTPS) + +Adds a catalog entry to `.specify/extension-catalogs.yml`. + +### extension catalog remove + +**Usage**: `specify extension catalog remove NAME` + +**Arguments**: + +- `NAME` - Catalog name to remove + +Removes a catalog entry from `.specify/extension-catalogs.yml`. + ### extension add **Usage**: `specify extension add EXTENSION [OPTIONS]` @@ -551,13 +644,13 @@ EXECUTE_COMMAND: {command} - `--from URL` - Install from custom URL - `--dev PATH` - Install from local directory -- `--version VERSION` - Install specific version -- `--no-register` - Skip command registration **Arguments**: - `EXTENSION` - Extension name or URL +**Note**: Extensions from catalogs with `install_allowed: false` cannot be installed via this command. + ### extension remove **Usage**: `specify extension remove EXTENSION [OPTIONS]` @@ -575,6 +668,8 @@ EXECUTE_COMMAND: {command} **Usage**: `specify extension search [QUERY] [OPTIONS]` +Searches all active catalogs simultaneously. Results include source catalog name and install_allowed status. + **Options**: - `--tag TAG` - Filter by tag @@ -589,6 +684,8 @@ EXECUTE_COMMAND: {command} **Usage**: `specify extension info EXTENSION` +Shows source catalog and install_allowed status. + **Arguments**: - `EXTENSION` - Extension ID diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index f5b5befa..e561cf2b 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -76,7 +76,7 @@ vim .specify/extensions/jira/jira-config.yml ## Finding Extensions -**Note**: By default, `specify extension search` uses your organization's catalog (`catalog.json`). If the catalog is empty, you won't see any results. See [Extension Catalogs](#extension-catalogs) to learn how to populate your catalog from the community reference catalog. +`specify extension search` searches **all active catalogs** simultaneously, including the community catalog by default. Results are annotated with their source catalog and install status. ### Browse All Extensions @@ -84,7 +84,7 @@ vim .specify/extensions/jira/jira-config.yml specify extension search ``` -Shows all extensions in your organization's catalog. +Shows all extensions across all active catalogs (org-approved and community by default). ### Search by Keyword @@ -402,13 +402,13 @@ In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`), | Variable | Description | Default | |----------|-------------|---------| -| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog | +| `SPECKIT_CATALOG_URL` | Override the full catalog stack with a single URL (backward compat) | Built-in default stack | | `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None | #### Example: Using a custom catalog for testing ```bash -# Point to a local or alternative catalog +# Point to a local or alternative catalog (replaces the full stack) export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json" # Or use a staging catalog @@ -419,13 +419,73 @@ export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json" ## Extension Catalogs -For information about how Spec Kit's dual-catalog system works (`catalog.json` vs `catalog.community.json`), see the main [Extensions README](README.md#extension-catalogs). +Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simultaneously. By default, two catalogs are active: + +| Priority | Catalog | Install Allowed | Purpose | +|----------|---------|-----------------|---------| +| 1 | `catalog.json` (org-approved) | ✅ Yes | Extensions your org approves for installation | +| 2 | `catalog.community.json` (community) | ❌ No (discovery only) | Browse community extensions | + +### Listing Active Catalogs + +```bash +specify extension catalogs +``` + +### Adding a Catalog (Project-scoped) + +```bash +# Add an internal catalog that allows installs +specify extension catalog add \ + --name "internal" \ + --priority 2 \ + --install-allowed \ + https://internal.company.com/spec-kit/catalog.json + +# Add a discovery-only catalog +specify extension catalog add \ + --name "partner" \ + --priority 5 \ + https://partner.example.com/spec-kit/catalog.json +``` + +This creates or updates `.specify/extension-catalogs.yml`. + +### Removing a Catalog + +```bash +specify extension catalog remove internal +``` + +### Manual Config File + +You can also edit `.specify/extension-catalogs.yml` directly: + +```yaml +catalogs: + - name: "org-approved" + url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" + priority: 1 + install_allowed: true + + - name: "internal" + url: "https://internal.company.com/spec-kit/catalog.json" + priority: 2 + install_allowed: true + + - name: "community" + url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" + priority: 3 + install_allowed: false +``` + +A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. Project-level config takes full precedence when present. ## Organization Catalog Customization ### Why Customize Your Catalog -Organizations customize their `catalog.json` to: +Organizations customize their catalogs to: - **Control available extensions** - Curate which extensions your team can install - **Host private extensions** - Internal tools that shouldn't be public @@ -503,24 +563,40 @@ Options for hosting your catalog: #### 3. Configure Your Environment -##### Option A: Environment variable (recommended for CI/CD) +##### Option A: Catalog stack config file (recommended) + +Add to `.specify/extension-catalogs.yml` in your project: + +```yaml +catalogs: + - name: "org-approved" + url: "https://your-org.com/spec-kit/catalog.json" + priority: 1 + install_allowed: true +``` + +Or use the CLI: + +```bash +specify extension catalog add \ + --name "org-approved" \ + --install-allowed \ + https://your-org.com/spec-kit/catalog.json +``` + +##### Option B: Environment variable (recommended for CI/CD, single-catalog) ```bash # In ~/.bashrc, ~/.zshrc, or CI pipeline export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" ``` -##### Option B: Per-project configuration - -Create `.env` or set in your shell before running spec-kit commands: - -```bash -SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" specify extension search -``` - #### 4. Verify Configuration ```bash +# List active catalogs +specify extension catalogs + # Search should now show your catalog's extensions specify extension search diff --git a/extensions/RFC-EXTENSION-SYSTEM.md b/extensions/RFC-EXTENSION-SYSTEM.md index 248e6275..7df07e42 100644 --- a/extensions/RFC-EXTENSION-SYSTEM.md +++ b/extensions/RFC-EXTENSION-SYSTEM.md @@ -868,7 +868,7 @@ Spec Kit uses two catalog files with different purposes: - **Purpose**: Organization's curated catalog of approved extensions - **Default State**: Empty by design - users populate with extensions they trust -- **Usage**: Default catalog used by `specify extension` CLI commands +- **Usage**: Primary catalog (priority 1, `install_allowed: true`) in the default stack - **Control**: Organizations maintain their own fork/version for their teams #### Community Reference Catalog (`catalog.community.json`) @@ -879,16 +879,16 @@ Spec Kit uses two catalog files with different purposes: - **Verification**: Community extensions may have `verified: false` initially - **Status**: Active - open for community contributions - **Submission**: Via Pull Request following the Extension Publishing Guide -- **Usage**: Browse to discover extensions, then copy to your `catalog.json` +- **Usage**: Secondary catalog (priority 2, `install_allowed: false`) in the default stack — discovery only -**How It Works:** +**How It Works (default stack):** -1. **Discover**: Browse `catalog.community.json` to find available extensions -2. **Review**: Evaluate extensions for security, quality, and organizational fit -3. **Curate**: Copy approved extension entries from community catalog to your `catalog.json` -4. **Install**: Use `specify extension add ` (pulls from your curated catalog) +1. **Discover**: `specify extension search` searches both catalogs — community extensions appear automatically +2. **Review**: Evaluate community extensions for security, quality, and organizational fit +3. **Curate**: Copy approved entries from community catalog to your `catalog.json`, or add to `.specify/extension-catalogs.yml` with `install_allowed: true` +4. **Install**: Use `specify extension add ` — only allowed from `install_allowed: true` catalogs -This approach gives organizations full control over which extensions are available to their teams while maintaining a shared community resource for discovery. +This approach gives organizations full control over which extensions can be installed while still providing community discoverability out of the box. ### Catalog Format @@ -961,30 +961,89 @@ specify extension info jira ### Custom Catalogs -**⚠️ FUTURE FEATURE - NOT YET IMPLEMENTED** +Spec Kit supports a **catalog stack** — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to benefit from org-approved extensions, an internal catalog, and community discovery all at once. -The following catalog management commands are proposed design concepts but are not yet available in the current implementation: +#### Catalog Stack Resolution -```bash -# Add custom catalog (FUTURE - NOT AVAILABLE) -specify extension add-catalog https://internal.company.com/spec-kit/catalog.json +The active catalog stack is resolved in this order (first match wins): -# Set as default (FUTURE - NOT AVAILABLE) -specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json +1. **`SPECKIT_CATALOG_URL` environment variable** — single catalog replacing all defaults (backward compat) +2. **Project-level `.specify/extension-catalogs.yml`** — full control for the project +3. **User-level `~/.specify/extension-catalogs.yml`** — personal defaults +4. **Built-in default stack** — `catalog.json` (install_allowed: true) + `catalog.community.json` (install_allowed: false) -# List catalogs (FUTURE - NOT AVAILABLE) -specify extension catalogs +#### Default Built-in Stack + +When no config file exists, the CLI uses: + +| Priority | Catalog | install_allowed | Purpose | +|----------|---------|-----------------|---------| +| 1 | `catalog.json` (org-approved) | `true` | Extensions your org approves for installation | +| 2 | `catalog.community.json` (community) | `false` | Discovery only — browse but not install | + +This means `specify extension search` surfaces community extensions out of the box, while `specify extension add` is still restricted to org-approved entries. + +#### `.specify/extension-catalogs.yml` Config File + +```yaml +catalogs: + - name: "org-approved" + url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" + priority: 1 # Highest — only approved entries can be installed + install_allowed: true + + - name: "internal" + url: "https://internal.company.com/spec-kit/catalog.json" + priority: 2 + install_allowed: true + + - name: "community" + url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" + priority: 3 # Lowest — discovery only, not installable + install_allowed: false ``` -**Proposed catalog priority** (future design): +A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. When a project-level config is present, it takes full control and the built-in defaults are not applied. -1. Project-specific catalog (`.specify/extension-catalogs.yml`) - *not implemented* -2. User-level catalog (`~/.specify/extension-catalogs.yml`) - *not implemented* -3. Default GitHub catalog +#### Catalog CLI Commands -#### Current Implementation: SPECKIT_CATALOG_URL +```bash +# List active catalogs with name, URL, priority, and install_allowed +specify extension catalogs -**The currently available method** for using custom catalogs is the `SPECKIT_CATALOG_URL` environment variable: +# Add a catalog (project-scoped) +specify extension catalog add --name "internal" --install-allowed \ + https://internal.company.com/spec-kit/catalog.json + +# Add a discovery-only catalog +specify extension catalog add --name "community" \ + https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json + +# Remove a catalog +specify extension catalog remove internal + +# Show which catalog an extension came from +specify extension info jira +# → Source catalog: org-approved +``` + +#### Merge Conflict Resolution + +When the same extension `id` appears in multiple catalogs, the higher-priority (lower priority number) catalog wins. Extensions from lower-priority catalogs with the same `id` are ignored. + +#### `install_allowed: false` Behavior + +Extensions from discovery-only catalogs are shown in `specify extension search` results but cannot be installed directly: + +``` +⚠ 'linear' is available in the 'community' catalog but installation is not allowed from that catalog. + +To enable installation, add 'linear' to an approved catalog (install_allowed: true) in .specify/extension-catalogs.yml. +``` + +#### `SPECKIT_CATALOG_URL` (Backward Compatibility) + +The `SPECKIT_CATALOG_URL` environment variable still works — it is treated as a single `install_allowed: true` catalog, **replacing both defaults** for full backward compatibility: ```bash # Point to your organization's catalog diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 9f4256b4..b8756025 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1878,8 +1878,10 @@ def extension_catalogs(): console.print() config_path = project_root / ".specify" / "extension-catalogs.yml" - if config_path.exists(): + if config_path.exists() and catalog._load_catalog_config(config_path) is not None: console.print(f"[dim]Config: {config_path.relative_to(project_root)}[/dim]") + elif os.environ.get("SPECKIT_CATALOG_URL"): + console.print("[dim]Catalog configured via SPECKIT_CATALOG_URL environment variable.[/dim]") else: console.print("[dim]Using built-in default catalog stack.[/dim]") console.print(