mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 19:03:08 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f523ede22 | ||
|
|
68d1d3a0fc | ||
|
|
07077d0fc2 | ||
|
|
aeed11f735 | ||
|
|
12405c01e1 | ||
|
|
fc3b98ea09 | ||
|
|
6150f1e317 | ||
|
|
6fca5d83b2 | ||
|
|
465acd9024 | ||
|
|
04fc3fd1ba | ||
|
|
24d76b5d92 | ||
|
|
0f7d04b12b | ||
|
|
9402ebd00a | ||
|
|
d410d188fc | ||
|
|
686c91f94e | ||
|
|
22036732d8 | ||
|
|
c78f8423f6 | ||
|
|
76cca34293 | ||
|
|
9a1e3037b0 | ||
|
|
f14a47ea7d | ||
|
|
36d97235ad |
141
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
Normal file
141
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
name: Agent Request
|
||||||
|
description: Request support for a new AI agent/assistant in Spec Kit
|
||||||
|
title: "[Agent]: Add support for "
|
||||||
|
labels: ["agent-request", "enhancement", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
|
||||||
|
|
||||||
|
**Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Amazon Q Developer CLI, Amp, SHAI, IBM Bob, Antigravity
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: agent-name
|
||||||
|
attributes:
|
||||||
|
label: Agent Name
|
||||||
|
description: What is the name of the AI agent/assistant?
|
||||||
|
placeholder: "e.g., SuperCoder AI"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: website
|
||||||
|
attributes:
|
||||||
|
label: Official Website
|
||||||
|
description: Link to the agent's official website or documentation
|
||||||
|
placeholder: "https://..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: agent-type
|
||||||
|
attributes:
|
||||||
|
label: Agent Type
|
||||||
|
description: How is the agent accessed?
|
||||||
|
options:
|
||||||
|
- CLI tool (command-line interface)
|
||||||
|
- IDE extension/plugin
|
||||||
|
- Both CLI and IDE
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: cli-command
|
||||||
|
attributes:
|
||||||
|
label: CLI Command (if applicable)
|
||||||
|
description: What command is used to invoke the agent from terminal?
|
||||||
|
placeholder: "e.g., supercode, ai-assistant"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: install-method
|
||||||
|
attributes:
|
||||||
|
label: Installation Method
|
||||||
|
description: How is the agent installed?
|
||||||
|
placeholder: "e.g., npm install -g supercode, pip install supercode, IDE marketplace"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: command-structure
|
||||||
|
attributes:
|
||||||
|
label: Command/Workflow Structure
|
||||||
|
description: How does the agent define custom commands or workflows?
|
||||||
|
placeholder: |
|
||||||
|
- Command file format (Markdown, YAML, TOML, etc.)
|
||||||
|
- Directory location (e.g., .supercode/commands/)
|
||||||
|
- Example command file structure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: argument-pattern
|
||||||
|
attributes:
|
||||||
|
label: Argument Passing Pattern
|
||||||
|
description: How does the agent handle arguments in commands?
|
||||||
|
placeholder: |
|
||||||
|
e.g., Uses {{args}}, $ARGUMENTS, %ARGS%, or other placeholder format
|
||||||
|
Example: "Run test suite with {{args}}"
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: popularity
|
||||||
|
attributes:
|
||||||
|
label: Popularity/Usage
|
||||||
|
description: How widely is this agent used?
|
||||||
|
options:
|
||||||
|
- Widely used (thousands+ of users)
|
||||||
|
- Growing adoption (hundreds of users)
|
||||||
|
- New/emerging (less than 100 users)
|
||||||
|
- Unknown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: documentation
|
||||||
|
attributes:
|
||||||
|
label: Documentation Links
|
||||||
|
description: Links to relevant documentation for custom commands/workflows
|
||||||
|
placeholder: |
|
||||||
|
- Command documentation: https://...
|
||||||
|
- API/CLI reference: https://...
|
||||||
|
- Examples: https://...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-case
|
||||||
|
attributes:
|
||||||
|
label: Use Case
|
||||||
|
description: Why do you want this agent supported in Spec Kit?
|
||||||
|
placeholder: Explain your workflow and how this agent fits into your development process
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: example-command
|
||||||
|
attributes:
|
||||||
|
label: Example Command File
|
||||||
|
description: If possible, provide an example of a command file for this agent
|
||||||
|
render: markdown
|
||||||
|
placeholder: |
|
||||||
|
```toml
|
||||||
|
description = "Example command"
|
||||||
|
prompt = "Do something with {{args}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: contribution
|
||||||
|
attributes:
|
||||||
|
label: Contribution
|
||||||
|
description: Are you willing to help implement support for this agent?
|
||||||
|
options:
|
||||||
|
- label: I can help test the integration
|
||||||
|
- label: I can provide example command files
|
||||||
|
- label: I can help with documentation
|
||||||
|
- label: I can submit a pull request for the integration
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Any other relevant information about this agent
|
||||||
|
placeholder: Screenshots, community links, comparison to existing agents, etc.
|
||||||
118
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
118
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report a bug or unexpected behavior in Specify CLI or Spec Kit
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to report a bug! Please fill out the sections below to help us diagnose and fix the issue.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Bug Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: What went wrong?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Steps to reproduce the behavior
|
||||||
|
placeholder: |
|
||||||
|
1. Run command '...'
|
||||||
|
2. Execute script '...'
|
||||||
|
3. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: Describe the expected outcome
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: What actually happened?
|
||||||
|
placeholder: Describe what happened instead
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Specify CLI Version
|
||||||
|
description: "Run `specify --version` or `pip show spec-kit`"
|
||||||
|
placeholder: "e.g., 1.3.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: ai-agent
|
||||||
|
attributes:
|
||||||
|
label: AI Agent
|
||||||
|
description: Which AI agent are you using?
|
||||||
|
options:
|
||||||
|
- Claude Code
|
||||||
|
- Gemini CLI
|
||||||
|
- GitHub Copilot
|
||||||
|
- Cursor
|
||||||
|
- Qwen Code
|
||||||
|
- opencode
|
||||||
|
- Codex CLI
|
||||||
|
- Windsurf
|
||||||
|
- Kilo Code
|
||||||
|
- Auggie CLI
|
||||||
|
- Roo Code
|
||||||
|
- CodeBuddy
|
||||||
|
- Qoder CLI
|
||||||
|
- Amazon Q Developer CLI
|
||||||
|
- Amp
|
||||||
|
- SHAI
|
||||||
|
- IBM Bob
|
||||||
|
- Antigravity
|
||||||
|
- Not applicable
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: Your operating system and version
|
||||||
|
placeholder: "e.g., macOS 14.2, Ubuntu 22.04, Windows 11"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: python
|
||||||
|
attributes:
|
||||||
|
label: Python Version
|
||||||
|
description: "Run `python --version` or `python3 --version`"
|
||||||
|
placeholder: "e.g., Python 3.11.5"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Error Logs
|
||||||
|
description: Please paste any relevant error messages or logs
|
||||||
|
render: shell
|
||||||
|
placeholder: Paste error output here
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context about the problem
|
||||||
|
placeholder: Screenshots, related issues, workarounds attempted, etc.
|
||||||
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 💬 General Discussion
|
||||||
|
url: https://github.com/github/spec-kit/discussions
|
||||||
|
about: Ask questions, share ideas, or discuss Spec-Driven Development
|
||||||
|
- name: 📖 Documentation
|
||||||
|
url: https://github.com/github/spec-kit/blob/main/README.md
|
||||||
|
about: Read the Spec Kit documentation and guides
|
||||||
|
- name: 🛠️ Extension Development Guide
|
||||||
|
url: https://github.com/manfredseee/spec-kit/blob/main/extensions/EXTENSION-DEVELOPMENT-GUIDE.md
|
||||||
|
about: Learn how to develop and publish Spec Kit extensions
|
||||||
|
- name: 🤝 Contributing Guide
|
||||||
|
url: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md
|
||||||
|
about: Learn how to contribute to Spec Kit
|
||||||
|
- name: 🔒 Security Issues
|
||||||
|
url: https://github.com/github/spec-kit/blob/main/SECURITY.md
|
||||||
|
about: Report security vulnerabilities privately
|
||||||
280
.github/ISSUE_TEMPLATE/extension_submission.yml
vendored
Normal file
280
.github/ISSUE_TEMPLATE/extension_submission.yml
vendored
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
name: Extension Submission
|
||||||
|
description: Submit your extension to the Spec Kit catalog
|
||||||
|
title: "[Extension]: Add "
|
||||||
|
labels: ["extension-submission", "enhancement", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for contributing an extension! This template helps you submit your extension to the community catalog.
|
||||||
|
|
||||||
|
**Before submitting:**
|
||||||
|
- Review the [Extension Publishing Guide](https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-PUBLISHING-GUIDE.md)
|
||||||
|
- Ensure your extension has a valid `extension.yml` manifest
|
||||||
|
- Create a GitHub release with a version tag (e.g., v1.0.0)
|
||||||
|
- Test installation: `specify extension add --from <your-release-url>`
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: extension-id
|
||||||
|
attributes:
|
||||||
|
label: Extension ID
|
||||||
|
description: Unique extension identifier (lowercase with hyphens only)
|
||||||
|
placeholder: "e.g., jira-integration"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: extension-name
|
||||||
|
attributes:
|
||||||
|
label: Extension Name
|
||||||
|
description: Human-readable extension name
|
||||||
|
placeholder: "e.g., Jira Integration"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: Semantic version number
|
||||||
|
placeholder: "e.g., 1.0.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Brief description of what your extension does (under 200 characters)
|
||||||
|
placeholder: Integrates Jira issue tracking with Spec Kit workflows for seamless task management
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: author
|
||||||
|
attributes:
|
||||||
|
label: Author
|
||||||
|
description: Your name or organization
|
||||||
|
placeholder: "e.g., John Doe or Acme Corp"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: repository
|
||||||
|
attributes:
|
||||||
|
label: Repository URL
|
||||||
|
description: GitHub repository URL for your extension
|
||||||
|
placeholder: "https://github.com/your-org/spec-kit-your-extension"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: download-url
|
||||||
|
attributes:
|
||||||
|
label: Download URL
|
||||||
|
description: URL to the GitHub release archive (e.g., v1.0.0.zip)
|
||||||
|
placeholder: "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: license
|
||||||
|
attributes:
|
||||||
|
label: License
|
||||||
|
description: Open source license type
|
||||||
|
placeholder: "e.g., MIT, Apache-2.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: homepage
|
||||||
|
attributes:
|
||||||
|
label: Homepage (optional)
|
||||||
|
description: Link to extension homepage or documentation site
|
||||||
|
placeholder: "https://..."
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: documentation
|
||||||
|
attributes:
|
||||||
|
label: Documentation URL (optional)
|
||||||
|
description: Link to detailed documentation
|
||||||
|
placeholder: "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: changelog
|
||||||
|
attributes:
|
||||||
|
label: Changelog URL (optional)
|
||||||
|
description: Link to changelog file
|
||||||
|
placeholder: "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: speckit-version
|
||||||
|
attributes:
|
||||||
|
label: Required Spec Kit Version
|
||||||
|
description: Minimum Spec Kit version required
|
||||||
|
placeholder: "e.g., >=0.1.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: required-tools
|
||||||
|
attributes:
|
||||||
|
label: Required Tools (optional)
|
||||||
|
description: List any external tools or dependencies required
|
||||||
|
placeholder: |
|
||||||
|
- jira-cli (>=1.0.0) - required
|
||||||
|
- python (>=3.8) - optional
|
||||||
|
render: markdown
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: commands-count
|
||||||
|
attributes:
|
||||||
|
label: Number of Commands
|
||||||
|
description: How many commands does your extension provide?
|
||||||
|
placeholder: "e.g., 3"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: hooks-count
|
||||||
|
attributes:
|
||||||
|
label: Number of Hooks (optional)
|
||||||
|
description: How many hooks does your extension provide?
|
||||||
|
placeholder: "e.g., 0"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: tags
|
||||||
|
attributes:
|
||||||
|
label: Tags
|
||||||
|
description: 2-5 relevant tags (lowercase, separated by commas)
|
||||||
|
placeholder: "issue-tracking, jira, atlassian, automation"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: features
|
||||||
|
attributes:
|
||||||
|
label: Key Features
|
||||||
|
description: List the main features and capabilities of your extension
|
||||||
|
placeholder: |
|
||||||
|
- Create Jira issues from specs
|
||||||
|
- Sync task status with Jira
|
||||||
|
- Link specs to existing issues
|
||||||
|
- Generate Jira reports
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: testing
|
||||||
|
attributes:
|
||||||
|
label: Testing Checklist
|
||||||
|
description: Confirm that your extension has been tested
|
||||||
|
options:
|
||||||
|
- label: Extension installs successfully via download URL
|
||||||
|
required: true
|
||||||
|
- label: All commands execute without errors
|
||||||
|
required: true
|
||||||
|
- label: Documentation is complete and accurate
|
||||||
|
required: true
|
||||||
|
- label: No security vulnerabilities identified
|
||||||
|
required: true
|
||||||
|
- label: Tested on at least one real project
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: requirements
|
||||||
|
attributes:
|
||||||
|
label: Submission Requirements
|
||||||
|
description: Verify your extension meets all requirements
|
||||||
|
options:
|
||||||
|
- label: Valid `extension.yml` manifest included
|
||||||
|
required: true
|
||||||
|
- label: README.md with installation and usage instructions
|
||||||
|
required: true
|
||||||
|
- label: LICENSE file included
|
||||||
|
required: true
|
||||||
|
- label: GitHub release created with version tag
|
||||||
|
required: true
|
||||||
|
- label: All command files exist and are properly formatted
|
||||||
|
required: true
|
||||||
|
- label: Extension ID follows naming conventions (lowercase-with-hyphens)
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: testing-details
|
||||||
|
attributes:
|
||||||
|
label: Testing Details
|
||||||
|
description: Describe how you tested your extension
|
||||||
|
placeholder: |
|
||||||
|
**Tested on:**
|
||||||
|
- macOS 14.0 with Spec Kit v0.1.0
|
||||||
|
- Linux Ubuntu 22.04 with Spec Kit v0.1.0
|
||||||
|
|
||||||
|
**Test project:** [Link or description]
|
||||||
|
|
||||||
|
**Test scenarios:**
|
||||||
|
1. Installed extension
|
||||||
|
2. Configured settings
|
||||||
|
3. Ran all commands
|
||||||
|
4. Verified outputs
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: example-usage
|
||||||
|
attributes:
|
||||||
|
label: Example Usage
|
||||||
|
description: Provide a simple example of using your extension
|
||||||
|
render: markdown
|
||||||
|
placeholder: |
|
||||||
|
```bash
|
||||||
|
# Install extension
|
||||||
|
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
|
|
||||||
|
# Use a command
|
||||||
|
/speckit.your-extension.command-name arg1 arg2
|
||||||
|
```
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: catalog-entry
|
||||||
|
attributes:
|
||||||
|
label: Proposed Catalog Entry
|
||||||
|
description: Provide the JSON entry for catalog.json (helps reviewers)
|
||||||
|
render: json
|
||||||
|
placeholder: |
|
||||||
|
{
|
||||||
|
"your-extension": {
|
||||||
|
"name": "Your Extension",
|
||||||
|
"id": "your-extension",
|
||||||
|
"description": "Brief description",
|
||||||
|
"author": "Your Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/your-org/spec-kit-your-extension",
|
||||||
|
"homepage": "https://github.com/your-org/spec-kit-your-extension",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 3
|
||||||
|
},
|
||||||
|
"tags": ["category", "tool"],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-02-20T00:00:00Z",
|
||||||
|
"updated_at": "2026-02-20T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Any other information that would help reviewers
|
||||||
|
placeholder: Screenshots, demo videos, links to related projects, etc.
|
||||||
104
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
104
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or enhancement for Specify CLI or Spec Kit
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["enhancement", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for suggesting a feature! Please provide details below to help us understand and evaluate your request.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem Statement
|
||||||
|
description: Is your feature request related to a problem? Please describe.
|
||||||
|
placeholder: "I'm frustrated when..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: Describe the solution you'd like
|
||||||
|
placeholder: What would you like to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives Considered
|
||||||
|
description: Have you considered any alternative solutions or workarounds?
|
||||||
|
placeholder: What other approaches might work?
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: component
|
||||||
|
attributes:
|
||||||
|
label: Component
|
||||||
|
description: Which component does this feature relate to?
|
||||||
|
options:
|
||||||
|
- Specify CLI (initialization, commands)
|
||||||
|
- Spec templates (BDD, Testing Strategy, etc.)
|
||||||
|
- Agent integrations (command files, workflows)
|
||||||
|
- Scripts (Bash/PowerShell utilities)
|
||||||
|
- Documentation
|
||||||
|
- CI/CD workflows
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: ai-agent
|
||||||
|
attributes:
|
||||||
|
label: AI Agent (if applicable)
|
||||||
|
description: Does this feature relate to a specific AI agent?
|
||||||
|
options:
|
||||||
|
- All agents
|
||||||
|
- Claude Code
|
||||||
|
- Gemini CLI
|
||||||
|
- GitHub Copilot
|
||||||
|
- Cursor
|
||||||
|
- Qwen Code
|
||||||
|
- opencode
|
||||||
|
- Codex CLI
|
||||||
|
- Windsurf
|
||||||
|
- Kilo Code
|
||||||
|
- Auggie CLI
|
||||||
|
- Roo Code
|
||||||
|
- CodeBuddy
|
||||||
|
- Qoder CLI
|
||||||
|
- Amazon Q Developer CLI
|
||||||
|
- Amp
|
||||||
|
- SHAI
|
||||||
|
- IBM Bob
|
||||||
|
- Antigravity
|
||||||
|
- Not applicable
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-cases
|
||||||
|
attributes:
|
||||||
|
label: Use Cases
|
||||||
|
description: Describe specific use cases where this feature would be valuable
|
||||||
|
placeholder: |
|
||||||
|
1. When working on large projects...
|
||||||
|
2. During spec review...
|
||||||
|
3. When integrating with CI/CD...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: acceptance
|
||||||
|
attributes:
|
||||||
|
label: Acceptance Criteria
|
||||||
|
description: How would you know this feature is complete and working?
|
||||||
|
placeholder: |
|
||||||
|
- [ ] Feature does X
|
||||||
|
- [ ] Documentation is updated
|
||||||
|
- [ ] Works with all supported agents
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context, screenshots, or examples
|
||||||
|
placeholder: Links to similar features, mockups, related discussions, etc.
|
||||||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- What does this PR do? Why is it needed? -->
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
<!-- How did you test your changes? -->
|
||||||
|
|
||||||
|
- [ ] Tested locally with `uv run specify --help`
|
||||||
|
- [ ] Ran existing tests with `uv sync && uv run pytest`
|
||||||
|
- [ ] Tested with a sample project (if applicable)
|
||||||
|
|
||||||
|
## AI Disclosure
|
||||||
|
|
||||||
|
<!-- Per our Contributing guidelines, AI assistance must be disclosed. -->
|
||||||
|
<!-- See: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#ai-contributions-in-spec-kit -->
|
||||||
|
|
||||||
|
- [ ] I **did not** use AI assistance for this contribution
|
||||||
|
- [ ] I **did** use AI assistance (describe below)
|
||||||
|
|
||||||
|
<!-- If you used AI, briefly describe how (e.g., "Code generated by Copilot", "Consulted ChatGPT for approach"): -->
|
||||||
|
|
||||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
32
.github/workflows/codeql.yml
vendored
Normal file
32
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'actions', 'python' ]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -17,4 +17,6 @@ jobs:
|
|||||||
- name: Run markdownlint-cli2
|
- name: Run markdownlint-cli2
|
||||||
uses: DavidAnson/markdownlint-cli2-action@v19
|
uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
with:
|
with:
|
||||||
globs: '**/*.md'
|
globs: |
|
||||||
|
'**/*.md'
|
||||||
|
!extensions/**/*.md
|
||||||
|
|||||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'memory/**'
|
- 'memory/**'
|
||||||
- 'scripts/**'
|
- 'scripts/**'
|
||||||
|
- 'src/**'
|
||||||
- 'templates/**'
|
- 'templates/**'
|
||||||
- '.github/workflows/**'
|
- '.github/workflows/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -57,4 +58,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/update-version.sh
|
chmod +x .github/workflows/scripts/update-version.sh
|
||||||
.github/workflows/scripts/update-version.sh ${{ steps.get_tag.outputs.new_version }}
|
.github/workflows/scripts/update-version.sh ${{ steps.get_tag.outputs.new_version }}
|
||||||
|
- name: Commit version bump to main
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add pyproject.toml
|
||||||
|
git diff --cached --quiet || git commit -m "chore: bump version to ${{ steps.get_tag.outputs.new_version }} [skip ci]"
|
||||||
|
git push
|
||||||
|
|
||||||
|
|||||||
@@ -40,15 +40,19 @@ gh release create "$VERSION" \
|
|||||||
.genreleases/spec-kit-template-roo-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-roo-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-qoder-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-qodercli-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-qoder-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-qodercli-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-amp-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-amp-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-q-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-q-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-q-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-q-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-agy-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-agy-ps-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
|
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
|
||||||
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
|
||||||
--title "Spec Kit Templates - $VERSION_NO_V" \
|
--title "Spec Kit Templates - $VERSION_NO_V" \
|
||||||
--notes-file release_notes.md
|
--notes-file release_notes.md
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
.PARAMETER Agents
|
.PARAMETER Agents
|
||||||
Comma or space separated subset of agents to build (default: all)
|
Comma or space separated subset of agents to build (default: all)
|
||||||
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, q, bob, qoder
|
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, q, bob, qodercli, shai, agy, generic
|
||||||
|
|
||||||
.PARAMETER Scripts
|
.PARAMETER Scripts
|
||||||
Comma or space separated subset of script types to build (default: both)
|
Comma or space separated subset of script types to build (default: both)
|
||||||
@@ -343,9 +343,13 @@ function Build-Variant {
|
|||||||
$cmdDir = Join-Path $baseDir ".bob/commands"
|
$cmdDir = Join-Path $baseDir ".bob/commands"
|
||||||
Generate-Commands -Agent 'bob' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'bob' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
}
|
}
|
||||||
'qoder' {
|
'qodercli' {
|
||||||
$cmdDir = Join-Path $baseDir ".qoder/commands"
|
$cmdDir = Join-Path $baseDir ".qoder/commands"
|
||||||
Generate-Commands -Agent 'qoder' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
Generate-Commands -Agent 'qodercli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'generic' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
||||||
|
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +360,7 @@ function Build-Variant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define all agents and scripts
|
# Define all agents and scripts
|
||||||
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'q', 'bob', 'qoder')
|
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'q', 'bob', 'qodercli', 'shai', 'agy', 'generic')
|
||||||
$AllScripts = @('sh', 'ps')
|
$AllScripts = @('sh', 'ps')
|
||||||
|
|
||||||
function Normalize-List {
|
function Normalize-List {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
|||||||
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
||||||
# Version argument should include leading 'v'.
|
# Version argument should include leading 'v'.
|
||||||
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
|
||||||
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex amp shai bob (default: all)
|
# AGENTS : space or comma separated subset of: claude gemini copilot cursor-agent qwen opencode windsurf codex amp shai bob generic (default: all)
|
||||||
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
||||||
# Examples:
|
# Examples:
|
||||||
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
||||||
@@ -34,7 +34,8 @@ rewrite_paths() {
|
|||||||
sed -E \
|
sed -E \
|
||||||
-e 's@(/?)memory/@.specify/memory/@g' \
|
-e 's@(/?)memory/@.specify/memory/@g' \
|
||||||
-e 's@(/?)scripts/@.specify/scripts/@g' \
|
-e 's@(/?)scripts/@.specify/scripts/@g' \
|
||||||
-e 's@(/?)templates/@.specify/templates/@g'
|
-e 's@(/?)templates/@.specify/templates/@g' \
|
||||||
|
-e 's@\.specify\.specify/@.specify/@g'
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_commands() {
|
generate_commands() {
|
||||||
@@ -202,9 +203,9 @@ build_variant() {
|
|||||||
codebuddy)
|
codebuddy)
|
||||||
mkdir -p "$base_dir/.codebuddy/commands"
|
mkdir -p "$base_dir/.codebuddy/commands"
|
||||||
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
|
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
|
||||||
qoder)
|
qodercli)
|
||||||
mkdir -p "$base_dir/.qoder/commands"
|
mkdir -p "$base_dir/.qoder/commands"
|
||||||
generate_commands qoder md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
|
generate_commands qodercli md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
|
||||||
amp)
|
amp)
|
||||||
mkdir -p "$base_dir/.agents/commands"
|
mkdir -p "$base_dir/.agents/commands"
|
||||||
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
|
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
|
||||||
@@ -214,16 +215,22 @@ build_variant() {
|
|||||||
q)
|
q)
|
||||||
mkdir -p "$base_dir/.amazonq/prompts"
|
mkdir -p "$base_dir/.amazonq/prompts"
|
||||||
generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;;
|
generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/prompts" "$script" ;;
|
||||||
|
agy)
|
||||||
|
mkdir -p "$base_dir/.agent/workflows"
|
||||||
|
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$script" ;;
|
||||||
bob)
|
bob)
|
||||||
mkdir -p "$base_dir/.bob/commands"
|
mkdir -p "$base_dir/.bob/commands"
|
||||||
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
||||||
|
generic)
|
||||||
|
mkdir -p "$base_dir/.speckit/commands"
|
||||||
|
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
||||||
esac
|
esac
|
||||||
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
|
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
|
||||||
echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
|
echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Determine agent list
|
# Determine agent list
|
||||||
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai q bob qoder)
|
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai q agy bob qodercli generic)
|
||||||
ALL_SCRIPTS=(sh ps)
|
ALL_SCRIPTS=(sh ps)
|
||||||
|
|
||||||
norm_list() {
|
norm_list() {
|
||||||
|
|||||||
42
.github/workflows/stale.yml
vendored
Normal file
42
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: 'Close stale issues and PRs'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # Run daily at midnight UTC
|
||||||
|
workflow_dispatch: # Allow manual triggering
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
# Days of inactivity before an issue or PR becomes stale
|
||||||
|
days-before-stale: 150
|
||||||
|
# Days of inactivity before a stale issue or PR is closed (after being marked stale)
|
||||||
|
days-before-close: 30
|
||||||
|
|
||||||
|
# Stale issue settings
|
||||||
|
stale-issue-message: 'This issue has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
|
||||||
|
close-issue-message: 'This issue has been automatically closed due to inactivity (180 days total). If you believe this issue is still relevant, please reopen it or create a new issue.'
|
||||||
|
stale-issue-label: 'stale'
|
||||||
|
|
||||||
|
# Stale PR settings
|
||||||
|
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
|
||||||
|
close-pr-message: 'This pull request has been automatically closed due to inactivity (180 days total). If you believe this PR is still relevant, please reopen it or create a new PR.'
|
||||||
|
stale-pr-label: 'stale'
|
||||||
|
|
||||||
|
# Exempt issues and PRs with these labels from being marked as stale
|
||||||
|
exempt-issue-labels: 'pinned,security'
|
||||||
|
exempt-pr-labels: 'pinned,security'
|
||||||
|
|
||||||
|
# Only issues or PRs with all of these labels are checked
|
||||||
|
# Leave empty to check all issues and PRs
|
||||||
|
any-of-labels: ''
|
||||||
|
|
||||||
|
# Operations per run (helps avoid rate limits)
|
||||||
|
operations-per-run: 100
|
||||||
50
.github/workflows/test.yml
vendored
Normal file
50
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Test & Lint Python
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ruff:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Run ruff check
|
||||||
|
run: uvx ruff check src/
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --extra test
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run pytest
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -44,3 +44,9 @@ env/
|
|||||||
.genreleases/
|
.genreleases/
|
||||||
*.zip
|
*.zip
|
||||||
sdd-*/
|
sdd-*/
|
||||||
|
docs/dev
|
||||||
|
|
||||||
|
# Extension system
|
||||||
|
.specify/extensions/.cache/
|
||||||
|
.specify/extensions/.backup/
|
||||||
|
.specify/extensions/*/local-config.yml
|
||||||
|
|||||||
10
AGENTS.md
10
AGENTS.md
@@ -43,11 +43,12 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **Auggie CLI** | `.augment/rules/` | Markdown | `auggie` | Auggie CLI |
|
| **Auggie CLI** | `.augment/rules/` | Markdown | `auggie` | Auggie CLI |
|
||||||
| **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE |
|
| **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE |
|
||||||
| **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI |
|
| **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI |
|
||||||
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qoder` | Qoder CLI |
|
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qodercli` | Qoder CLI |
|
||||||
| **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI |
|
| **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer CLI |
|
||||||
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
||||||
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
||||||
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
||||||
|
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
|
||||||
|
|
||||||
### Step-by-Step Integration Guide
|
### Step-by-Step Integration Guide
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ AGENT_CONFIG = {
|
|||||||
"new-agent-cli": { # Use the ACTUAL CLI tool name (what users type in terminal)
|
"new-agent-cli": { # Use the ACTUAL CLI tool name (what users type in terminal)
|
||||||
"name": "New Agent Display Name",
|
"name": "New Agent Display Name",
|
||||||
"folder": ".newagent/", # Directory for agent files
|
"folder": ".newagent/", # Directory for agent files
|
||||||
|
"commands_subdir": "commands", # Subdirectory name for command files (default: "commands")
|
||||||
"install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based)
|
"install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based)
|
||||||
"requires_cli": True, # True if CLI tool required, False for IDE-based agents
|
"requires_cli": True, # True if CLI tool required, False for IDE-based agents
|
||||||
},
|
},
|
||||||
@@ -82,6 +84,10 @@ This eliminates the need for special-case mappings throughout the codebase.
|
|||||||
|
|
||||||
- `name`: Human-readable display name shown to users
|
- `name`: Human-readable display name shown to users
|
||||||
- `folder`: Directory where agent-specific files are stored (relative to project root)
|
- `folder`: Directory where agent-specific files are stored (relative to project root)
|
||||||
|
- `commands_subdir`: Subdirectory name within the agent folder where command/prompt files are stored (default: `"commands"`)
|
||||||
|
- Most agents use `"commands"` (e.g., `.claude/commands/`)
|
||||||
|
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode, agy), `"prompts"` (codex, q), `"command"` (opencode - singular)
|
||||||
|
- This field enables `--ai-skills` to locate command templates correctly for skill generation
|
||||||
- `install_url`: Installation documentation URL (set to `None` for IDE-based agents)
|
- `install_url`: Installation documentation URL (set to `None` for IDE-based agents)
|
||||||
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
|
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
|
||||||
|
|
||||||
@@ -313,7 +319,7 @@ Require a command-line tool to be installed:
|
|||||||
- **opencode**: `opencode` CLI
|
- **opencode**: `opencode` CLI
|
||||||
- **Amazon Q Developer CLI**: `q` CLI
|
- **Amazon Q Developer CLI**: `q` CLI
|
||||||
- **CodeBuddy CLI**: `codebuddy` CLI
|
- **CodeBuddy CLI**: `codebuddy` CLI
|
||||||
- **Qoder CLI**: `qoder` CLI
|
- **Qoder CLI**: `qodercli` CLI
|
||||||
- **Amp**: `amp` CLI
|
- **Amp**: `amp` CLI
|
||||||
- **SHAI**: `shai` CLI
|
- **SHAI**: `shai` CLI
|
||||||
|
|
||||||
|
|||||||
222
CHANGELOG.md
222
CHANGELOG.md
@@ -7,177 +7,69 @@ All notable 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [0.0.22] - 2025-11-07
|
## [0.1.5] - Unreleased
|
||||||
|
|
||||||
- Support for VS Code/Copilot agents, and moving away from prompts to proper agents with hand-offs.
|
|
||||||
- Move to use `AGENTS.md` for Copilot workloads, since it's already supported out-of-the-box.
|
|
||||||
- Adds support for the version command. ([#486](https://github.com/github/spec-kit/issues/486))
|
|
||||||
- Fixes potential bug with the `create-new-feature.ps1` script that ignores existing feature branches when determining next feature number ([#975](https://github.com/github/spec-kit/issues/975))
|
|
||||||
- Add graceful fallback and logging for GitHub API rate-limiting during template fetch ([#970](https://github.com/github/spec-kit/issues/970))
|
|
||||||
|
|
||||||
## [0.0.21] - 2025-10-21
|
|
||||||
|
|
||||||
- Fixes [#975](https://github.com/github/spec-kit/issues/975) (thank you [@fgalarraga](https://github.com/fgalarraga)).
|
|
||||||
- Adds support for Amp CLI.
|
|
||||||
- Adds support for VS Code hand-offs and moves prompts to be full-fledged chat modes.
|
|
||||||
- Adds support for `version` command (addresses [#811](https://github.com/github/spec-kit/issues/811) and [#486](https://github.com/github/spec-kit/issues/486), thank you [@mcasalaina](https://github.com/mcasalaina) and [@dentity007](https://github.com/dentity007)).
|
|
||||||
- Adds support for rendering the rate limit errors from the CLI when encountered ([#970](https://github.com/github/spec-kit/issues/970), thank you [@psmman](https://github.com/psmman)).
|
|
||||||
|
|
||||||
## [0.0.20] - 2025-10-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- **Intelligent Branch Naming**: `create-new-feature` scripts now support `--short-name` parameter for custom branch names
|
|
||||||
- When `--short-name` provided: Uses the custom name directly (cleaned and formatted)
|
|
||||||
- When omitted: Automatically generates meaningful names using stop word filtering and length-based filtering
|
|
||||||
- Filters out common stop words (I, want, to, the, for, etc.)
|
|
||||||
- Removes words shorter than 3 characters (unless they're uppercase acronyms)
|
|
||||||
- Takes 3-4 most meaningful words from the description
|
|
||||||
- **Enforces GitHub's 244-byte branch name limit** with automatic truncation and warnings
|
|
||||||
- Examples:
|
|
||||||
- "I want to create user authentication" → `001-create-user-authentication`
|
|
||||||
- "Implement OAuth2 integration for API" → `001-implement-oauth2-integration-api`
|
|
||||||
- "Fix payment processing bug" → `001-fix-payment-processing`
|
|
||||||
- Very long descriptions are automatically truncated at word boundaries to stay within limits
|
|
||||||
- Designed for AI agents to provide semantic short names while maintaining standalone usability
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Enhanced help documentation for `create-new-feature.sh` and `create-new-feature.ps1` scripts with examples
|
|
||||||
- Branch names now validated against GitHub's 244-byte limit with automatic truncation if needed
|
|
||||||
|
|
||||||
## [0.0.19] - 2025-10-10
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for CodeBuddy (thank you to [@lispking](https://github.com/lispking) for the contribution).
|
|
||||||
- You can now see Git-sourced errors in the Specify CLI.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Fixed the path to the constitution in `plan.md` (thank you to [@lyzno1](https://github.com/lyzno1) for spotting).
|
|
||||||
- Fixed backslash escapes in generated TOML files for Gemini (thank you to [@hsin19](https://github.com/hsin19) for the contribution).
|
|
||||||
- Implementation command now ensures that the correct ignore files are added (thank you to [@sigent-amazon](https://github.com/sigent-amazon) for the contribution).
|
|
||||||
|
|
||||||
## [0.0.18] - 2025-10-06
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for using `.` as a shorthand for current directory in `specify init .` command, equivalent to `--here` flag but more intuitive for users.
|
|
||||||
- Use the `/speckit.` command prefix to easily discover Spec Kit-related commands.
|
|
||||||
- Refactor the prompts and templates to simplify their capabilities and how they are tracked. No more polluting things with tests when they are not needed.
|
|
||||||
- Ensure that tasks are created per user story (simplifies testing and validation).
|
|
||||||
- Add support for Visual Studio Code prompt shortcuts and automatic script execution.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- All command files now prefixed with `speckit.` (e.g., `speckit.specify.md`, `speckit.plan.md`) for better discoverability and differentiation in IDE/CLI command palettes and file explorers
|
|
||||||
|
|
||||||
## [0.0.17] - 2025-09-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- New `/clarify` command template to surface up to 5 targeted clarification questions for an existing spec and persist answers into a Clarifications section in the spec.
|
|
||||||
- New `/analyze` command template providing a non-destructive cross-artifact discrepancy and alignment report (spec, clarifications, plan, tasks, constitution) inserted after `/tasks` and before `/implement`.
|
|
||||||
- Note: Constitution rules are explicitly treated as non-negotiable; any conflict is a CRITICAL finding requiring artifact remediation, not weakening of principles.
|
|
||||||
|
|
||||||
## [0.0.16] - 2025-09-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `--force` flag for `init` command to bypass confirmation when using `--here` in a non-empty directory and proceed with merging/overwriting files.
|
|
||||||
|
|
||||||
## [0.0.15] - 2025-09-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for Roo Code.
|
|
||||||
|
|
||||||
## [0.0.14] - 2025-09-21
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Error messages are now shown consistently.
|
|
||||||
|
|
||||||
## [0.0.13] - 2025-09-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for Kilo Code. Thank you [@shahrukhkhan489](https://github.com/shahrukhkhan489) with [#394](https://github.com/github/spec-kit/pull/394).
|
|
||||||
- Support for Auggie CLI. Thank you [@hungthai1401](https://github.com/hungthai1401) with [#137](https://github.com/github/spec-kit/pull/137).
|
|
||||||
- Agent folder security notice displayed after project provisioning completion, warning users that some agents may store credentials or auth tokens in their agent folders and recommending adding relevant folders to `.gitignore` to prevent accidental credential leakage.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Warning displayed to ensure that folks are aware that they might need to add their agent folder to `.gitignore`.
|
|
||||||
- Cleaned up the `check` command output.
|
|
||||||
|
|
||||||
## [0.0.12] - 2025-09-21
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Added additional context for OpenAI Codex users - they need to set an additional environment variable, as described in [#417](https://github.com/github/spec-kit/issues/417).
|
|
||||||
|
|
||||||
## [0.0.11] - 2025-09-20
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Codex CLI support (thank you [@honjo-hiroaki-gtt](https://github.com/honjo-hiroaki-gtt) for the contribution in [#14](https://github.com/github/spec-kit/pull/14))
|
|
||||||
- Codex-aware context update tooling (Bash and PowerShell) so feature plans refresh `AGENTS.md` alongside existing assistants without manual edits.
|
|
||||||
|
|
||||||
## [0.0.10] - 2025-09-20
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Addressed [#378](https://github.com/github/spec-kit/issues/378) where a GitHub token may be attached to the request when it was empty.
|
- **AI Skills Installation Bug (#1658)**: Fixed `--ai-skills` flag not generating skill files for GitHub Copilot and other agents with non-standard command directory structures
|
||||||
|
- Added `commands_subdir` field to `AGENT_CONFIG` to explicitly specify the subdirectory name for each agent
|
||||||
|
- Affected agents now work correctly: copilot (`.github/agents/`), opencode (`.opencode/command/`), windsurf (`.windsurf/workflows/`), codex (`.codex/prompts/`), kilocode (`.kilocode/workflows/`), q (`.amazonq/prompts/`), and agy (`.agent/workflows/`)
|
||||||
|
- The `install_ai_skills()` function now uses the correct path for all agents instead of assuming `commands/` for everyone
|
||||||
|
|
||||||
## [0.0.9] - 2025-09-19
|
## [0.1.4] - Unreleased
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Improved agent selector UI with cyan highlighting for agent keys and gray parentheses for full names
|
|
||||||
|
|
||||||
## [0.0.8] - 2025-09-19
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Windsurf IDE support as additional AI assistant option (thank you [@raedkit](https://github.com/raedkit) for the work in [#151](https://github.com/github/spec-kit/pull/151))
|
|
||||||
- GitHub token support for API requests to handle corporate environments and rate limiting (contributed by [@zryfish](https://github.com/@zryfish) in [#243](https://github.com/github/spec-kit/pull/243))
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Updated README with Windsurf examples and GitHub token usage
|
|
||||||
- Enhanced release workflow to include Windsurf templates
|
|
||||||
|
|
||||||
## [0.0.7] - 2025-09-18
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Updated command instructions in the CLI.
|
|
||||||
- Cleaned up the code to not render agent-specific information when it's generic.
|
|
||||||
|
|
||||||
## [0.0.6] - 2025-09-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- opencode support as additional AI assistant option
|
|
||||||
|
|
||||||
## [0.0.5] - 2025-09-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Qwen Code support as additional AI assistant option
|
|
||||||
|
|
||||||
## [0.0.4] - 2025-09-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- SOCKS proxy support for corporate environments via `httpx[socks]` dependency
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
N/A
|
- **Qoder CLI detection**: Renamed `AGENT_CONFIG` key from `"qoder"` to `"qodercli"` to match the actual executable name, fixing `specify check` and `specify init --ai` detection failures
|
||||||
|
|
||||||
### Changed
|
## [0.1.3] - Unreleased
|
||||||
|
|
||||||
N/A
|
### Added
|
||||||
|
|
||||||
|
- **Generic Agent Support**: Added `--ai generic` option for unsupported AI agents ("bring your own agent")
|
||||||
|
- Requires `--ai-commands-dir <path>` to specify where the agent reads commands from
|
||||||
|
- Generates Markdown commands with `$ARGUMENTS` format (compatible with most agents)
|
||||||
|
- Example: `specify init my-project --ai generic --ai-commands-dir .myagent/commands/`
|
||||||
|
- Enables users to start with Spec Kit immediately while their agent awaits formal support
|
||||||
|
|
||||||
|
## [0.0.102] - 2026-02-20
|
||||||
|
|
||||||
|
- fix: include 'src/**' path in release workflow triggers (#1646)
|
||||||
|
|
||||||
|
## [0.0.101] - 2026-02-19
|
||||||
|
|
||||||
|
- chore(deps): bump github/codeql-action from 3 to 4 (#1635)
|
||||||
|
|
||||||
|
## [0.0.100] - 2026-02-19
|
||||||
|
|
||||||
|
- Add pytest and Python linting (ruff) to CI (#1637)
|
||||||
|
- feat: add pull request template for better contribution guidelines (#1634)
|
||||||
|
|
||||||
|
## [0.0.99] - 2026-02-19
|
||||||
|
|
||||||
|
- Feat/ai skills (#1632)
|
||||||
|
|
||||||
|
## [0.0.98] - 2026-02-19
|
||||||
|
|
||||||
|
- chore(deps): bump actions/stale from 9 to 10 (#1623)
|
||||||
|
- feat: add dependabot configuration for pip and GitHub Actions updates (#1622)
|
||||||
|
|
||||||
|
## [0.0.97] - 2026-02-18
|
||||||
|
|
||||||
|
- Remove Maintainers section from README.md (#1618)
|
||||||
|
|
||||||
|
## [0.0.96] - 2026-02-17
|
||||||
|
|
||||||
|
- fix: typo in plan-template.md (#1446)
|
||||||
|
|
||||||
|
## [0.0.95] - 2026-02-12
|
||||||
|
|
||||||
|
- Feat: add a new agent: Google Anti Gravity (#1220)
|
||||||
|
|
||||||
|
## [0.0.94] - 2026-02-11
|
||||||
|
|
||||||
|
- Add stale workflow for 180-day inactive issues and PRs (#1594)
|
||||||
|
|
||||||
|
## [0.0.93] - 2026-02-10
|
||||||
|
|
||||||
|
- Add modular extension system (#1551)
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -31,7 +31,6 @@
|
|||||||
- [📖 Learn More](#-learn-more)
|
- [📖 Learn More](#-learn-more)
|
||||||
- [📋 Detailed Process](#-detailed-process)
|
- [📋 Detailed Process](#-detailed-process)
|
||||||
- [🔍 Troubleshooting](#-troubleshooting)
|
- [🔍 Troubleshooting](#-troubleshooting)
|
||||||
- [👥 Maintainers](#-maintainers)
|
|
||||||
- [💬 Support](#-support)
|
- [💬 Support](#-support)
|
||||||
- [🙏 Acknowledgements](#-acknowledgements)
|
- [🙏 Acknowledgements](#-acknowledgements)
|
||||||
- [📄 License](#-license)
|
- [📄 License](#-license)
|
||||||
@@ -162,6 +161,8 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
|
|||||||
| [Roo Code](https://roocode.com/) | ✅ | |
|
| [Roo Code](https://roocode.com/) | ✅ | |
|
||||||
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
||||||
| [Windsurf](https://windsurf.com/) | ✅ | |
|
| [Windsurf](https://windsurf.com/) | ✅ | |
|
||||||
|
| [Antigravity (agy)](https://agy.ai/) | ✅ | |
|
||||||
|
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
||||||
|
|
||||||
## 🔧 Specify CLI Reference
|
## 🔧 Specify CLI Reference
|
||||||
|
|
||||||
@@ -172,14 +173,15 @@ The `specify` command supports the following options:
|
|||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `init` | Initialize a new Specify project from the latest template |
|
| `init` | Initialize a new Specify project from the latest template |
|
||||||
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`, `qoder`) |
|
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`, `qodercli`) |
|
||||||
|
|
||||||
### `specify init` Arguments & Options
|
### `specify init` Arguments & Options
|
||||||
|
|
||||||
| Argument/Option | Type | Description |
|
| Argument/Option | Type | Description |
|
||||||
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
||||||
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `q`, `bob`, or `qoder` |
|
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `q`, `agy`, `bob`, `qodercli`, or `generic` (requires `--ai-commands-dir`) |
|
||||||
|
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
||||||
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||||
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
||||||
| `--no-git` | Flag | Skip git repository initialization |
|
| `--no-git` | Flag | Skip git repository initialization |
|
||||||
@@ -188,6 +190,7 @@ The `specify` command supports the following options:
|
|||||||
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
|
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
|
||||||
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
|
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
|
||||||
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
|
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
|
||||||
|
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`) |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ specify init my-project --ai claude
|
|||||||
specify init my-project --ai cursor-agent
|
specify init my-project --ai cursor-agent
|
||||||
|
|
||||||
# Initialize with Qoder support
|
# Initialize with Qoder support
|
||||||
specify init my-project --ai qoder
|
specify init my-project --ai qodercli
|
||||||
|
|
||||||
# Initialize with Windsurf support
|
# Initialize with Windsurf support
|
||||||
specify init my-project --ai windsurf
|
specify init my-project --ai windsurf
|
||||||
@@ -216,6 +219,9 @@ specify init my-project --ai shai
|
|||||||
# Initialize with IBM Bob support
|
# Initialize with IBM Bob support
|
||||||
specify init my-project --ai bob
|
specify init my-project --ai bob
|
||||||
|
|
||||||
|
# Initialize with an unsupported agent (generic / bring your own agent)
|
||||||
|
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
||||||
|
|
||||||
# Initialize with PowerShell scripts (Windows/cross-platform)
|
# Initialize with PowerShell scripts (Windows/cross-platform)
|
||||||
specify init my-project --ai copilot --script ps
|
specify init my-project --ai copilot --script ps
|
||||||
|
|
||||||
@@ -238,6 +244,12 @@ specify init my-project --ai claude --debug
|
|||||||
# Use GitHub token for API requests (helpful for corporate environments)
|
# Use GitHub token for API requests (helpful for corporate environments)
|
||||||
specify init my-project --ai claude --github-token ghp_your_token_here
|
specify init my-project --ai claude --github-token ghp_your_token_here
|
||||||
|
|
||||||
|
# Install agent skills with the project
|
||||||
|
specify init my-project --ai claude --ai-skills
|
||||||
|
|
||||||
|
# Initialize in current directory with agent skills
|
||||||
|
specify init --here --ai gemini --ai-skills
|
||||||
|
|
||||||
# Check system requirements
|
# Check system requirements
|
||||||
specify check
|
specify check
|
||||||
```
|
```
|
||||||
@@ -636,11 +648,6 @@ echo "Cleaning up..."
|
|||||||
rm gcm-linux_amd64.2.6.1.deb
|
rm gcm-linux_amd64.2.6.1.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
## 👥 Maintainers
|
|
||||||
|
|
||||||
- Den Delimarsky ([@localden](https://github.com/localden))
|
|
||||||
- John Lam ([@jflam](https://github.com/jflam))
|
|
||||||
|
|
||||||
## 💬 Support
|
## 💬 Support
|
||||||
|
|
||||||
For support, please open a [GitHub issue](https://github.com/github/spec-kit/issues/new). We welcome bug reports, feature requests, and questions about using Spec-Driven Development.
|
For support, please open a [GitHub issue](https://github.com/github/spec-kit/issues/new). We welcome bug reports, feature requests, and questions about using Spec-Driven Development.
|
||||||
|
|||||||
714
extensions/EXTENSION-API-REFERENCE.md
Normal file
714
extensions/EXTENSION-API-REFERENCE.md
Normal file
@@ -0,0 +1,714 @@
|
|||||||
|
# Extension API Reference
|
||||||
|
|
||||||
|
Technical reference for Spec Kit extension system APIs and manifest schema.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Extension Manifest](#extension-manifest)
|
||||||
|
2. [Python API](#python-api)
|
||||||
|
3. [Command File Format](#command-file-format)
|
||||||
|
4. [Configuration Schema](#configuration-schema)
|
||||||
|
5. [Hook System](#hook-system)
|
||||||
|
6. [CLI Commands](#cli-commands)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extension Manifest
|
||||||
|
|
||||||
|
### Schema Version 1.0
|
||||||
|
|
||||||
|
File: `extension.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
schema_version: "1.0" # Required
|
||||||
|
|
||||||
|
extension:
|
||||||
|
id: string # Required, pattern: ^[a-z0-9-]+$
|
||||||
|
name: string # Required, human-readable name
|
||||||
|
version: string # Required, semantic version (X.Y.Z)
|
||||||
|
description: string # Required, brief description (<200 chars)
|
||||||
|
author: string # Required
|
||||||
|
repository: string # Required, valid URL
|
||||||
|
license: string # Required (e.g., "MIT", "Apache-2.0")
|
||||||
|
homepage: string # Optional, valid URL
|
||||||
|
|
||||||
|
requires:
|
||||||
|
speckit_version: string # Required, version specifier (>=X.Y.Z)
|
||||||
|
tools: # Optional, array of tool requirements
|
||||||
|
- name: string # Tool name
|
||||||
|
version: string # Optional, version specifier
|
||||||
|
required: boolean # Optional, default: false
|
||||||
|
|
||||||
|
provides:
|
||||||
|
commands: # Required, at least one command
|
||||||
|
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
|
||||||
|
file: string # Required, relative path to command file
|
||||||
|
description: string # Required
|
||||||
|
aliases: [string] # Optional, array of alternate names
|
||||||
|
|
||||||
|
config: # Optional, array of config files
|
||||||
|
- name: string # Config file name
|
||||||
|
template: string # Template file path
|
||||||
|
description: string
|
||||||
|
required: boolean # Default: false
|
||||||
|
|
||||||
|
hooks: # Optional, event hooks
|
||||||
|
event_name: # e.g., "after_tasks", "after_implement"
|
||||||
|
command: string # Command to execute
|
||||||
|
optional: boolean # Default: true
|
||||||
|
prompt: string # Prompt text for optional hooks
|
||||||
|
description: string # Hook description
|
||||||
|
condition: string # Optional, condition expression
|
||||||
|
|
||||||
|
tags: # Optional, array of tags (2-10 recommended)
|
||||||
|
- string
|
||||||
|
|
||||||
|
defaults: # Optional, default configuration values
|
||||||
|
key: value # Any YAML structure
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Specifications
|
||||||
|
|
||||||
|
#### `extension.id`
|
||||||
|
|
||||||
|
- **Type**: string
|
||||||
|
- **Pattern**: `^[a-z0-9-]+$`
|
||||||
|
- **Description**: Unique extension identifier
|
||||||
|
- **Examples**: `jira`, `linear`, `azure-devops`
|
||||||
|
- **Invalid**: `Jira`, `my_extension`, `extension.id`
|
||||||
|
|
||||||
|
#### `extension.version`
|
||||||
|
|
||||||
|
- **Type**: string
|
||||||
|
- **Format**: Semantic versioning (X.Y.Z)
|
||||||
|
- **Description**: Extension version
|
||||||
|
- **Examples**: `1.0.0`, `0.9.5`, `2.1.3`
|
||||||
|
- **Invalid**: `v1.0`, `1.0`, `1.0.0-beta`
|
||||||
|
|
||||||
|
#### `requires.speckit_version`
|
||||||
|
|
||||||
|
- **Type**: string
|
||||||
|
- **Format**: Version specifier
|
||||||
|
- **Description**: Required spec-kit version range
|
||||||
|
- **Examples**:
|
||||||
|
- `>=0.1.0` - Any version 0.1.0 or higher
|
||||||
|
- `>=0.1.0,<2.0.0` - Version 0.1.x or 1.x
|
||||||
|
- `==0.1.0` - Exactly 0.1.0
|
||||||
|
- **Invalid**: `0.1.0`, `>= 0.1.0` (space), `latest`
|
||||||
|
|
||||||
|
#### `provides.commands[].name`
|
||||||
|
|
||||||
|
- **Type**: string
|
||||||
|
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
|
||||||
|
- **Description**: Namespaced command name
|
||||||
|
- **Format**: `speckit.{extension-id}.{command-name}`
|
||||||
|
- **Examples**: `speckit.jira.specstoissues`, `speckit.linear.sync`
|
||||||
|
- **Invalid**: `jira.specstoissues`, `speckit.command`, `speckit.jira.CreateIssues`
|
||||||
|
|
||||||
|
#### `hooks`
|
||||||
|
|
||||||
|
- **Type**: object
|
||||||
|
- **Keys**: Event names (e.g., `after_tasks`, `after_implement`, `before_commit`)
|
||||||
|
- **Description**: Hooks that execute at lifecycle events
|
||||||
|
- **Events**: Defined by core spec-kit commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python API
|
||||||
|
|
||||||
|
### ExtensionManifest
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import ExtensionManifest
|
||||||
|
|
||||||
|
manifest = ExtensionManifest(Path("extension.yml"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Properties**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
manifest.id # str: Extension ID
|
||||||
|
manifest.name # str: Extension name
|
||||||
|
manifest.version # str: Version
|
||||||
|
manifest.description # str: Description
|
||||||
|
manifest.requires_speckit_version # str: Required spec-kit version
|
||||||
|
manifest.commands # List[Dict]: Command definitions
|
||||||
|
manifest.hooks # Dict: Hook definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
manifest.get_hash() # str: SHA256 hash of manifest file
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exceptions**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ValidationError # Invalid manifest structure
|
||||||
|
CompatibilityError # Incompatible with current spec-kit version
|
||||||
|
```
|
||||||
|
|
||||||
|
### ExtensionRegistry
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import ExtensionRegistry
|
||||||
|
|
||||||
|
registry = ExtensionRegistry(extensions_dir)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add extension to registry
|
||||||
|
registry.add(extension_id: str, metadata: dict)
|
||||||
|
|
||||||
|
# Remove extension from registry
|
||||||
|
registry.remove(extension_id: str)
|
||||||
|
|
||||||
|
# Get extension metadata
|
||||||
|
metadata = registry.get(extension_id: str) # Optional[dict]
|
||||||
|
|
||||||
|
# List all extensions
|
||||||
|
extensions = registry.list() # Dict[str, dict]
|
||||||
|
|
||||||
|
# Check if installed
|
||||||
|
is_installed = registry.is_installed(extension_id: str) # bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Registry Format**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"source": "catalog",
|
||||||
|
"manifest_hash": "sha256...",
|
||||||
|
"enabled": true,
|
||||||
|
"registered_commands": ["speckit.jira.specstoissues", ...],
|
||||||
|
"installed_at": "2026-01-28T..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ExtensionManager
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import ExtensionManager
|
||||||
|
|
||||||
|
manager = ExtensionManager(project_root)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Install from directory
|
||||||
|
manifest = manager.install_from_directory(
|
||||||
|
source_dir: Path,
|
||||||
|
speckit_version: str,
|
||||||
|
register_commands: bool = True
|
||||||
|
) # Returns: ExtensionManifest
|
||||||
|
|
||||||
|
# Install from ZIP
|
||||||
|
manifest = manager.install_from_zip(
|
||||||
|
zip_path: Path,
|
||||||
|
speckit_version: str
|
||||||
|
) # Returns: ExtensionManifest
|
||||||
|
|
||||||
|
# Remove extension
|
||||||
|
success = manager.remove(
|
||||||
|
extension_id: str,
|
||||||
|
keep_config: bool = False
|
||||||
|
) # Returns: bool
|
||||||
|
|
||||||
|
# List installed extensions
|
||||||
|
extensions = manager.list_installed() # List[Dict]
|
||||||
|
|
||||||
|
# Get extension manifest
|
||||||
|
manifest = manager.get_extension(extension_id: str) # Optional[ExtensionManifest]
|
||||||
|
|
||||||
|
# Check compatibility
|
||||||
|
manager.check_compatibility(
|
||||||
|
manifest: ExtensionManifest,
|
||||||
|
speckit_version: str
|
||||||
|
) # Raises: CompatibilityError if incompatible
|
||||||
|
```
|
||||||
|
|
||||||
|
### ExtensionCatalog
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import ExtensionCatalog
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_root)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Fetch catalog
|
||||||
|
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
|
||||||
|
|
||||||
|
# Search extensions
|
||||||
|
results = catalog.search(
|
||||||
|
query: Optional[str] = None,
|
||||||
|
tag: Optional[str] = None,
|
||||||
|
author: Optional[str] = None,
|
||||||
|
verified_only: bool = False
|
||||||
|
) # Returns: List[Dict]
|
||||||
|
|
||||||
|
# Get extension info
|
||||||
|
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
|
||||||
|
|
||||||
|
# Check cache validity
|
||||||
|
is_valid = catalog.is_cache_valid() # bool
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
catalog.clear_cache()
|
||||||
|
```
|
||||||
|
|
||||||
|
### HookExecutor
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import HookExecutor
|
||||||
|
|
||||||
|
hook_executor = HookExecutor(project_root)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get project config
|
||||||
|
config = hook_executor.get_project_config() # Dict
|
||||||
|
|
||||||
|
# Save project config
|
||||||
|
hook_executor.save_project_config(config: Dict)
|
||||||
|
|
||||||
|
# Register hooks
|
||||||
|
hook_executor.register_hooks(manifest: ExtensionManifest)
|
||||||
|
|
||||||
|
# Unregister hooks
|
||||||
|
hook_executor.unregister_hooks(extension_id: str)
|
||||||
|
|
||||||
|
# Get hooks for event
|
||||||
|
hooks = hook_executor.get_hooks_for_event(event_name: str) # List[Dict]
|
||||||
|
|
||||||
|
# Check if hook should execute
|
||||||
|
should_run = hook_executor.should_execute_hook(hook: Dict) # bool
|
||||||
|
|
||||||
|
# Format hook message
|
||||||
|
message = hook_executor.format_hook_message(
|
||||||
|
event_name: str,
|
||||||
|
hooks: List[Dict]
|
||||||
|
) # str
|
||||||
|
```
|
||||||
|
|
||||||
|
### CommandRegistrar
|
||||||
|
|
||||||
|
**Module**: `specify_cli.extensions`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import CommandRegistrar
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Register commands for Claude Code
|
||||||
|
registered = registrar.register_commands_for_claude(
|
||||||
|
manifest: ExtensionManifest,
|
||||||
|
extension_dir: Path,
|
||||||
|
project_root: Path
|
||||||
|
) # Returns: List[str] (command names)
|
||||||
|
|
||||||
|
# Parse frontmatter
|
||||||
|
frontmatter, body = registrar.parse_frontmatter(content: str)
|
||||||
|
|
||||||
|
# Render frontmatter
|
||||||
|
yaml_text = registrar.render_frontmatter(frontmatter: Dict) # str
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command File Format
|
||||||
|
|
||||||
|
### Universal Command Format
|
||||||
|
|
||||||
|
**File**: `commands/{command-name}.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
description: "Command description"
|
||||||
|
tools:
|
||||||
|
- 'mcp-server/tool_name'
|
||||||
|
- 'other-mcp-server/other_tool'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Command Title
|
||||||
|
|
||||||
|
Command documentation in Markdown.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Requirement 1
|
||||||
|
2. Requirement 2
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Description
|
||||||
|
|
||||||
|
Instruction text...
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# Shell commands
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
### Step 2: Another Step
|
||||||
|
|
||||||
|
More instructions...
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
Information about configuration options.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Additional notes and tips.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontmatter Fields
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
description: string # Required, brief command description
|
||||||
|
tools: [string] # Optional, MCP tools required
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Variables
|
||||||
|
|
||||||
|
- `$ARGUMENTS` - Placeholder for user-provided arguments
|
||||||
|
- Extension context automatically injected:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<!-- Extension: {extension-id} -->
|
||||||
|
<!-- Config: .specify/extensions/{extension-id}/ -->
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Schema
|
||||||
|
|
||||||
|
### Extension Config File
|
||||||
|
|
||||||
|
**File**: `.specify/extensions/{extension-id}/{extension-id}-config.yml`
|
||||||
|
|
||||||
|
Extensions define their own config schema. Common patterns:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Connection settings
|
||||||
|
connection:
|
||||||
|
url: string
|
||||||
|
api_key: string
|
||||||
|
|
||||||
|
# Project settings
|
||||||
|
project:
|
||||||
|
key: string
|
||||||
|
workspace: string
|
||||||
|
|
||||||
|
# Feature flags
|
||||||
|
features:
|
||||||
|
enabled: boolean
|
||||||
|
auto_sync: boolean
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
defaults:
|
||||||
|
labels: [string]
|
||||||
|
assignee: string
|
||||||
|
|
||||||
|
# Custom fields
|
||||||
|
field_mappings:
|
||||||
|
internal_name: "external_field_id"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config Layers
|
||||||
|
|
||||||
|
1. **Extension Defaults** (from `extension.yml` `defaults` section)
|
||||||
|
2. **Project Config** (`{extension-id}-config.yml`)
|
||||||
|
3. **Local Override** (`{extension-id}-config.local.yml`, gitignored)
|
||||||
|
4. **Environment Variables** (`SPECKIT_{EXTENSION}_*`)
|
||||||
|
|
||||||
|
### Environment Variable Pattern
|
||||||
|
|
||||||
|
Format: `SPECKIT_{EXTENSION}_{KEY}`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `SPECKIT_JIRA_PROJECT_KEY`
|
||||||
|
- `SPECKIT_LINEAR_API_KEY`
|
||||||
|
- `SPECKIT_GITHUB_TOKEN`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hook System
|
||||||
|
|
||||||
|
### Hook Definition
|
||||||
|
|
||||||
|
**In extension.yml**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hooks:
|
||||||
|
after_tasks:
|
||||||
|
command: "speckit.jira.specstoissues"
|
||||||
|
optional: true
|
||||||
|
prompt: "Create Jira issues from tasks?"
|
||||||
|
description: "Automatically create Jira hierarchy"
|
||||||
|
condition: null
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Events
|
||||||
|
|
||||||
|
Standard events (defined by core):
|
||||||
|
|
||||||
|
- `after_tasks` - After task generation
|
||||||
|
- `after_implement` - After implementation
|
||||||
|
- `before_commit` - Before git commit
|
||||||
|
- `after_commit` - After git commit
|
||||||
|
|
||||||
|
### Hook Configuration
|
||||||
|
|
||||||
|
**In `.specify/extensions.yml`**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hooks:
|
||||||
|
after_tasks:
|
||||||
|
- extension: jira
|
||||||
|
command: speckit.jira.specstoissues
|
||||||
|
enabled: true
|
||||||
|
optional: true
|
||||||
|
prompt: "Create Jira issues from tasks?"
|
||||||
|
description: "..."
|
||||||
|
condition: null
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook Message Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Extension Hooks
|
||||||
|
|
||||||
|
**Optional Hook**: {extension}
|
||||||
|
Command: `/{command}`
|
||||||
|
Description: {description}
|
||||||
|
|
||||||
|
Prompt: {prompt}
|
||||||
|
To execute: `/{command}`
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for mandatory hooks:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Automatic Hook**: {extension}
|
||||||
|
Executing: `/{command}`
|
||||||
|
EXECUTE_COMMAND: {command}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Commands
|
||||||
|
|
||||||
|
### extension list
|
||||||
|
|
||||||
|
**Usage**: `specify extension list [OPTIONS]`
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- `--available` - Show available extensions from catalog
|
||||||
|
- `--all` - Show both installed and available
|
||||||
|
|
||||||
|
**Output**: List of installed extensions with metadata
|
||||||
|
|
||||||
|
### extension add
|
||||||
|
|
||||||
|
**Usage**: `specify extension add EXTENSION [OPTIONS]`
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- `--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
|
||||||
|
|
||||||
|
### extension remove
|
||||||
|
|
||||||
|
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- `--keep-config` - Preserve config files
|
||||||
|
- `--force` - Skip confirmation
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `EXTENSION` - Extension ID
|
||||||
|
|
||||||
|
### extension search
|
||||||
|
|
||||||
|
**Usage**: `specify extension search [QUERY] [OPTIONS]`
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- `--tag TAG` - Filter by tag
|
||||||
|
- `--author AUTHOR` - Filter by author
|
||||||
|
- `--verified` - Show only verified extensions
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `QUERY` - Optional search query
|
||||||
|
|
||||||
|
### extension info
|
||||||
|
|
||||||
|
**Usage**: `specify extension info EXTENSION`
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `EXTENSION` - Extension ID
|
||||||
|
|
||||||
|
### extension update
|
||||||
|
|
||||||
|
**Usage**: `specify extension update [EXTENSION]`
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `EXTENSION` - Optional, extension ID (default: all)
|
||||||
|
|
||||||
|
### extension enable
|
||||||
|
|
||||||
|
**Usage**: `specify extension enable EXTENSION`
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `EXTENSION` - Extension ID
|
||||||
|
|
||||||
|
### extension disable
|
||||||
|
|
||||||
|
**Usage**: `specify extension disable EXTENSION`
|
||||||
|
|
||||||
|
**Arguments**:
|
||||||
|
|
||||||
|
- `EXTENSION` - Extension ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exceptions
|
||||||
|
|
||||||
|
### ValidationError
|
||||||
|
|
||||||
|
Raised when extension manifest validation fails.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import ValidationError
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = ExtensionManifest(path)
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f"Invalid manifest: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### CompatibilityError
|
||||||
|
|
||||||
|
Raised when extension is incompatible with current spec-kit version.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import CompatibilityError
|
||||||
|
|
||||||
|
try:
|
||||||
|
manager.check_compatibility(manifest, "0.1.0")
|
||||||
|
except CompatibilityError as e:
|
||||||
|
print(f"Incompatible: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### ExtensionError
|
||||||
|
|
||||||
|
Base exception for all extension-related errors.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import ExtensionError
|
||||||
|
|
||||||
|
try:
|
||||||
|
manager.install_from_directory(path, "0.1.0")
|
||||||
|
except ExtensionError as e:
|
||||||
|
print(f"Extension error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Functions
|
||||||
|
|
||||||
|
### version_satisfies
|
||||||
|
|
||||||
|
Check if a version satisfies a specifier.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from specify_cli.extensions import version_satisfies
|
||||||
|
|
||||||
|
# True if 1.2.3 satisfies >=1.0.0,<2.0.0
|
||||||
|
satisfied = version_satisfies("1.2.3", ">=1.0.0,<2.0.0") # bool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File System Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
.specify/
|
||||||
|
├── extensions/
|
||||||
|
│ ├── .registry # Extension registry (JSON)
|
||||||
|
│ ├── .cache/ # Catalog cache
|
||||||
|
│ │ ├── catalog.json
|
||||||
|
│ │ └── catalog-metadata.json
|
||||||
|
│ ├── .backup/ # Config backups
|
||||||
|
│ │ └── {ext}-{config}.yml
|
||||||
|
│ ├── {extension-id}/ # Extension directory
|
||||||
|
│ │ ├── extension.yml # Manifest
|
||||||
|
│ │ ├── {ext}-config.yml # User config
|
||||||
|
│ │ ├── {ext}-config.local.yml # Local overrides (gitignored)
|
||||||
|
│ │ ├── {ext}-config.template.yml # Template
|
||||||
|
│ │ ├── commands/ # Command files
|
||||||
|
│ │ │ └── *.md
|
||||||
|
│ │ ├── scripts/ # Helper scripts
|
||||||
|
│ │ │ └── *.sh
|
||||||
|
│ │ ├── docs/ # Documentation
|
||||||
|
│ │ └── README.md
|
||||||
|
│ └── extensions.yml # Project extension config
|
||||||
|
└── scripts/ # (existing spec-kit)
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
└── commands/
|
||||||
|
└── speckit.{ext}.{cmd}.md # Registered commands
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: 2026-01-28*
|
||||||
|
*API Version: 1.0*
|
||||||
|
*Spec Kit Version: 0.1.0*
|
||||||
649
extensions/EXTENSION-DEVELOPMENT-GUIDE.md
Normal file
649
extensions/EXTENSION-DEVELOPMENT-GUIDE.md
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
# 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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: Extension Catalog (Future)
|
||||||
|
|
||||||
|
Submit to official catalog:
|
||||||
|
|
||||||
|
1. **Fork** spec-kit repository
|
||||||
|
2. **Add entry** to `extensions/catalog.json`
|
||||||
|
3. **Create PR**
|
||||||
|
4. **After merge**, users can install with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension add my-ext # No URL needed!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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! 🚀
|
||||||
530
extensions/EXTENSION-PUBLISHING-GUIDE.md
Normal file
530
extensions/EXTENSION-PUBLISHING-GUIDE.md
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
# Extension Publishing Guide
|
||||||
|
|
||||||
|
This guide explains how to publish your extension to the Spec Kit extension catalog, making it discoverable by `specify extension search`.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Prerequisites](#prerequisites)
|
||||||
|
2. [Prepare Your Extension](#prepare-your-extension)
|
||||||
|
3. [Submit to Catalog](#submit-to-catalog)
|
||||||
|
4. [Verification Process](#verification-process)
|
||||||
|
5. [Release Workflow](#release-workflow)
|
||||||
|
6. [Best Practices](#best-practices)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before publishing an extension, ensure you have:
|
||||||
|
|
||||||
|
1. **Valid Extension**: A working extension with a valid `extension.yml` manifest
|
||||||
|
2. **Git Repository**: Extension hosted on GitHub (or other public git hosting)
|
||||||
|
3. **Documentation**: README.md with installation and usage instructions
|
||||||
|
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
|
||||||
|
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
|
||||||
|
6. **Testing**: Extension tested on real projects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prepare Your Extension
|
||||||
|
|
||||||
|
### 1. Extension Structure
|
||||||
|
|
||||||
|
Ensure your extension follows the standard structure:
|
||||||
|
|
||||||
|
```text
|
||||||
|
your-extension/
|
||||||
|
├── extension.yml # Required: Extension manifest
|
||||||
|
├── README.md # Required: Documentation
|
||||||
|
├── LICENSE # Required: License file
|
||||||
|
├── CHANGELOG.md # Recommended: Version history
|
||||||
|
├── .gitignore # Recommended: Git ignore rules
|
||||||
|
│
|
||||||
|
├── commands/ # Extension commands
|
||||||
|
│ ├── command1.md
|
||||||
|
│ └── command2.md
|
||||||
|
│
|
||||||
|
├── config-template.yml # Config template (if needed)
|
||||||
|
│
|
||||||
|
└── docs/ # Additional documentation
|
||||||
|
├── usage.md
|
||||||
|
└── examples/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. extension.yml Validation
|
||||||
|
|
||||||
|
Verify your manifest is valid:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
schema_version: "1.0"
|
||||||
|
|
||||||
|
extension:
|
||||||
|
id: "your-extension" # Unique lowercase-hyphenated ID
|
||||||
|
name: "Your Extension Name" # Human-readable name
|
||||||
|
version: "1.0.0" # Semantic version
|
||||||
|
description: "Brief description (one sentence)"
|
||||||
|
author: "Your Name or Organization"
|
||||||
|
repository: "https://github.com/your-org/spec-kit-your-extension"
|
||||||
|
license: "MIT"
|
||||||
|
homepage: "https://github.com/your-org/spec-kit-your-extension"
|
||||||
|
|
||||||
|
requires:
|
||||||
|
speckit_version: ">=0.1.0" # Required spec-kit version
|
||||||
|
|
||||||
|
provides:
|
||||||
|
commands: # List all commands
|
||||||
|
- name: "speckit.your-extension.command"
|
||||||
|
file: "commands/command.md"
|
||||||
|
description: "Command description"
|
||||||
|
|
||||||
|
tags: # 2-5 relevant tags
|
||||||
|
- "category"
|
||||||
|
- "tool-name"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Checklist**:
|
||||||
|
|
||||||
|
- ✅ `id` is lowercase with hyphens only (no underscores, spaces, or special characters)
|
||||||
|
- ✅ `version` follows semantic versioning (X.Y.Z)
|
||||||
|
- ✅ `description` is concise (under 100 characters)
|
||||||
|
- ✅ `repository` URL is valid and public
|
||||||
|
- ✅ All command files exist in the extension directory
|
||||||
|
- ✅ Tags are lowercase and descriptive
|
||||||
|
|
||||||
|
### 3. Create GitHub Release
|
||||||
|
|
||||||
|
Create a GitHub release for your extension version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tag the release
|
||||||
|
git tag v1.0.0
|
||||||
|
git push origin v1.0.0
|
||||||
|
|
||||||
|
# Create release on GitHub
|
||||||
|
# Go to: https://github.com/your-org/spec-kit-your-extension/releases/new
|
||||||
|
# - Tag: v1.0.0
|
||||||
|
# - Title: v1.0.0 - Release Name
|
||||||
|
# - Description: Changelog/release notes
|
||||||
|
```
|
||||||
|
|
||||||
|
The release archive URL will be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Installation
|
||||||
|
|
||||||
|
Test that users can install from your release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test dev installation
|
||||||
|
specify extension add --dev /path/to/your-extension
|
||||||
|
|
||||||
|
# Test from GitHub archive
|
||||||
|
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submit to Catalog
|
||||||
|
|
||||||
|
### 1. Fork the spec-kit Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fork on GitHub
|
||||||
|
# https://github.com/statsperform/spec-kit/fork
|
||||||
|
|
||||||
|
# Clone your fork
|
||||||
|
git clone https://github.com/YOUR-USERNAME/spec-kit.git
|
||||||
|
cd spec-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add Extension to Catalog
|
||||||
|
|
||||||
|
Edit `extensions/catalog.json` and add your extension:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"updated_at": "2026-01-28T15:54:00Z",
|
||||||
|
"catalog_url": "https://raw.githubusercontent.com/statsperform/spec-kit/main/extensions/catalog.json",
|
||||||
|
"extensions": {
|
||||||
|
"your-extension": {
|
||||||
|
"name": "Your Extension Name",
|
||||||
|
"id": "your-extension",
|
||||||
|
"description": "Brief description of your extension",
|
||||||
|
"author": "Your Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/your-org/spec-kit-your-extension",
|
||||||
|
"homepage": "https://github.com/your-org/spec-kit-your-extension",
|
||||||
|
"documentation": "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/",
|
||||||
|
"changelog": "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "required-mcp-tool",
|
||||||
|
"version": ">=1.0.0",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 3,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"category",
|
||||||
|
"tool-name",
|
||||||
|
"feature"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-01-28T00:00:00Z",
|
||||||
|
"updated_at": "2026-01-28T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**:
|
||||||
|
|
||||||
|
- Set `verified: false` (maintainers will verify)
|
||||||
|
- Set `downloads: 0` and `stars: 0` (auto-updated later)
|
||||||
|
- Use current timestamp for `created_at` and `updated_at`
|
||||||
|
- Update the top-level `updated_at` to current time
|
||||||
|
|
||||||
|
### 3. Submit Pull Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a branch
|
||||||
|
git checkout -b add-your-extension
|
||||||
|
|
||||||
|
# Commit your changes
|
||||||
|
git add extensions/catalog.json
|
||||||
|
git commit -m "Add your-extension to catalog
|
||||||
|
|
||||||
|
- Extension ID: your-extension
|
||||||
|
- Version: 1.0.0
|
||||||
|
- Author: Your Name
|
||||||
|
- Description: Brief description
|
||||||
|
"
|
||||||
|
|
||||||
|
# Push to your fork
|
||||||
|
git push origin add-your-extension
|
||||||
|
|
||||||
|
# Create Pull Request on GitHub
|
||||||
|
# https://github.com/statsperform/spec-kit/compare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pull Request Template**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Extension Submission
|
||||||
|
|
||||||
|
**Extension Name**: Your Extension Name
|
||||||
|
**Extension ID**: your-extension
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Author**: Your Name
|
||||||
|
**Repository**: https://github.com/your-org/spec-kit-your-extension
|
||||||
|
|
||||||
|
### Description
|
||||||
|
Brief description of what your extension does.
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [x] Valid extension.yml manifest
|
||||||
|
- [x] README.md with installation and usage docs
|
||||||
|
- [x] LICENSE file included
|
||||||
|
- [x] GitHub release created (v1.0.0)
|
||||||
|
- [x] Extension tested on real project
|
||||||
|
- [x] All commands working
|
||||||
|
- [x] No security vulnerabilities
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
Tested on:
|
||||||
|
- macOS 13.0+ with spec-kit 0.1.0
|
||||||
|
- Project: [Your test project]
|
||||||
|
|
||||||
|
### Additional Notes
|
||||||
|
Any additional context or notes for reviewers.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Process
|
||||||
|
|
||||||
|
### What Happens After Submission
|
||||||
|
|
||||||
|
1. **Automated Checks** (if available):
|
||||||
|
- Manifest validation
|
||||||
|
- Download URL accessibility
|
||||||
|
- Repository existence
|
||||||
|
- License file presence
|
||||||
|
|
||||||
|
2. **Manual Review**:
|
||||||
|
- Code quality review
|
||||||
|
- Security audit
|
||||||
|
- Functionality testing
|
||||||
|
- Documentation review
|
||||||
|
|
||||||
|
3. **Verification**:
|
||||||
|
- If approved, `verified: true` is set
|
||||||
|
- Extension appears in `specify extension search --verified`
|
||||||
|
|
||||||
|
### Verification Criteria
|
||||||
|
|
||||||
|
To be verified, your extension must:
|
||||||
|
|
||||||
|
✅ **Functionality**:
|
||||||
|
|
||||||
|
- Works as described in documentation
|
||||||
|
- All commands execute without errors
|
||||||
|
- No breaking changes to user workflows
|
||||||
|
|
||||||
|
✅ **Security**:
|
||||||
|
|
||||||
|
- No known vulnerabilities
|
||||||
|
- No malicious code
|
||||||
|
- Safe handling of user data
|
||||||
|
- Proper validation of inputs
|
||||||
|
|
||||||
|
✅ **Code Quality**:
|
||||||
|
|
||||||
|
- Clean, readable code
|
||||||
|
- Follows extension best practices
|
||||||
|
- Proper error handling
|
||||||
|
- Helpful error messages
|
||||||
|
|
||||||
|
✅ **Documentation**:
|
||||||
|
|
||||||
|
- Clear installation instructions
|
||||||
|
- Usage examples
|
||||||
|
- Troubleshooting section
|
||||||
|
- Accurate description
|
||||||
|
|
||||||
|
✅ **Maintenance**:
|
||||||
|
|
||||||
|
- Active repository
|
||||||
|
- Responsive to issues
|
||||||
|
- Regular updates
|
||||||
|
- Semantic versioning followed
|
||||||
|
|
||||||
|
### Typical Review Timeline
|
||||||
|
|
||||||
|
- **Automated checks**: Immediate (if implemented)
|
||||||
|
- **Manual review**: 3-7 business days
|
||||||
|
- **Verification**: After successful review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release Workflow
|
||||||
|
|
||||||
|
### Publishing New Versions
|
||||||
|
|
||||||
|
When releasing a new version:
|
||||||
|
|
||||||
|
1. **Update version** in `extension.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
extension:
|
||||||
|
version: "1.1.0" # Updated version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update CHANGELOG.md**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [1.1.0] - 2026-02-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New feature X
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Bug fix Y
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create GitHub release**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v1.1.0
|
||||||
|
git push origin v1.1.0
|
||||||
|
# Create release on GitHub
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Update catalog**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fork spec-kit repo (or update existing fork)
|
||||||
|
cd spec-kit
|
||||||
|
|
||||||
|
# Update extensions/catalog.json
|
||||||
|
jq '.extensions["your-extension"].version = "1.1.0"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
|
||||||
|
jq '.extensions["your-extension"].download_url = "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.1.0.zip"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
|
||||||
|
jq '.extensions["your-extension"].updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
|
||||||
|
jq '.updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
|
||||||
|
|
||||||
|
# Submit PR
|
||||||
|
git checkout -b update-your-extension-v1.1.0
|
||||||
|
git add extensions/catalog.json
|
||||||
|
git commit -m "Update your-extension to v1.1.0"
|
||||||
|
git push origin update-your-extension-v1.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Submit update PR** with changelog in description
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Extension Design
|
||||||
|
|
||||||
|
1. **Single Responsibility**: Each extension should focus on one tool/integration
|
||||||
|
2. **Clear Naming**: Use descriptive, unambiguous names
|
||||||
|
3. **Minimal Dependencies**: Avoid unnecessary dependencies
|
||||||
|
4. **Backward Compatibility**: Follow semantic versioning strictly
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
1. **README.md Structure**:
|
||||||
|
- Overview and features
|
||||||
|
- Installation instructions
|
||||||
|
- Configuration guide
|
||||||
|
- Usage examples
|
||||||
|
- Troubleshooting
|
||||||
|
- Contributing guidelines
|
||||||
|
|
||||||
|
2. **Command Documentation**:
|
||||||
|
- Clear description
|
||||||
|
- Prerequisites listed
|
||||||
|
- Step-by-step instructions
|
||||||
|
- Error handling guidance
|
||||||
|
- Examples
|
||||||
|
|
||||||
|
3. **Configuration**:
|
||||||
|
- Provide template file
|
||||||
|
- Document all options
|
||||||
|
- Include examples
|
||||||
|
- Explain defaults
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
1. **Input Validation**: Validate all user inputs
|
||||||
|
2. **No Hardcoded Secrets**: Never include credentials
|
||||||
|
3. **Safe Dependencies**: Only use trusted dependencies
|
||||||
|
4. **Audit Regularly**: Check for vulnerabilities
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
1. **Respond to Issues**: Address issues within 1-2 weeks
|
||||||
|
2. **Regular Updates**: Keep dependencies updated
|
||||||
|
3. **Changelog**: Maintain detailed changelog
|
||||||
|
4. **Deprecation**: Give advance notice for breaking changes
|
||||||
|
|
||||||
|
### Community
|
||||||
|
|
||||||
|
1. **License**: Use permissive open-source license (MIT, Apache 2.0)
|
||||||
|
2. **Contributing**: Welcome contributions
|
||||||
|
3. **Code of Conduct**: Be respectful and inclusive
|
||||||
|
4. **Support**: Provide ways to get help (issues, discussions, email)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Q: Can I publish private/proprietary extensions?
|
||||||
|
|
||||||
|
A: The main catalog is for public extensions only. For private extensions:
|
||||||
|
|
||||||
|
- Host your own catalog.json file
|
||||||
|
- Users add your catalog: `specify extension add-catalog https://your-domain.com/catalog.json`
|
||||||
|
- Not yet implemented - coming in Phase 4
|
||||||
|
|
||||||
|
### Q: How long does verification take?
|
||||||
|
|
||||||
|
A: Typically 3-7 business days for initial review. Updates to verified extensions are usually faster.
|
||||||
|
|
||||||
|
### Q: What if my extension is rejected?
|
||||||
|
|
||||||
|
A: You'll receive feedback on what needs to be fixed. Make the changes and resubmit.
|
||||||
|
|
||||||
|
### Q: Can I update my extension anytime?
|
||||||
|
|
||||||
|
A: Yes, submit a PR to update the catalog with your new version. Verified status may be re-evaluated for major changes.
|
||||||
|
|
||||||
|
### Q: Do I need to be verified to be in the catalog?
|
||||||
|
|
||||||
|
A: No, unverified extensions are still searchable. Verification just adds trust and visibility.
|
||||||
|
|
||||||
|
### Q: Can extensions have paid features?
|
||||||
|
|
||||||
|
A: Extensions should be free and open-source. Commercial support/services are allowed, but core functionality must be free.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Catalog Issues**: <https://github.com/statsperform/spec-kit/issues>
|
||||||
|
- **Extension Template**: <https://github.com/statsperform/spec-kit-extension-template> (coming soon)
|
||||||
|
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
|
||||||
|
- **Community**: Discussions and Q&A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Catalog Schema
|
||||||
|
|
||||||
|
### Complete Catalog Entry Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "string (required)",
|
||||||
|
"id": "string (required, unique)",
|
||||||
|
"description": "string (required, <200 chars)",
|
||||||
|
"author": "string (required)",
|
||||||
|
"version": "string (required, semver)",
|
||||||
|
"download_url": "string (required, valid URL)",
|
||||||
|
"repository": "string (required, valid URL)",
|
||||||
|
"homepage": "string (optional, valid URL)",
|
||||||
|
"documentation": "string (optional, valid URL)",
|
||||||
|
"changelog": "string (optional, valid URL)",
|
||||||
|
"license": "string (required)",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": "string (required, version specifier)",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "string (required)",
|
||||||
|
"version": "string (optional, version specifier)",
|
||||||
|
"required": "boolean (default: false)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": "integer (optional)",
|
||||||
|
"hooks": "integer (optional)"
|
||||||
|
},
|
||||||
|
"tags": ["array of strings (2-10 tags)"],
|
||||||
|
"verified": "boolean (default: false)",
|
||||||
|
"downloads": "integer (auto-updated)",
|
||||||
|
"stars": "integer (auto-updated)",
|
||||||
|
"created_at": "string (ISO 8601 datetime)",
|
||||||
|
"updated_at": "string (ISO 8601 datetime)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Valid Tags
|
||||||
|
|
||||||
|
Recommended tag categories:
|
||||||
|
|
||||||
|
- **Integration**: jira, linear, github, gitlab, azure-devops
|
||||||
|
- **Category**: issue-tracking, vcs, ci-cd, documentation, testing
|
||||||
|
- **Platform**: atlassian, microsoft, google
|
||||||
|
- **Feature**: automation, reporting, deployment, monitoring
|
||||||
|
|
||||||
|
Use 2-5 tags that best describe your extension.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: 2026-01-28*
|
||||||
|
*Catalog Format Version: 1.0*
|
||||||
885
extensions/EXTENSION-USER-GUIDE.md
Normal file
885
extensions/EXTENSION-USER-GUIDE.md
Normal file
@@ -0,0 +1,885 @@
|
|||||||
|
# Extension User Guide
|
||||||
|
|
||||||
|
Complete guide for using Spec Kit extensions to enhance your workflow.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Introduction](#introduction)
|
||||||
|
2. [Getting Started](#getting-started)
|
||||||
|
3. [Finding Extensions](#finding-extensions)
|
||||||
|
4. [Installing Extensions](#installing-extensions)
|
||||||
|
5. [Using Extensions](#using-extensions)
|
||||||
|
6. [Managing Extensions](#managing-extensions)
|
||||||
|
7. [Configuration](#configuration)
|
||||||
|
8. [Troubleshooting](#troubleshooting)
|
||||||
|
9. [Best Practices](#best-practices)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
### What are Extensions?
|
||||||
|
|
||||||
|
Extensions are modular packages that add new commands and functionality to Spec Kit without bloating the core framework. They allow you to:
|
||||||
|
|
||||||
|
- **Integrate** with external tools (Jira, Linear, GitHub, etc.)
|
||||||
|
- **Automate** repetitive tasks with hooks
|
||||||
|
- **Customize** workflows for your team
|
||||||
|
- **Share** solutions across projects
|
||||||
|
|
||||||
|
### Why Use Extensions?
|
||||||
|
|
||||||
|
- **Clean Core**: Keeps spec-kit lightweight and focused
|
||||||
|
- **Optional Features**: Only install what you need
|
||||||
|
- **Community Driven**: Anyone can create and share extensions
|
||||||
|
- **Version Controlled**: Extensions are versioned independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Spec Kit version 0.1.0 or higher
|
||||||
|
- A spec-kit project (directory with `.specify/` folder)
|
||||||
|
|
||||||
|
### Check Your Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify --version
|
||||||
|
# Should show 0.1.0 or higher
|
||||||
|
```
|
||||||
|
|
||||||
|
### First Extension
|
||||||
|
|
||||||
|
Let's install the Jira extension as an example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Search for the extension
|
||||||
|
specify extension search jira
|
||||||
|
|
||||||
|
# 2. Get detailed information
|
||||||
|
specify extension info jira
|
||||||
|
|
||||||
|
# 3. Install it
|
||||||
|
specify extension add jira
|
||||||
|
|
||||||
|
# 4. Configure it
|
||||||
|
vim .specify/extensions/jira/jira-config.yml
|
||||||
|
|
||||||
|
# 5. Use it
|
||||||
|
# (Commands are now available in Claude Code)
|
||||||
|
/speckit.jira.specstoissues
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding Extensions
|
||||||
|
|
||||||
|
### Browse All Extensions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension search
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows all available extensions in the catalog.
|
||||||
|
|
||||||
|
### Search by Keyword
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search for "jira"
|
||||||
|
specify extension search jira
|
||||||
|
|
||||||
|
# Search for "issue tracking"
|
||||||
|
specify extension search issue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by Tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find all issue-tracking extensions
|
||||||
|
specify extension search --tag issue-tracking
|
||||||
|
|
||||||
|
# Find all Atlassian tools
|
||||||
|
specify extension search --tag atlassian
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filter by Author
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extensions by Stats Perform
|
||||||
|
specify extension search --author "Stats Perform"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Show Verified Only
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Only show verified extensions
|
||||||
|
specify extension search --verified
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Extension Details
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Detailed information
|
||||||
|
specify extension info jira
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows:
|
||||||
|
|
||||||
|
- Description
|
||||||
|
- Requirements
|
||||||
|
- Commands provided
|
||||||
|
- Hooks available
|
||||||
|
- Links (documentation, repository, changelog)
|
||||||
|
- Installation status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installing Extensions
|
||||||
|
|
||||||
|
### Install from Catalog
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# By name (from catalog)
|
||||||
|
specify extension add jira
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
1. Download the extension from GitHub
|
||||||
|
2. Validate the manifest
|
||||||
|
3. Check compatibility with your spec-kit version
|
||||||
|
4. Install to `.specify/extensions/jira/`
|
||||||
|
5. Register commands with your AI agent
|
||||||
|
6. Create config template
|
||||||
|
|
||||||
|
### Install from URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From GitHub release
|
||||||
|
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install from Local Directory (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For testing or development
|
||||||
|
specify extension add --dev /path/to/extension
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation Output
|
||||||
|
|
||||||
|
```text
|
||||||
|
✓ Extension installed successfully!
|
||||||
|
|
||||||
|
Jira Integration (v1.0.0)
|
||||||
|
Create Jira Epics, Stories, and Issues from spec-kit artifacts
|
||||||
|
|
||||||
|
Provided commands:
|
||||||
|
• speckit.jira.specstoissues - Create Jira hierarchy from spec and tasks
|
||||||
|
• speckit.jira.discover-fields - Discover Jira custom fields for configuration
|
||||||
|
• speckit.jira.sync-status - Sync task completion status to Jira
|
||||||
|
|
||||||
|
⚠ Configuration may be required
|
||||||
|
Check: .specify/extensions/jira/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using Extensions
|
||||||
|
|
||||||
|
### Using Extension Commands
|
||||||
|
|
||||||
|
Extensions add commands that appear in your AI agent (Claude Code):
|
||||||
|
|
||||||
|
```text
|
||||||
|
# In Claude Code
|
||||||
|
> /speckit.jira.specstoissues
|
||||||
|
|
||||||
|
# Or use short alias (if provided)
|
||||||
|
> /speckit.specstoissues
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extension Configuration
|
||||||
|
|
||||||
|
Most extensions require configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Find the config file
|
||||||
|
ls .specify/extensions/jira/
|
||||||
|
|
||||||
|
# 2. Copy template to config
|
||||||
|
cp .specify/extensions/jira/jira-config.template.yml \
|
||||||
|
.specify/extensions/jira/jira-config.yml
|
||||||
|
|
||||||
|
# 3. Edit configuration
|
||||||
|
vim .specify/extensions/jira/jira-config.yml
|
||||||
|
|
||||||
|
# 4. Use the extension
|
||||||
|
# (Commands will now work with your config)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extension Hooks
|
||||||
|
|
||||||
|
Some extensions provide hooks that execute after core commands:
|
||||||
|
|
||||||
|
**Example**: Jira extension hooks into `/speckit.tasks`
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Run core command
|
||||||
|
> /speckit.tasks
|
||||||
|
|
||||||
|
# Output includes:
|
||||||
|
## Extension Hooks
|
||||||
|
|
||||||
|
**Optional Hook**: jira
|
||||||
|
Command: `/speckit.jira.specstoissues`
|
||||||
|
Description: Automatically create Jira hierarchy after task generation
|
||||||
|
|
||||||
|
Prompt: Create Jira issues from tasks?
|
||||||
|
To execute: `/speckit.jira.specstoissues`
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then choose to run the hook or skip it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Managing Extensions
|
||||||
|
|
||||||
|
### List Installed Extensions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension list
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Installed Extensions:
|
||||||
|
|
||||||
|
✓ Jira Integration (v1.0.0)
|
||||||
|
Create Jira Epics, Stories, and Issues from spec-kit artifacts
|
||||||
|
Commands: 3 | Hooks: 1 | Status: Enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Extensions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for updates (all extensions)
|
||||||
|
specify extension update
|
||||||
|
|
||||||
|
# Update specific extension
|
||||||
|
specify extension update jira
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🔄 Checking for updates...
|
||||||
|
|
||||||
|
Updates available:
|
||||||
|
|
||||||
|
• jira: 1.0.0 → 1.1.0
|
||||||
|
|
||||||
|
Update these extensions? [y/N]:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable Extension Temporarily
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Disable without removing
|
||||||
|
specify extension disable jira
|
||||||
|
|
||||||
|
✓ Extension 'jira' disabled
|
||||||
|
|
||||||
|
Commands will no longer be available. Hooks will not execute.
|
||||||
|
To re-enable: specify extension enable jira
|
||||||
|
```
|
||||||
|
|
||||||
|
### Re-enable Extension
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension enable jira
|
||||||
|
|
||||||
|
✓ Extension 'jira' enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Extension
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove extension (with confirmation)
|
||||||
|
specify extension remove jira
|
||||||
|
|
||||||
|
# Keep configuration when removing
|
||||||
|
specify extension remove jira --keep-config
|
||||||
|
|
||||||
|
# Force removal (no confirmation)
|
||||||
|
specify extension remove jira --force
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
Extensions can have multiple configuration files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.specify/extensions/jira/
|
||||||
|
├── jira-config.yml # Main config (version controlled)
|
||||||
|
├── jira-config.local.yml # Local overrides (gitignored)
|
||||||
|
└── jira-config.template.yml # Template (reference)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Layers
|
||||||
|
|
||||||
|
Configuration is merged in this order (highest priority last):
|
||||||
|
|
||||||
|
1. **Extension defaults** (from `extension.yml`)
|
||||||
|
2. **Project config** (`jira-config.yml`)
|
||||||
|
3. **Local overrides** (`jira-config.local.yml`)
|
||||||
|
4. **Environment variables** (`SPECKIT_JIRA_*`)
|
||||||
|
|
||||||
|
### Example: Jira Configuration
|
||||||
|
|
||||||
|
**Project config** (`.specify/extensions/jira/jira-config.yml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
project:
|
||||||
|
key: "MSATS"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
epic:
|
||||||
|
labels: ["spec-driven"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Local override** (`.specify/extensions/jira/jira-config.local.yml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
project:
|
||||||
|
key: "MYTEST" # Override for local development
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variable**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SPECKIT_JIRA_PROJECT_KEY="DEVTEST"
|
||||||
|
```
|
||||||
|
|
||||||
|
Final resolved config uses `DEVTEST` from environment variable.
|
||||||
|
|
||||||
|
### Project-Wide Extension Settings
|
||||||
|
|
||||||
|
File: `.specify/extensions.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Extensions installed in this project
|
||||||
|
installed:
|
||||||
|
- jira
|
||||||
|
- linear
|
||||||
|
|
||||||
|
# Global settings
|
||||||
|
settings:
|
||||||
|
auto_execute_hooks: true
|
||||||
|
|
||||||
|
# Hook configuration
|
||||||
|
hooks:
|
||||||
|
after_tasks:
|
||||||
|
- extension: jira
|
||||||
|
command: speckit.jira.specstoissues
|
||||||
|
enabled: true
|
||||||
|
optional: true
|
||||||
|
prompt: "Create Jira issues from tasks?"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Environment Variables
|
||||||
|
|
||||||
|
In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`), spec-kit supports core environment variables:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog |
|
||||||
|
| `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
|
||||||
|
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
||||||
|
|
||||||
|
# Or use a staging catalog
|
||||||
|
export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Organization Catalog Customization
|
||||||
|
|
||||||
|
### Why the Default Catalog is Empty
|
||||||
|
|
||||||
|
The default spec-kit catalog ships empty by design. This allows organizations to:
|
||||||
|
|
||||||
|
- **Control available extensions** - Curate which extensions your team can install
|
||||||
|
- **Host private extensions** - Internal tools that shouldn't be public
|
||||||
|
- **Customize for compliance** - Meet security/audit requirements
|
||||||
|
- **Support air-gapped environments** - Work without internet access
|
||||||
|
|
||||||
|
### Setting Up a Custom Catalog
|
||||||
|
|
||||||
|
#### 1. Create Your Catalog File
|
||||||
|
|
||||||
|
Create a `catalog.json` file with your extensions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"updated_at": "2026-02-03T00:00:00Z",
|
||||||
|
"catalog_url": "https://your-org.com/spec-kit/catalog.json",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"name": "Jira Integration",
|
||||||
|
"id": "jira",
|
||||||
|
"description": "Create Jira issues from spec-kit artifacts",
|
||||||
|
"author": "Your Organization",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"download_url": "https://github.com/your-org/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
|
||||||
|
"repository": "https://github.com/your-org/spec-kit-jira",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0",
|
||||||
|
"tools": [
|
||||||
|
{"name": "atlassian-mcp-server", "required": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 3,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": ["jira", "atlassian", "issue-tracking"],
|
||||||
|
"verified": true
|
||||||
|
},
|
||||||
|
"internal-tool": {
|
||||||
|
"name": "Internal Tool Integration",
|
||||||
|
"id": "internal-tool",
|
||||||
|
"description": "Connect to internal company systems",
|
||||||
|
"author": "Your Organization",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://internal.your-org.com/extensions/internal-tool-1.0.0.zip",
|
||||||
|
"repository": "https://github.internal.your-org.com/spec-kit-internal",
|
||||||
|
"license": "Proprietary",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2
|
||||||
|
},
|
||||||
|
"tags": ["internal", "proprietary"],
|
||||||
|
"verified": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Host the Catalog
|
||||||
|
|
||||||
|
Options for hosting your catalog:
|
||||||
|
|
||||||
|
| Method | URL Example | Use Case |
|
||||||
|
| ------ | ----------- | -------- |
|
||||||
|
| GitHub Pages | `https://your-org.github.io/spec-kit-catalog/catalog.json` | Public or org-visible |
|
||||||
|
| Internal web server | `https://internal.company.com/spec-kit/catalog.json` | Corporate network |
|
||||||
|
| S3/Cloud storage | `https://s3.amazonaws.com/your-bucket/catalog.json` | Cloud-hosted teams |
|
||||||
|
| Local file server | `http://localhost:8000/catalog.json` | Development/testing |
|
||||||
|
|
||||||
|
**Security requirement**: URLs must use HTTPS (except `localhost` for testing).
|
||||||
|
|
||||||
|
#### 3. Configure Your Environment
|
||||||
|
|
||||||
|
##### Option A: Environment variable (recommended for CI/CD)
|
||||||
|
|
||||||
|
```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
|
||||||
|
# Search should now show your catalog's extensions
|
||||||
|
specify extension search
|
||||||
|
|
||||||
|
# Install from your catalog
|
||||||
|
specify extension add jira
|
||||||
|
```
|
||||||
|
|
||||||
|
### Catalog JSON Schema
|
||||||
|
|
||||||
|
Required fields for each extension entry:
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
| ----- | ---- | -------- | ----------- |
|
||||||
|
| `name` | string | Yes | Human-readable name |
|
||||||
|
| `id` | string | Yes | Unique identifier (lowercase, hyphens) |
|
||||||
|
| `version` | string | Yes | Semantic version (X.Y.Z) |
|
||||||
|
| `download_url` | string | Yes | URL to ZIP archive |
|
||||||
|
| `repository` | string | Yes | Source code URL |
|
||||||
|
| `description` | string | No | Brief description |
|
||||||
|
| `author` | string | No | Author/organization |
|
||||||
|
| `license` | string | No | SPDX license identifier |
|
||||||
|
| `requires.speckit_version` | string | No | Version constraint |
|
||||||
|
| `requires.tools` | array | No | Required external tools |
|
||||||
|
| `provides.commands` | number | No | Number of commands |
|
||||||
|
| `provides.hooks` | number | No | Number of hooks |
|
||||||
|
| `tags` | array | No | Search tags |
|
||||||
|
| `verified` | boolean | No | Verification status |
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
#### Private/Internal Extensions
|
||||||
|
|
||||||
|
Host proprietary extensions that integrate with internal systems:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"internal-auth": {
|
||||||
|
"name": "Internal SSO Integration",
|
||||||
|
"download_url": "https://artifactory.company.com/spec-kit/internal-auth-1.0.0.zip",
|
||||||
|
"verified": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Curated Team Catalog
|
||||||
|
|
||||||
|
Limit which extensions your team can install:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"extensions": {
|
||||||
|
"jira": { "..." },
|
||||||
|
"github": { "..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Only `jira` and `github` will appear in `specify extension search`.
|
||||||
|
|
||||||
|
#### Air-Gapped Environments
|
||||||
|
|
||||||
|
For networks without internet access:
|
||||||
|
|
||||||
|
1. Download extension ZIPs to internal file server
|
||||||
|
2. Create catalog pointing to internal URLs
|
||||||
|
3. Host catalog on internal web server
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jira": {
|
||||||
|
"download_url": "https://files.internal/spec-kit/jira-2.1.0.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Development/Testing
|
||||||
|
|
||||||
|
Test new extensions before publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start local server
|
||||||
|
python -m http.server 8000 --directory ./my-catalog/
|
||||||
|
|
||||||
|
# Point spec-kit to local catalog
|
||||||
|
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
||||||
|
|
||||||
|
# Test installation
|
||||||
|
specify extension add my-new-extension
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combining with Direct Installation
|
||||||
|
|
||||||
|
You can still install extensions not in your catalog using `--from`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From catalog
|
||||||
|
specify extension add jira
|
||||||
|
|
||||||
|
# Direct URL (bypasses catalog)
|
||||||
|
specify extension add --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
|
||||||
|
|
||||||
|
# Local development
|
||||||
|
specify extension add --dev /path/to/extension
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Direct URL installation shows a security warning since the extension isn't from your configured catalog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Extension Not Found
|
||||||
|
|
||||||
|
**Error**: `Extension 'jira' not found in catalog
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check spelling: `specify extension search jira`
|
||||||
|
2. Refresh catalog: `specify extension search --help`
|
||||||
|
3. Check internet connection
|
||||||
|
4. Extension may not be published yet
|
||||||
|
|
||||||
|
### Configuration Not Found
|
||||||
|
|
||||||
|
**Error**: `Jira configuration not found`
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check if extension is installed: `specify extension list`
|
||||||
|
2. Create config from template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .specify/extensions/jira/jira-config.template.yml \
|
||||||
|
.specify/extensions/jira/jira-config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Reinstall extension: `specify extension remove jira && specify extension add jira`
|
||||||
|
|
||||||
|
### Command Not Available
|
||||||
|
|
||||||
|
**Issue**: Extension command not appearing in AI agent
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check extension is enabled: `specify extension list`
|
||||||
|
2. Restart AI agent (Claude Code)
|
||||||
|
3. Check command file exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls .claude/commands/speckit.jira.*.md
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Reinstall extension
|
||||||
|
|
||||||
|
### Incompatible Version
|
||||||
|
|
||||||
|
**Error**: `Extension requires spec-kit >=0.2.0, but you have 0.1.0`
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Upgrade spec-kit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv tool upgrade specify-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install older version of extension:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
specify extension add --from https://github.com/org/ext/archive/v1.0.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Tool Not Available
|
||||||
|
|
||||||
|
**Error**: `Tool 'jira-mcp-server/epic_create' not found`
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check MCP server is installed
|
||||||
|
2. Check AI agent MCP configuration
|
||||||
|
3. Restart AI agent
|
||||||
|
4. Check extension requirements: `specify extension info jira`
|
||||||
|
|
||||||
|
### Permission Denied
|
||||||
|
|
||||||
|
**Error**: `Permission denied` when accessing Jira
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check Jira credentials in MCP server config
|
||||||
|
2. Verify project permissions in Jira
|
||||||
|
3. Test MCP server connection independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Version Control
|
||||||
|
|
||||||
|
**Do commit**:
|
||||||
|
|
||||||
|
- `.specify/extensions.yml` (project extension config)
|
||||||
|
- `.specify/extensions/*/jira-config.yml` (project config)
|
||||||
|
|
||||||
|
**Don't commit**:
|
||||||
|
|
||||||
|
- `.specify/extensions/.cache/` (catalog cache)
|
||||||
|
- `.specify/extensions/.backup/` (config backups)
|
||||||
|
- `.specify/extensions/*/*.local.yml` (local overrides)
|
||||||
|
- `.specify/extensions/.registry` (installation state)
|
||||||
|
|
||||||
|
Add to `.gitignore`:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
.specify/extensions/.cache/
|
||||||
|
.specify/extensions/.backup/
|
||||||
|
.specify/extensions/*/*.local.yml
|
||||||
|
.specify/extensions/.registry
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Team Workflows
|
||||||
|
|
||||||
|
**For teams**:
|
||||||
|
|
||||||
|
1. Agree on which extensions to use
|
||||||
|
2. Commit extension configuration
|
||||||
|
3. Document extension usage in README
|
||||||
|
4. Keep extensions updated together
|
||||||
|
|
||||||
|
**Example README section**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Extensions
|
||||||
|
|
||||||
|
This project uses:
|
||||||
|
- **jira** (v1.0.0) - Jira integration
|
||||||
|
- Config: `.specify/extensions/jira/jira-config.yml`
|
||||||
|
- Requires: jira-mcp-server
|
||||||
|
|
||||||
|
To install: `specify extension add jira`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Local Development
|
||||||
|
|
||||||
|
Use local config for development:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .specify/extensions/jira/jira-config.local.yml
|
||||||
|
project:
|
||||||
|
key: "DEVTEST" # Your test project
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
task:
|
||||||
|
custom_fields:
|
||||||
|
customfield_10002: 1 # Lower story points for testing
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Environment-Specific Config
|
||||||
|
|
||||||
|
Use environment variables for CI/CD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .github/workflows/deploy.yml
|
||||||
|
env:
|
||||||
|
SPECKIT_JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT }}
|
||||||
|
|
||||||
|
- name: Create Jira Issues
|
||||||
|
run: specify extension add jira && ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Extension Updates
|
||||||
|
|
||||||
|
**Check for updates regularly**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Weekly or before major releases
|
||||||
|
specify extension update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pin versions for stability**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .specify/extensions.yml
|
||||||
|
installed:
|
||||||
|
- id: jira
|
||||||
|
version: "1.0.0" # Pin to specific version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Minimal Extensions
|
||||||
|
|
||||||
|
Only install extensions you actively use:
|
||||||
|
|
||||||
|
- Reduces complexity
|
||||||
|
- Faster command loading
|
||||||
|
- Less configuration
|
||||||
|
|
||||||
|
### 7. Documentation
|
||||||
|
|
||||||
|
Document extension usage in your project:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# PROJECT.md
|
||||||
|
|
||||||
|
## Working with Jira
|
||||||
|
|
||||||
|
After creating tasks, sync to Jira:
|
||||||
|
1. Run `/speckit.tasks` to generate tasks
|
||||||
|
2. Run `/speckit.jira.specstoissues` to create Jira issues
|
||||||
|
3. Run `/speckit.jira.sync-status` to update status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Q: Can I use multiple extensions at once?
|
||||||
|
|
||||||
|
**A**: Yes! Extensions are designed to work together. Install as many as you need.
|
||||||
|
|
||||||
|
### Q: Do extensions slow down spec-kit?
|
||||||
|
|
||||||
|
**A**: No. Extensions are loaded on-demand and only when their commands are used.
|
||||||
|
|
||||||
|
### Q: Can I create private extensions?
|
||||||
|
|
||||||
|
**A**: Yes. Install with `--dev` or `--from` and keep private. Public catalog submission is optional.
|
||||||
|
|
||||||
|
### Q: How do I know if an extension is safe?
|
||||||
|
|
||||||
|
**A**: Look for the ✓ Verified badge. Verified extensions are reviewed by maintainers. Always review extension code before installing.
|
||||||
|
|
||||||
|
### Q: Can extensions modify spec-kit core?
|
||||||
|
|
||||||
|
**A**: No. Extensions can only add commands and hooks. They cannot modify core functionality.
|
||||||
|
|
||||||
|
### Q: What happens if two extensions have the same command name?
|
||||||
|
|
||||||
|
**A**: Extensions use namespaced commands (`speckit.{extension}.{command}`), so conflicts are very rare. The extension system will warn you if conflicts occur.
|
||||||
|
|
||||||
|
### Q: Can I contribute to existing extensions?
|
||||||
|
|
||||||
|
**A**: Yes! Most extensions are open source. Check the repository link in `specify extension info {extension}`.
|
||||||
|
|
||||||
|
### Q: How do I report extension bugs?
|
||||||
|
|
||||||
|
**A**: Go to the extension's repository (shown in `specify extension info`) and create an issue.
|
||||||
|
|
||||||
|
### Q: Can extensions work offline?
|
||||||
|
|
||||||
|
**A**: Once installed, extensions work offline. However, some extensions may require internet for their functionality (e.g., Jira requires Jira API access).
|
||||||
|
|
||||||
|
### Q: How do I backup my extension configuration?
|
||||||
|
|
||||||
|
**A**: Extension configs are in `.specify/extensions/{extension}/`. Back up this directory or commit configs to git.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Extension Issues**: Report to extension repository (see `specify extension info`)
|
||||||
|
- **Spec Kit Issues**: <https://github.com/statsperform/spec-kit/issues>
|
||||||
|
- **Extension Catalog**: <https://github.com/statsperform/spec-kit/tree/main/extensions>
|
||||||
|
- **Documentation**: See EXTENSION-DEVELOPMENT-GUIDE.md and EXTENSION-PUBLISHING-GUIDE.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: 2026-01-28*
|
||||||
|
*Spec Kit Version: 0.1.0*
|
||||||
13
extensions/README.md
Normal file
13
extensions/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Spec Kit Community Extensions
|
||||||
|
|
||||||
|
Community-contributed extensions for [Spec Kit](https://github.com/github/spec-kit).
|
||||||
|
|
||||||
|
## Available Extensions
|
||||||
|
|
||||||
|
| Extension | Purpose | URL |
|
||||||
|
|-----------|---------|-----|
|
||||||
|
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||||
|
|
||||||
|
## Adding Your Extension
|
||||||
|
|
||||||
|
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for instructions on how to submit your extension to the community catalog.
|
||||||
1791
extensions/RFC-EXTENSION-SYSTEM.md
Normal file
1791
extensions/RFC-EXTENSION-SYSTEM.md
Normal file
File diff suppressed because it is too large
Load Diff
33
extensions/catalog.community.json
Normal file
33
extensions/catalog.community.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"updated_at": "2026-02-20T00:00:00Z",
|
||||||
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||||
|
"extensions": {
|
||||||
|
"v-model": {
|
||||||
|
"name": "V-Model Extension Pack",
|
||||||
|
"id": "v-model",
|
||||||
|
"description": "Enforces V-Model paired generation of development specs and test specs with full traceability.",
|
||||||
|
"author": "leocamello",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"download_url": "https://github.com/leocamello/spec-kit-v-model/archive/refs/tags/v0.2.0.zip",
|
||||||
|
"repository": "https://github.com/leocamello/spec-kit-v-model",
|
||||||
|
"homepage": "https://github.com/leocamello/spec-kit-v-model",
|
||||||
|
"documentation": "https://github.com/leocamello/spec-kit-v-model/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/leocamello/spec-kit-v-model/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 5,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": ["v-model", "traceability", "testing", "compliance", "safety-critical"],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-02-20T00:00:00Z",
|
||||||
|
"updated_at": "2026-02-20T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
extensions/catalog.json
Normal file
6
extensions/catalog.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"updated_at": "2026-02-03T00:00:00Z",
|
||||||
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
||||||
|
"extensions": {}
|
||||||
|
}
|
||||||
39
extensions/template/.gitignore
vendored
Normal file
39
extensions/template/.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Local configuration overrides
|
||||||
|
*-config.local.yml
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
.cache/
|
||||||
39
extensions/template/CHANGELOG.md
Normal file
39
extensions/template/CHANGELOG.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this extension will be documented in this file.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Planned
|
||||||
|
|
||||||
|
- Feature ideas for future versions
|
||||||
|
- Enhancements
|
||||||
|
- Bug fixes
|
||||||
|
|
||||||
|
## [1.0.0] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial release of extension
|
||||||
|
- Command: `/speckit.my-extension.example` - Example command functionality
|
||||||
|
- Configuration system with template
|
||||||
|
- Documentation and examples
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Feature 1 description
|
||||||
|
- Feature 2 description
|
||||||
|
- Feature 3 description
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Spec Kit: >=0.1.0
|
||||||
|
- External dependencies (if any)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/your-org/spec-kit-my-extension/compare/v1.0.0...HEAD
|
||||||
|
[1.0.0]: https://github.com/your-org/spec-kit-my-extension/releases/tag/v1.0.0
|
||||||
158
extensions/template/EXAMPLE-README.md
Normal file
158
extensions/template/EXAMPLE-README.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# EXAMPLE: Extension README
|
||||||
|
|
||||||
|
This is an example of what your extension README should look like after customization.
|
||||||
|
**Delete this file and replace README.md with content similar to this.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# My Extension
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Replace with your extension description -->
|
||||||
|
|
||||||
|
Brief description of what your extension does and why it's useful.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: List key features -->
|
||||||
|
|
||||||
|
- Feature 1: Description
|
||||||
|
- Feature 2: Description
|
||||||
|
- Feature 3: Description
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install from catalog
|
||||||
|
specify extension add my-extension
|
||||||
|
|
||||||
|
# Or install from local development directory
|
||||||
|
specify extension add --dev /path/to/my-extension
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Create configuration file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .specify/extensions/my-extension/config-template.yml \
|
||||||
|
.specify/extensions/my-extension/my-extension-config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vim .specify/extensions/my-extension/my-extension-config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set required values:
|
||||||
|
<!-- CUSTOMIZE: List required configuration -->
|
||||||
|
```yaml
|
||||||
|
connection:
|
||||||
|
url: "https://api.example.com"
|
||||||
|
api_key: "your-api-key"
|
||||||
|
|
||||||
|
project:
|
||||||
|
id: "your-project-id"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add usage examples -->
|
||||||
|
|
||||||
|
### Command: example
|
||||||
|
|
||||||
|
Description of what this command does.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In Claude Code
|
||||||
|
> /speckit.my-extension.example
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
|
||||||
|
- Prerequisite 1
|
||||||
|
- Prerequisite 2
|
||||||
|
|
||||||
|
**Output**:
|
||||||
|
|
||||||
|
- What this command produces
|
||||||
|
- Where results are saved
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Document all configuration options -->
|
||||||
|
|
||||||
|
### Connection Settings
|
||||||
|
|
||||||
|
| Setting | Type | Required | Description |
|
||||||
|
|---------|------|----------|-------------|
|
||||||
|
| `connection.url` | string | Yes | API endpoint URL |
|
||||||
|
| `connection.api_key` | string | Yes | API authentication key |
|
||||||
|
|
||||||
|
### Project Settings
|
||||||
|
|
||||||
|
| Setting | Type | Required | Description |
|
||||||
|
|---------|------|----------|-------------|
|
||||||
|
| `project.id` | string | Yes | Project identifier |
|
||||||
|
| `project.workspace` | string | No | Workspace or organization |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Override configuration with environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Override connection settings
|
||||||
|
export SPECKIT_MY_EXTENSION_CONNECTION_URL="https://custom-api.com"
|
||||||
|
export SPECKIT_MY_EXTENSION_CONNECTION_API_KEY="custom-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add real-world examples -->
|
||||||
|
|
||||||
|
### Example 1: Basic Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Create specification
|
||||||
|
> /speckit.spec
|
||||||
|
|
||||||
|
# Step 2: Generate tasks
|
||||||
|
> /speckit.tasks
|
||||||
|
|
||||||
|
# Step 3: Use extension
|
||||||
|
> /speckit.my-extension.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add common issues -->
|
||||||
|
|
||||||
|
### Issue: Configuration not found
|
||||||
|
|
||||||
|
**Solution**: Create config from template (see Configuration section)
|
||||||
|
|
||||||
|
### Issue: Command not available
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check extension is installed: `specify extension list`
|
||||||
|
2. Restart AI agent
|
||||||
|
3. Reinstall extension
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: <https://github.com/your-org/spec-kit-my-extension/issues>
|
||||||
|
- **Spec Kit Docs**: <https://github.com/statsperform/spec-kit>
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Extension Version: 1.0.0*
|
||||||
|
*Spec Kit: >=0.1.0*
|
||||||
21
extensions/template/LICENSE
Normal file
21
extensions/template/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 [Your Name or Organization]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
79
extensions/template/README.md
Normal file
79
extensions/template/README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Extension Template
|
||||||
|
|
||||||
|
Starter template for creating a Spec Kit extension.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Copy this template**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r extensions/template my-extension
|
||||||
|
cd my-extension
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Customize `extension.yml`**:
|
||||||
|
- Change extension ID, name, description
|
||||||
|
- Update author and repository
|
||||||
|
- Define your commands
|
||||||
|
|
||||||
|
3. **Create commands**:
|
||||||
|
- Add command files in `commands/` directory
|
||||||
|
- Use Markdown format with YAML frontmatter
|
||||||
|
|
||||||
|
4. **Create config template**:
|
||||||
|
- Define configuration options
|
||||||
|
- Document all settings
|
||||||
|
|
||||||
|
5. **Write documentation**:
|
||||||
|
- Update README.md with usage instructions
|
||||||
|
- Add examples
|
||||||
|
|
||||||
|
6. **Test locally**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/spec-kit-project
|
||||||
|
specify extension add --dev /path/to/my-extension
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Publish** (optional):
|
||||||
|
- Create GitHub repository
|
||||||
|
- Create release
|
||||||
|
- Submit to catalog (see EXTENSION-PUBLISHING-GUIDE.md)
|
||||||
|
|
||||||
|
## Files in This Template
|
||||||
|
|
||||||
|
- `extension.yml` - Extension manifest (CUSTOMIZE THIS)
|
||||||
|
- `config-template.yml` - Configuration template (CUSTOMIZE THIS)
|
||||||
|
- `commands/example.md` - Example command (REPLACE THIS)
|
||||||
|
- `README.md` - Extension documentation (REPLACE THIS)
|
||||||
|
- `LICENSE` - MIT License (REVIEW THIS)
|
||||||
|
- `CHANGELOG.md` - Version history (UPDATE THIS)
|
||||||
|
- `.gitignore` - Git ignore rules
|
||||||
|
|
||||||
|
## Customization Checklist
|
||||||
|
|
||||||
|
- [ ] Update `extension.yml` with your extension details
|
||||||
|
- [ ] Change extension ID to your extension name
|
||||||
|
- [ ] Update author information
|
||||||
|
- [ ] Define your commands
|
||||||
|
- [ ] Create command files in `commands/`
|
||||||
|
- [ ] Update config template
|
||||||
|
- [ ] Write README with usage instructions
|
||||||
|
- [ ] Add examples
|
||||||
|
- [ ] Update LICENSE if needed
|
||||||
|
- [ ] Test extension locally
|
||||||
|
- [ ] Create git repository
|
||||||
|
- [ ] Create first release
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
|
||||||
|
- **API Reference**: See EXTENSION-API-REFERENCE.md
|
||||||
|
- **Publishing Guide**: See EXTENSION-PUBLISHING-GUIDE.md
|
||||||
|
- **User Guide**: See EXTENSION-USER-GUIDE.md
|
||||||
|
|
||||||
|
## Template Version
|
||||||
|
|
||||||
|
- Version: 1.0.0
|
||||||
|
- Last Updated: 2026-01-28
|
||||||
|
- Compatible with Spec Kit: >=0.1.0
|
||||||
210
extensions/template/commands/example.md
Normal file
210
extensions/template/commands/example.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
description: "Example command that demonstrates extension functionality"
|
||||||
|
# CUSTOMIZE: List MCP tools this command uses
|
||||||
|
tools:
|
||||||
|
- 'example-mcp-server/example_tool'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Example Command
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Replace this entire file with your command documentation -->
|
||||||
|
|
||||||
|
This is an example command that demonstrates how to create commands for Spec Kit extensions.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Describe what this command does and when to use it.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
List requirements before using this command:
|
||||||
|
|
||||||
|
1. Prerequisite 1 (e.g., "MCP server configured")
|
||||||
|
2. Prerequisite 2 (e.g., "Configuration file exists")
|
||||||
|
3. Prerequisite 3 (e.g., "Valid API credentials")
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Load Configuration
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Replace with your actual steps -->
|
||||||
|
|
||||||
|
Load extension configuration from the project:
|
||||||
|
|
||||||
|
``bash
|
||||||
|
config_file=".specify/extensions/my-extension/my-extension-config.yml"
|
||||||
|
|
||||||
|
if [ ! -f "$config_file" ]; then
|
||||||
|
echo "❌ Error: Configuration not found at $config_file"
|
||||||
|
echo "Run 'specify extension add my-extension' to install and configure"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read configuration values
|
||||||
|
|
||||||
|
setting_value=$(yq eval '.settings.key' "$config_file")
|
||||||
|
|
||||||
|
# Apply environment variable overrides
|
||||||
|
|
||||||
|
setting_value="${SPECKIT_MY_EXTENSION_KEY:-$setting_value}"
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
|
||||||
|
if [ -z "$setting_value" ]; then
|
||||||
|
echo "❌ Error: Configuration value not set"
|
||||||
|
echo "Edit $config_file and set 'settings.key'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 Configuration loaded: $setting_value"
|
||||||
|
``
|
||||||
|
|
||||||
|
### Step 2: Perform Main Action
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Replace with your command logic -->
|
||||||
|
|
||||||
|
Describe what this step does:
|
||||||
|
|
||||||
|
``markdown
|
||||||
|
Use MCP tools to perform the main action:
|
||||||
|
|
||||||
|
- Tool: example-mcp-server example_tool
|
||||||
|
- Parameters: { "key": "$setting_value" }
|
||||||
|
|
||||||
|
This calls the MCP server tool to execute the operation.
|
||||||
|
``
|
||||||
|
|
||||||
|
### Step 3: Process Results
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add more steps as needed -->
|
||||||
|
|
||||||
|
Process the results and provide output:
|
||||||
|
|
||||||
|
`` bash
|
||||||
|
echo ""
|
||||||
|
echo "✅ Command completed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Results:"
|
||||||
|
echo " • Item 1: Value"
|
||||||
|
echo " • Item 2: Value"
|
||||||
|
echo ""
|
||||||
|
``
|
||||||
|
|
||||||
|
### Step 4: Save Output (Optional)
|
||||||
|
|
||||||
|
Save results to a file if needed:
|
||||||
|
|
||||||
|
``bash
|
||||||
|
output_file=".specify/my-extension-output.json"
|
||||||
|
|
||||||
|
cat > "$output_file" <<EOF
|
||||||
|
{
|
||||||
|
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
||||||
|
"setting": "$setting_value",
|
||||||
|
"results": []
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "💾 Output saved to $output_file"
|
||||||
|
``
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Document configuration options -->
|
||||||
|
|
||||||
|
This command uses the following configuration from `my-extension-config.yml`:
|
||||||
|
|
||||||
|
- **settings.key**: Description of what this setting does
|
||||||
|
- Type: string
|
||||||
|
- Required: Yes
|
||||||
|
- Example: `"example-value"`
|
||||||
|
|
||||||
|
- **settings.another_key**: Description of another setting
|
||||||
|
- Type: boolean
|
||||||
|
- Required: No
|
||||||
|
- Default: `false`
|
||||||
|
- Example: `true`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Document environment variable overrides -->
|
||||||
|
|
||||||
|
Configuration can be overridden with environment variables:
|
||||||
|
|
||||||
|
- `SPECKIT_MY_EXTENSION_KEY` - Overrides `settings.key`
|
||||||
|
- `SPECKIT_MY_EXTENSION_ANOTHER_KEY` - Overrides `settings.another_key`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
``bash
|
||||||
|
export SPECKIT_MY_EXTENSION_KEY="override-value"
|
||||||
|
``
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add common issues and solutions -->
|
||||||
|
|
||||||
|
### "Configuration not found"
|
||||||
|
|
||||||
|
**Solution**: Install the extension and create configuration:
|
||||||
|
``bash
|
||||||
|
specify extension add my-extension
|
||||||
|
cp .specify/extensions/my-extension/config-template.yml \
|
||||||
|
.specify/extensions/my-extension/my-extension-config.yml
|
||||||
|
``
|
||||||
|
|
||||||
|
### "MCP tool not available"
|
||||||
|
|
||||||
|
**Solution**: Ensure MCP server is configured in your AI agent settings.
|
||||||
|
|
||||||
|
### "Permission denied"
|
||||||
|
|
||||||
|
**Solution**: Check credentials and permissions in the external service.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add helpful notes and tips -->
|
||||||
|
|
||||||
|
- This command requires an active connection to the external service
|
||||||
|
- Results are cached for performance
|
||||||
|
- Re-run the command to refresh data
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<!-- CUSTOMIZE: Add usage examples -->
|
||||||
|
|
||||||
|
### Example 1: Basic Usage
|
||||||
|
|
||||||
|
``bash
|
||||||
|
|
||||||
|
# Run with default configuration
|
||||||
|
>
|
||||||
|
> /speckit.my-extension.example
|
||||||
|
``
|
||||||
|
|
||||||
|
### Example 2: With Environment Override
|
||||||
|
|
||||||
|
``bash
|
||||||
|
|
||||||
|
# Override configuration with environment variable
|
||||||
|
|
||||||
|
export SPECKIT_MY_EXTENSION_KEY="custom-value"
|
||||||
|
> /speckit.my-extension.example
|
||||||
|
``
|
||||||
|
|
||||||
|
### Example 3: After Core Command
|
||||||
|
|
||||||
|
``bash
|
||||||
|
|
||||||
|
# Use as part of a workflow
|
||||||
|
>
|
||||||
|
> /speckit.tasks
|
||||||
|
> /speckit.my-extension.example
|
||||||
|
``
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For more information, see the extension README or run `specify extension info my-extension`*
|
||||||
75
extensions/template/config-template.yml
Normal file
75
extensions/template/config-template.yml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Extension Configuration Template
|
||||||
|
# Copy this to my-extension-config.yml and customize for your project
|
||||||
|
|
||||||
|
# CUSTOMIZE: Add your configuration sections below
|
||||||
|
|
||||||
|
# Example: Connection settings
|
||||||
|
connection:
|
||||||
|
# URL to external service
|
||||||
|
url: "" # REQUIRED: e.g., "https://api.example.com"
|
||||||
|
|
||||||
|
# API key or token
|
||||||
|
api_key: "" # REQUIRED: Your API key
|
||||||
|
|
||||||
|
# Example: Project settings
|
||||||
|
project:
|
||||||
|
# Project identifier
|
||||||
|
id: "" # REQUIRED: e.g., "my-project"
|
||||||
|
|
||||||
|
# Workspace or organization
|
||||||
|
workspace: "" # OPTIONAL: e.g., "my-org"
|
||||||
|
|
||||||
|
# Example: Feature flags
|
||||||
|
features:
|
||||||
|
# Enable/disable main functionality
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Automatic synchronization
|
||||||
|
auto_sync: false
|
||||||
|
|
||||||
|
# Verbose logging
|
||||||
|
verbose: false
|
||||||
|
|
||||||
|
# Example: Default values
|
||||||
|
defaults:
|
||||||
|
# Labels to apply
|
||||||
|
labels: [] # e.g., ["automated", "spec-kit"]
|
||||||
|
|
||||||
|
# Priority level
|
||||||
|
priority: "medium" # Options: "low", "medium", "high"
|
||||||
|
|
||||||
|
# Assignee
|
||||||
|
assignee: "" # OPTIONAL: Default assignee
|
||||||
|
|
||||||
|
# Example: Field mappings
|
||||||
|
# Map internal names to external field IDs
|
||||||
|
field_mappings:
|
||||||
|
# Example mappings
|
||||||
|
# internal_field: "external_field_id"
|
||||||
|
# status: "customfield_10001"
|
||||||
|
|
||||||
|
# Example: Advanced settings
|
||||||
|
advanced:
|
||||||
|
# Timeout in seconds
|
||||||
|
timeout: 30
|
||||||
|
|
||||||
|
# Retry attempts
|
||||||
|
retry_count: 3
|
||||||
|
|
||||||
|
# Cache duration in seconds
|
||||||
|
cache_duration: 3600
|
||||||
|
|
||||||
|
# Environment Variable Overrides:
|
||||||
|
# You can override any setting with environment variables using this pattern:
|
||||||
|
# SPECKIT_MY_EXTENSION_{SECTION}_{KEY}
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# - SPECKIT_MY_EXTENSION_CONNECTION_API_KEY: Override connection.api_key
|
||||||
|
# - SPECKIT_MY_EXTENSION_PROJECT_ID: Override project.id
|
||||||
|
# - SPECKIT_MY_EXTENSION_FEATURES_ENABLED: Override features.enabled
|
||||||
|
#
|
||||||
|
# Note: Use uppercase and replace dots with underscores
|
||||||
|
|
||||||
|
# Local Overrides:
|
||||||
|
# For local development, create my-extension-config.local.yml (gitignored)
|
||||||
|
# to override settings without affecting the team configuration
|
||||||
97
extensions/template/extension.yml
Normal file
97
extensions/template/extension.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
schema_version: "1.0"
|
||||||
|
|
||||||
|
extension:
|
||||||
|
# CUSTOMIZE: Change 'my-extension' to your extension ID (lowercase, hyphen-separated)
|
||||||
|
id: "my-extension"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Human-readable name for your extension
|
||||||
|
name: "My Extension"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Update version when releasing (semantic versioning: X.Y.Z)
|
||||||
|
version: "1.0.0"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Brief description (under 200 characters)
|
||||||
|
description: "Brief description of what your extension does"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Your name or organization name
|
||||||
|
author: "Your Name"
|
||||||
|
|
||||||
|
# CUSTOMIZE: GitHub repository URL (create before publishing)
|
||||||
|
repository: "https://github.com/your-org/spec-kit-my-extension"
|
||||||
|
|
||||||
|
# REVIEW: License (MIT is recommended for open source)
|
||||||
|
license: "MIT"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Extension homepage (can be same as repository)
|
||||||
|
homepage: "https://github.com/your-org/spec-kit-my-extension"
|
||||||
|
|
||||||
|
# Requirements for this extension
|
||||||
|
requires:
|
||||||
|
# CUSTOMIZE: Minimum spec-kit version required
|
||||||
|
# Use >=X.Y.Z for minimum version
|
||||||
|
# Use >=X.Y.Z,<Y.0.0 for version range
|
||||||
|
speckit_version: ">=0.1.0"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Add MCP tools or other dependencies
|
||||||
|
# Remove if no external tools required
|
||||||
|
tools:
|
||||||
|
- name: "example-mcp-server"
|
||||||
|
version: ">=1.0.0"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
# Commands provided by this extension
|
||||||
|
provides:
|
||||||
|
commands:
|
||||||
|
# CUSTOMIZE: Define your commands
|
||||||
|
# Pattern: speckit.{extension-id}.{command-name}
|
||||||
|
- name: "speckit.my-extension.example"
|
||||||
|
file: "commands/example.md"
|
||||||
|
description: "Example command that demonstrates functionality"
|
||||||
|
# Optional: Add aliases for shorter command names
|
||||||
|
aliases: ["speckit.example"]
|
||||||
|
|
||||||
|
# ADD MORE COMMANDS: Copy this block for each command
|
||||||
|
# - name: "speckit.my-extension.another-command"
|
||||||
|
# file: "commands/another-command.md"
|
||||||
|
# description: "Another command"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Define configuration files
|
||||||
|
config:
|
||||||
|
- name: "my-extension-config.yml"
|
||||||
|
template: "config-template.yml"
|
||||||
|
description: "Extension configuration"
|
||||||
|
required: true # Set to false if config is optional
|
||||||
|
|
||||||
|
# CUSTOMIZE: Define hooks (optional)
|
||||||
|
# Remove if no hooks needed
|
||||||
|
hooks:
|
||||||
|
# Hook that runs after /speckit.tasks
|
||||||
|
after_tasks:
|
||||||
|
command: "speckit.my-extension.example"
|
||||||
|
optional: true # User will be prompted
|
||||||
|
prompt: "Run example command?"
|
||||||
|
description: "Demonstrates hook functionality"
|
||||||
|
condition: null # Future: conditional execution
|
||||||
|
|
||||||
|
# ADD MORE HOOKS: Copy this block for other events
|
||||||
|
# after_implement:
|
||||||
|
# command: "speckit.my-extension.another"
|
||||||
|
# optional: false # Auto-execute without prompting
|
||||||
|
# description: "Runs automatically after implementation"
|
||||||
|
|
||||||
|
# CUSTOMIZE: Add relevant tags (2-5 recommended)
|
||||||
|
# Used for discovery in catalog
|
||||||
|
tags:
|
||||||
|
- "example"
|
||||||
|
- "template"
|
||||||
|
# ADD MORE: "category", "tool-name", etc.
|
||||||
|
|
||||||
|
# CUSTOMIZE: Default configuration values (optional)
|
||||||
|
# These are merged with user config
|
||||||
|
defaults:
|
||||||
|
# Example default values
|
||||||
|
feature:
|
||||||
|
enabled: true
|
||||||
|
auto_sync: false
|
||||||
|
|
||||||
|
# ADD MORE: Any default settings for your extension
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.0.22"
|
version = "0.1.5"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typer",
|
"typer",
|
||||||
|
"click>=8.1",
|
||||||
"rich",
|
"rich",
|
||||||
"httpx[socks]",
|
"httpx[socks]",
|
||||||
"platformdirs",
|
"platformdirs",
|
||||||
"readchar",
|
"readchar",
|
||||||
"truststore>=0.10.4",
|
"truststore>=0.10.4",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"packaging>=23.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -22,3 +25,30 @@ build-backend = "hatchling.build"
|
|||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/specify_cli"]
|
packages = ["src/specify_cli"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-cov>=4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"-v",
|
||||||
|
"--strict-markers",
|
||||||
|
"--tb=short",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["src"]
|
||||||
|
omit = ["*/tests/*", "*/__pycache__/*"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
precision = 2
|
||||||
|
show_missing = true
|
||||||
|
skip_covered = false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
#
|
#
|
||||||
# 5. Multi-Agent Support
|
# 5. Multi-Agent Support
|
||||||
# - Handles agent-specific file paths and naming conventions
|
# - Handles agent-specific file paths and naming conventions
|
||||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, or Amazon Q Developer CLI
|
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Amazon Q Developer CLI, or Antigravity
|
||||||
# - Can update single agents or all existing agent files
|
# - Can update single agents or all existing agent files
|
||||||
# - Creates default Claude file if no agent files exist
|
# - Creates default Claude file if no agent files exist
|
||||||
#
|
#
|
||||||
# Usage: ./update-agent-context.sh [agent_type]
|
# Usage: ./update-agent-context.sh [agent_type]
|
||||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|shai|q|bob|qoder
|
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli
|
||||||
# Leave empty to update all existing agent files
|
# Leave empty to update all existing agent files
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -74,6 +74,7 @@ QODER_FILE="$REPO_ROOT/QODER.md"
|
|||||||
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||||
Q_FILE="$REPO_ROOT/AGENTS.md"
|
Q_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
|
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||||
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
|
|
||||||
# Template file
|
# Template file
|
||||||
@@ -618,7 +619,7 @@ update_specific_agent() {
|
|||||||
codebuddy)
|
codebuddy)
|
||||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||||
;;
|
;;
|
||||||
qoder)
|
qodercli)
|
||||||
update_agent_file "$QODER_FILE" "Qoder CLI"
|
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||||
;;
|
;;
|
||||||
amp)
|
amp)
|
||||||
@@ -630,12 +631,18 @@ update_specific_agent() {
|
|||||||
q)
|
q)
|
||||||
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
|
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
|
||||||
;;
|
;;
|
||||||
|
agy)
|
||||||
|
update_agent_file "$AGY_FILE" "Antigravity"
|
||||||
|
;;
|
||||||
bob)
|
bob)
|
||||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||||
;;
|
;;
|
||||||
|
generic)
|
||||||
|
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
log_error "Unknown agent type '$agent_type'"
|
log_error "Unknown agent type '$agent_type'"
|
||||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|amp|shai|q|bob|qoder"
|
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -714,7 +721,11 @@ update_all_existing_agents() {
|
|||||||
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
|
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
|
||||||
found_agent=true
|
found_agent=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$AGY_FILE" ]]; then
|
||||||
|
update_agent_file "$AGY_FILE" "Antigravity"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
if [[ -f "$BOB_FILE" ]]; then
|
if [[ -f "$BOB_FILE" ]]; then
|
||||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||||
found_agent=true
|
found_agent=true
|
||||||
@@ -744,7 +755,7 @@ print_summary() {
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|codebuddy|shai|q|bob|qoder]"
|
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli]"
|
||||||
}
|
}
|
||||||
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
|
|||||||
2. Plan Data Extraction
|
2. Plan Data Extraction
|
||||||
3. Agent File Management (create from template or update existing)
|
3. Agent File Management (create from template or update existing)
|
||||||
4. Content Generation (technology stack, recent changes, timestamp)
|
4. Content Generation (technology stack, recent changes, timestamp)
|
||||||
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, q, bob, qoder)
|
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, q, agy, bob, qodercli)
|
||||||
|
|
||||||
.PARAMETER AgentType
|
.PARAMETER AgentType
|
||||||
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
|
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
|
||||||
@@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
|
|||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
[Parameter(Position=0)]
|
[Parameter(Position=0)]
|
||||||
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','q','bob','qoder')]
|
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','q','agy','bob','qodercli','generic')]
|
||||||
[string]$AgentType
|
[string]$AgentType
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ $QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
|
|||||||
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
|
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
|
||||||
$Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
|
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
|
||||||
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
|
|
||||||
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
||||||
@@ -383,12 +384,14 @@ function Update-SpecificAgent {
|
|||||||
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
|
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
|
||||||
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
|
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
|
||||||
'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' }
|
'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' }
|
||||||
'qoder' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
|
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
|
||||||
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
|
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
|
||||||
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
|
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
|
||||||
'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' }
|
'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' }
|
||||||
|
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
|
||||||
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
||||||
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|bob|qoder'; return $false }
|
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
||||||
|
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic'; return $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +412,7 @@ function Update-AllExistingAgents {
|
|||||||
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI')) { $ok = $false }; $found = $true }
|
||||||
|
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
|
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
|
||||||
if (-not $found) {
|
if (-not $found) {
|
||||||
Write-Info 'No existing agent files found, creating default Claude file...'
|
Write-Info 'No existing agent files found, creating default Claude file...'
|
||||||
@@ -424,7 +428,7 @@ function Print-Summary {
|
|||||||
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
||||||
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|bob|qoder]'
|
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic]'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Main {
|
function Main {
|
||||||
|
|||||||
8
spec-kit.code-workspace
Normal file
8
spec-kit.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
1785
src/specify_cli/extensions.py
Normal file
1785
src/specify_cli/extensions.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -75,10 +75,11 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
- Validation rules from requirements
|
- Validation rules from requirements
|
||||||
- State transitions if applicable
|
- State transitions if applicable
|
||||||
|
|
||||||
2. **Generate API contracts** from functional requirements:
|
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
|
||||||
- For each user action → endpoint
|
- Identify what interfaces the project exposes to users or other systems
|
||||||
- Use standard REST/GraphQL patterns
|
- Document the contract format appropriate for the project type
|
||||||
- Output OpenAPI/GraphQL schema to `/contracts/`
|
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
|
||||||
|
- Skip if project is purely internal (build scripts, one-off tools, etc.)
|
||||||
|
|
||||||
3. **Agent context update**:
|
3. **Agent context update**:
|
||||||
- Run `{AGENT_SCRIPT}`
|
- Run `{AGENT_SCRIPT}`
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ When creating this spec from a user prompt:
|
|||||||
- Performance targets: Standard web/mobile app expectations unless specified
|
- Performance targets: Standard web/mobile app expectations unless specified
|
||||||
- Error handling: User-friendly messages with appropriate fallbacks
|
- Error handling: User-friendly messages with appropriate fallbacks
|
||||||
- Authentication method: Standard session-based or OAuth2 for web apps
|
- Authentication method: Standard session-based or OAuth2 for web apps
|
||||||
- Integration patterns: RESTful APIs unless specified otherwise
|
- Integration patterns: Use project-appropriate patterns (REST/GraphQL for web services, function calls for libraries, CLI args for tools, etc.)
|
||||||
|
|
||||||
### Success Criteria Guidelines
|
### Success Criteria Guidelines
|
||||||
|
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|||||||
|
|
||||||
2. **Load design documents**: Read from FEATURE_DIR:
|
2. **Load design documents**: Read from FEATURE_DIR:
|
||||||
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
|
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
|
||||||
- **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios)
|
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
|
||||||
- Note: Not all projects have all documents. Generate tasks based on what's available.
|
- Note: Not all projects have all documents. Generate tasks based on what's available.
|
||||||
|
|
||||||
3. **Execute task generation workflow**:
|
3. **Execute task generation workflow**:
|
||||||
- Load plan.md and extract tech stack, libraries, project structure
|
- Load plan.md and extract tech stack, libraries, project structure
|
||||||
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
|
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
|
||||||
- If data-model.md exists: Extract entities and map to user stories
|
- If data-model.md exists: Extract entities and map to user stories
|
||||||
- If contracts/ exists: Map endpoints to user stories
|
- If contracts/ exists: Map interface contracts to user stories
|
||||||
- If research.md exists: Extract decisions for setup tasks
|
- If research.md exists: Extract decisions for setup tasks
|
||||||
- Generate tasks organized by user story (see Task Generation Rules below)
|
- Generate tasks organized by user story (see Task Generation Rules below)
|
||||||
- Generate dependency graph showing user story completion order
|
- Generate dependency graph showing user story completion order
|
||||||
@@ -112,13 +112,13 @@ Every task MUST strictly follow this format:
|
|||||||
- Map all related components to their story:
|
- Map all related components to their story:
|
||||||
- Models needed for that story
|
- Models needed for that story
|
||||||
- Services needed for that story
|
- Services needed for that story
|
||||||
- Endpoints/UI needed for that story
|
- Interfaces/UI needed for that story
|
||||||
- If tests requested: Tests specific to that story
|
- If tests requested: Tests specific to that story
|
||||||
- Mark story dependencies (most stories should be independent)
|
- Mark story dependencies (most stories should be independent)
|
||||||
|
|
||||||
2. **From Contracts**:
|
2. **From Contracts**:
|
||||||
- Map each contract/endpoint → to the user story it serves
|
- Map each interface contract → to the user story it serves
|
||||||
- If tests requested: Each contract → contract test task [P] before implementation in that story's phase
|
- If tests requested: Each interface contract → contract test task [P] before implementation in that story's phase
|
||||||
|
|
||||||
3. **From Data Model**:
|
3. **From Data Model**:
|
||||||
- Map each entity to the user story(ies) that need it
|
- Map each entity to the user story(ies) that need it
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||||
|
|
||||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
||||||
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
||||||
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
||||||
**Project Type**: [single/web/mobile - determines source structure]
|
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
|
||||||
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
||||||
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
||||||
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
|
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
|
||||||
|
|||||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Unit tests for Spec Kit."""
|
||||||
632
tests/test_ai_skills.py
Normal file
632
tests/test_ai_skills.py
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for AI agent skills installation.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Skills directory resolution for different agents (_get_skills_dir)
|
||||||
|
- YAML frontmatter parsing and SKILL.md generation (install_ai_skills)
|
||||||
|
- Cleanup of duplicate command files when --ai-skills is used
|
||||||
|
- Missing templates directory handling
|
||||||
|
- Malformed template error handling
|
||||||
|
- CLI validation: --ai-skills requires --ai
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import pytest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import specify_cli
|
||||||
|
|
||||||
|
from specify_cli import (
|
||||||
|
_get_skills_dir,
|
||||||
|
install_ai_skills,
|
||||||
|
AGENT_SKILLS_DIR_OVERRIDES,
|
||||||
|
DEFAULT_SKILLS_DIR,
|
||||||
|
SKILL_DESCRIPTIONS,
|
||||||
|
AGENT_CONFIG,
|
||||||
|
app,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Fixtures =====
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_dir():
|
||||||
|
"""Create a temporary directory for tests."""
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
yield Path(tmpdir)
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def project_dir(temp_dir):
|
||||||
|
"""Create a mock project directory."""
|
||||||
|
proj_dir = temp_dir / "test-project"
|
||||||
|
proj_dir.mkdir()
|
||||||
|
return proj_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def templates_dir(project_dir):
|
||||||
|
"""Create mock command templates in the project's agent commands directory.
|
||||||
|
|
||||||
|
This simulates what download_and_extract_template() does: it places
|
||||||
|
command .md files into project_path/<agent_folder>/commands/.
|
||||||
|
install_ai_skills() now reads from here instead of from the repo
|
||||||
|
source tree.
|
||||||
|
"""
|
||||||
|
tpl_root = project_dir / ".claude" / "commands"
|
||||||
|
tpl_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Template with valid YAML frontmatter
|
||||||
|
(tpl_root / "specify.md").write_text(
|
||||||
|
"---\n"
|
||||||
|
"description: Create or update the feature specification.\n"
|
||||||
|
"handoffs:\n"
|
||||||
|
" - label: Build Plan\n"
|
||||||
|
" agent: speckit.plan\n"
|
||||||
|
"scripts:\n"
|
||||||
|
" sh: scripts/bash/create-new-feature.sh\n"
|
||||||
|
"---\n"
|
||||||
|
"\n"
|
||||||
|
"# Specify Command\n"
|
||||||
|
"\n"
|
||||||
|
"Run this to create a spec.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Template with minimal frontmatter
|
||||||
|
(tpl_root / "plan.md").write_text(
|
||||||
|
"---\n"
|
||||||
|
"description: Generate implementation plan.\n"
|
||||||
|
"---\n"
|
||||||
|
"\n"
|
||||||
|
"# Plan Command\n"
|
||||||
|
"\n"
|
||||||
|
"Plan body content.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Template with no frontmatter
|
||||||
|
(tpl_root / "tasks.md").write_text(
|
||||||
|
"# Tasks Command\n"
|
||||||
|
"\n"
|
||||||
|
"Body without frontmatter.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Template with empty YAML frontmatter (yaml.safe_load returns None)
|
||||||
|
(tpl_root / "empty_fm.md").write_text(
|
||||||
|
"---\n"
|
||||||
|
"---\n"
|
||||||
|
"\n"
|
||||||
|
"# Empty Frontmatter Command\n"
|
||||||
|
"\n"
|
||||||
|
"Body with empty frontmatter.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
return tpl_root
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def commands_dir_claude(project_dir):
|
||||||
|
"""Create a populated .claude/commands directory simulating template extraction."""
|
||||||
|
cmd_dir = project_dir / ".claude" / "commands"
|
||||||
|
cmd_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name in ["speckit.specify.md", "speckit.plan.md", "speckit.tasks.md"]:
|
||||||
|
(cmd_dir / name).write_text(f"# {name}\nContent here\n")
|
||||||
|
return cmd_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def commands_dir_gemini(project_dir):
|
||||||
|
"""Create a populated .gemini/commands directory (TOML format)."""
|
||||||
|
cmd_dir = project_dir / ".gemini" / "commands"
|
||||||
|
cmd_dir.mkdir(parents=True)
|
||||||
|
for name in ["speckit.specify.toml", "speckit.plan.toml", "speckit.tasks.toml"]:
|
||||||
|
(cmd_dir / name).write_text(f'[command]\nname = "{name}"\n')
|
||||||
|
return cmd_dir
|
||||||
|
|
||||||
|
|
||||||
|
# ===== _get_skills_dir Tests =====
|
||||||
|
|
||||||
|
class TestGetSkillsDir:
|
||||||
|
"""Test the _get_skills_dir() helper function."""
|
||||||
|
|
||||||
|
def test_claude_skills_dir(self, project_dir):
|
||||||
|
"""Claude should use .claude/skills/."""
|
||||||
|
result = _get_skills_dir(project_dir, "claude")
|
||||||
|
assert result == project_dir / ".claude" / "skills"
|
||||||
|
|
||||||
|
def test_gemini_skills_dir(self, project_dir):
|
||||||
|
"""Gemini should use .gemini/skills/."""
|
||||||
|
result = _get_skills_dir(project_dir, "gemini")
|
||||||
|
assert result == project_dir / ".gemini" / "skills"
|
||||||
|
|
||||||
|
def test_copilot_skills_dir(self, project_dir):
|
||||||
|
"""Copilot should use .github/skills/."""
|
||||||
|
result = _get_skills_dir(project_dir, "copilot")
|
||||||
|
assert result == project_dir / ".github" / "skills"
|
||||||
|
|
||||||
|
def test_codex_uses_override(self, project_dir):
|
||||||
|
"""Codex should use the AGENT_SKILLS_DIR_OVERRIDES value."""
|
||||||
|
result = _get_skills_dir(project_dir, "codex")
|
||||||
|
assert result == project_dir / ".agents" / "skills"
|
||||||
|
|
||||||
|
def test_cursor_agent_skills_dir(self, project_dir):
|
||||||
|
"""Cursor should use .cursor/skills/."""
|
||||||
|
result = _get_skills_dir(project_dir, "cursor-agent")
|
||||||
|
assert result == project_dir / ".cursor" / "skills"
|
||||||
|
|
||||||
|
def test_unknown_agent_uses_default(self, project_dir):
|
||||||
|
"""Unknown agents should fall back to DEFAULT_SKILLS_DIR."""
|
||||||
|
result = _get_skills_dir(project_dir, "nonexistent-agent")
|
||||||
|
assert result == project_dir / DEFAULT_SKILLS_DIR
|
||||||
|
|
||||||
|
def test_all_configured_agents_resolve(self, project_dir):
|
||||||
|
"""Every agent in AGENT_CONFIG should resolve to a valid path."""
|
||||||
|
for agent_key in AGENT_CONFIG:
|
||||||
|
result = _get_skills_dir(project_dir, agent_key)
|
||||||
|
assert result is not None
|
||||||
|
assert str(result).startswith(str(project_dir))
|
||||||
|
# Should always end with "skills"
|
||||||
|
assert result.name == "skills"
|
||||||
|
|
||||||
|
def test_override_takes_precedence_over_config(self, project_dir):
|
||||||
|
"""AGENT_SKILLS_DIR_OVERRIDES should take precedence over AGENT_CONFIG."""
|
||||||
|
for agent_key in AGENT_SKILLS_DIR_OVERRIDES:
|
||||||
|
result = _get_skills_dir(project_dir, agent_key)
|
||||||
|
expected = project_dir / AGENT_SKILLS_DIR_OVERRIDES[agent_key]
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
# ===== install_ai_skills Tests =====
|
||||||
|
|
||||||
|
class TestInstallAiSkills:
|
||||||
|
"""Test SKILL.md generation and installation logic."""
|
||||||
|
|
||||||
|
def test_skills_installed_with_correct_structure(self, project_dir, templates_dir):
|
||||||
|
"""Verify SKILL.md files have correct agentskills.io structure."""
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
skills_dir = project_dir / ".claude" / "skills"
|
||||||
|
assert skills_dir.exists()
|
||||||
|
|
||||||
|
# Check that skill directories were created
|
||||||
|
skill_dirs = sorted([d.name for d in skills_dir.iterdir() if d.is_dir()])
|
||||||
|
assert "speckit-plan" in skill_dirs
|
||||||
|
assert "speckit-specify" in skill_dirs
|
||||||
|
assert "speckit-tasks" in skill_dirs
|
||||||
|
assert "speckit-empty_fm" in skill_dirs
|
||||||
|
|
||||||
|
# Verify SKILL.md content for speckit-specify
|
||||||
|
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
|
||||||
|
assert skill_file.exists()
|
||||||
|
content = skill_file.read_text()
|
||||||
|
|
||||||
|
# Check agentskills.io frontmatter
|
||||||
|
assert content.startswith("---\n")
|
||||||
|
assert "name: speckit-specify" in content
|
||||||
|
assert "description:" in content
|
||||||
|
assert "compatibility:" in content
|
||||||
|
assert "metadata:" in content
|
||||||
|
assert "author: github-spec-kit" in content
|
||||||
|
assert "source: templates/commands/specify.md" in content
|
||||||
|
|
||||||
|
# Check body content is included
|
||||||
|
assert "# Speckit Specify Skill" in content
|
||||||
|
assert "Run this to create a spec." in content
|
||||||
|
|
||||||
|
def test_generated_skill_has_parseable_yaml(self, project_dir, templates_dir):
|
||||||
|
"""Generated SKILL.md should contain valid, parseable YAML frontmatter."""
|
||||||
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
|
||||||
|
content = skill_file.read_text()
|
||||||
|
|
||||||
|
# Extract and parse frontmatter
|
||||||
|
assert content.startswith("---\n")
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
assert len(parts) >= 3
|
||||||
|
parsed = yaml.safe_load(parts[1])
|
||||||
|
assert isinstance(parsed, dict)
|
||||||
|
assert "name" in parsed
|
||||||
|
assert parsed["name"] == "speckit-specify"
|
||||||
|
assert "description" in parsed
|
||||||
|
|
||||||
|
def test_empty_yaml_frontmatter(self, project_dir, templates_dir):
|
||||||
|
"""Templates with empty YAML frontmatter (---\\n---) should not crash."""
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
skill_file = project_dir / ".claude" / "skills" / "speckit-empty_fm" / "SKILL.md"
|
||||||
|
assert skill_file.exists()
|
||||||
|
content = skill_file.read_text()
|
||||||
|
assert "name: speckit-empty_fm" in content
|
||||||
|
assert "Body with empty frontmatter." in content
|
||||||
|
|
||||||
|
def test_enhanced_descriptions_used_when_available(self, project_dir, templates_dir):
|
||||||
|
"""SKILL_DESCRIPTIONS take precedence over template frontmatter descriptions."""
|
||||||
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
|
||||||
|
content = skill_file.read_text()
|
||||||
|
|
||||||
|
# Parse the generated YAML to compare the description value
|
||||||
|
# (yaml.safe_dump may wrap long strings across multiple lines)
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
parsed = yaml.safe_load(parts[1])
|
||||||
|
|
||||||
|
if "specify" in SKILL_DESCRIPTIONS:
|
||||||
|
assert parsed["description"] == SKILL_DESCRIPTIONS["specify"]
|
||||||
|
|
||||||
|
def test_template_without_frontmatter(self, project_dir, templates_dir):
|
||||||
|
"""Templates without YAML frontmatter should still produce valid skills."""
|
||||||
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
skill_file = project_dir / ".claude" / "skills" / "speckit-tasks" / "SKILL.md"
|
||||||
|
assert skill_file.exists()
|
||||||
|
content = skill_file.read_text()
|
||||||
|
|
||||||
|
# Should still have valid SKILL.md structure
|
||||||
|
assert "name: speckit-tasks" in content
|
||||||
|
assert "Body without frontmatter." in content
|
||||||
|
|
||||||
|
def test_missing_templates_directory(self, project_dir):
|
||||||
|
"""Returns False when no command templates exist anywhere."""
|
||||||
|
# No .claude/commands/ exists, and __file__ fallback won't find anything
|
||||||
|
fake_init = project_dir / "nonexistent" / "src" / "specify_cli" / "__init__.py"
|
||||||
|
fake_init.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fake_init.touch()
|
||||||
|
|
||||||
|
with patch.object(specify_cli, "__file__", str(fake_init)):
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
# Skills directory should not exist
|
||||||
|
skills_dir = project_dir / ".claude" / "skills"
|
||||||
|
assert not skills_dir.exists()
|
||||||
|
|
||||||
|
def test_empty_templates_directory(self, project_dir):
|
||||||
|
"""Returns False when commands directory has no .md files."""
|
||||||
|
# Create empty .claude/commands/
|
||||||
|
empty_cmds = project_dir / ".claude" / "commands"
|
||||||
|
empty_cmds.mkdir(parents=True)
|
||||||
|
|
||||||
|
# Block the __file__ fallback so it can't find real templates
|
||||||
|
fake_init = project_dir / "nowhere" / "src" / "specify_cli" / "__init__.py"
|
||||||
|
fake_init.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fake_init.touch()
|
||||||
|
|
||||||
|
with patch.object(specify_cli, "__file__", str(fake_init)):
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_malformed_yaml_frontmatter(self, project_dir):
|
||||||
|
"""Malformed YAML in a template should be handled gracefully, not crash."""
|
||||||
|
# Create .claude/commands/ with a broken template
|
||||||
|
cmds_dir = project_dir / ".claude" / "commands"
|
||||||
|
cmds_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
(cmds_dir / "broken.md").write_text(
|
||||||
|
"---\n"
|
||||||
|
"description: [unclosed bracket\n"
|
||||||
|
" invalid: yaml: content: here\n"
|
||||||
|
"---\n"
|
||||||
|
"\n"
|
||||||
|
"# Broken\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not raise — errors are caught per-file
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
# The broken template should be skipped but not crash the process
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_additive_does_not_overwrite_other_files(self, project_dir, templates_dir):
|
||||||
|
"""Installing skills should not remove non-speckit files in the skills dir."""
|
||||||
|
# Pre-create a custom skill
|
||||||
|
custom_dir = project_dir / ".claude" / "skills" / "my-custom-skill"
|
||||||
|
custom_dir.mkdir(parents=True)
|
||||||
|
custom_file = custom_dir / "SKILL.md"
|
||||||
|
custom_file.write_text("# My Custom Skill\n")
|
||||||
|
|
||||||
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
# Custom skill should still exist
|
||||||
|
assert custom_file.exists()
|
||||||
|
assert custom_file.read_text() == "# My Custom Skill\n"
|
||||||
|
|
||||||
|
def test_return_value(self, project_dir, templates_dir):
|
||||||
|
"""install_ai_skills returns True when skills installed, False otherwise."""
|
||||||
|
assert install_ai_skills(project_dir, "claude") is True
|
||||||
|
|
||||||
|
def test_return_false_when_no_templates(self, project_dir):
|
||||||
|
"""install_ai_skills returns False when no templates found."""
|
||||||
|
fake_init = project_dir / "missing" / "src" / "specify_cli" / "__init__.py"
|
||||||
|
fake_init.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fake_init.touch()
|
||||||
|
|
||||||
|
with patch.object(specify_cli, "__file__", str(fake_init)):
|
||||||
|
assert install_ai_skills(project_dir, "claude") is False
|
||||||
|
|
||||||
|
def test_non_md_commands_dir_falls_back(self, project_dir):
|
||||||
|
"""When extracted commands are .toml (e.g. gemini), fall back to repo templates."""
|
||||||
|
# Simulate gemini template extraction: .gemini/commands/ with .toml files only
|
||||||
|
cmds_dir = project_dir / ".gemini" / "commands"
|
||||||
|
cmds_dir.mkdir(parents=True)
|
||||||
|
(cmds_dir / "speckit.specify.toml").write_text('[command]\nname = "specify"\n')
|
||||||
|
(cmds_dir / "speckit.plan.toml").write_text('[command]\nname = "plan"\n')
|
||||||
|
|
||||||
|
# The __file__ fallback should find the real repo templates/commands/*.md
|
||||||
|
result = install_ai_skills(project_dir, "gemini")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
skills_dir = project_dir / ".gemini" / "skills"
|
||||||
|
assert skills_dir.exists()
|
||||||
|
# Should have installed skills from the fallback .md templates
|
||||||
|
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
||||||
|
assert len(skill_dirs) >= 1
|
||||||
|
# .toml commands should be untouched
|
||||||
|
assert (cmds_dir / "speckit.specify.toml").exists()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("agent_key", [k for k in AGENT_CONFIG.keys() if k != "generic"])
|
||||||
|
def test_skills_install_for_all_agents(self, temp_dir, agent_key):
|
||||||
|
"""install_ai_skills should produce skills for every configured agent."""
|
||||||
|
proj = temp_dir / f"proj-{agent_key}"
|
||||||
|
proj.mkdir()
|
||||||
|
|
||||||
|
# Place .md templates in the agent's commands directory
|
||||||
|
agent_folder = AGENT_CONFIG[agent_key]["folder"]
|
||||||
|
cmds_dir = proj / agent_folder.rstrip("/") / "commands"
|
||||||
|
cmds_dir.mkdir(parents=True)
|
||||||
|
(cmds_dir / "specify.md").write_text(
|
||||||
|
"---\ndescription: Test command\n---\n\n# Test\n\nBody.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = install_ai_skills(proj, agent_key)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
skills_dir = _get_skills_dir(proj, agent_key)
|
||||||
|
assert skills_dir.exists()
|
||||||
|
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
||||||
|
assert "speckit-specify" in skill_dirs
|
||||||
|
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommandCoexistence:
|
||||||
|
"""Verify install_ai_skills never touches command files.
|
||||||
|
|
||||||
|
Cleanup of freshly-extracted commands for NEW projects is handled
|
||||||
|
in init(), not in install_ai_skills(). These tests confirm that
|
||||||
|
install_ai_skills leaves existing commands intact.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_existing_commands_preserved_claude(self, project_dir, templates_dir, commands_dir_claude):
|
||||||
|
"""install_ai_skills must NOT remove pre-existing .claude/commands files."""
|
||||||
|
# Verify commands exist before
|
||||||
|
assert len(list(commands_dir_claude.glob("speckit.*"))) == 3
|
||||||
|
|
||||||
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
# Commands must still be there — install_ai_skills never touches them
|
||||||
|
remaining = list(commands_dir_claude.glob("speckit.*"))
|
||||||
|
assert len(remaining) == 3
|
||||||
|
|
||||||
|
def test_existing_commands_preserved_gemini(self, project_dir, templates_dir, commands_dir_gemini):
|
||||||
|
"""install_ai_skills must NOT remove pre-existing .gemini/commands files."""
|
||||||
|
assert len(list(commands_dir_gemini.glob("speckit.*"))) == 3
|
||||||
|
|
||||||
|
install_ai_skills(project_dir, "gemini")
|
||||||
|
|
||||||
|
remaining = list(commands_dir_gemini.glob("speckit.*"))
|
||||||
|
assert len(remaining) == 3
|
||||||
|
|
||||||
|
def test_commands_dir_not_removed(self, project_dir, templates_dir, commands_dir_claude):
|
||||||
|
"""install_ai_skills must not remove the commands directory."""
|
||||||
|
install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
assert commands_dir_claude.exists()
|
||||||
|
|
||||||
|
def test_no_commands_dir_no_error(self, project_dir, templates_dir):
|
||||||
|
"""No error when installing skills — commands dir has templates and is preserved."""
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
# Should succeed since templates are in .claude/commands/ via fixture
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
# ===== New-Project Command Skip Tests =====
|
||||||
|
|
||||||
|
class TestNewProjectCommandSkip:
|
||||||
|
"""Test that init() removes extracted commands for new projects only.
|
||||||
|
|
||||||
|
These tests run init() end-to-end via CliRunner with
|
||||||
|
download_and_extract_template patched to create local fixtures.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _fake_extract(self, agent, project_path, **_kwargs):
|
||||||
|
"""Simulate template extraction: create agent commands dir."""
|
||||||
|
agent_cfg = AGENT_CONFIG.get(agent, {})
|
||||||
|
agent_folder = agent_cfg.get("folder", "")
|
||||||
|
if agent_folder:
|
||||||
|
cmds_dir = project_path / agent_folder.rstrip("/") / "commands"
|
||||||
|
cmds_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(cmds_dir / "speckit.specify.md").write_text("# spec")
|
||||||
|
|
||||||
|
def test_new_project_commands_removed_after_skills_succeed(self, tmp_path):
|
||||||
|
"""For new projects, commands should be removed when skills succeed."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
target = tmp_path / "new-proj"
|
||||||
|
|
||||||
|
def fake_download(project_path, *args, **kwargs):
|
||||||
|
self._fake_extract("claude", project_path)
|
||||||
|
|
||||||
|
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
|
||||||
|
patch("specify_cli.ensure_executable_scripts"), \
|
||||||
|
patch("specify_cli.ensure_constitution_from_template"), \
|
||||||
|
patch("specify_cli.install_ai_skills", return_value=True) as mock_skills, \
|
||||||
|
patch("specify_cli.is_git_repo", return_value=False), \
|
||||||
|
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
|
||||||
|
result = runner.invoke(app, ["init", str(target), "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"])
|
||||||
|
|
||||||
|
# Skills should have been called
|
||||||
|
mock_skills.assert_called_once()
|
||||||
|
|
||||||
|
# Commands dir should have been removed after skills succeeded
|
||||||
|
cmds_dir = target / ".claude" / "commands"
|
||||||
|
assert not cmds_dir.exists()
|
||||||
|
|
||||||
|
def test_commands_preserved_when_skills_fail(self, tmp_path):
|
||||||
|
"""If skills fail, commands should NOT be removed (safety net)."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
target = tmp_path / "fail-proj"
|
||||||
|
|
||||||
|
def fake_download(project_path, *args, **kwargs):
|
||||||
|
self._fake_extract("claude", project_path)
|
||||||
|
|
||||||
|
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
|
||||||
|
patch("specify_cli.ensure_executable_scripts"), \
|
||||||
|
patch("specify_cli.ensure_constitution_from_template"), \
|
||||||
|
patch("specify_cli.install_ai_skills", return_value=False), \
|
||||||
|
patch("specify_cli.is_git_repo", return_value=False), \
|
||||||
|
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
|
||||||
|
result = runner.invoke(app, ["init", str(target), "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"])
|
||||||
|
|
||||||
|
# Commands should still exist since skills failed
|
||||||
|
cmds_dir = target / ".claude" / "commands"
|
||||||
|
assert cmds_dir.exists()
|
||||||
|
assert (cmds_dir / "speckit.specify.md").exists()
|
||||||
|
|
||||||
|
def test_here_mode_commands_preserved(self, tmp_path, monkeypatch):
|
||||||
|
"""For --here on existing repos, commands must NOT be removed."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
# Create a mock existing project with commands already present
|
||||||
|
target = tmp_path / "existing"
|
||||||
|
target.mkdir()
|
||||||
|
agent_folder = AGENT_CONFIG["claude"]["folder"]
|
||||||
|
cmds_dir = target / agent_folder.rstrip("/") / "commands"
|
||||||
|
cmds_dir.mkdir(parents=True)
|
||||||
|
(cmds_dir / "speckit.specify.md").write_text("# spec")
|
||||||
|
|
||||||
|
# --here uses CWD, so chdir into the target
|
||||||
|
monkeypatch.chdir(target)
|
||||||
|
|
||||||
|
def fake_download(project_path, *args, **kwargs):
|
||||||
|
pass # commands already exist, no need to re-create
|
||||||
|
|
||||||
|
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
|
||||||
|
patch("specify_cli.ensure_executable_scripts"), \
|
||||||
|
patch("specify_cli.ensure_constitution_from_template"), \
|
||||||
|
patch("specify_cli.install_ai_skills", return_value=True), \
|
||||||
|
patch("specify_cli.is_git_repo", return_value=True), \
|
||||||
|
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
|
||||||
|
result = runner.invoke(app, ["init", "--here", "--ai", "claude", "--ai-skills", "--script", "sh", "--no-git"])
|
||||||
|
|
||||||
|
# Commands must remain for --here
|
||||||
|
assert cmds_dir.exists()
|
||||||
|
assert (cmds_dir / "speckit.specify.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Skip-If-Exists Tests =====
|
||||||
|
|
||||||
|
class TestSkipIfExists:
|
||||||
|
"""Test that install_ai_skills does not overwrite existing SKILL.md files."""
|
||||||
|
|
||||||
|
def test_existing_skill_not_overwritten(self, project_dir, templates_dir):
|
||||||
|
"""Pre-existing SKILL.md should not be replaced on re-run."""
|
||||||
|
# Pre-create a custom SKILL.md for speckit-specify
|
||||||
|
skill_dir = project_dir / ".claude" / "skills" / "speckit-specify"
|
||||||
|
skill_dir.mkdir(parents=True)
|
||||||
|
custom_content = "# My Custom Specify Skill\nUser-modified content\n"
|
||||||
|
(skill_dir / "SKILL.md").write_text(custom_content)
|
||||||
|
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
# The custom SKILL.md should be untouched
|
||||||
|
assert (skill_dir / "SKILL.md").read_text() == custom_content
|
||||||
|
|
||||||
|
# But other skills should still be installed
|
||||||
|
assert result is True
|
||||||
|
assert (project_dir / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||||
|
assert (project_dir / ".claude" / "skills" / "speckit-tasks" / "SKILL.md").exists()
|
||||||
|
|
||||||
|
def test_fresh_install_writes_all_skills(self, project_dir, templates_dir):
|
||||||
|
"""On first install (no pre-existing skills), all should be written."""
|
||||||
|
result = install_ai_skills(project_dir, "claude")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
skills_dir = project_dir / ".claude" / "skills"
|
||||||
|
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
||||||
|
# All 4 templates should produce skills (specify, plan, tasks, empty_fm)
|
||||||
|
assert len(skill_dirs) == 4
|
||||||
|
|
||||||
|
|
||||||
|
# ===== SKILL_DESCRIPTIONS Coverage Tests =====
|
||||||
|
|
||||||
|
class TestSkillDescriptions:
|
||||||
|
"""Test SKILL_DESCRIPTIONS constants."""
|
||||||
|
|
||||||
|
def test_all_known_commands_have_descriptions(self):
|
||||||
|
"""All standard spec-kit commands should have enhanced descriptions."""
|
||||||
|
expected_commands = [
|
||||||
|
"specify", "plan", "tasks", "implement", "analyze",
|
||||||
|
"clarify", "constitution", "checklist", "taskstoissues",
|
||||||
|
]
|
||||||
|
for cmd in expected_commands:
|
||||||
|
assert cmd in SKILL_DESCRIPTIONS, f"Missing description for '{cmd}'"
|
||||||
|
assert len(SKILL_DESCRIPTIONS[cmd]) > 20, f"Description for '{cmd}' is too short"
|
||||||
|
|
||||||
|
|
||||||
|
# ===== CLI Validation Tests =====
|
||||||
|
|
||||||
|
class TestCliValidation:
|
||||||
|
"""Test --ai-skills CLI flag validation."""
|
||||||
|
|
||||||
|
def test_ai_skills_without_ai_fails(self):
|
||||||
|
"""--ai-skills without --ai should fail with exit code 1."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(app, ["init", "test-proj", "--ai-skills"])
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "--ai-skills requires --ai" in result.output
|
||||||
|
|
||||||
|
def test_ai_skills_without_ai_shows_usage(self):
|
||||||
|
"""Error message should include usage hint."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(app, ["init", "test-proj", "--ai-skills"])
|
||||||
|
|
||||||
|
assert "Usage:" in result.output
|
||||||
|
assert "--ai" in result.output
|
||||||
|
|
||||||
|
def test_ai_skills_flag_appears_in_help(self):
|
||||||
|
"""--ai-skills should appear in init --help output."""
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(app, ["init", "--help"])
|
||||||
|
|
||||||
|
plain = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
|
||||||
|
assert "--ai-skills" in plain
|
||||||
|
assert "agent skills" in plain.lower()
|
||||||
989
tests/test_extensions.py
Normal file
989
tests/test_extensions.py
Normal file
@@ -0,0 +1,989 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for the extension system.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Extension manifest validation
|
||||||
|
- Extension registry operations
|
||||||
|
- Extension manager installation/removal
|
||||||
|
- Command registration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from specify_cli.extensions import (
|
||||||
|
ExtensionManifest,
|
||||||
|
ExtensionRegistry,
|
||||||
|
ExtensionManager,
|
||||||
|
CommandRegistrar,
|
||||||
|
ExtensionCatalog,
|
||||||
|
ExtensionError,
|
||||||
|
ValidationError,
|
||||||
|
CompatibilityError,
|
||||||
|
version_satisfies,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Fixtures =====
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_dir():
|
||||||
|
"""Create a temporary directory for tests."""
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
yield Path(tmpdir)
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def valid_manifest_data():
|
||||||
|
"""Valid extension manifest data."""
|
||||||
|
return {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extension": {
|
||||||
|
"id": "test-ext",
|
||||||
|
"name": "Test Extension",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A test extension",
|
||||||
|
"author": "Test Author",
|
||||||
|
"repository": "https://github.com/test/test-ext",
|
||||||
|
"license": "MIT",
|
||||||
|
},
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0",
|
||||||
|
"commands": ["speckit.tasks"],
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "speckit.test.hello",
|
||||||
|
"file": "commands/hello.md",
|
||||||
|
"description": "Test command",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"after_tasks": {
|
||||||
|
"command": "speckit.test.hello",
|
||||||
|
"optional": True,
|
||||||
|
"prompt": "Run test?",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": ["testing", "example"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def extension_dir(temp_dir, valid_manifest_data):
|
||||||
|
"""Create a complete extension directory structure."""
|
||||||
|
ext_dir = temp_dir / "test-ext"
|
||||||
|
ext_dir.mkdir()
|
||||||
|
|
||||||
|
# Write manifest
|
||||||
|
import yaml
|
||||||
|
manifest_path = ext_dir / "extension.yml"
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
yaml.dump(valid_manifest_data, f)
|
||||||
|
|
||||||
|
# Create commands directory
|
||||||
|
commands_dir = ext_dir / "commands"
|
||||||
|
commands_dir.mkdir()
|
||||||
|
|
||||||
|
# Write command file
|
||||||
|
cmd_file = commands_dir / "hello.md"
|
||||||
|
cmd_file.write_text("""---
|
||||||
|
description: "Test hello command"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Hello Command
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
""")
|
||||||
|
|
||||||
|
return ext_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def project_dir(temp_dir):
|
||||||
|
"""Create a mock spec-kit project directory."""
|
||||||
|
proj_dir = temp_dir / "project"
|
||||||
|
proj_dir.mkdir()
|
||||||
|
|
||||||
|
# Create .specify directory
|
||||||
|
specify_dir = proj_dir / ".specify"
|
||||||
|
specify_dir.mkdir()
|
||||||
|
|
||||||
|
return proj_dir
|
||||||
|
|
||||||
|
|
||||||
|
# ===== ExtensionManifest Tests =====
|
||||||
|
|
||||||
|
class TestExtensionManifest:
|
||||||
|
"""Test ExtensionManifest validation and parsing."""
|
||||||
|
|
||||||
|
def test_valid_manifest(self, extension_dir):
|
||||||
|
"""Test loading a valid manifest."""
|
||||||
|
manifest_path = extension_dir / "extension.yml"
|
||||||
|
manifest = ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
assert manifest.id == "test-ext"
|
||||||
|
assert manifest.name == "Test Extension"
|
||||||
|
assert manifest.version == "1.0.0"
|
||||||
|
assert manifest.description == "A test extension"
|
||||||
|
assert len(manifest.commands) == 1
|
||||||
|
assert manifest.commands[0]["name"] == "speckit.test.hello"
|
||||||
|
|
||||||
|
def test_missing_required_field(self, temp_dir):
|
||||||
|
"""Test manifest missing required field."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
manifest_path = temp_dir / "extension.yml"
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
yaml.dump({"schema_version": "1.0"}, f) # Missing 'extension'
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Missing required field"):
|
||||||
|
ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
def test_invalid_extension_id(self, temp_dir, valid_manifest_data):
|
||||||
|
"""Test manifest with invalid extension ID format."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
valid_manifest_data["extension"]["id"] = "Invalid_ID" # Uppercase not allowed
|
||||||
|
|
||||||
|
manifest_path = temp_dir / "extension.yml"
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
yaml.dump(valid_manifest_data, f)
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Invalid extension ID"):
|
||||||
|
ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
def test_invalid_version(self, temp_dir, valid_manifest_data):
|
||||||
|
"""Test manifest with invalid semantic version."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
valid_manifest_data["extension"]["version"] = "invalid"
|
||||||
|
|
||||||
|
manifest_path = temp_dir / "extension.yml"
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
yaml.dump(valid_manifest_data, f)
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Invalid version"):
|
||||||
|
ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
def test_invalid_command_name(self, temp_dir, valid_manifest_data):
|
||||||
|
"""Test manifest with invalid command name format."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
valid_manifest_data["provides"]["commands"][0]["name"] = "invalid-name"
|
||||||
|
|
||||||
|
manifest_path = temp_dir / "extension.yml"
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
yaml.dump(valid_manifest_data, f)
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="Invalid command name"):
|
||||||
|
ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
def test_no_commands(self, temp_dir, valid_manifest_data):
|
||||||
|
"""Test manifest with no commands provided."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
valid_manifest_data["provides"]["commands"] = []
|
||||||
|
|
||||||
|
manifest_path = temp_dir / "extension.yml"
|
||||||
|
with open(manifest_path, 'w') as f:
|
||||||
|
yaml.dump(valid_manifest_data, f)
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match="must provide at least one command"):
|
||||||
|
ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
def test_manifest_hash(self, extension_dir):
|
||||||
|
"""Test manifest hash calculation."""
|
||||||
|
manifest_path = extension_dir / "extension.yml"
|
||||||
|
manifest = ExtensionManifest(manifest_path)
|
||||||
|
|
||||||
|
hash_value = manifest.get_hash()
|
||||||
|
assert hash_value.startswith("sha256:")
|
||||||
|
assert len(hash_value) > 10
|
||||||
|
|
||||||
|
|
||||||
|
# ===== ExtensionRegistry Tests =====
|
||||||
|
|
||||||
|
class TestExtensionRegistry:
|
||||||
|
"""Test ExtensionRegistry operations."""
|
||||||
|
|
||||||
|
def test_empty_registry(self, temp_dir):
|
||||||
|
"""Test creating a new empty registry."""
|
||||||
|
extensions_dir = temp_dir / "extensions"
|
||||||
|
extensions_dir.mkdir()
|
||||||
|
|
||||||
|
registry = ExtensionRegistry(extensions_dir)
|
||||||
|
|
||||||
|
assert registry.data["schema_version"] == "1.0"
|
||||||
|
assert registry.data["extensions"] == {}
|
||||||
|
assert len(registry.list()) == 0
|
||||||
|
|
||||||
|
def test_add_extension(self, temp_dir):
|
||||||
|
"""Test adding an extension to registry."""
|
||||||
|
extensions_dir = temp_dir / "extensions"
|
||||||
|
extensions_dir.mkdir()
|
||||||
|
|
||||||
|
registry = ExtensionRegistry(extensions_dir)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"source": "local",
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
registry.add("test-ext", metadata)
|
||||||
|
|
||||||
|
assert registry.is_installed("test-ext")
|
||||||
|
ext_data = registry.get("test-ext")
|
||||||
|
assert ext_data["version"] == "1.0.0"
|
||||||
|
assert "installed_at" in ext_data
|
||||||
|
|
||||||
|
def test_remove_extension(self, temp_dir):
|
||||||
|
"""Test removing an extension from registry."""
|
||||||
|
extensions_dir = temp_dir / "extensions"
|
||||||
|
extensions_dir.mkdir()
|
||||||
|
|
||||||
|
registry = ExtensionRegistry(extensions_dir)
|
||||||
|
registry.add("test-ext", {"version": "1.0.0"})
|
||||||
|
|
||||||
|
assert registry.is_installed("test-ext")
|
||||||
|
|
||||||
|
registry.remove("test-ext")
|
||||||
|
|
||||||
|
assert not registry.is_installed("test-ext")
|
||||||
|
assert registry.get("test-ext") is None
|
||||||
|
|
||||||
|
def test_registry_persistence(self, temp_dir):
|
||||||
|
"""Test that registry persists to disk."""
|
||||||
|
extensions_dir = temp_dir / "extensions"
|
||||||
|
extensions_dir.mkdir()
|
||||||
|
|
||||||
|
# Create registry and add extension
|
||||||
|
registry1 = ExtensionRegistry(extensions_dir)
|
||||||
|
registry1.add("test-ext", {"version": "1.0.0"})
|
||||||
|
|
||||||
|
# Load new registry instance
|
||||||
|
registry2 = ExtensionRegistry(extensions_dir)
|
||||||
|
|
||||||
|
# Should still have the extension
|
||||||
|
assert registry2.is_installed("test-ext")
|
||||||
|
assert registry2.get("test-ext")["version"] == "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
# ===== ExtensionManager Tests =====
|
||||||
|
|
||||||
|
class TestExtensionManager:
|
||||||
|
"""Test ExtensionManager installation and removal."""
|
||||||
|
|
||||||
|
def test_check_compatibility_valid(self, extension_dir, project_dir):
|
||||||
|
"""Test compatibility check with valid version."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||||
|
|
||||||
|
# Should not raise
|
||||||
|
result = manager.check_compatibility(manifest, "0.1.0")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_check_compatibility_invalid(self, extension_dir, project_dir):
|
||||||
|
"""Test compatibility check with invalid version."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||||
|
|
||||||
|
# Requires >=0.1.0, but we have 0.0.1
|
||||||
|
with pytest.raises(CompatibilityError, match="Extension requires spec-kit"):
|
||||||
|
manager.check_compatibility(manifest, "0.0.1")
|
||||||
|
|
||||||
|
def test_install_from_directory(self, extension_dir, project_dir):
|
||||||
|
"""Test installing extension from directory."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
manifest = manager.install_from_directory(
|
||||||
|
extension_dir,
|
||||||
|
"0.1.0",
|
||||||
|
register_commands=False # Skip command registration for now
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manifest.id == "test-ext"
|
||||||
|
assert manager.registry.is_installed("test-ext")
|
||||||
|
|
||||||
|
# Check extension directory was copied
|
||||||
|
ext_dir = project_dir / ".specify" / "extensions" / "test-ext"
|
||||||
|
assert ext_dir.exists()
|
||||||
|
assert (ext_dir / "extension.yml").exists()
|
||||||
|
assert (ext_dir / "commands" / "hello.md").exists()
|
||||||
|
|
||||||
|
def test_install_duplicate(self, extension_dir, project_dir):
|
||||||
|
"""Test installing already installed extension."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
# Install once
|
||||||
|
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
|
# Try to install again
|
||||||
|
with pytest.raises(ExtensionError, match="already installed"):
|
||||||
|
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
|
def test_remove_extension(self, extension_dir, project_dir):
|
||||||
|
"""Test removing an installed extension."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
# Install extension
|
||||||
|
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
|
ext_dir = project_dir / ".specify" / "extensions" / "test-ext"
|
||||||
|
assert ext_dir.exists()
|
||||||
|
|
||||||
|
# Remove extension
|
||||||
|
result = manager.remove("test-ext", keep_config=False)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert not manager.registry.is_installed("test-ext")
|
||||||
|
assert not ext_dir.exists()
|
||||||
|
|
||||||
|
def test_remove_nonexistent(self, project_dir):
|
||||||
|
"""Test removing non-existent extension."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
result = manager.remove("nonexistent")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_list_installed(self, extension_dir, project_dir):
|
||||||
|
"""Test listing installed extensions."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
# Initially empty
|
||||||
|
assert len(manager.list_installed()) == 0
|
||||||
|
|
||||||
|
# Install extension
|
||||||
|
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
|
# Should have one extension
|
||||||
|
installed = manager.list_installed()
|
||||||
|
assert len(installed) == 1
|
||||||
|
assert installed[0]["id"] == "test-ext"
|
||||||
|
assert installed[0]["name"] == "Test Extension"
|
||||||
|
assert installed[0]["version"] == "1.0.0"
|
||||||
|
assert installed[0]["command_count"] == 1
|
||||||
|
assert installed[0]["hook_count"] == 1
|
||||||
|
|
||||||
|
def test_config_backup_on_remove(self, extension_dir, project_dir):
|
||||||
|
"""Test that config files are backed up on removal."""
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
# Install extension
|
||||||
|
manager.install_from_directory(extension_dir, "0.1.0", register_commands=False)
|
||||||
|
|
||||||
|
# Create a config file
|
||||||
|
ext_dir = project_dir / ".specify" / "extensions" / "test-ext"
|
||||||
|
config_file = ext_dir / "test-ext-config.yml"
|
||||||
|
config_file.write_text("test: config")
|
||||||
|
|
||||||
|
# Remove extension (without keep_config)
|
||||||
|
manager.remove("test-ext", keep_config=False)
|
||||||
|
|
||||||
|
# Check backup was created (now in subdirectory per extension)
|
||||||
|
backup_dir = project_dir / ".specify" / "extensions" / ".backup" / "test-ext"
|
||||||
|
backup_file = backup_dir / "test-ext-config.yml"
|
||||||
|
assert backup_file.exists()
|
||||||
|
assert backup_file.read_text() == "test: config"
|
||||||
|
|
||||||
|
|
||||||
|
# ===== CommandRegistrar Tests =====
|
||||||
|
|
||||||
|
class TestCommandRegistrar:
|
||||||
|
"""Test CommandRegistrar command registration."""
|
||||||
|
|
||||||
|
def test_parse_frontmatter_valid(self):
|
||||||
|
"""Test parsing valid YAML frontmatter."""
|
||||||
|
content = """---
|
||||||
|
description: "Test command"
|
||||||
|
tools:
|
||||||
|
- tool1
|
||||||
|
- tool2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Command body
|
||||||
|
$ARGUMENTS
|
||||||
|
"""
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
frontmatter, body = registrar.parse_frontmatter(content)
|
||||||
|
|
||||||
|
assert frontmatter["description"] == "Test command"
|
||||||
|
assert frontmatter["tools"] == ["tool1", "tool2"]
|
||||||
|
assert "Command body" in body
|
||||||
|
assert "$ARGUMENTS" in body
|
||||||
|
|
||||||
|
def test_parse_frontmatter_no_frontmatter(self):
|
||||||
|
"""Test parsing content without frontmatter."""
|
||||||
|
content = "# Just a command\n$ARGUMENTS"
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
frontmatter, body = registrar.parse_frontmatter(content)
|
||||||
|
|
||||||
|
assert frontmatter == {}
|
||||||
|
assert body == content
|
||||||
|
|
||||||
|
def test_render_frontmatter(self):
|
||||||
|
"""Test rendering frontmatter to YAML."""
|
||||||
|
frontmatter = {
|
||||||
|
"description": "Test command",
|
||||||
|
"tools": ["tool1", "tool2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
output = registrar.render_frontmatter(frontmatter)
|
||||||
|
|
||||||
|
assert output.startswith("---\n")
|
||||||
|
assert output.endswith("---\n")
|
||||||
|
assert "description: Test command" in output
|
||||||
|
|
||||||
|
def test_register_commands_for_claude(self, extension_dir, project_dir):
|
||||||
|
"""Test registering commands for Claude agent."""
|
||||||
|
# Create .claude directory
|
||||||
|
claude_dir = project_dir / ".claude" / "commands"
|
||||||
|
claude_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
ExtensionManager(project_dir) # Initialize manager (side effects only)
|
||||||
|
manifest = ExtensionManifest(extension_dir / "extension.yml")
|
||||||
|
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
registered = registrar.register_commands_for_claude(
|
||||||
|
manifest,
|
||||||
|
extension_dir,
|
||||||
|
project_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(registered) == 1
|
||||||
|
assert "speckit.test.hello" in registered
|
||||||
|
|
||||||
|
# Check command file was created
|
||||||
|
cmd_file = claude_dir / "speckit.test.hello.md"
|
||||||
|
assert cmd_file.exists()
|
||||||
|
|
||||||
|
content = cmd_file.read_text()
|
||||||
|
assert "description: Test hello command" in content
|
||||||
|
assert "<!-- Extension: test-ext -->" in content
|
||||||
|
assert "<!-- Config: .specify/extensions/test-ext/ -->" in content
|
||||||
|
|
||||||
|
def test_command_with_aliases(self, project_dir, temp_dir):
|
||||||
|
"""Test registering a command with aliases."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# Create extension with command alias
|
||||||
|
ext_dir = temp_dir / "ext-alias"
|
||||||
|
ext_dir.mkdir()
|
||||||
|
|
||||||
|
manifest_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extension": {
|
||||||
|
"id": "ext-alias",
|
||||||
|
"name": "Extension with Alias",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Test",
|
||||||
|
},
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0",
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "speckit.alias.cmd",
|
||||||
|
"file": "commands/cmd.md",
|
||||||
|
"aliases": ["speckit.shortcut"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(ext_dir / "extension.yml", 'w') as f:
|
||||||
|
yaml.dump(manifest_data, f)
|
||||||
|
|
||||||
|
(ext_dir / "commands").mkdir()
|
||||||
|
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nTest")
|
||||||
|
|
||||||
|
claude_dir = project_dir / ".claude" / "commands"
|
||||||
|
claude_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
manifest = ExtensionManifest(ext_dir / "extension.yml")
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir)
|
||||||
|
|
||||||
|
assert len(registered) == 2
|
||||||
|
assert "speckit.alias.cmd" in registered
|
||||||
|
assert "speckit.shortcut" in registered
|
||||||
|
assert (claude_dir / "speckit.alias.cmd.md").exists()
|
||||||
|
assert (claude_dir / "speckit.shortcut.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Utility Function Tests =====
|
||||||
|
|
||||||
|
class TestVersionSatisfies:
|
||||||
|
"""Test version_satisfies utility function."""
|
||||||
|
|
||||||
|
def test_version_satisfies_simple(self):
|
||||||
|
"""Test simple version comparison."""
|
||||||
|
assert version_satisfies("1.0.0", ">=1.0.0")
|
||||||
|
assert version_satisfies("1.0.1", ">=1.0.0")
|
||||||
|
assert not version_satisfies("0.9.9", ">=1.0.0")
|
||||||
|
|
||||||
|
def test_version_satisfies_range(self):
|
||||||
|
"""Test version range."""
|
||||||
|
assert version_satisfies("1.5.0", ">=1.0.0,<2.0.0")
|
||||||
|
assert not version_satisfies("2.0.0", ">=1.0.0,<2.0.0")
|
||||||
|
assert not version_satisfies("0.9.0", ">=1.0.0,<2.0.0")
|
||||||
|
|
||||||
|
def test_version_satisfies_complex(self):
|
||||||
|
"""Test complex version specifier."""
|
||||||
|
assert version_satisfies("1.0.5", ">=1.0.0,!=1.0.3")
|
||||||
|
assert not version_satisfies("1.0.3", ">=1.0.0,!=1.0.3")
|
||||||
|
|
||||||
|
def test_version_satisfies_invalid(self):
|
||||||
|
"""Test invalid version strings."""
|
||||||
|
assert not version_satisfies("invalid", ">=1.0.0")
|
||||||
|
assert not version_satisfies("1.0.0", "invalid specifier")
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Integration Tests =====
|
||||||
|
|
||||||
|
class TestIntegration:
|
||||||
|
"""Integration tests for complete workflows."""
|
||||||
|
|
||||||
|
def test_full_install_and_remove_workflow(self, extension_dir, project_dir):
|
||||||
|
"""Test complete installation and removal workflow."""
|
||||||
|
# Create Claude directory
|
||||||
|
(project_dir / ".claude" / "commands").mkdir(parents=True)
|
||||||
|
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
# Install
|
||||||
|
manager.install_from_directory(
|
||||||
|
extension_dir,
|
||||||
|
"0.1.0",
|
||||||
|
register_commands=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
assert manager.registry.is_installed("test-ext")
|
||||||
|
installed = manager.list_installed()
|
||||||
|
assert len(installed) == 1
|
||||||
|
assert installed[0]["id"] == "test-ext"
|
||||||
|
|
||||||
|
# Verify command registered
|
||||||
|
cmd_file = project_dir / ".claude" / "commands" / "speckit.test.hello.md"
|
||||||
|
assert cmd_file.exists()
|
||||||
|
|
||||||
|
# Verify registry has registered commands (now a dict keyed by agent)
|
||||||
|
metadata = manager.registry.get("test-ext")
|
||||||
|
registered_commands = metadata["registered_commands"]
|
||||||
|
# Check that the command is registered for at least one agent
|
||||||
|
assert any(
|
||||||
|
"speckit.test.hello" in cmds
|
||||||
|
for cmds in registered_commands.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
result = manager.remove("test-ext")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
# Verify removal
|
||||||
|
assert not manager.registry.is_installed("test-ext")
|
||||||
|
assert not cmd_file.exists()
|
||||||
|
assert len(manager.list_installed()) == 0
|
||||||
|
|
||||||
|
def test_multiple_extensions(self, temp_dir, project_dir):
|
||||||
|
"""Test installing multiple extensions."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# Create two extensions
|
||||||
|
for i in range(1, 3):
|
||||||
|
ext_dir = temp_dir / f"ext{i}"
|
||||||
|
ext_dir.mkdir()
|
||||||
|
|
||||||
|
manifest_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extension": {
|
||||||
|
"id": f"ext{i}",
|
||||||
|
"name": f"Extension {i}",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": f"Extension {i}",
|
||||||
|
},
|
||||||
|
"requires": {"speckit_version": ">=0.1.0"},
|
||||||
|
"provides": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": f"speckit.ext{i}.cmd",
|
||||||
|
"file": "commands/cmd.md",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(ext_dir / "extension.yml", 'w') as f:
|
||||||
|
yaml.dump(manifest_data, f)
|
||||||
|
|
||||||
|
(ext_dir / "commands").mkdir()
|
||||||
|
(ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\nTest")
|
||||||
|
|
||||||
|
manager = ExtensionManager(project_dir)
|
||||||
|
|
||||||
|
# Install both
|
||||||
|
manager.install_from_directory(temp_dir / "ext1", "0.1.0", register_commands=False)
|
||||||
|
manager.install_from_directory(temp_dir / "ext2", "0.1.0", register_commands=False)
|
||||||
|
|
||||||
|
# Verify both installed
|
||||||
|
installed = manager.list_installed()
|
||||||
|
assert len(installed) == 2
|
||||||
|
assert {ext["id"] for ext in installed} == {"ext1", "ext2"}
|
||||||
|
|
||||||
|
# Remove first
|
||||||
|
manager.remove("ext1")
|
||||||
|
|
||||||
|
# Verify only second remains
|
||||||
|
installed = manager.list_installed()
|
||||||
|
assert len(installed) == 1
|
||||||
|
assert installed[0]["id"] == "ext2"
|
||||||
|
|
||||||
|
|
||||||
|
# ===== Extension Catalog Tests =====
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtensionCatalog:
|
||||||
|
"""Test extension catalog functionality."""
|
||||||
|
|
||||||
|
def test_catalog_initialization(self, temp_dir):
|
||||||
|
"""Test catalog initialization."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
assert catalog.project_root == project_dir
|
||||||
|
assert catalog.cache_dir == project_dir / ".specify" / "extensions" / ".cache"
|
||||||
|
|
||||||
|
def test_cache_directory_creation(self, temp_dir):
|
||||||
|
"""Test catalog cache directory is created when fetching."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create mock catalog data
|
||||||
|
catalog_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"test-ext": {
|
||||||
|
"name": "Test Extension",
|
||||||
|
"id": "test-ext",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Test",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Manually save to cache to test cache reading
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": "http://test.com/catalog.json",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should use cache
|
||||||
|
result = catalog.fetch_catalog()
|
||||||
|
assert result == catalog_data
|
||||||
|
|
||||||
|
def test_cache_expiration(self, temp_dir):
|
||||||
|
"""Test that expired cache is not used."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create expired cache
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog_data = {"schema_version": "1.0", "extensions": {}}
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
|
||||||
|
# Set cache time to 2 hours ago (expired)
|
||||||
|
expired_time = datetime.now(timezone.utc).timestamp() - 7200
|
||||||
|
expired_datetime = datetime.fromtimestamp(expired_time, tz=timezone.utc)
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": expired_datetime.isoformat(),
|
||||||
|
"catalog_url": "http://test.com/catalog.json",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cache should be invalid
|
||||||
|
assert not catalog.is_cache_valid()
|
||||||
|
|
||||||
|
def test_search_all_extensions(self, temp_dir):
|
||||||
|
"""Test searching all extensions without filters."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create mock catalog
|
||||||
|
catalog_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"name": "Jira Integration",
|
||||||
|
"id": "jira",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Jira integration",
|
||||||
|
"author": "Stats Perform",
|
||||||
|
"tags": ["issue-tracking", "jira"],
|
||||||
|
"verified": True,
|
||||||
|
},
|
||||||
|
"linear": {
|
||||||
|
"name": "Linear Integration",
|
||||||
|
"id": "linear",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"description": "Linear integration",
|
||||||
|
"author": "Community",
|
||||||
|
"tags": ["issue-tracking"],
|
||||||
|
"verified": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save to cache
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": "http://test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search without filters
|
||||||
|
results = catalog.search()
|
||||||
|
assert len(results) == 2
|
||||||
|
|
||||||
|
def test_search_by_query(self, temp_dir):
|
||||||
|
"""Test searching by query text."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create mock catalog
|
||||||
|
catalog_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"name": "Jira Integration",
|
||||||
|
"id": "jira",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Jira issue tracking",
|
||||||
|
"tags": ["jira"],
|
||||||
|
},
|
||||||
|
"linear": {
|
||||||
|
"name": "Linear Integration",
|
||||||
|
"id": "linear",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Linear project management",
|
||||||
|
"tags": ["linear"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": "http://test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search for "jira"
|
||||||
|
results = catalog.search(query="jira")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0]["id"] == "jira"
|
||||||
|
|
||||||
|
def test_search_by_tag(self, temp_dir):
|
||||||
|
"""Test searching by tag."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create mock catalog
|
||||||
|
catalog_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"name": "Jira",
|
||||||
|
"id": "jira",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Jira",
|
||||||
|
"tags": ["issue-tracking", "jira"],
|
||||||
|
},
|
||||||
|
"linear": {
|
||||||
|
"name": "Linear",
|
||||||
|
"id": "linear",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Linear",
|
||||||
|
"tags": ["issue-tracking", "linear"],
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"name": "GitHub",
|
||||||
|
"id": "github",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "GitHub",
|
||||||
|
"tags": ["vcs", "github"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": "http://test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search by tag "issue-tracking"
|
||||||
|
results = catalog.search(tag="issue-tracking")
|
||||||
|
assert len(results) == 2
|
||||||
|
assert {r["id"] for r in results} == {"jira", "linear"}
|
||||||
|
|
||||||
|
def test_search_verified_only(self, temp_dir):
|
||||||
|
"""Test searching verified extensions only."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create mock catalog
|
||||||
|
catalog_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"name": "Jira",
|
||||||
|
"id": "jira",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Jira",
|
||||||
|
"verified": True,
|
||||||
|
},
|
||||||
|
"linear": {
|
||||||
|
"name": "Linear",
|
||||||
|
"id": "linear",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Linear",
|
||||||
|
"verified": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": "http://test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search verified only
|
||||||
|
results = catalog.search(verified_only=True)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0]["id"] == "jira"
|
||||||
|
|
||||||
|
def test_get_extension_info(self, temp_dir):
|
||||||
|
"""Test getting specific extension info."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create mock catalog
|
||||||
|
catalog_data = {
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"extensions": {
|
||||||
|
"jira": {
|
||||||
|
"name": "Jira Integration",
|
||||||
|
"id": "jira",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Jira integration",
|
||||||
|
"author": "Stats Perform",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text(json.dumps(catalog_data))
|
||||||
|
catalog.cache_metadata_file.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"catalog_url": "http://test.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get extension info
|
||||||
|
info = catalog.get_extension_info("jira")
|
||||||
|
assert info is not None
|
||||||
|
assert info["id"] == "jira"
|
||||||
|
assert info["name"] == "Jira Integration"
|
||||||
|
|
||||||
|
# Non-existent extension
|
||||||
|
info = catalog.get_extension_info("nonexistent")
|
||||||
|
assert info is None
|
||||||
|
|
||||||
|
def test_clear_cache(self, temp_dir):
|
||||||
|
"""Test clearing catalog cache."""
|
||||||
|
project_dir = temp_dir / "project"
|
||||||
|
project_dir.mkdir()
|
||||||
|
(project_dir / ".specify").mkdir()
|
||||||
|
|
||||||
|
catalog = ExtensionCatalog(project_dir)
|
||||||
|
|
||||||
|
# Create cache
|
||||||
|
catalog.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
catalog.cache_file.write_text("{}")
|
||||||
|
catalog.cache_metadata_file.write_text("{}")
|
||||||
|
|
||||||
|
assert catalog.cache_file.exists()
|
||||||
|
assert catalog.cache_metadata_file.exists()
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
catalog.clear_cache()
|
||||||
|
|
||||||
|
assert not catalog.cache_file.exists()
|
||||||
|
assert not catalog.cache_metadata_file.exists()
|
||||||
Reference in New Issue
Block a user