Skip to content

feat(extensions): support multiple active catalogs simultaneously #1707

@mnriem

Description

@mnriem

Summary

The current extension catalog system supports only a single active catalog at a time — selectable via SPECKIT_CATALOG_URL or defaulting to the built-in catalog.json. This creates a fundamental conflict: catalog.json is intentionally empty (organizations curate their own approved extensions), while catalog.community.json exists as a shared discovery resource, and organizations also need to point at their own private/internal catalogs. Users currently have no way to benefit from all three simultaneously.

Problem Statement

The RFC describes a "Dual Catalog System" but the two catalogs serve different purposes and neither is composable with the other today:

Catalog Purpose Current Behavior
catalog.json Org-curated approved extensions Empty by design; sole default search target
catalog.community.json Community discovery resource Never searched by CLI commands
Custom URL Private/internal org catalog Replaces default entirely via env var

Real-world usage requires all three active at once: search the community catalog for discoverability, restrict installs to org-approved entries, and also pull from an internal catalog — with clear precedence rules between them.

Proposed Solution

Introduce a catalog stack — an ordered list of catalogs the CLI merges and searches across. Each catalog entry has a url, an optional name, a priority, and an install_allowed flag.

Default built-in stack (no config required)

When no .specify/extension-catalogs.yml exists, the CLI uses a built-in default stack:

  1. catalog.json (org-curated, install_allowed: true, priority 1)
  2. catalog.community.json (community discovery, install_allowed: false, priority 2)

This means specify extension search works out of the box and surfaces community extensions, while specify extension add is still restricted to whatever is in catalog.json — preserving the existing curation/trust model.

New config file: .specify/extension-catalogs.yml (project-scoped)

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

An equivalent user-level config lives at ~/.specify/extension-catalogs.yml for user-wide defaults. When a project-level config is present, it takes full control and the built-in defaults are not applied.

Resolution order

When a user runs specify extension search or specify extension add <name>, the CLI:

  1. Loads all configured catalogs in priority order
  2. Merges results, with higher-priority catalogs winning on conflicts (same extension id)
  3. Respects install_allowed: false — extensions from discovery-only catalogs are shown in search results but cannot be installed directly

CLI additions

# List active catalogs
specify extension catalogs

# Add a catalog (project-scoped)
specify extension catalog add --name "internal" https://internal.company.com/spec-kit/catalog.json

# Remove a catalog
specify extension catalog remove internal

# Show which catalog an extension came from
specify extension info jira
# → Source catalog: org-approved

Backward compatibility

  • Built-in default stack includes catalog.community.json as install_allowed: false — no config needed to get community discoverability
  • SPECKIT_CATALOG_URL env var still works: treated as a single install_allowed: true catalog, replacing both defaults for full backward compat
  • Explicit .specify/extension-catalogs.yml overrides all defaults entirely

Acceptance Criteria

  • specify extension catalogs lists all active catalogs with name, URL, priority, and install_allowed
  • When no catalog config exists, catalog.community.json is included in the default stack as install_allowed: false at priority 2
  • specify extension search aggregates results across all active catalogs, annotating each result with its source catalog
  • specify extension add respects install_allowed: false and rejects installs from discovery-only catalogs with a clear message: "'linear' is available in the community catalog but not in your approved catalog. Add it to .specify/extension-catalogs.yml with install_allowed: true to enable installation."
  • .specify/extension-catalogs.yml and ~/.specify/extension-catalogs.yml are both read, with project-level taking precedence
  • SPECKIT_CATALOG_URL env var still overrides the full stack (backward compat)
  • All catalog URLs must use HTTPS (localhost HTTP allowed for development, consistent with current policy)
  • Unit tests cover merge conflict resolution (same extension id in two catalogs)
  • Docs updated: RFC, EXTENSION-USER-GUIDE.md, EXTENSION-API-REFERENCE.md

Out of Scope

  • Catalog authentication / private registry tokens (separate issue)
  • Automatic synchronization / caching of catalog contents (separate issue)
  • Signature verification for catalog entries (tracked in RFC under future security work)

References

  • RFC: extensions/RFC-EXTENSION-SYSTEM.md — "Custom Catalogs" section (currently marked ⚠️ FUTURE FEATURE)
  • extensions/catalog.json — empty by design
  • extensions/catalog.community.json
  • extensions/EXTENSION-USER-GUIDE.md

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions