Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ Recent changes to the Specify CLI and templates are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.7] - 2026-02-27

### Added

- **Multi-Catalog Support (#1707)**: Extension catalog system now supports multiple active catalogs simultaneously via a catalog stack
- New `specify extension catalogs` command lists all active catalogs with name, URL, priority, and `install_allowed` status
- New `specify extension catalog add` and `specify extension catalog remove` commands for project-scoped catalog management
- Default built-in stack includes `catalog.json` (org-approved, installable) and `catalog.community.json` (discovery only) — community extensions are now surfaced in search results out of the box
- `specify extension search` aggregates results across all active catalogs, annotating each result with source catalog
- `specify extension add` enforces `install_allowed` policy — extensions from discovery-only catalogs cannot be installed directly
- Project-level `.specify/extension-catalogs.yml` and user-level `~/.specify/extension-catalogs.yml` config files supported, with project-level taking precedence
- `SPECKIT_CATALOG_URL` environment variable still works for backward compatibility (replaces full stack with single catalog)
- All catalog URLs require HTTPS (HTTP allowed for localhost development)
- New `CatalogEntry` dataclass in `extensions.py` for catalog stack representation
- Per-URL hash-based caching for non-default catalogs; legacy cache preserved for default catalog
- Higher-priority catalogs win on merge conflicts (same extension id in multiple catalogs)
- 13 new tests covering catalog stack resolution, merge conflicts, URL validation, and `install_allowed` enforcement
- Updated RFC, Extension User Guide, and Extension API Reference documentation

## [0.1.6] - 2026-02-23

### Fixed
Expand Down
113 changes: 105 additions & 8 deletions extensions/EXTENSION-API-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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`
Expand Down Expand Up @@ -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]`
Expand All @@ -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]`
Expand All @@ -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
Expand All @@ -589,6 +684,8 @@ EXECUTE_COMMAND: {command}

**Usage**: `specify extension info EXTENSION`

Shows source catalog and install_allowed status.

**Arguments**:

- `EXTENSION` - Extension ID
Expand Down
102 changes: 89 additions & 13 deletions extensions/EXTENSION-USER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ 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

```bash
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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)

```bash
# In ~/.bashrc, ~/.zshrc, or CI pipeline
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
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
```

##### Option B: Per-project configuration
Or use the CLI:

```bash
specify extension catalog add \
--name "org-approved" \
--install-allowed \
https://your-org.com/spec-kit/catalog.json
```

Create `.env` or set in your shell before running spec-kit commands:
##### Option B: Environment variable (recommended for CI/CD, single-catalog)

```bash
SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" specify extension search
# In ~/.bashrc, ~/.zshrc, or CI pipeline
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
```

#### 4. Verify Configuration

```bash
# List active catalogs
specify extension catalogs

# Search should now show your catalog's extensions
specify extension search

Expand Down
Loading