mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 02:43:08 +00:00
* feat(extensions): support .extensionignore to exclude files during install Add .extensionignore support so extension authors can exclude files and folders from being copied when users run 'specify extension add'. The file uses glob-style patterns (one per line), supports comments (#), blank lines, trailing-slash directory patterns, and relative path matching. The .extensionignore file itself is always excluded from the copy. - Add _load_extensionignore() to ExtensionManager - Integrate ignore function into shutil.copytree in install_from_directory - Document .extensionignore in EXTENSION-DEVELOPMENT-GUIDE.md - Add 6 tests covering all pattern matching scenarios - Bump version to 0.1.14 * fix(extensions): use pathspec for gitignore-compatible .extensionignore matching Replace fnmatch with pathspec.GitIgnoreSpec to get proper .gitignore semantics where * does not cross directory boundaries. This addresses review feedback on #1781. Changes: - Switch from fnmatch to pathspec>=0.12.0 (GitIgnoreSpec.from_lines) - Normalize backslashes in patterns for cross-platform compatibility - Distinguish directories from files for trailing-slash patterns - Update docs to accurately describe supported pattern semantics - Add edge-case tests: .., absolute paths, empty file, backslashes, * vs ** boundary behavior, and ! negation - Move changelog entry to [Unreleased] section
713 lines
16 KiB
Markdown
713 lines
16 KiB
Markdown
# Extension Development Guide
|
|
|
|
A guide for creating Spec Kit extensions.
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### 1. Create Extension Directory
|
|
|
|
```bash
|
|
mkdir my-extension
|
|
cd my-extension
|
|
```
|
|
|
|
### 2. Create `extension.yml` Manifest
|
|
|
|
```yaml
|
|
schema_version: "1.0"
|
|
|
|
extension:
|
|
id: "my-ext" # Lowercase, alphanumeric + hyphens only
|
|
name: "My Extension"
|
|
version: "1.0.0" # Semantic versioning
|
|
description: "My custom extension"
|
|
author: "Your Name"
|
|
repository: "https://github.com/you/spec-kit-my-ext"
|
|
license: "MIT"
|
|
|
|
requires:
|
|
speckit_version: ">=0.1.0" # Minimum spec-kit version
|
|
tools: # Optional: External tools required
|
|
- name: "my-tool"
|
|
required: true
|
|
version: ">=1.0.0"
|
|
commands: # Optional: Core commands needed
|
|
- "speckit.tasks"
|
|
|
|
provides:
|
|
commands:
|
|
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
|
|
file: "commands/hello.md"
|
|
description: "Say hello"
|
|
aliases: ["speckit.hello"] # Optional aliases
|
|
|
|
config: # Optional: Config files
|
|
- name: "my-ext-config.yml"
|
|
template: "my-ext-config.template.yml"
|
|
description: "Extension configuration"
|
|
required: false
|
|
|
|
hooks: # Optional: Integration hooks
|
|
after_tasks:
|
|
command: "speckit.my-ext.hello"
|
|
optional: true
|
|
prompt: "Run hello command?"
|
|
|
|
tags: # Optional: For catalog search
|
|
- "example"
|
|
- "utility"
|
|
```
|
|
|
|
### 3. Create Commands Directory
|
|
|
|
```bash
|
|
mkdir commands
|
|
```
|
|
|
|
### 4. Create Command File
|
|
|
|
**File**: `commands/hello.md`
|
|
|
|
```markdown
|
|
---
|
|
description: "Say hello command"
|
|
tools: # Optional: AI tools this command uses
|
|
- 'some-tool/function'
|
|
scripts: # Optional: Helper scripts
|
|
sh: ../../scripts/bash/helper.sh
|
|
ps: ../../scripts/powershell/helper.ps1
|
|
---
|
|
|
|
# Hello Command
|
|
|
|
This command says hello!
|
|
|
|
## User Input
|
|
|
|
$ARGUMENTS
|
|
|
|
## Steps
|
|
|
|
1. Greet the user
|
|
2. Show extension is working
|
|
|
|
```bash
|
|
echo "Hello from my extension!"
|
|
echo "Arguments: $ARGUMENTS"
|
|
```
|
|
|
|
## Extension Configuration
|
|
|
|
Load extension config from `.specify/extensions/my-ext/my-ext-config.yml`.
|
|
|
|
### 5. Test Locally
|
|
|
|
```bash
|
|
cd /path/to/spec-kit-project
|
|
specify extension add --dev /path/to/my-extension
|
|
```
|
|
|
|
### 6. Verify Installation
|
|
|
|
```bash
|
|
specify extension list
|
|
|
|
# Should show:
|
|
# ✓ My Extension (v1.0.0)
|
|
# My custom extension
|
|
# Commands: 1 | Hooks: 1 | Status: Enabled
|
|
```
|
|
|
|
### 7. Test Command
|
|
|
|
If using Claude:
|
|
|
|
```bash
|
|
claude
|
|
> /speckit.my-ext.hello world
|
|
```
|
|
|
|
The command will be available in `.claude/commands/speckit.my-ext.hello.md`.
|
|
|
|
---
|
|
|
|
## Manifest Schema Reference
|
|
|
|
### Required Fields
|
|
|
|
#### `schema_version`
|
|
|
|
Extension manifest schema version. Currently: `"1.0"`
|
|
|
|
#### `extension`
|
|
|
|
Extension metadata block.
|
|
|
|
**Required sub-fields**:
|
|
|
|
- `id`: Extension identifier (lowercase, alphanumeric, hyphens)
|
|
- `name`: Human-readable name
|
|
- `version`: Semantic version (e.g., "1.0.0")
|
|
- `description`: Short description
|
|
|
|
**Optional sub-fields**:
|
|
|
|
- `author`: Extension author
|
|
- `repository`: Source code URL
|
|
- `license`: SPDX license identifier
|
|
- `homepage`: Extension homepage URL
|
|
|
|
#### `requires`
|
|
|
|
Compatibility requirements.
|
|
|
|
**Required sub-fields**:
|
|
|
|
- `speckit_version`: Semantic version specifier (e.g., ">=0.1.0,<2.0.0")
|
|
|
|
**Optional sub-fields**:
|
|
|
|
- `tools`: External tools required (array of tool objects)
|
|
- `commands`: Core spec-kit commands needed (array of command names)
|
|
- `scripts`: Core scripts required (array of script names)
|
|
|
|
#### `provides`
|
|
|
|
What the extension provides.
|
|
|
|
**Required sub-fields**:
|
|
|
|
- `commands`: Array of command objects (must have at least one)
|
|
|
|
**Command object**:
|
|
|
|
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
|
|
- `file`: Path to command file (relative to extension root)
|
|
- `description`: Command description (optional)
|
|
- `aliases`: Alternative command names (optional, array)
|
|
|
|
### Optional Fields
|
|
|
|
#### `hooks`
|
|
|
|
Integration hooks for automatic execution.
|
|
|
|
Available hook points:
|
|
|
|
- `after_tasks`: After `/speckit.tasks` completes
|
|
- `after_implement`: After `/speckit.implement` completes (future)
|
|
|
|
Hook object:
|
|
|
|
- `command`: Command to execute (must be in `provides.commands`)
|
|
- `optional`: If true, prompt user before executing
|
|
- `prompt`: Prompt text for optional hooks
|
|
- `description`: Hook description
|
|
- `condition`: Execution condition (future)
|
|
|
|
#### `tags`
|
|
|
|
Array of tags for catalog discovery.
|
|
|
|
#### `defaults`
|
|
|
|
Default extension configuration values.
|
|
|
|
#### `config_schema`
|
|
|
|
JSON Schema for validating extension configuration.
|
|
|
|
---
|
|
|
|
## Command File Format
|
|
|
|
### Frontmatter (YAML)
|
|
|
|
```yaml
|
|
---
|
|
description: "Command description" # Required
|
|
tools: # Optional
|
|
- 'tool-name/function'
|
|
scripts: # Optional
|
|
sh: ../../scripts/bash/helper.sh
|
|
ps: ../../scripts/powershell/helper.ps1
|
|
---
|
|
```
|
|
|
|
### Body (Markdown)
|
|
|
|
Use standard Markdown with special placeholders:
|
|
|
|
- `$ARGUMENTS`: User-provided arguments
|
|
- `{SCRIPT}`: Replaced with script path during registration
|
|
|
|
**Example**:
|
|
|
|
````markdown
|
|
## Steps
|
|
|
|
1. Parse arguments
|
|
2. Execute logic
|
|
|
|
```bash
|
|
args="$ARGUMENTS"
|
|
echo "Running with args: $args"
|
|
```
|
|
````
|
|
|
|
### Script Path Rewriting
|
|
|
|
Extension commands use relative paths that get rewritten during registration:
|
|
|
|
**In extension**:
|
|
|
|
```yaml
|
|
scripts:
|
|
sh: ../../scripts/bash/helper.sh
|
|
```
|
|
|
|
**After registration**:
|
|
|
|
```yaml
|
|
scripts:
|
|
sh: .specify/scripts/bash/helper.sh
|
|
```
|
|
|
|
This allows scripts to reference core spec-kit scripts.
|
|
|
|
---
|
|
|
|
## Configuration Files
|
|
|
|
### Config Template
|
|
|
|
**File**: `my-ext-config.template.yml`
|
|
|
|
```yaml
|
|
# My Extension Configuration
|
|
# Copy this to my-ext-config.yml and customize
|
|
|
|
# Example configuration
|
|
api:
|
|
endpoint: "https://api.example.com"
|
|
timeout: 30
|
|
|
|
features:
|
|
feature_a: true
|
|
feature_b: false
|
|
|
|
credentials:
|
|
# DO NOT commit credentials!
|
|
# Use environment variables instead
|
|
api_key: "${MY_EXT_API_KEY}"
|
|
```
|
|
|
|
### Config Loading
|
|
|
|
In your command, load config with layered precedence:
|
|
|
|
1. Extension defaults (`extension.yml` → `defaults`)
|
|
2. Project config (`.specify/extensions/my-ext/my-ext-config.yml`)
|
|
3. Local overrides (`.specify/extensions/my-ext/my-ext-config.local.yml` - gitignored)
|
|
4. Environment variables (`SPECKIT_MY_EXT_*`)
|
|
|
|
**Example loading script**:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
EXT_DIR=".specify/extensions/my-ext"
|
|
|
|
# Load and merge config
|
|
config=$(yq eval '.' "$EXT_DIR/my-ext-config.yml" -o=json)
|
|
|
|
# Apply env overrides
|
|
if [ -n "${SPECKIT_MY_EXT_API_KEY:-}" ]; then
|
|
config=$(echo "$config" | jq ".api.api_key = \"$SPECKIT_MY_EXT_API_KEY\"")
|
|
fi
|
|
|
|
echo "$config"
|
|
```
|
|
|
|
---
|
|
|
|
## Excluding Files with `.extensionignore`
|
|
|
|
Extension authors can create a `.extensionignore` file in the extension root to exclude files and folders from being copied when a user installs the extension with `specify extension add`. This is useful for keeping development-only files (tests, CI configs, docs source, etc.) out of the installed copy.
|
|
|
|
### Format
|
|
|
|
The file uses `.gitignore`-compatible patterns (one per line), powered by the [`pathspec`](https://pypi.org/project/pathspec/) library:
|
|
|
|
- Blank lines are ignored
|
|
- Lines starting with `#` are comments
|
|
- `*` matches anything **except** `/` (does not cross directory boundaries)
|
|
- `**` matches zero or more directories (e.g., `docs/**/*.draft.md`)
|
|
- `?` matches any single character except `/`
|
|
- A trailing `/` restricts a pattern to directories only
|
|
- Patterns containing `/` (other than a trailing slash) are anchored to the extension root
|
|
- Patterns without `/` match at any depth in the tree
|
|
- `!` negates a previously excluded pattern (re-includes a file)
|
|
- Backslashes in patterns are normalised to forward slashes for cross-platform compatibility
|
|
- The `.extensionignore` file itself is always excluded automatically
|
|
|
|
### Example
|
|
|
|
```gitignore
|
|
# .extensionignore
|
|
|
|
# Development files
|
|
tests/
|
|
.github/
|
|
.gitignore
|
|
|
|
# Build artifacts
|
|
__pycache__/
|
|
*.pyc
|
|
dist/
|
|
|
|
# Documentation source (keep only the built README)
|
|
docs/
|
|
CONTRIBUTING.md
|
|
```
|
|
|
|
### Pattern Matching
|
|
|
|
| Pattern | Matches | Does NOT match |
|
|
|---------|---------|----------------|
|
|
| `*.pyc` | Any `.pyc` file in any directory | — |
|
|
| `tests/` | The `tests` directory (and all its contents) | A file named `tests` |
|
|
| `docs/*.draft.md` | `docs/api.draft.md` (directly inside `docs/`) | `docs/sub/api.draft.md` (nested) |
|
|
| `.env` | The `.env` file at any level | — |
|
|
| `!README.md` | Re-includes `README.md` even if matched by an earlier pattern | — |
|
|
| `docs/**/*.draft.md` | `docs/api.draft.md`, `docs/sub/api.draft.md` | — |
|
|
|
|
### Unsupported Features
|
|
|
|
The following `.gitignore` features are **not applicable** in this context:
|
|
|
|
- **Multiple `.extensionignore` files**: Only a single file at the extension root is supported (`.gitignore` supports files in subdirectories)
|
|
- **`$GIT_DIR/info/exclude` and `core.excludesFile`**: These are Git-specific and have no equivalent here
|
|
- **Negation inside excluded directories**: Because file copying uses `shutil.copytree`, excluding a directory prevents recursion into it entirely. A negation pattern cannot re-include a file inside a directory that was itself excluded. For example, the combination `tests/` followed by `!tests/important.py` will **not** preserve `tests/important.py` — the `tests/` directory is skipped at the root level and its contents are never evaluated. To work around this, exclude the directory's contents individually instead of the directory itself (e.g., `tests/*.pyc` and `tests/.cache/` rather than `tests/`).
|
|
|
|
---
|
|
|
|
## Validation Rules
|
|
|
|
### Extension ID
|
|
|
|
- **Pattern**: `^[a-z0-9-]+$`
|
|
- **Valid**: `my-ext`, `tool-123`, `awesome-plugin`
|
|
- **Invalid**: `MyExt` (uppercase), `my_ext` (underscore), `my ext` (space)
|
|
|
|
### Extension Version
|
|
|
|
- **Format**: Semantic versioning (MAJOR.MINOR.PATCH)
|
|
- **Valid**: `1.0.0`, `0.1.0`, `2.5.3`
|
|
- **Invalid**: `1.0`, `v1.0.0`, `1.0.0-beta`
|
|
|
|
### Command Name
|
|
|
|
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
|
|
- **Valid**: `speckit.my-ext.hello`, `speckit.tool.cmd`
|
|
- **Invalid**: `my-ext.hello` (missing prefix), `speckit.hello` (no extension namespace)
|
|
|
|
### Command File Path
|
|
|
|
- **Must be** relative to extension root
|
|
- **Valid**: `commands/hello.md`, `commands/subdir/cmd.md`
|
|
- **Invalid**: `/absolute/path.md`, `../outside.md`
|
|
|
|
---
|
|
|
|
## Testing Extensions
|
|
|
|
### Manual Testing
|
|
|
|
1. **Create test extension**
|
|
2. **Install locally**:
|
|
|
|
```bash
|
|
specify extension add --dev /path/to/extension
|
|
```
|
|
|
|
3. **Verify installation**:
|
|
|
|
```bash
|
|
specify extension list
|
|
```
|
|
|
|
4. **Test commands** with your AI agent
|
|
5. **Check command registration**:
|
|
|
|
```bash
|
|
ls .claude/commands/speckit.my-ext.*
|
|
```
|
|
|
|
6. **Remove extension**:
|
|
|
|
```bash
|
|
specify extension remove my-ext
|
|
```
|
|
|
|
### Automated Testing
|
|
|
|
Create tests for your extension:
|
|
|
|
```python
|
|
# tests/test_my_extension.py
|
|
import pytest
|
|
from pathlib import Path
|
|
from specify_cli.extensions import ExtensionManifest
|
|
|
|
def test_manifest_valid():
|
|
"""Test extension manifest is valid."""
|
|
manifest = ExtensionManifest(Path("extension.yml"))
|
|
assert manifest.id == "my-ext"
|
|
assert len(manifest.commands) >= 1
|
|
|
|
def test_command_files_exist():
|
|
"""Test all command files exist."""
|
|
manifest = ExtensionManifest(Path("extension.yml"))
|
|
for cmd in manifest.commands:
|
|
cmd_file = Path(cmd["file"])
|
|
assert cmd_file.exists(), f"Command file not found: {cmd_file}"
|
|
```
|
|
|
|
---
|
|
|
|
## Distribution
|
|
|
|
### Option 1: GitHub Repository
|
|
|
|
1. **Create repository**: `spec-kit-my-ext`
|
|
2. **Add files**:
|
|
|
|
```text
|
|
spec-kit-my-ext/
|
|
├── extension.yml
|
|
├── commands/
|
|
├── scripts/
|
|
├── docs/
|
|
├── README.md
|
|
├── LICENSE
|
|
└── CHANGELOG.md
|
|
```
|
|
|
|
3. **Create release**: Tag with version (e.g., `v1.0.0`)
|
|
4. **Install from repo**:
|
|
|
|
```bash
|
|
git clone https://github.com/you/spec-kit-my-ext
|
|
specify extension add --dev spec-kit-my-ext/
|
|
```
|
|
|
|
### Option 2: ZIP Archive (Future)
|
|
|
|
Create ZIP archive and host on GitHub Releases:
|
|
|
|
```bash
|
|
zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/
|
|
```
|
|
|
|
Users install with:
|
|
|
|
```bash
|
|
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
|
```
|
|
|
|
### Option 3: Community Reference Catalog
|
|
|
|
Submit to the community catalog for public discovery:
|
|
|
|
1. **Fork** spec-kit repository
|
|
2. **Add entry** to `extensions/catalog.community.json`
|
|
3. **Update** `extensions/README.md` with your extension
|
|
4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md)
|
|
5. **After merge**, your extension becomes available:
|
|
- Users can browse `catalog.community.json` to discover your extension
|
|
- Users copy the entry to their own `catalog.json`
|
|
- Users install with: `specify extension add my-ext` (from their catalog)
|
|
|
|
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed submission instructions.
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### Naming Conventions
|
|
|
|
- **Extension ID**: Use descriptive, hyphenated names (`jira-integration`, not `ji`)
|
|
- **Commands**: Use verb-noun pattern (`create-issue`, `sync-status`)
|
|
- **Config files**: Match extension ID (`jira-config.yml`)
|
|
|
|
### Documentation
|
|
|
|
- **README.md**: Overview, installation, usage
|
|
- **CHANGELOG.md**: Version history
|
|
- **docs/**: Detailed guides
|
|
- **Command descriptions**: Clear, concise
|
|
|
|
### Versioning
|
|
|
|
- **Follow SemVer**: `MAJOR.MINOR.PATCH`
|
|
- **MAJOR**: Breaking changes
|
|
- **MINOR**: New features
|
|
- **PATCH**: Bug fixes
|
|
|
|
### Security
|
|
|
|
- **Never commit secrets**: Use environment variables
|
|
- **Validate input**: Sanitize user arguments
|
|
- **Document permissions**: What files/APIs are accessed
|
|
|
|
### Compatibility
|
|
|
|
- **Specify version range**: Don't require exact version
|
|
- **Test with multiple versions**: Ensure compatibility
|
|
- **Graceful degradation**: Handle missing features
|
|
|
|
---
|
|
|
|
## Example Extensions
|
|
|
|
### Minimal Extension
|
|
|
|
Smallest possible extension:
|
|
|
|
```yaml
|
|
# extension.yml
|
|
schema_version: "1.0"
|
|
extension:
|
|
id: "minimal"
|
|
name: "Minimal Extension"
|
|
version: "1.0.0"
|
|
description: "Minimal example"
|
|
requires:
|
|
speckit_version: ">=0.1.0"
|
|
provides:
|
|
commands:
|
|
- name: "speckit.minimal.hello"
|
|
file: "commands/hello.md"
|
|
```
|
|
|
|
````markdown
|
|
<!-- commands/hello.md -->
|
|
---
|
|
description: "Hello command"
|
|
---
|
|
|
|
# Hello World
|
|
|
|
```bash
|
|
echo "Hello, $ARGUMENTS!"
|
|
```
|
|
````
|
|
|
|
### Extension with Config
|
|
|
|
Extension using configuration:
|
|
|
|
```yaml
|
|
# extension.yml
|
|
# ... metadata ...
|
|
provides:
|
|
config:
|
|
- name: "tool-config.yml"
|
|
template: "tool-config.template.yml"
|
|
required: true
|
|
```
|
|
|
|
```yaml
|
|
# tool-config.template.yml
|
|
api_endpoint: "https://api.example.com"
|
|
timeout: 30
|
|
```
|
|
|
|
````markdown
|
|
<!-- commands/use-config.md -->
|
|
# Use Config
|
|
|
|
Load config:
|
|
```bash
|
|
config_file=".specify/extensions/tool/tool-config.yml"
|
|
endpoint=$(yq eval '.api_endpoint' "$config_file")
|
|
echo "Using endpoint: $endpoint"
|
|
```
|
|
````
|
|
|
|
### Extension with Hooks
|
|
|
|
Extension that runs automatically:
|
|
|
|
```yaml
|
|
# extension.yml
|
|
hooks:
|
|
after_tasks:
|
|
command: "speckit.auto.analyze"
|
|
optional: false # Always run
|
|
description: "Analyze tasks after generation"
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Extension won't install
|
|
|
|
**Error**: `Invalid extension ID`
|
|
|
|
- **Fix**: Use lowercase, alphanumeric + hyphens only
|
|
|
|
**Error**: `Extension requires spec-kit >=0.2.0`
|
|
|
|
- **Fix**: Update spec-kit with `uv tool install specify-cli --force`
|
|
|
|
**Error**: `Command file not found`
|
|
|
|
- **Fix**: Ensure command files exist at paths specified in manifest
|
|
|
|
### Commands not registered
|
|
|
|
**Symptom**: Commands don't appear in AI agent
|
|
|
|
**Check**:
|
|
|
|
1. `.claude/commands/` directory exists
|
|
2. Extension installed successfully
|
|
3. Commands registered in registry:
|
|
|
|
```bash
|
|
cat .specify/extensions/.registry
|
|
```
|
|
|
|
**Fix**: Reinstall extension to trigger registration
|
|
|
|
### Config not loading
|
|
|
|
**Check**:
|
|
|
|
1. Config file exists: `.specify/extensions/{ext-id}/{ext-id}-config.yml`
|
|
2. YAML syntax is valid: `yq eval '.' config.yml`
|
|
3. Environment variables set correctly
|
|
|
|
---
|
|
|
|
## Getting Help
|
|
|
|
- **Issues**: Report bugs at GitHub repository
|
|
- **Discussions**: Ask questions in GitHub Discussions
|
|
- **Examples**: See `spec-kit-jira` for full-featured example (Phase B)
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. **Create your extension** following this guide
|
|
2. **Test locally** with `--dev` flag
|
|
3. **Share with community** (GitHub, catalog)
|
|
4. **Iterate** based on feedback
|
|
|
|
Happy extending! 🚀
|